Thursday, February 21, 2019

Discovering Active Directory groups with PowerShell

I wrote a PowerShell program that reads in a comma-separated list of user names, looks them up in the Windows Active Directory, and then outputs a comma-separated file listing each user's groups, with one group per line.

In most programs I write, you'll probably find an InitializeVariables function. While some variables sometimes need to be set at the top of the program, this function will usually hold the majority of my variables' starting values.


function InitializeVariables
    {
    $script:input_file = ""
    $script:output_file = ""
    $script:not_found_file = ""

    # Set the input + output files, depending on the presence of invocation parameters.
    if (-not $script:in)
        {
        $script:input_file = $script:default_input_file
        }
    else
        {
        $script:input_file = $script:in
        }

    if (-not $script:out)
        {
        $script:output_file = $script:default_output_file
        }
    else
        {
        $script:output_file = $script:out
        }

    if (-not $script:lost)
        {
        $script:not_found_file = $script:default_not_found_file
        }
    else
        {
        $script:not_found_file = $script:lost
        }
    }  #InitializeVariables

In the interest of flexibility, I give the user the option of overwriting the output of previous runs or retaining them with a timestamp.


function CleanOutputFiles
    {
    # Remove existing output files, first preserving them if instructed.

    $overwrite_time = Get-Date -Format "yyyyMMdd-HHmmss"

    if ($script:preserve_previous_output -and (Test-Path -LiteralPath $script:output_file))
        {
        $old_output_file = $script:output_file.Replace(".csv", "-$overwrite_time.csv")
        Rename-Item $script:output_file $old_output_file
        }
    elseif (Test-Path -LiteralPath $script:output_file)
        {
        Remove-Item $script:output_file
        }

    if ($script:preserve_previous_output -and (Test-Path -LiteralPath $script:not_found_file))
        {
        $old_not_found_file = $script:not_found_file.Replace(".txt", "-$overwrite_time.txt")
        Rename-Item $script:not_found_file $old_not_found_file
        }
    elseif (Test-Path -LiteralPath $script:not_found_file)
        {
        Remove-Item $script:not_found_file
        }
    }  #CleanOutputFiles

A fairly standard reading of the input file.


function ReadUserList
    {
    $script:user_list = Get-Content $script:input_file
    if (-not $script:suppress_screen_output)
        {
        Write-Host ""
        Write-Host "user_list is" $script:user_list.count "lines long."
        }
    }  #ReadUserList

This is the meat of the program. The users are stored internally in a multidimensional arraylist--an arraylist whose members are arraylists--as a method of keeping users together with their groups, even if at some point in the future I add sorting functionality.


function CreateUserGroupList
    {
    # Create the main/outer arraylist that will hold user arraylists.
    $script:user_group_list = New-Object System.Collections.ArrayList

    forEach ($user in $script:user_list)
        {
        # Create an arraylist for each user.
        $single_user = New-Object System.Collections.ArrayList

        # The first element in a user's arraylist is the user's name.
        [void]$single_user.Add("$user")

        # Add the user's arraylist as an element of the main arraylist.
        [void]$script:user_group_list.Add($single_user)
        }

    if ($script:strip_header_from_input)
        {
        [void]$script:user_group_list.Remove($script:user_group_list[0])
        }

    if (-not $script:suppress_screen_output)
        {
        Write-Host "user_group_list is" $script:user_group_list.Count "items long" -NoNewline

        if ($script:strip_header_from_input)
            {
            Write-Host " (the header line has been stripped)."
            Write-Host ""
            }
        else
            {
            Write-Host "."
            Write-Host ""
            }
        }

    for($loopCounter = 0; $loopCounter -lt $script:user_group_list.Count; $loopCounter++)
        {
        try
            {
            $AD_entry = (New-Object System.DirectoryServices.DirectorySearcher("(&(objectCategory=User)(samAccountName=$($script:user_group_list[$loopCounter][0])))")).FindOne().GetDirectoryEntry().memberOf

            forEach ($line in $AD_entry)
                {
                # Extract the Common Name from each line, which is structured as:
                #   CN=[Common Name],OU=[Organizational Unit],OU=[Organizational Unit],DC=[Domain Component],[Domain Component],[Domain Component]
                [void]$script:user_group_list[$loopCounter].Add($line.Substring(3, $line.IndexOf(",") - 3))
                }

            if (-not $script:suppress_screen_output)
                {
                $user_number = $loopCounter + 1
                $user_name = $script:user_group_list[$loopCounter][0]
                $group_count = $script:user_group_list[$loopCounter].Count - 1

                $output_line = "${user_number}: $user_name, $group_count groups."
                Write-Host $output_line
                Write-Host ""
                }
            }  #try

        catch [System.Management.Automation.RuntimeException]
            {
            # The user was not found in the Active Directory search.

            if (-not $script:suppress_screen_output)
                {
                $user_number = $loopCounter + 1
                $user_name = $script:user_group_list[$loopCounter][0]

                $output_line = "${user_number}: $user_name was not found."
                Write-Host $output_line
                Write-Host ""
                }

            Write-Output $script:user_group_list[$loopCounter][0] >> $script:not_found_file
            }  #catch
        }  #loopCounter
    }  #CreateUserGroupList

