Sunday, April 14, 2024

Off.

 I finally get it.

I never really understood why people who were on medication would ever intentionally stop taking them, short of the medication making them feel worse than they did without it.

But what about making them feel less than they did without it?

I've noticed during the past few years that I wasn't crying at movies anymore, even movies that were once guaranteed to make me at least tear up, no matter how many times I'd seen them. I could sense the emotions present in YouTube videos of animals rescued from abusive situations or military personnel surprising their kids by returning from overseas and showing up at some school event. Both of those used to be automatic waterworks triggers but now, nothing.

I missed it. I missed the emotional release of letting tears flow, then gathering myself with a few breaths and moving on.

Wednesday, April 3, 2019

Creating and Updating Jira Issues with PowerShell

In attempting to find a way to programmatically manipulate Jira, I discovered that the information out there on the Internet is somewhat scattered. I couldn't find one central location that gave me everything I needed to move forward. After a period of trial and error, I managed to piece together the disparate parts into a PowerShell solution.

This is my basic function for getting a user's name and password.

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

function IdentifyUser
    {
    # Prompt user for logon credentials.
    $script:MyCredential = Get-Credential -Message "Please enter your network user ID and password:" -UserName $env:UserName
    if ($script:MyCredential)
        {
        $script:attemptConnection = $true
        $script:username = $script:MyCredential.Username
        $script:password = $script:MyCredential.GetNetworkCredential().password
        }
    else
        {
        Write-Host ""
        Write-Host $script:equalsLine
        Write-Host "Logon canceled by user."
        Write-Host $script:equalsLine
        }
    }  #IdentifyUser


This function contains many of the fields associated with a Jira issue. This is where values would be altered for creating or updating a specific case.

function InitializeVariables
    {
    $script:target = "https://[your Jira base URL]"
    $script:projectKey = "[your Jira project key]";

#===================================
#   Available variables/Jira fields
#-----------------------------------

    $script:createOrUpdate = "update"
    $script:issueKey = "$script:projectKey-1" # this is ignored if creating an issue

    $script:acceptanceCriteria = "If you build it, they will come."
    $script:assignee = "[a Jira user name]"
    $script:description = "This issue was created from PowerShell using Jira REST API."
    $script:dueDate = "2020-01-01"
    $script:issueType = "Task"
    $script:label_1 = "Development"
    $script:label_2 = "Request"
    $script:priority = "Lowest"
    $script:summary = "Jira workflow examination"

#===================================

    $script:attemptConnection = $false
    $script:equalsLine = "=" * 130

    if ($script:createOrUpdate.ToLower() -eq "create")
        {
        $script:method = "POST"
        $script:requestUri = "$script:target/jira/rest/api/latest/issue"
        }
    elseif ($script:createOrUpdate.ToLower() -eq "update")
        {
        $script:method = "PUT"
        $script:requestUri = "$script:target/jira/rest/api/latest/issue/$script:issueKey"
        }
    else
        {
        $script:method = "[ error ]"
        $script:requestUri = "[ error ]"
        }
    }  #InitializeVariables


Here I create the JSON structure containing the field values and submit it to my Jira instance. The REST method invoked is POST for creating an issue, and PUT for updating one (this has been established in the InitializeVariables function.).

function PerformOperation
    {
    if ($script:attemptConnection)
        {
        $bodyString = @{
            update = @{
            }

            fields=@{

            project=@{key="$script:projectKey"} # can be omitted when updating an issue; or it can just "change" it to the value it already has

            #assignee = @{name = "$script:assignee"}
            description = "$script:description"
            duedate = "$script:dueDate"
            issuetype = @{name = "$script:issueType"}
            labels =  [string[]]"$script:label_1"
            priority = @{name = "$script:priority"}
            summary = "$script:summary"
            
            # comment out a line to omit it from item creation/update

            }}

        $bodyJSON = $bodyString | ConvertTo-Json -Depth 2 #-Compress

        try {
            $basicAuth = "Basic " + [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$($script:Username):$script:password"))
            $headers = @{
                "Authorization" = $basicAuth
                "Content-Type"="application/json"
                }

            $response = Invoke-RestMethod -Uri $script:requestUri -Method $script:method -Headers $headers -Body $bodyJSON -UseBasicParsing

            if ($script:createOrUpdate.ToLower() -eq "create")
                {
                Write-Output "ID: $($response.id)"
                Write-Output "Key: $($response.key)"
                Write-Output "Self: $($response.self)"    
                }
            }
        catch {
            Write-Warning "Remote Server Response: $($_.Exception.Message)"
            Write-Output "Status Code: $($_.Exception.Response.StatusCode)"
            }
        }
    }  #PerformOperation


All that's left is to call these functions in sequence.

InitializeVariables
IdentifyUser
PerformOperation

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.