Thursday, April 30, 2009

Silly Rabbit, KiXtart is for Scripting?

I must be on a KiXtart kick or something.  I don't know.  It's an addictive scripting language to work with.  As much as CFML seems dated to me now, there are some similarities with it.  They are both relatively easy to pick up and get going on.  A low learning ramp.  But both can (or could) support you as you expand your skills into intermediate and onward through advanced/expert and into the metaphysical.  Where you drink hot tea with incense burning in a temple of computers and hum Tibetan chants, tap a gong, and say something ethereal like "I see, the path, to enlightenment.... it is .... (sip)... over there on... (cough)... the thumbdrive."  You may leave the temple now.

Geez that was dumb.  Where was I going with that?  Who knows.

On to the MEAT!...

First up: Script example of Reading crap from an Excel Spreadsheet


;----------------------------------------------------------------
; Filename..: excel_read.kix
; Author....: David M. Stein
; Date......: 04/30/2009
; Purpose...: read rows and columns in excel spreadsheet table
;----------------------------------------------------------------
$filePath = "c:\somefile.xls" ; make sure you update this
$columns = "1,2,4,6" ; columns to read from, suit yourself
$row = 3 ; row to start reading from, suit yourself
$numRows = 10 ; max number of rows to read from start row

; Do not modify below this point!
$objExcel = CreateObject("Excel.Application")
$objWorkbook = $objExcel.Workbooks.Open($filePath)
;$objExcel.Visible = 1 ; uncomment if want something dumb to look at

$hrow = "Results"

For each $x in Split($columns, ",")
$hrow = $hrow+Chr(9)+"[$x]"
Next
? Trim($hrow)

While ($row < $numRows)
$rowString = ""
For each $colnum in Split($columns, ",")
$cellValue = Trim($objExcel.Cells($row, Val($colnum)).Value)
If $cellValue <> ''
$rowString = $rowString+Chr(9)+$cellValue
Else
$rowString = $rowString+Chr(9)+"--"
EndIf
Next
? "$row"+Chr(9)+Trim($rowString)
$row = $row + 1
Loop

$objExcel.Quit


And, 2nd up... An Example of Writing crap to a Spreadsheet


;----------------------------------------------------------------
; Filename..: excel_write.kix
; Author....: David M. Stein
; Date......: 04/30/2009
; Purpose...: write rows and columns in excel spreadsheet table
; Adapted from KiXtart User's Guide for 4.60 (www.kixtart.org)
;----------------------------------------------------------------
$objExcel = CreateObject("Excel.Application")
If @ERROR = 0
$objExcel.Visible = 1 ; make Excel visible to the user
$Rc = $objExcel.Workbooks.Add ; add a new workbook
$array = "Order #", "Amount", "Tax"
$objExcel.Range("A1:C1").Value = $array ;add some columns

; populate the spreadsheet cells with data
For $i = 0 To 19
$objExcel.Cells(($i+2),1).Value = "ORD" + ($i + 1000)
$objExcel.Cells(($i+2),2).Value = Rnd() / 100
Next

; fill the last column with a formula to compute the sales tax.
$objExcel.Range("C2").Resize(20, 1).Formula = "=B2*0.07"

; format the worksheet
$objExcel.Range("A1:C1").Font.Bold = 1
$objExcel.Range("B2:C22").Style = "Currency"

; add sum-total and double-line separator
$objExcel.Range("B22").Select
$objExcel.Range("B22").Formula = "=SUM(B2:B21)"
$objExcel.Range("B21").Borders(9).LineStyle = -4119 ; xlDouble
$objExcel.Range("B21").Borders(9).Weight = 4 ; xlThick

; apply color fills to heading and left-hand column cells
$objExcel.Range("A1:C1").Interior.Pattern = 1
$objExcel.Range("A1:C1").Interior.TintAndShade = -0.249977111117893
$objExcel.Range("A1:C1").Interior.ThemeColor = 3
$objExcel.Range("A1:C1").Interior.PatternTintAndShade = 0

$objExcel.Range("A2:A21").Interior.Pattern = 1
$objExcel.Range("A2:A21").Interior.TintAndShade = -4.99893185216834E-02
$objExcel.Range("A2:A21").Interior.ThemeColor = 3
$objExcel.Range("A2:A21").Interior.PatternTintAndShade = 0

; auto-fit columns and finish up
$Rc = $objExcel.Range("A1:C1").EntireColumn.AutoFit
$objExcel.UserControl = 1
Else
? @ERROR + " / " @SERROR
EndIf

Tuesday, April 28, 2009

Follow-Up Comments on our MacKay Island Trip

I forgot to mention a few things about our trip last Saturday:
  • The water levels were the highest I've ever seen them.  That was both inside and outside the lock system.  Even in the interior flood plane was filled in.  The canal along the main entrance road was up to the edge of the gravel roadway also.
  • The large open field to the North of Knapp's old house location, which was the site of his 9-hole golf course, was recently mowed to a 6-inch height.  I have never seen that section of the island mowed before.  Not sure why that is.
  • The main trail from the parking area near the fishing area, back to the split at the mid-point trail has been built up with multiple layers of dirt and gravel.  It is now about 3 or 4 feet higher than it was.  At the mid-point trail it drops back to the level it was, which is only a foot above the mean water level in the surrounding creeks.
  • There are cylindrical crab cages in the creeks all around the island's outer waterways.  One of them had an animal trapped inside and was moving quite a bit, but we couldn't see the animal itself.  No idea why they have those positioned everywhere.  I would estimate somewhere between eight and ten overall.

KiXtart Script: Enumerate Files in Folders and Sub-Folders

This is the third and final script post-of-the-day. This one simply enumerates all files of a given type (i.e. extension) within a specified folder, and all sub-folders beneath it. It recurses the folder you start with and crawl down into all the nooks and crannies like ants finding their way into your kitchen in the Summer time.


;----------------------------------------------------------------
; Filename..: Enum_Files.kix
; Author....: David M. Stein
; Date......: 04/28/09
; Purpose...: recurse all sub-folders and files within each
;----------------------------------------------------------------
Break ON

If Not $StartPath
$=ShowUsage()
Else
If Left($StartPath,1) = "%"
$StartPath = ExpandEnvironmentVars("%ProgramFiles%")
EndIf
EndIf

If Exist($StartPath)
? "computer...: "+@wksta
? "date.......: "+@date
$FileSpec = "exe"
$=Enum_SubFolders($StartPath)
Else
? "error: folder path not found"
EndIf

;----------------------------------------------------------------
; comment:
;----------------------------------------------------------------

Function Enum_SubFolders($Path)
Dim $FileName
$FileName = Dir("$Path\*.*")
While $FileName <> "" And @ERROR = 0
If $FileName <> "." And $FileName <> ".."
If GetFileAttr("$Path\$FileName") & 16
? "$Path\$FileName"
$=Enum_Files("$Path\$FileName", $FileSpec)
$=Enum_SubFolders("$Path\$FileName")
EndIf
EndIf
$FileName = Dir() ; retrieve next file/folder
Loop
EndFunction

;----------------------------------------------------------------
; comment:
;----------------------------------------------------------------

Function Enum_Files($Path, $Ext)
Dim $fName
$fName = Dir("$Path\*.$Ext")
While $fName <> "" And @ERROR = 0
? Chr(9)+"filename...: $fName"
? Chr(9)+Chr(9)+"file size......: "+GetFileSize("$Path\$fName")
? Chr(9)+Chr(9)+"date created...: "+GetFileTime("$Path\$fName", 1)
? Chr(9)+Chr(9)+"company name...: "+
GetFileVersion("$Path\$fName", "CompanyName")
? Chr(9)+Chr(9)+"product name...: "+
GetFileVersion("$Path\$fName", "ProductName")
? Chr(9)+Chr(9)+"product version: "+
GetFileVersion("$Path\$fName", "ProductVersion")
$fName = Dir()
Loop
EndFunction

Function ShowUsage()
?
? "Usage: enum_files.kix $$StartPath=[PATH]"
?
EndFunction

KiXtart Script: Delete Files in Selected Folder

This is another VBScript-to-KiXtart conversion diversion transfusion illusion, which prompts for a folder, and then deletes all the files in the folder.  This one just clears out the selected folder.  It won't delete subfolders or recurse into subfolders to delete their files.  I could add that but I saw the weather was too nice out and got distracted.  Basically, I'm just too lazy.



;----------------------------------------------------------------
; Filename..: CleanOut_Folder.kix
; Author....: David M. Stein
; Date......: mm/dd/yyyy
; Purpose...: select folder to clean out files within
;----------------------------------------------------------------
Break ON