Constructing the output file. In this case, the specs required that each line start with a user name, followed by one user group name.


function WriteOutputFile
    {
    if (-not $script:suppress_screen_output)
        {
        Write-Host ""
        Write-Host "Writing output file..."
        Write-Host ""
        }

    forEach($user_group in $script:user_group_list)
        {
        for($loopCounter = 1; $loopCounter -lt $user_group.Count; $loopCounter++)
            {
            # Each line in the output file will be formatted as:
            #   [user name], [one user group]
            $output_line = $user_group[0] + "," + $user_group[$loopCounter]
            Write-Output $output_line >> $script:output_file
            }
        }
    }  #WriteOutputFile

Here is the structure of the remainder of the program, beginning with (optional) parameters and concluding with the trail of function calls that do the actual work of the program


param (
    [string]$in,
    [string]$out,
    [string]$lost
)

#********************************************************************************
#
# This program can be invoked with or without file name parameters:
#
# powershell -File AD_user_groups.ps1
# powershell -File AD_user_groups.ps1 myinput.csv myoutput.csv not_found.txt
# powershell -File AD_user_groups.ps1 -in myinput.csv -out myoutput.csv -lost not_found.txt
# powershell -File AD_user_groups.ps1 -in myinput.csv
# powershell -File AD_user_groups.ps1 -out myoutput.csv
# powershell -File AD_user_groups.ps1 -lost not_found.txt
#
# A default file will be used if a file name is not specified.
#
#********************************************************************************

# Set to $true to rename previous output files before overwriting them.
$script:preserve_previous_output = $true

# Set to $true if the input file contains a header row.
$script:strip_header_from_input = $true

# Set to $false to see program progress on the screen.
$script:suppress_screen_output = $true

# These are the file names that will be used if file names are not provided as program parameters.
$script:default_input_file = "$PSScriptRoot\users.csv"
$script:default_output_file = "$PSScriptRoot\user_groups.csv"
$script:default_not_found_file = "$PSScriptRoot\users_not_found.txt"

#********************************************************************************


# Function definitions appear here.


#********************************************************************************


InitializeVariables
CleanOutputFiles
ReadUserList
CreateUserGroupList
WriteOutputFile

Wednesday, January 9, 2019

Ctrl+X, Ctrl+V

This project is noteworthy to me because of how little intention was involved. I have long been frustrated by a lack of time, energy, and drive to create new art. I often think I've built it up so much that I needed whatever I make next to be PHENOMENAL and noteworthy and laden with meaning. This has created a sort of creativity paralysis, where I was unsure where to start. Forget the fear of a blank page or blank canvas, I couldn't even break out a sketchbook.

However, this past weekend brought a bit of unfocused inspiration. I couldn't say why, but the idea of making a collage crossed my mind. It has been forever since I did anything like this, but it seemed a very low pressure piece to do.

So Saturday I went to Michaels (no relation) arts and crafts store to buy our old friend Mod Podge. I picked up the classic jar along with one that provided an antique (read: yellowing/browning) effect. It was very relaxing to snip out pictures and words from a newspaper and arrange them not-quite-haphazardly on a large piece of paper.

Sunday I decided I wanted even more material, so I went to a convenience store to pick up a few more newspapers. I didn't have any specific subject matter in mind, so as long as they had a variety of types of photos, graphics, and headlines, I was satisfied.


Honestly, I still don't know where I'm going with this, but it sure is fun--and relaxing! It's interesting to give a certain amount of thought to the position and orientation of snippets and then randomly slap something on top of it later, obscuring it anyway.

I'm toying with the idea of making this the groundwork of some drawing or painting, using the collage as more of a visually textured paper upon which I draw something else. That may take some trial and error, because I didn't give any thought to the option of drawing on top of it, so I'm not certain what I can use that won't be hampered by the glossy coatings of Mod Podge.

Saturday, December 29, 2018

Sleeping Single in a Single Bed

I am aging. I'm not yet falling apart, but the flaws in my construction are becoming more and more pronounced. Most recently, I am in an ongoing battle with sleep. I fall asleep after reading only a few pages of my book, but then I wake up a few hours later. I stay awake for at least two hours (which often extends to three or four.) This, of course, means that once I finally manage to fall back asleep, I am in no way ready to wake up when I'm "supposed to."

I'm not (completely) ignorant of factors contributing to this chronic restlessness. Diet, caffeine intake, exercise: these are all areas in which I need to make drastic improvements. But if I'm being meticulous in my examination of potential factors, I must include environment.

Am I at that point where I consider having my own bedroom? Looked at logically, it simply doesn't make sense to assume that two people will continue to have the exact same sleep cycles and routines as one another, or even retain complementary habits, for decades.

Every time I wake up and leave the room, I feel like I'm waking up my wife. And I don't want to wake her up again by coming back to bed hours later, so I end up sleeping on the couch or spare bed anyway.

Add in blanket preference, room temperature, music, lights out time, and--let's face it: snoring, it makes complete sense that Grammie and Poppa had separate bedrooms. Maybe if I bit the bullet and did that, I wouldn't have insomnia to the degree that I've experienced for quite some time.

One of the more dangerous aspects of this sleeplessness is that the products in some of these late-night commercials are starting to sound pretty good...

Wednesday, March 25, 2015

"Magic is a drug. Careful how you use it."

You can't judge a book by its cover--granted. However, a really great cover did entice me to pick up Dirty Magic from the shelf of my local bookstore. Once I read the back of the book and flipped through a few random pages to get a sense of the writing style, I knew I'd be taking the book home with me. After reading a few chapters, I knew I'd be picking up the rest of the series (however long it went) and also adding the author's name to my list of "always keep on the radar; buy anything written by".



Jaye Wells has created a world (continued in Cursed Moon and Deadly Spells) with a tremendous amount of depth and a wealth of enticing characters. Magic exists, but unfortunately can be as addictive and detrimental as drugs. The lead character is Kate Prospero, a magic adept and police officer. As the trilogy is named "Prospero's War", you know you'll be delving into her life for at least the duration of the series, and you won't be disappointed. However, even the supporting characters are so well fleshed out that it's difficult to think of them as "supporting". I wanted to learn so much more about Kate's colleagues, and I think there's at least a novel's worth of exploration for each and every character Wells devised. And that's only if the author gets bored of the world of Babylon, Ohio. She could do a series featuring any one of the members of the Magical Enforcement Agency task force and I'd be all in, each and every time!



Both the author and the main character are women. The only reason this is noteworthy is because it appears that women are still given short shrift in the world of fantasy/science fiction literature, even after so many strong female characters and so many incredibly talented female authors.It is my hope that the works created by brilliant authors such as Jaye Wells help to dissolve this issue once and for all. Personally, I didn't consider NOT purchasing Dirty Magic because of the gender of the author nor of the hero. It was an intriguing premise set in a fascinating world--that's why I bought it. I have no reservations about recommending these novels to anyone interested in supernatural fiction, urban fantasy, or crime stories, simply because the writing is "too feminine"--whatever that might mean. It is flat out a great series written by an incredible author.


I don't pre-order many books these days, simply because the "to be read" stack on my bookshelf has grown so large that it is now spawning little offspring stacks. But in this case, I bought Cursed Moon and pre-ordered Deadly Spells at the same time, because I knew I'd be anxious to read the third installment ASAP after finishing the second. Jaye published the novella Fire Water between books two and three as an e-book. I purchased that one as well; it was a nice diversion while waiting for the third book's release date (but it made the wait for a longer adventure that much more difficult!)


I'm now making my way through some of the author's other series, and what I've read so far is as engrossing as this one. However, "Prospero's War" will always be the series that introduced Jaye Wells to my bookshelves, and so I'll always feel a special affinity for the gritty world of Babylon, Ohio.

Monday, April 21, 2014

The Discard Pile

Perhaps I'm becoming something of a wastrel in my old age, but I think I'm pretty much done saving things just in case they might one day come in handy.