$FileSpec = "*.*"

$objShell = CreateObject("Shell.Application")
$objFolder = $objShell.BrowseForFolder (0, "Select The Folder To
Enumerate :", (0))

If $objFolder = 0
Exit
Else
$objFolderItem = $objFolder.Self
$objPath = $objFolderItem.Path
EndIf

? "Path: $objPath"

$FileCount = 0
$FileName = Dir("$objPath\$FileSpec")
While $FileName <> "" And @ERROR = 0
If $FileName <> "." And $FileName <> ".."
? "Deleting: $objPath\$FileName"
Del "$objPath\$FileName" /f /h /c
$FileCount = $FileCount + 1
EndIf
$FileName = Dir()
Loop

? "Deleted $FileCount files"

KiXtart Script: Building an MMC Console

This is based on a VBScript example posted a while back by the renown, the incredible, Mr. Don Hite. I mixed in some code from the MSDN MMC Automation object model reference documentation, poured it all into a KiXtart blender and punched the puree button. Be mindful of word-wrapping on the longer strings.

;----------------------------------------------------------------
; Filename..: mmc.kix
; Author....: David M. Stein
; Date......: 04/28/2009
; Purpose...: build and save a custom MMC console
;----------------------------------------------------------------
Break ON

$consoleName = "C:\MyConsole.msc"

If Exist($consoleName)
? "info: console already created as $consoleName"
Else
? "info: console not found, building now..."
$objMMC = CreateObject("MMC20.Application")
$objMMC.Show
$objDoc = $objMMC.Document
$objSnapIns = $objDoc.SnapIns
$objFolder = $objSnapIns.Add("Folder")
$nul = $objMMC.Document.SnapIns.Add("Active Directory Users and
Computers")
$nul = $objMMC.Document.SnapIns.Add("Active Directory Sites and
Services")
$nul = $objMMC.Document.SnapIns.Add("Active Directory Domains
and Trusts")
$objMMC.UserControl = 1
$objMMC.Document.Name = $consoleName
$objMMC.Document.Save()
? "info: console built and saved as $consoleName"
EndIf

Monday, April 27, 2009

ITIL v3 Foundations Exam

I passed my ITIL v3 Foundations exam with a score of 93! Holy crap. It wasn't the toughest exam I've taken, but it was one of the most confusing. I left the classroom thinking I might have scored 65 or 70 (26 of 40 questions to pass). Cool.

Sunday, April 26, 2009

Return to MacKay Island N.W.R.

It's been two years since the last time I hiked the entire perimeter of MacKay Island N.W.R. So today I drove down with my two oldest daughters, Rachel and Sarah, to hike it again. Rachel had never been there. Sarah was with me the last time I went, so both were eager to get out and see the land and the inhabitants.








To view more photos: Click Here

Information about MacKay Island NWR: Click Here

Status of Living Things: Vegetation near the main entrance is still waiting to spring back to life, however, from the first stop-gate on the vegetation is bright green and teeming with life. Turtles were in high abundance along the main driving trail. Mostly adults or half-grown. No babies. We counted at least thirty along a 100 yard distance, sitting on branches or logs in the water. The only insects we saw today were butterflies, a few "ground bugs", and a slight population of gnats and mosquitoes. The dear fly abundance was pretty low. Almost absent. Birds we saw were Bald Eagles (we counted six), a few Egrets, a bunch of white Cranes, two Grey Heron, plenty of red-wing blackbirds, and one very angry Osprey guarding her nest (which we accidentally turned a corner and happened upon). She screeched and swooped a few times but we moved on quickly. Unlike warmer months, there were absolutely NO snakes, spiders, lizards or centipedes.

For the first time ever, we saw a full-grown deer, eating out in the wet marsh. This was along the North trail, just East of the mid-point trail, off to the North. It was maybe 300 yards out but clearly visible. No way we could approach it, or it us. Too much water in between

The Eagles were full-grown and all were in flight. None were perched or near their nests. They were all in a general area swarming in circles near the forest, which covers the middle section of the Reserve.

MacKay Island is part of Knott's Island, which is just at the edge of the North Carolina border with Virginia to the East.  From my house in Virginia Beach, it's about a 40 minute drive.  We hiked the outer of both the "MacKay Island Trail" and the "Live Oak Point Trail", which we call the "outer loop trail" which is roughly nine (9) miles in all, sort of.  We meander off the trail while at the point where Knapp's house was, so we add some distance to the trip.

The girls and I made the full perimeter in good time. Only stopping to take pictures in a few spots, and moving on. Thankfully I packed enough crap in my backpack to cover us the whole time and in the 90 degree heat. There was a good breeze around the North trail and the tip of the island (where Joseph Knapp's house once stood). The trip back along the South trail was a dead heat with no breeze and where we encountered the most gnats. All told, we had a great time and my girls thanked me for making the trip.

Saturday, April 25, 2009

Run the Outlook Profile Migration Utility in a Login Script

Run the Microsoft Outlook Profile Migration utility from a KiXtart script at login (yes, part of a login script). Just drop the exprofre.exe file into the NETLOGON share on your domain controller and drop this crap (oops, script code) into your KiX login script, suck down a coffee and scream "Yeah baby!!!!"


Function MigrateOutlookProfile()
Dim $dc, $clientlog, $migprof, $exprof, $temp
$dc = SubStr(@LSERVER,3)
$exprof = @LSERVER+"\Netlogon\exprofre.exe"
$temp = ExpandEnvironmentVars("%temp%")
$clientlog = $temp+"\exprof_"+@wksta+".log"
If Exist($exprof) = 0
? "mgrOutlookProfile...: error - unable to locate exprofre.exe!"
Return
EndIf
If Exist($clientlog)
? "mgrOutlookProfile...: deleting previous log file..."
Del $clientlog /c
If @ERROR > 0
? "mgrOutlookProfile...: failed to delete log file!"
EndIf
EndIf
? "mgrOutlookProfile...: migrating outlook profile..."
$migprof = $exprof+" /targetgc="+$dc+" /v /n /logfile="+$clientlog
? "mgrOutlookProfile...: $migprof"
Run $migprof
? "mgrOutlookProfile...: process has been initiated and will continue"
? "mgrOutlookProfile...: check for the following log when finished..."
? "mgrOutlookProfile...: $clientlog"
EndFunction

Friday, April 24, 2009

Windows Scripting Languages at the 50,000 Foot Level

I hear a lot of mindless, useless jabbering in and around the office regarding "best" this and that, when it comes to technology. A recent discussion touched on scripting tools, which is something near and dear to my heart, so I thought I'd digress a little on this subject. While I may tend to harp on one or two in particular, I strongly believe all languages have a place in our toolboxes. They each have an ideal purpose.

What are the major players in the world of scripting in a Microsoft Windows environment?
  • BAT / CMD script

  • Windows Scripting Host --> VBScript, JScript, ActivePerl

  • KiXtart

  • PowerShell

Each is worthy of strong consideration. Each offers a staggering volume of utility and flexibility. Each is "free" with some caveates (after all: nothing is ever truly "free").

How each is judged depends upon the context in which it is used. By that I mean which version of Windows is on all clients involved in the scripting task or process.

One thing that struck me as unique to KiXtart is that it is the only portable scripting engine of the four. The others must reside on the client where a task is executed. Even if you run a remote process, the engine must be installed on the client where the task is initiated. If a remote process invokes another instance which is local to the remote client (huh?) it too must have the engine installed.

KiXtart does not require this much effort.

You can put the KiXtart engein (Kix32.exe) on a server, share it out, and run all scripts on any clients by invoking the engine from the network share. There's no local installation ever required. Sure, there's a small processing burden (latency mostly) but if the script is rather linear, it typically works just fine and executes fast enough to barely notice a difference. Think "login scripts" here. The Kix32.exe engine is often copied into the NETLOGON share of the domain controller along with the scripts themselves, and any necessary support files which your scripts may rely upon or invoke. Clients simply run the scripts and script engine from the NETLOGON share.

I say this also because in the 25 or so years I've been writing program code, I've seen quite a few administrators knee-jerk and copy the Kix32.exe engine to each and every client and run it locally. This might be worthwhile on laptops which are often disconnected from the home network but completely unnecessary for desktops and servers within the home network.

BAT/CMD probably the most pervasive because they are uniquely tied to the Windows shell and therefore ubiquitous. The same is true for WSH, but only for Windows 2000 SP3 or later. Actually SP4 I believe is where WSH 5.6 becomes baked in. But PowerShell is still the odd man out, even with Windows Vista. Windows 7 finally bakes it in as part of the standard build, but that's technically in the future (right now: April 2009).

I hate to keep beating a dead horse, but KiXtart is a holistic approach to scripting in terms of the language, the toolset and the pervasiveness aspects. The language essentially combines best features of BAT with VBScript, while the engine is self-contained and entirely portable. This makes it very simple to "deploy" in any network environment. Post it into a central location and run everything from there, or run it locally by invoking it from the central location. Your choice. Pretty cool.

While PowerShell is undoubtedly more tuned to .NET interfaces and object-oriented processing and structures, it also incurs a steeper learning curve and a bit more complexity within the raw coding phase of development. The emergence of newer and more specialized cmdlets make it increasingly attractive, and for good reason. But iterative, procedural languages are NOT DEAD by any means. They're not even in decline. They continue to grow. Both of them continue to grow. There's room for both at the table. If you have time to learn both of them, then you should. But don't exclude one for the other either.

WMIC Quips Tips Flips and Blips

WMIC, or WMI Command-line, or Windows Management Instrumentation Command-line, or whatever, is a condensed-form utility for running queries, pushing updates, and performing various WMI-related stuff via the CMD console.

It's very handy for quick checks on what your system is doing. For example:

> wmic os list brief

Will show you a top-level summary of your operating system information. In my case it shows the following:

BuildNumber Organization RegisteredUser SerialNumber SystemDirectory Version
7068 [blank] dood 00000-0... C:\Windows\system32 6.1.7068

Due to word-wrapping issues with Blogger, it's a bit easier to force a reformatting as:

BuildNumber: 7068
Organization:
RegisteredUser: dood
SerialNumber: 00000-000-0000000-00000
SystemDirectory: C:\Windows\System32
Version: 6.1.7068

Another useful query is:

> wmic os list free

This will dump a report of "free" physical and virtual memory, virtual page file space and the full name of your operating system:

FreePhysicalMemory: 1932116
FreeSpaceInPagingFiles: 3143596
FreeVirtualMemory: 4976816
Name: Microsoft Windows 7 Ultimate C:\Windows\Device\Harddisk0\Partition2

These are just two quick examples obviously. To find out more, open a CMD console on Windows XP, Vista or Windows 7 (or Windows Server 2003 or 2008 as well), and type "wmic -?" or drill-down like "wmic os -?" and it will provide syntax information for how to use this properly.

Thursday, April 23, 2009

More Reasons Computers Should Drive Our Cars

Computer controlled cars. No steering wheels. No pedals. No shifter. Get in and relax, enjoy the ride.

  • No more drunk-driving incidents. Think of how many lives would be saved
  • No more stress-related accidents. Go ahead, fight with your spouse.
  • No more distraction-related accidents. Go ahead, eat, pet your dog, talk on the phone.
  • No more watching idiots get in the way of fire trucks, ambulances, police en route to emergencies. Cars would automatically get out of the way.
  • No more rubber-necking hold-ups
  • No more car-jackings
  • No more car-thefts
  • No more traffic lights
  • No more speeding tickets
  • No more watching criminals outrun cops (where would they go?)
  • Greatly reduced chaparone needs
  • More productive passengers (driver can do other things now)
  • No more running out of gas (computer would know where you're going and whether it has enough to make it without stopping first to gas-up or power-up)

Imagine these scenarios as well...

  • Your car malfunctions and stops (aka "breaks down"). It knows another car is coming by without any occupants and advises you to stay calm and wait for another car to come by and "pick you up" to continue on where you need to go.
  • Your car sits all day in the parking lot at work, but your wife/neighbor/kid/brother/sister/mom/dad/whatever needs a ride while you're at work. No problem, they send a request to your phone. You approve it, the car starts up and drives itself over to pick them up and comes back to your office to park itself when done.
  • Reduced need for tow trucks, ambulances, paramedics, fire trucks, traffic police
  • Reduced need for insurance filings
  • Reduced crime (theft, carjacking, getaways)
  • Reduced death rates
  • More efficient traffic management and automatic rerouting
  • Safer roads

When you get down to it, automobiles have not really radically transformed from the 1930's. They still have gasoline-powered engines, the same basic form factor (body structure, doors, windows, mirros, dashboard, steering wheel, seats, trunk, headlights, etc. etc.). All we've seen are constant refinements. Even the hybrid concept isn't "new". That idea was kicked around half a century ago, but nobody cared back then. All of the problems mentioned above are directly attributable to humans and human operation of machinery. Why do we continue to accept this as the "best way"? It's stupid!

We agree that robots do a better job at handling tasks humans consider dangerous or tedious. Driving is dangerous and (mostly) tedious. We kill more people "trying" to "drive" cars than any other accident method known. Come on people: wake up. Let's push our "leaders" to get this going. We can put things on Mars and Venus. We can send things beyond our solar system. We can shrink circuit chips smaller than a human hair. Can't we do this?

Another Other Way to Reset Local Admin Passwords

I realized that, as usual, I was too focused on demonstrating a methodology that I overlooked the simplest of solutions to the problem being "solved". I am referring to my earlier post on "Another Way to Reset Local Passwords on Remote Computers"

Another way would be to meet the fully-baked process (pspasswd.exe) half-way with another semi-fully-baked process (a script to fetch the computer names and write them into a file). This was because of the amazing RTFM syndrome. I use RTFM all the time on others, so it's only fitting that I was bitten by it myself. In my case, it should be RTFMC, where "C" stands for "Carefully". Because I tend to rush when I'm stressed about something and that day I was stressed from idiot humans pretending to drive cars in rush hour. I'll say it again: Humans are incapable of driving machinery. It's a job for computers.

So, here goes. First we fetch the computer names from an AD query, then write them into an ASCII text file, one name per row. Then we call the pspasswd utility and provide the file input and let it do its thing. Rather than do this with the same old boring VBScript mindset, I wanted to spice this baby up a bit and toss in a Red Bull, a box of Viagra and some KiXtart coding, and "Bam!" let 'er rip.

First, the boring VBScript version...


Option Explicit
Const cmdStr = "pspasswd.exe"
Const strUser = "Administrator"
Const strPwd = "P@ssW0rd$123"
Const ADSI_DN = "mydomain.com"
Const inputFile = "computerlist.txt"
Const exclude = "SERVER1 SERVER2 COMPUTER3"

'----------------------------------------------------------------
' comment: do not change any code below this point!
'----------------------------------------------------------------

Wscript.Echo "info: starting processing: " & Now

Dim objShell, objX, objFSO, objFile, runCmd, objDom, strComputer
Set objFSO = CreateObject("Scripting.FileSystemObject")

Wscript.Echo "info: verifying pspasswd.exe exists..."
If objFSO.FileExists(cmdstr) Then
runCmd = cmdStr & " @" & inputFile & " " & strUser & _
" " & strPwd & " >pspasswd.log"
Set objShell = CreateObject("Wscript.Shell")
Set objDom = GetObject("WinNT://" & ADSI_DN)
Set objFile = objFSO.CreateTextFile(inputFile, True)

Wscript.Echo "info: querying computer names from active directory..."

For each objX in objDom
If Lcase(objX.Class) = "computer" Then
strComputer = Ucase(objX.Name)
If InStr(1, exclude, strComputer) <> 0 Then
Wscript.Echo "info: exluding = " & strComputer
Else
Wscript.Echo "info: computername = " & strComputer
objFile.WriteLine(strComputer)
End If
End If
Next
objFile.Close
Set objFile = Nothing
Wscript.Echo "info: finished building input file"
Wscript.Echo "info: runCmd = " & runCmd
objShell.Run "%COMSPEC% /c " & runCmd, 1, True
Set objShell = Nothing
Else
Wscript.Echo "error: pspasswd.exe not found"
End If
Set objFSO = Nothing
Wscript.Echo "info: completed processing: " & Now



Now, as long as you put the script in the same folder as pspasswd.exe, and you run the script using an account that has admin rights on all the remote computers, and they're all online and accessible, it should work fine. You may notice I spruced it up a little with a few lines of code and a spritz of air freshener.

Now for the more interesting KiXtart version...


break on

$cmdStr = "s:\utils\sysinternals\pspasswd.exe"
$strUser = "Administrator"
$strPwd = "P@@ssW0rd$$123"
$ADSI_DN = "mydomain.com"
$inputFile = "computerlist.txt"
$exclude = "SERVER1 SERVER2 COMPUTER3"

;----------------------------------------------------------------
; comment: do not change any code below this point!
;----------------------------------------------------------------

? "info: starting processing: "+@date+" "+@time

Dim $objX, $runCmd, $objDom, $strComputer

? "info: verifying pspasswd.exe exists..."
If Exist($cmdstr)
$runCmd = "$cmdStr @@$inputFile $strUser $strPwd >pspasswd.log"
If Open(1, $inputFile, 5) = 0
? "info: querying computer names from active directory..."
$objDom = GetObject("WinNT://$ADSI_DN")
For each $objX in $objDom
If Lcase($objX.Class) = "computer"
$strComputer = Ucase($objX.Name)
If InStr($exclude, $strComputer) > 0
? "info: exluding = "+$strComputer
Else
? "info: computername = "+$strComputer
$=WriteLine(1, $strComputer+@CRLF)
EndIf
EndIf
Next
Close(1)
? "info: finished building input file"
? "info: runCmd = "+$runCmd
;Shell "%COMSPEC% /c "+$runCmd
EndIf
Else
? "error: pspasswd.exe not found"
EndIf
? "info: completed processing: "+@date+" "+@time


Not radically different really. However, you may notice some easier handling of checking for files, opening and writing to files, and executing shell processes. It also shows how you can leverage in-line variable expansion within strings to eliminate explicit concatenation; not always, but more often than not. An interesting (and important) aspect of this feature is to be careful of escaping special characters within strings to avoid confusion during processing. This is why you see doubled up "@" and "$" entries in some of the strings.

Wednesday, April 22, 2009

VBScript vs KiXtart - Round 20

Another comparison of VBScript and KiXtart to show how different scripting tools can be. In this case, here's a chunk of code from a login script that reads a user's "full name" from Active Directory, and sets a local User variable to that value. This is often necessary for legacy applications that look for environment values with specific names because they suffer from either (A) lack of customer budget to buy the newer product version which knows how to query Active Directory, or (B) lack of vendor intelligence in producing a hotfix or upgrade to address the shortcoming. In either case, here's how it might work with VBScript:


Set objSysInfo = CreateObject("ADSystemInfo")
strUser = objSysInfo.UserName
Set objUser = GetObject("LDAP://" & strUser)
strFullName = objUser.givenName & " " & objUser.sn
Set objShell = CreateObject( "WScript.Shell" )
objShell.Environment("User").Item("FullName") = strFullName


Not bad. It works. But if you do this same chore using KiXtart 4.6x, it would look like this:


$strFullName = @fullname
SET "FullName=$strFullName"


Hmm. This example is obviously not all-inclusive of comparison between the strengths of each language, but it is just one illustration of how different they can be.

Tuesday, April 21, 2009

Self-Reporting Reports that Report on their Reporting

I love stupid titles. Maybe because I'm stoopid too. America loves stupid, after all. I was thinking about how I typically weave start/stop times into my scripts. I usually do it to help with diagnosing things in the log files they leave behind. For example, you may notice that I often use a dedicated Sub() to echo status and use a constant variable to toggle it on/off.


Sub DebugPrint(s)
If Verbosity = True Then
Wscript.Echo Now & vbTab & s
End If
End Sub


Well, it's pretty basic obviously, but it can be useful. But what about overall start and stop times?


t1 = Now
'...
' do a bunch of time-consuming junk here...
'...
t2 = Now
Wscript.Echo Abs(DateDiff("s", t2, t1)) & " seconds elapsed"


Taking this a little further, you can easily extend this to define another constant variable to specify the expected elapsed runtime for your script to complete all processing. That way, if the script takes too long, or finishes too quick, you can take action to report that if desired.


' define expected runtime in seconds
Const runtime = 30000

'...
If Abs(DateDiff("s", t2, t1)) > runtime Then
' do something to notify or log the overrun
End If


You could also wrap the runtime variable with a multiplier to pad it with some preferred margin of error. Something like this...


If Abs(DateDiff("s", t2, t1)) > (runtime * 1.50) Then
' do something to notify or log the overrun
End If


The action you take could be sending an e-mail alert, writing and event log entry, writing a log file, running another script, or whatever you can dream up.

The advantages of this approach are that you don't have to employ a separate solution to monitor when scripts take too long. They can handle the notifications themselves. This also makes them more "event-driven" rather than batch-processed, since they will report as the event occurs, rather than waiting to be reported on by another service later. I'm sure that there are drawbacks to this, after all, nothing is a panacea. But this might prove useful for you in some situations. Enjoy!

Saturday, April 18, 2009

Example AD LDAP Queries for When You Really Get Bored

Stuck inside on a rainy day? I'm indoors on probably THE most beautiful day of the year, and it's a Saturday. I must be a complete idiot. Ok, one post and I'm outdoors the rest of the day, I promise.

Here is a chunk of code you can use, abuse, misuse (all warrantees voided, disclaimers disclaimed, liabilities denied, etc. etc.) dissect, bisect, and resect if you really want. I hope it doesn't need explanation.


Const strDN = "dc=contoso,dc=local"

q1 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='computer'"
q2 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='computer' AND operatingSystem <> 'Windows*Server*'"
q3 = "SELECT * FROM 'LDAP://[DN]' WHERE objectCategory='person'"
q4 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='organizationalUnit'"
q5 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='container'"
q6 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='group'"
q7 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='computer' AND operatingSystem = 'Windows*XP*'"
q8 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='computer' AND operatingSystem = 'Windows*Vista*'"
q9 = "SELECT * FROM 'LDAP://[DN]' WHERE objectClass='computer' AND operatingSystem = 'Windows*7*'"

selectQuery = q7

'----------------------------------------------------------------

strQuery = Replace(selectQuery, "[DN]", strDN)

Const ADS_SCOPE_SUBTREE = 2

On Error Resume Next
Set objConn = CreateObject("ADODB.Connection")
Set objCmd = CreateObject("ADODB.Command")
objConn.Provider = "ADsDSOObject"
objConn.Open "Active Directory Provider"
Set objCmd.ActiveConnection = objConn

objCmd.CommandText = strQuery

objCmd.Properties("Page Size") = 1000
objCmd.Properties("Timeout") = 30
objCmd.Properties("Searchscope") = ADS_SCOPE_SUBTREE
objCmd.Properties("Cache Results") = False
Set objRs = objCmd.Execute
If Err.Number <> 0 Then
Set objShell = Nothing
Wscript.Echo "error: " & Err.Number & " - " & Err.Description
Wscript.Quit
End If
objRs.MoveFirst

If objRs.BOF and objRs.EOF Then
Wscript.Echo "no records found"
Else
Do Until objRs.EOF
For i = 0 to objRs.Fields.Count-1
Wscript.Echo objRs.Fields(i).Value
Next
objRs.MoveNext
Loop
End If

objRs.Close
objConn.Close
Set objRs = Nothing
Set objCmd = Nothing
Set objConn = Nothing

Friday, April 17, 2009

Another Way to Reset Local Passwords on Remote Computers

I should have titled this one "More ways to do more with more and more" or something dumber, but I'm just getting home on a BEAUTIFUL, warm, sunny Friday afternoon. I sat on the HRBT (that's "Hampton Roads Bridge-Tunnel" for you foreigners, or as we call it "the bridge-tunnel to nowhere") for 45 minutes while they cleared another wreck from the entrance. As always, it's someone with license plates from Maryland, Delaware, New Jersey or Pennsylvania, going to fast and thinking they can handle the sheer mind stress of seeing their wide-open view of the Hampton Roads Bay shrink to a miniscule concrete encased box of a tube entrance. Not that the entrance is really that significant, it just looks that way to those not familiar with commuting through it twice a day, every day.

Sorry. See what happens when tourists f**k up a perfectly good Friday?

Ok, let's get on with it then. Here are two versions (that's right: 2) for resetting the local "Administrator" account password on all the computers within your Active Directory domain. The first one is the "easy", quick, simple, version, without bells-and-whistles or error handling. The second one is the beefier, bloatier, buffier (is that a word?) version which lets you restrict your wild and crazy behavior to just non-server clients, or if you prefer: all clients (servers included). It also provides a basic amount of error handling, but not too crazy.

So, here's the BASIC version:

Option Explicit

Const cmdStr = "pspasswd.exe \\COMPUTER Administrator P@ssW0rd$1$2$3"
Const ADSI_DN = "mydomain.com"

'----------------------------------------------------------------
' comment: do not change any code below this point!
'----------------------------------------------------------------

Wscript.Echo "info: starting processing: " & Now
Dim objShell, objX
Set objShell = CreateObject("Wscript.Shell")
Set objDom = GetObject("WinNT://" & ADSI_DN)
For each objX in objDom
If Lcase(objX.Class) = "computer" Then
Wscript.Echo "info: connecting to " & objX.Name
objShell.Run(Replace(cmdStr, "COMPUTER", objX.Name), 1, True)
End If
Next

Wscript.Echo "info: completed processing: " & Now

And, here's the DELUXE version:

Option Explicit

Const cmdStr = "pspasswd.exe \\COMPUTER Administrator P@ssW0rd$1$2$3"
Const RunMode = "TEST"
Const QueryScope = "NO_SERVERS"

'----------------------------------------------------------------
' comment: do not change any code below this point!
'----------------------------------------------------------------

Const NTDSDSA_OPT_IS_GC = 1
Const ADS_SCOPE_SUBTREE = 2
Const ADS_UF_ACCOUNTDISABLE = 2

Dim objRootDSE, LDAP_DN, ADSI_DN, objShell, t1, t2

Wscript.Echo "info: beginning processing: " & Now
t1 = Now

Set objRootDSE = GetObject("LDAP://rootDSE")
Set objShell = CreateObject("Wscript.Shell")
LDAP_DN = Domain_LDAP()
ADSI_DN = Domain_NetBIOS(LDAP_DN)

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Wscript.Echo "info: ldap_dn = " & LDAP_DN
Wscript.Echo "info: adsi_dn = " & ADSI_DN

Dim objConn, objCmd, objRs
Dim retval : retval = 0
Dim strQuery, strComputer, runCmd

If QueryScope = "NO_SERVERS" Then
Wscript.Echo "info: enumerating non-server computer objects..." & vbCRLF
strQuery = "SELECT Name FROM 'LDAP://" & LDAP_DN & _
"' WHERE objectClass='computer' AND operatingSystem <> 'Windows*Server*'"
Else
Wscript.Echo "info: enumerating all computer objects..." & vbCRLF
strQuery = "SELECT Name FROM 'LDAP://" & LDAP_DN & _
"' WHERE objectClass='computer'"
End If

On Error Resume Next
Set objConn = CreateObject("ADODB.Connection")
Set objCmd = CreateObject("ADODB.Command")
objConn.Provider = "ADsDSOObject"
objConn.Open "Active Directory Provider"
Set objCmd.ActiveConnection = objConn
objCmd.CommandText = strQuery
objCmd.Properties("Page Size") = 1000
objCmd.Properties("Timeout") = 30
objCmd.Properties("Searchscope") = ADS_SCOPE_SUBTREE
objCmd.Properties("Cache Results") = False
Set objRs = objCmd.Execute
If Err.Number <> 0 Then
Set objShell = Nothing
Wscript.Echo "error: " & Err.Number & " - " & Err.Description
Wscript.Quit
End If
objRs.MoveFirst

Do Until objRs.EOF
strComputer = objRs.Fields("Name").Value
runCmd = Replace(cmdStr, "COMPUTER", strComputer)
If RunMode = "TEST" Then
Wscript.Echo "exec: " & runCmd
Else
If IsOnline(strComputer) Then
Wscript.Echo "info: connecting to " & strComputer
objShell.Run(runCmd, 1, True)
End If
End If
objRs.MoveNext
Loop

Wscript.Echo "info: completed processing: " & Now
Wscript.Echo "info: total runtime was " & Abs(DateDiff("s", t1, t2)) & " seconds"

Set objShell = Nothing

'----------------------------------------------------------------
' function: query for domain LDAP-FQDN (e.g. "dc=contoso,dc=msft")
'----------------------------------------------------------------

Function Domain_LDAP()
Domain_LDAP = objRootDSE.Get("defaultNamingContext")
End Function

'----------------------------------------------------------------
' function: query for domain NetBIOS suffix (e.g. "contoso.msft")
'----------------------------------------------------------------

Function Domain_NetBIOS(ldapdn)
Domain_NetBIOS = Replace(Replace(ldapdn,"DC=",""),",",".")
End Function

'----------------------------------------------------------------
' function: verify computer is accessible over the network
'----------------------------------------------------------------

Function IsOnline(strComputer)
Dim objPing, objStatus, retval, strQuery
strQuery = "select * from Win32_PingStatus where address = '" & strComputer & "'"
If strComputer <> "" Then
Set objPing = GetObject("winmgmts:{impersonationLevel=impersonate}")._
ExecQuery(strQuery)
For Each objStatus in objPing
If Not(IsNull(objStatus.StatusCode)) And objStatus.StatusCode = 0 Then
IsOnline = True
End If
Next
End If
End Function


Well, enjoy your weekend! I hope these are helpful, useful or at least mildly entertaining to you. If you have questions, comments or good jokes to share, just drop a comment via the feedback. Thanks!

Wednesday, April 15, 2009

Computer-Operated Cars


Oh yes... I have indeed blabbered on endlessly about the desire to prevent humans from "attempting" to drive automotive vehicles. The reason is simple: humans can't drive.

But I thought about digressing a bit on this subject to see where it leads.





The current problems:


Humans die from car "accidents" more frequently than any other form of "accidents" combined. Automotive crashes claim more lives than most diseases as well. The number one cause of car/truck/motorcycle accidents? Human error.

That's right. Human error is the root cause of the vast majority of crashes involving cars, trucks, motorcycles, boats, trains, aircraft and even lawn mowing equipment. If it moves under its own power: a human can crash it. And in most cases, take a life or two with it.

Be honest. How many times have you been in traffic and seen someone in another vehicle doing something so completely stupid that it just makes you shake your head in disbelief? Putting on make-up. Grooming. Eating. Reading (I've seen maps, newspapers and even books opened up with eyes fixated on the pages). How about idiots that drive with their dogs in their lap, between their chest and the steering wheel? I haven't even gotten to the drunk driving part yet. It's amazing we don't die in larger numbers every day from dumbass habits like these.

The Ideal?

A car without a steering wheel, gas or brake pedals. Just doors, windows, seats and something to listen to (or watch) for entertainment along the journey. Get in, tell it where you want to go, and sit back and enjoy the ride. A taxi without another human to worry about.

The Advantages?

  • Vastly fewer accidents. Even the worst-case computer glitch is more reliable than the average human "driver". Even DOS version 4 couldn't crash as often as some of the buffoons I see on the road every day.
  • No more drunk driving concerns. Go ahead. Get drunk. Get in and let it drive you home.
  • More efficient time usage. Tired of dropping what you're doing to drive someone else somewhere, only to drop them off and return? Let the car take them. It can even drive itself back, in case you need it later. While you're at work, your family or friend could request the car (with your approval) to drive itself over to pick them up. It doesn't have to sit in a parking lot all day doing nothing.
  • Predictable traffic management: Computer networked cars can communicate among each other to identify bottlenecks (which would be rare) and re-route automatically. Much more reliably than humans listening to radios for traffic reports.
  • No more "rubber-necking" hold-ups. Even if a car malfunctions and goes stupid (or should I say "goes normal" in human terms?) the computer-driven car won't bother slowing down to gawk at the mess. No more back-ups from nosey idiots.
  • More lives saved: How many people would be alive today if they hadn't driven home drunk or fell asleep from working overtime hours?
  • Theft? What's that? If it's computer controlled, it can be built to authenticate the driver. Person breaks in and does what? Hot-wires the ignition? How will they drive it without a steering wheel, gas pedal or brakes? Don't think that today's hackable technology is something that will always be around. There are computer systems that cannot be hacked, you just don't hear about them.
The Downside?

Not to sound "dark" or morbid or anything, but putting this in terms that were commonly used by the likes of Chrysler, GM and Ford throughout the 1950's, 60's and 70's... maybe saving lives isn't economically the best idea. I'm all for moving forward with cars that drive themselves, but I have to play devil's advocate here and argue the counterpoints to see how feasible this really is.

The first air bag prototypes appeared in the 1960's but were deemed too expensive for the simple benefit of "saving lives". The same was true for Anti-Lock brakes, and RADAR collision avoidance systems (which are just now being promised).

So, if we eliminated all the bad things that we currently accept as "normal", what could some of the negative ramifications be?
  • Negative impact on accident recovery services: Ambulances, tow trucks, fire and rescue, body shops, and possibly insurance providers
  • Reduced Hospital admissions
  • Reduced need for ambulances and (oops? ambulance drivers?)
  • Traffic cops? Will they need to write any more tickets? Ever?!??
We've all seen the Sci-Fi versions of this concept. From decades back, described in books, up to today with movies like Minority Report, I Robot and so on. The technology exists today. Technology is not the problem. It's not the hold-up. Humans are the hold-up. Humans are always the hold-up. Humans are what hold us up in traffic every day. I say: give computers a chance. It might fail and crash a few, but at least I won't die knowing it was due to someone reading a paper, eating a burger, petting their lap dog, putting on lipstick, drunk off their ass, and tired from working 16 hours and heading home at 1:00 AM. Go ahead and snicker. But think about it seriously for a moment and ask "why not?"

List all Groups on All Computers in a Domain, Caffeine Free version

This code consists of two main sections and a third section which is run at the very end. The first section simply queries Active Directory to fetch computer accounts. It then builds and populates a dictionary object with the "name" and "operatingSystem" values. The second portion sorts the dictionary object by the "name" key and then iterates the sorted list to query each computer for the groups it has.


Option Explicit

Const ADS_SCOPE_SUBTREE = 2
Const dictKey = 1
Const dictItem = 2

Dim srvcount : srvcount = 0
Dim objRootDSE, strLDAP, computerName, computerOS
Dim d, i, strQuery, objConnection, objCommand, objRecordSet

Set objRootDSE = GetObject("LDAP://rootDSE")
strLDAP = objRootDSE.Get("defaultNamingContext")

Set d = CreateObject("Scripting.Dictionary")

strQuery = "Select Name, operatingSystem from 'LDAP://" & strLDAP & "' Where objectClass='computer'"

Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCOmmand.ActiveConnection = objConnection
objCommand.CommandText = strQuery
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Set objRecordSet = objCommand.Execute
objRecordSet.MoveFirst

Do Until objRecordSet.EOF
computerName = Ucase(objRecordSet("Name").value)
computerOS = objRecordSet("operatingSystem").value
srvcount = srvcount + 1
d.Add computerName, computerOS
objRecordSet.MoveNext
Loop

'----------------------------------------------------------------
' comment: request dictionary sort and display results
'----------------------------------------------------------------

SortDictionary d, dictKey

For Each i In d
computerName = i
computerOS = d(i)
Wscript.Echo computerName & vbTab & computerOS
If PingOnline(computerName) Then
ListGroups computerName
Else
Wscript.Echo vbTab & "(offline)"
End If
Next

'----------------------------------------------------------------
' function: sort dictionary object
'----------------------------------------------------------------

Function SortDictionary(objDict,intSort)
Dim strDict()
Dim objKey, strKey, strItem, X, Y, Z
Z = objDict.Count
If Z > 1 Then
ReDim strDict(Z,2)
X = 0
For Each objKey In objDict
strDict(X,dictKey) = CStr(objKey)
strDict(X,dictItem) = CStr(objDict(objKey))
X = X + 1
Next

For X = 0 to (Z - 2)
For Y = X to (Z - 1)
If StrComp(strDict(X,intSort),strDict(Y,intSort),vbTextCompare) > 0 Then
strKey = strDict(X,dictKey)
strItem = strDict(X,dictItem)
strDict(X,dictKey) = strDict(Y,dictKey)
strDict(X,dictItem) = strDict(Y,dictItem)
strDict(Y,dictKey) = strKey
strDict(Y,dictItem) = strItem
End If
Next
Next
objDict.RemoveAll
For X = 0 to (Z - 1)
objDict.Add strDict(X,dictKey), strDict(X,dictItem)
Next
End If
End Function

'----------------------------------------------------------------
' function: query groups on remote computer
'----------------------------------------------------------------

Sub ListGroups(strComputer)
On Error Resume Next
Dim objComputer, objX
Set objComputer = GetObject("WinNT://" & strComputer)
If err.Number <> 0 Then
Wscript.Echo vbTab & "*** unable to query qroups ***"
Exit Sub
Else
For each objX in objComputer
If Lcase(objX.Class) = "group" Then
Wscript.Echo vbTab & objX.Name
End If
Next
End If
Set objComputer = Nothing
End Sub

'----------------------------------------------------------------
' function: attempt ping to verify availability of computer
'----------------------------------------------------------------

Function PingOnline(strComputer)
Dim objPing, objStatus
Set objPing = GetObject("winmgmts:")._
ExecQuery("select * from Win32_PingStatus where address = '" & strComputer & "'")
For Each objStatus In objPing
If IsNull(objStatus.StatusCode) Or (objStatus.StatusCode <> 0) Then
Exit Function
End If
PingOnline = True
Exit Function
Next
End Function

Tuesday, April 14, 2009

Domain LDAP RootDSE with Fries and a Shake

How many times have you seen a script that pulls something from Active Directory, but it has the LDAP domain suffix hard-coded? I'm talking about "dc=contoso,dc=com" and so on. It's just dumb. Don't do that! Stop! Put down the keyboard and back away with your beer in the air. There's a better way, and it's actually EASIER. Imagine that. I actually used it in a previous script post for inventorying (say that ten times, fast) so it's not really new here. You'll find this in many other places as well, posted long before me, probably dating back to the prehistoric Ldapian era when X500 dinosaurs ruled the Earth. I told you, I'm feeling real stupid right now.

'----------------------------------------------------------------
' comment: obtain object handle to RootDSE of domain
'----------------------------------------------------------------

Set objRootDSE = GetObject("LDAP://rootDSE")

'----------------------------------------------------------------
' comment: query for LDAP domain DN (eg. DC=domain,DC=com)
'----------------------------------------------------------------

LDAP_DN = Domain_LDAP()
ADSI_DN = Domain_NetBIOS(LDAP_DN)

'----------------------------------------------------------------

Function Domain_LDAP()
Domain_LDAP = objRootDSE.Get("defaultNamingContext")
End Function

'----------------------------------------------------------------

Function Domain_NetBIOS(ldapdn)
Domain_NetBIOS = Replace(Replace(ldapdn,"DC=",""),",",".")
End Function


So, anywhere in your vast array of VBscript files where you're querying Active Directory for information, and you have the LDAP string hard-coded, just replace it with the junk above and your code is instantly portable! Instantly! Just add hot water and stir. Makes its own sauce.

Uh, Because I had Nothing Better to Do?

I posted this over on MyItForum earlier, but I felt like maybe, just maybe, it was OK to post it here too, as well, also, in addition to, and so on. I've been sitting in afternoon rush hour traffic for an hour so I feel stupid right now. Stupid as stupid commuter drivers who drive stupid. I'll say it again: When do we get those computer-controlled cars without stupid wheels (oops, "STEERING" wheels)? Humans should not be allowed to drive - period. Because they can't.

Ok, here's the script: It iterates shortcuts and spits out their inerds (guts, insides, properties, ingredients, whatever). I didn't invent the concept, nor the methodology, but I did glue the code together since all the examples I found crawled either file/folder shortcuts or URL shortcuts, but not both. Mine does both, so there. Nah nah nah nah...


Set objShell = CreateObject("WScript.Shell")
Set objFSO = CreateObject("Scripting.FileSystemObject")
strAllUsers = objShell.ExpandEnvironmentStrings("%ALLUSERSPROFILE%")
strThisUser = objShell.ExpandEnvironmentStrings("%USERNAME%")

strUsers = objFSO.GetParentFolderName(strAllUsers)
DesktopPath = strUsers & "\" & strThisUser & "\Desktop"
Set objFolder = objFSO.GetFolder(DesktopPath)

For each objFile in objFolder.Files
strPath = objFile.Path

Select Case Right(Lcase(objFile),4)
Case ".lnk":
Set objShortcut = objShell.CreateShortcut(strPath)
Wscript.Echo "----------------------------------"
Wscript.Echo "Type : file-system shortcut"
WScript.Echo "Full Name : " & objShortcut.FullName
WScript.Echo "Arguments : " & objShortcut.Arguments
WScript.Echo "Working Directory : " & objShortcut.WorkingDirectory
WScript.Echo "Target Path : " & objShortcut.TargetPath
WScript.Echo "Icon Location : " & objShortcut.IconLocation
WScript.Echo "Hotkey : " & objShortcut.Hotkey
WScript.Echo "Window Style : " & objShortcut.WindowStyle
WScript.Echo "Description : " & objShortcut.Description
Case ".url":
Set objShortcut = objShell.CreateShortcut(strPath)
Wscript.Echo "----------------------------------"
Wscript.Echo "Type : internet shortcut"
WScript.Echo "Full Name : " & objShortcut.FullName
WScript.Echo "Target Path : " & objShortcut.TargetPath
End Select
Next
Set objFolder = Nothing
Set objFSO = Nothing
Set objShell = Nothing

Saturday, April 11, 2009

Login Scripts vs Start-Up Scripts

This isn't aimed at anyone in particular, but to a lot of folks in general that seem to be confused about the intent and differences of using login scripts versus start-up scripts. If you think I'm aiming this at *you* in particular, I apologize, not my intention. In any case, I find myself in the middle of explaining the differences quite often, as well as helping friends resolve problems with both. Some very common issues seem to occur with these which are usually simple to avoid or solve. In the simplest terms...

Login Scripts are executed each time a user logs onto a computer.

Start-up Scripts are executed each time a computer is powered on.

Login Scripts:
  • Run under the context of the user who is logged on
  • They have the same permissions and "reach" as the user
  • Since the user has network access, so does the script
Start-Up Scripts:
  • Run under the Local "SYSTEM" user context prior to the login screen
  • They run in SYSTEM context
  • Since the SYSTEM account has no network access, neither does the script (by default, but please read on...)
If a user does not have elevated permissions (i.e. is not a member of Administrators or Power Users group) the Login script can only do so much. It won't be able to stop privileged processes or threads, install or uninstall software, modify privileged services, modify protected folders and files or modify system registry keys (e.g. HKEY_LOCAL_MACHINE, etc.). If the user has administrative rights, the script can do pretty much anything. (note: God, I really hope you don't routinely give users administrative rights. There's rarely any reason for that nowadays. There are many options available.)

With start-up scripts, the processing occurs after the Windows kernel is loaded and initialized, but prior to the CTRL-ALT-DEL prompt. It runs under the local "SYSTEM" account context, so it has local administrative rights. However, by default, it does not have any rights outside of the computer. Whereas a user account typically is domain-based, it has rights to at least some remote resources (shares, folders, files) over the network. The SYSTEM account does not. There is another local account which is intended for such uses, named "Network" or "Network Service" (depending upon which Windows version you're using).

From the perspective of an Active Directory or NetBIOS (workgroup) environment, when a task runs as SYSTEM and tries to reach out to remote resources, it is seen as COMPUTER$ (where COMPUTER is the NetBIOS name of the computer). This account exists in Active Directory for the purposes of establishing and maintaining trusts and delegation rights to enable the computer to participate within the domain. Computer accounts are only added to the "Domain Computers" group, which in turn is NOT added to any other groups - by default. Therefore, it has no inherent (explicit or implicit) rights to anything over the network. This is the single-most often troublesome issue for people wanting to use start-up scripts. They forget to grant permissions to remote shares/folders for either the explicit COMPUTER$ account, or the "Domain Computers" group. Once that's done, things usually work well.

Anyone who has rolled out SMS 2003 or SCCM 2007 knows this all too well. The AD schema extension is only part of the work. Then you have grant permissions to the System Management container so that the SMS/SCCM server can "publish" things into it. This is because the service itself is running under the local SYSTEM context and reaches out as COMPUTER$ to touch things. I'm a HUGE proponent for using this same approach to all automation tasks. No more managing passwords or worrying who knows the password. Just restrict admin rights to the computer itself and it takes care of the rest. Simple. Clean. Reliable.

Back to the original rant.

If you need to run a script to perform tasks that are beyond the rights of the users who logon, you may want to consider a start-up script. If you want to run things within the bounds of what the users can do, a login script is fine.

Now, all that blabbering aside: If the computers you are dealing with are all desktops and servers, normally connected and powered on at all times, you can also employ a centralized script approach, where you run a script "on demand" from your workstation, to reach out and touch all the other computers. However, if you have an environment where some computers are often "off the network" (read: laptops), then a start-up or login script is probably a better approach. Because it essentially waits for the computer (or user) to invoke it, rather than taking the shotgun approach and risking a failure when the remote client is unavailable.

Phew. I'm done. I hope this makes sense and helps someone. I'm sure many of you will roll your eyes and think "I already know this" which is fine. I was aiming this at anyone who still isn't clear about these issues. I need to get something to eat now. Cheers!

Friday, April 10, 2009

Force WUAUCLT /DETECTNOW on a remote computer

You could use the Batch_Remote script I posted yesterday to do this. But for times when you just need to manually kick off a request to a specific computer, using Sysinternals' PsExec...

psexec \\computername -s wuauclt.exe /detectnow

(yes, replace "computername" with an actual computer name. And don't forget to run it using an account that has admin rights on the remote computer).

Enable Remote Desktop on a Remote Desktop...uhhhh... Remotely

Here's a little BAT script to turn on Remote Desktop availability on a remote computer. For those situations when you forgot to enable it manually, forgot to use Group Policy to enable it, and already drove/flew/rode/walked back to another location (far away of course) and just realized you forgot to take care of that. Not me. I've never done anything like that. :)

@echo off
if [%1]==[] goto ERRFAIL else goto POKE

:POKE
echo Checking to see if %1 is online...
if exist \\%1\ADMIN$\SYSTEM32\mstsc.exe goto CONFIG else goto ERRPOKE

:CONFIG
reg add "\\%1\HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0
shutdown /m \\%1 /r /f /t 0 /c "remote shutdown requested"
goto END

:ERRPOKE
echo Error: Computer is not accessible
echo .
goto END

:ERRFAIL
echo Error: No computer name was provided
echo .
echo Usage: rdpset.bat COMPUTERNAME
echo .
goto END

:END

A Little More Glue and Tape

Here’s another, rather simple script I wrote. This one simply scours a list of computers (text file, one computer name per line) and uses the FileSystemObject to query for a specific file on each computer. Of course, you need to ensure you run this with sufficient permissions to poke at all the remote computers C$ share (administrator rights), and that you don’t overlook client firewall settings or other such roadblocks to network communications.

Option Explicit

Const rmtPath = “\\COMPUTER\c$\Windows\System32\filename.exe”
Const inputFile = "computerlist.txt"
Const ForReading = 1
Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Dim linecount, objFSO, objFile, ln, objTextFile, strLine, strPath

linecount = 0

Set objFSO = CreateObject("Scripting.FileSystemObject")
On Error Resume Next
Set objTextFile = objFSO.OpenTextFile(inputFile, ForReading)
If Err.Number <> 0 Then
Wscript.Echo "error: " & Err.Number & " / " & Err.Description
Set objFSO = Nothing
Wscript.Quit
End If

Do Until objTextFile.AtEndOfStream
strLine = Trim(objTextFile.Readline)
If Left(strLine,1) <> ";" And strLine <> "" Then
Wscript.Echo "querying " & strLine
strPath = Replace(rmtPath, "COMPUTER", strLine)
Wscript.Echo vbTab & "searching for file: " & strPath
If objFSO.FileExists(strPath) = True Then
Wscript.Echo vbTab & "file was found!"
Else
Wscript.Echo vbTab & "file was not found"
End If
End If
Loop

objTextFile.Close
Set objTextFile = Nothing
Set objFSO = Nothing

Wednesday, April 8, 2009

Scripting with Glue and Tape

Well, I suppose that I’m back.
To me, writing script code is like assembling Lego blocks. For anyone that spent any time with Lego's, you will understand what I mean. I'm not talking about the brain-dead pre-arranged "kits" for building the helicopter, fire station or school house. I'm talking about the huge Lego construction kits, which were simplistic trays of various groupings of block types and colors. The intent was to let YOUR imagination run wild, rather than spoon-feed you with "here it is, build this thing right here."

In case you're wondering: No. That is not a picture of me. It's just a picture I found from a Google image search on the phrase "Lego blocks". Cute, huh? (Note: I would suggest that the unfinished arm signifies a metaphor for budget shortfalls with respect to software development projects. Clever? I didn't think so).

Scripting is scripting, in some respects. Be it PowerShell, VBScript, JavaScript, KiXtart, BAT, Korn, Bourne, Bash, or whatever. Even AutoLISP qualifies in most respects. Each one has a dictionary of "words" and phrases, which you can assemble to build whatever you need (or want). There are constraints obviously, but there are constraints with all things, even Lego blocks. The beauty, and the creativity, come with bending and stretching those constraints to their limit while pursuing some ideal or goal. Every script I've ever seen that was worth a crap was the result of trying to solve a real problem or task, rather than aimless "I wonder what I can do with this..." meandering. The same is true for software programming in general. I'm not saying that letting the mind wander is bad, it isn't. I'm only saying that a framework has to exist which is derived or driven from a need to solve a challenge.
In my case, I needed to QUICKLY solve a problem: Initiate an unknown collection of tasks on an unknown collection of remote Windows computers, from a single location.
There are many, MANY, examples of how this can be illustrated. Changing local Administrator passwords, modifying registry keys and values, stopping and starting services, installing or removing software applications, shutting down or restarting, modifying local file and folder permissions, and the list goes on and on.
Here is just one way, out of possibly thousands of ways.
Ingredients: Microsoft/Sysinternals' PSEXEC.exe, a BAT file, a VBS file, a cup of caffeinated liquid, some background music, some free time, happy thoughts, clean underwear.

PSEXEC.exe. If you haven't heard of PsExec.exe, oh boy. Stop right now and defer this reading until you've had time to ingest the general scope of "PSTools". You can learn all about it at the Microsoft Sysinternals web site (click here) and download any/all of the tools here --> http://live.sysinternals.com/

The BAT file. This is a generic wrapper or "package" which will essentially "pushed" to each remote computer on your list (or on your network, your choice, keep reading). You can use and reuse this as often or however you wish. Each time you run the root script (VBS in this case) it will re-deploy the BAT file and execute it on the remote computer. You can fork this (careful, let's keep this clean, ok?) into multiple root scripts that can each deploy a different BAT file and, well, you can multiply and diversify and magnify and exemplify... til your brain cells, um, fry. Ok, just kidding.

The LIST file. This is a ordinary ASCII text file with the NetBIOS name of each computer listed on its own row. To make it easier to manage, the VBS script will ignore lines that start with a semi-colon ";". This makes it easy for you to reuse the LIST file and simply prefix a semi-colon to "comment" it out and have it ignored by the Root script.

The VBS file. This is the "root script" I mentioned above. It is what you run on YOUR computer, which invokes PSEXEC.exe to deploy and execute the BAT file on each computer named in your ComputerList.txt file.
Place all of these into the same folder on your local hard drive. In this scenario, I chose "c:\scripts" because I'm really not that inventive and couldn't think of a more clever name.
The meat and potatoes: Here's the VBS (VBScript) file contents.
'****************************************************************
' Filename..: batch_remote.vbs
' Author....: David M. Stein
' Date......: 04/02/09
' Purpose...: execute batch script on list of remote computers
' using psexec.exe (sysinternals)
'****************************************************************
Option Explicit
Const psexec = "c:\scripts\psexec.exe"
Const package = "c:\scripts\runme.bat"
Const inputFile = "c:\scripts\computerlist.txt"
' the following line is expanded later in script
' replace /c with /k if you want to debug each computer step while running
Const cmd = "cmd.exe /c PSEXEC \\COMPUTER -i -c -f PKG"
Const TestMode = False
Const Verbose = True
' *** do NOT modify any code below this point in this file! ***
C
onst ForReading = 1
Const ForWriting = 2
Dim objFSO, objFile, ln, objTextFile, strLine
Dim objShell, cmdstr, runcmd, runcount
Sub DebugPrint(p, s)
If Verbose Then
WScript.Echo Now & vbTab & p & vbTab & s
End If
End Sub
runcount = 0
DebugPrint "info", "initializing script process"
If TestMode = True Then
DebugPrint "info", "runtime mode is TEST MODE"
Else
D
ebugPrint "info", "runtime mode is PRODUCTION"
End If
DebugPrint "info", "opening input file"
Set objFSO = CreateObject("Scripting.FileSystemObject")
On Error Resume Next
Set objTextFile = objFSO.OpenTextFile(inputFile, ForReading)
If Err.Number <> 0 Then
DebugPrint "error", "unable to open input file"
Set objFSO = Nothing
Wscript.Quit
End If
DebugPrint "info", "processing data from input file"
Set objShell = CreateObject("Wscript.Shell")
cmdstr = Replace(Replace(cmd, "PSEXEC", psexec), "PKG", package)
DebugPrint "info", "runtime script = " & cmdstr
Do Until objTextFile.AtEndOfStream
s
trLine = Trim(objTextFile.Readline)
If strLine <> "" And Left(strLine, 1) <> ";" Then
DebugPrint "info", "connecting to " & strLine
runcmd = Replace(cmdstr, "COMPUTER", strLine)
If TestMode <> True Then
objShell.Run runcmd, 1, True
End If
runcount = runcount + 1
End If
Loop
objTextFile.Close
Set objFSO = Nothing
DebugPrint "info", runcount & " lines were processed"
DebugPrint "info", "script process completed"


Here's the LIST text file contents...

; COMPUTERLIST.TXT
; Replace/Add/Delete to use actual computer names
Computer1
Computer2
Computer3

Here's the BAT file contents. Save as "RUNME.BAT"...

@ECHO OFF
REM -----------------------------------------------------
REM RUNME.BAT
REM
REM This script is called by BATCH_REMOTE.VBS and is
REM copied to each remote computer named in a separate
REM list file and executed on the remote computer using
REM Sysinternals' PSEXEC utility
REM -----------------------------------------------------
REM Below are some example tasks to illustrate how this
REM might be used
REM -----------------------------------------------------
REM EXAMPLE: Create a registry key and some values
REM
REM reg add HKLM\Software\TestKey /v TestValue1 /t REG_SZ /d "This is a String" /f
REM reg add HKLM\Software\TestKey /v TestValue2 /t REG_EXPAND_SZ /d ^%systemroot%^ /f
REM reg add HKLM\Software\TestKey /v TestValue3 /t REG_DWORD /d 1 /f
REM
REM -----------------------------------------------------
REM EXAMPLE: Import Registry Data File into local registry
REM
REM reg import \\server\path\my_reg_data_file.reg
REM
REM -----------------------------------------------------
REM EXAMPLE: Delete registry key and values
REM
REM reg delete HKLM\Software\TestKey /va /f
REM
REM -----------------------------------------------------
REM EXAMPLE: Stop and Start a Local Service
REM
REM sc stop "My Service Name"
REM sc start "My Service Name"
REM
REM -----------------------------------------------------
REM EXAMPLE: Force Automatic Updates detection cycle
REM
REM wuauclt /detectnow
REM
REM -----------------------------------------------------
REM EXAMPLE: Change / Set local Administrator password
REM
REM net user Administrator P@ssw0rD$123
REM
REM -----------------------------------------------------
REM EXAMPLE: Add Domain group into Local Group
REM
REM net localgroup Administrators /ADD "Desktop Admins" /DOMAIN
REM
REM -----------------------------------------------------
REM EXAMPLE: Download and register a DLL file
REM
REM copy \\servername\path\myfile.dll c:\windows\system32\
REM cd /d c:\windows\system32
REM regsvr32 /s myfile.dll
REM
REM -----------------------------------------------------
REM EXAMPLE: Uninstall a software application
REM
REM msiexec /x {SOME_REALLY_LONG_GUID} /norestart /qb! /Lvaio! c:\windows\temp\mylog.log
REM
REM -----------------------------------------------------
REM EXAMPLE: Force Group Policy Refresh
REM
REM gpupdate /force /wait:1000
REM
REM -----------------------------------------------------
REM EXAMPLE: Restart computer
REM
REM shutdown -r -f -t 30 -c "remote restart requested"
REM
REM -----------------------------------------------------
REM EXAMPLE: Shutdown computer
REM
REM shutdown -s -f -t 0 -c "remote shutdown requested"
REM
REM -----------------------------------------------------
REM EXAMPLE: Flush local DNS resolver cache
REM
REM ipconfig /flushdns

To help illustrate some tasks you might want to invoke on remote computers, I included a few in the above example. Remove the "REM" prefix to enable a given line to be executed, but be SURE to edit it to suit your needs before using it. Also, ALWAYS ALWAYS ALWAYS test this on computers in an isolated environment to avoid making mistakes in a production environment.
How to use it?
  1. Like I said, copy/save the files into your chosen folder (e.g. "c:\scripts").
  2. Open a CMD console (aka "DOS window" for you Windows 95 nut jobs).
  3. Change to the directory where your files are stored (e.g. "c:\scripts").
  4. Use CSCRIPT to run the root script as follows:
    C:\Scripts\> cscript batch_remote.vbs
  5. That's pretty much "it".
You should see it fire off and a separate CMD window will briefly open and close for each computer entered into your list file. If you think you have a bogus command in the BAT file, change the "cmd /c" entry to "cmd /k" in the root script (VBS) file to keep each CMD window open after it is finished running on each computer. This will act like a "pause" and let you see what's going on with each computer. As you close the CMD window, the next will pop-up and continue until the list is finished.
As always, feel free to use, adapt, modify, whatever. This is made available for informational purposes only and is not intended to be used “as-is” in a production or business-critical environment without user testing. Author assumes no responsibility, liability, eligibility, mobility, culpability, flexibility, gullibility or hill-billity for any use, misuse, consequential or inconsequential damages arising from any direct or indirect or misdirected use in any situation, scenario, environment, paradigm, universe, metaverse or omniverse. Not even during the third verse. In other words: USE THIS STUFF AT YOUR OWN RISK.
I hope this is useful/helpful to someone out there. If so, great. If not, well, never mind. Drop me a line if you have any questions or comments on this. I don't mind helping but I may not be able to respond immediately due to having a crazy schedule.