If I use three inches of black electrical tape per year, I think I'd rather go out and buy a new roll of tape, use the required amount, and throw away the rest of the roll. Then the next time I need black electrical tape, I know my first step is to go out and buy some black electrical tape.

In terms of the amount of time spent looking for the tape each year and the ratio of "stuff we have that goes years without being used" to "amount of space we have in our house", I think I'll still come out far ahead.

Sunday, June 16, 2013

Taking a Stand

A few months ago, I decided to try a standing workstation at my job. I'm a computer programmer; I know I slump and slouch throughout the day. I also know that's not the healthiest position for me.

Because I don't currently have a medical problem with my back, my employer won't provide me with one of those nifty electric adjustable desks that go from sitting height to standing height with the press of a button. (HR gets them regardless of physical condition, but that's a rant for another entry. Possibly.)

I decided it was important enough to me to consider paying for a standing workstation out of my own wallet. I poked around on-line and found a few candidates, but those started at around $400 and went up from there. I might even have considered that kind of investment, but none of the stores around me had anything like those models for me to examine, and I certainly wasn't going to mail-order something that pricey without physically seeing it first. Your loss, Staples and Office Depot.

So one day during my lunch hour I went to Menard's to see if there was some kind of bookshelf or other shelving unit I could easily adapt to my purposes. Nothing I saw there really struck a chord with me, so I headed for the exit. I was about to consider the entire idea a wash until I saw some stackable shelving crates on an endcap near the front of the store.

They're called "Stackübes" (I'm pretty sure they threw an umlaut over the U to make one think of Ikea), and they're fairly basic: a plain, white wooden box, open on the top with handles in two sides. Turn it on its side, and it's a component that can be added to other Stackübes for an entire shelving unit. I spent a few minutes trying both the small and the large boxes on their sides and upside down, and thought they were close enough to what I was looking for that I bought two large boxes and a small one.

Back at work, I put the boxes together and put my two monitors on the large ones, turned upright. Because the monitors are adjustable within a range of a few inches, I was able to get them to just the correct height for my eye level. I put the keyboard on the overturned small box; it was close, but not quite to where I could hold my arms at the ergonomically correct position.

I pulled down one monitor and tried the keyboard on the large box. Put on its side, the box positioned the keyboard pretty much exactly where it needed to be. So that same day after work, I went back to Menard's to buy one more of both the large and the small Stackübes.

It even adds storage space
The next day, I finished my desktop by adding the new large box for the keyboard, turning the small box on its end for the mouse (it matches the height of the large box in that position), and using the new small box (also on its end) for my phone. Strictly speaking, I'm not on the phone nearly enough to really need the phone elevated like everything else, but I felt it completed the setup.

I've been standing with my computer like this for about 3 months, now. The adjustment took only a couple of weeks. I brought in a stool so I could lean/sit for a bit when my legs got tired, but I've pretty much stopped using the stool at all.

The only problem that persisted after those first few weeks was that my feet were a little sore by the end of the day. Not having to consider shoes for being on my feet all day since my stint at Best Buy, my shoes weren't the kindest to me in that respect. A co-worker who also assembled her own version of a standing desk mentioned that she had a simple mat on the floor, and that helped her a lot. Since our floor is a thin institutional carpet on concrete or something just as unforgiving, I thought that was a good idea.

Memory Foam bath mat on top
I went to Bed, Bath, and Beyond to see what they could offer me, and I selected a bath mat with Memory Foam. It seemed pretty soft, so I gave it a try. After a few days of realizing that it was good but not great, I returned to Bed, Bath, and Beyond for an upgrade.

My next purchase was a GelPro chef's mat. This was more expensive, but I was all in at this point. It's a nice, thick, cushiony mat that has some give to it, but it still keeps a layer of gel between my feet and the floor.

GelPro chef's mat underneath
I considered using only the chef's mat, but since I already had the bath mat at work, I wondered what they would be like together. It actually works really well. The texture of the chef's mat along with the bottom of the bath mat keeps them from sliding against each other at all. Together, I get the softness of the bath mat with the cushioning of the gel mat.

I do take my shoes off when standing on my floor mats. The gel mat says it can resist breaking even when shoes are worn, although it does advise against wearing high heels on it. (Probably good advice for me in any situation.) But slipping off my shoes makes the mats that much more comfortable, and I don't have to worry about cleaning the bath mat as often. That is, unless my co-workers continue to not observe cubicle etiquette and tramp all over my rugs even when I'm standing there in sock feet. How rude!

So now I have my own standing workstation, and it's pretty much exactly what I want. It's comfortable to stand and do my work for the entire day, and I feel like I'm being a little more friendly to my own back. Unfortunately, I don't have the easy option of sitting down that those with the adjustable desks do. I have worked out that if I absolutely have to sit down and even the stool doesn't cut it, I only need to pull down one monitor and my keyboard/mouse. So I won't have to rearrange my entire desktop if I need some chair time.


Of course, nothing can stay perfect for long. Shortly after putting the finishing touches on my arrangement, my department was informed that it was time for our cubicles to be rearranged and condensed (translated: shrunk from 8x9 to 6x9 sq. ft.) so we would be moving into a temporary space, one cubicle row at a time.

This past Thursday was my move-out day. I considered storing my crates in my car for the week, but then I decided that I wouldn't allow them to shake up my new standing regimen.

Temporarily in cramped quarters
Even though we were supposed to let a PC Tech move our computers, I moved all of my own stuff the night before. It's simply easier to set up the computer at the same time as the crates, rather than letting them move the computer first and then try to work the crates in under everything.

My temporary desk is more shallow than my previous one, so it took a half-hour of tweaking the setup. I eventually got everything to fit--well enough, at least, for my week's stay. When I move back to my smartsized cubicle I'll still have the same desktop area I had before, minus one section, and I've already determined my original arrangement will fit there just fine. (I honestly think one thing they're counting on is that a week exiled to the "study carrels" will make us pine for the smaller cubicles.)

Some co-workers have asked how much I spent on this or that item, and they usually seem a bit shocked when I tell them. I haven't added up the total for anyone here for fear of completely blowing their minds:

Stackubes (large): 3 @ $19.99
Stackubes (small): 2 @ $14.99
Memory Foam bath mat:  $24.99
GelPro chef's mat:     $69.99
Total with tax:       $196.93

Now don't get me wrong: I would love to not have had to spend that two hundred dollars. I can find many more enjoyable ways to spend $200, even if it's only to give it to the credit card company to cover some other purchase I've already made. But I don't want to wait for my company to decide they can finally afford to give everybody the adjustable desks we were promised. And I'm at my desk for 7-8 hours a day, 40 hours a week; I don't want to cheap out on my own physical well-being. I consider that $200 well-spent.

Tuesday, May 1, 2012

Superhero No More

April 30, 2012. I'd never thought I'd be able to specifically pinpoint the (hopefully distant) future point at which I ceased to be everything in my little girl's eyes. But today, I broke something.

Don't get me wrong--I know she still loves me, and I also know that she knows I love her with all my heart. But I managed to disappoint her in a way I have thus far been able to avoid.

Her 2nd grade class is going to a local park/nature preserve tomorrow. Each 2nd grade class goes twice: once in the fall and again in the spring. A couple of years ago, Alex missed the fall trip due to illness but when his class went to the park in the spring, I was able to chaperone. This past fall, I went along with Cassie's class. I planned all along to skip the spring trip, since now I've made one trip with each child. For me, the day at the park gets really long, and to be honest, I'm not at my best with a group of kids; I just don't have that kind of innate patience. I thought Cassie had known I wasn't going all along. At least, I thought that when I said I'd join her class on their last two visits to the senior home this year, that I also mentioned that I wasn't signing up for the upcoming park trip.

At dinner tonight, she made some comment like, "Don't forget tomorrow!" I had a sneaking suspicion what she meant, so I asked, "What about tomorrow?" "It's the trip to the park, silly!" I asked, "Who's going to the park?" with a sense of dread. I knew where she was going with this. "You and me, Daddy."

I started to explain, "I thought you knew I wasn't going on this trip, sweetie." She looked at me with a smile and said, "Are you joking again?" To be fair, I joke with my kids a lot; they've learned to be skeptical.

"No, I'm really not," I started, trying to think what I could say to ease the blow. As she looked at me, I could see the tears welling up in her eyes. I asked if I made her sad, and she looked away and said into her sandwich, "A little." She was trying to be brave and hide how it made her feel, but the way her voice broke simply killed me.

I've made her cry before, usually for getting upset at the way she and her brother sometimes fight, or for some mess or other that she was responsible for. But the disappointment I instilled upon her tonight crushed me to an extent that losing my cool in front of the kids hasn't before.

I think today was the day I made her grow up a little and start to realize that I'm just a regular person.