An IC no longer

I’ve never been an Integrated Circuit, but it’s also been a while since I was an Individual Contributor in my career.

I like it, but it’s a weird shift that’s taken me way longer than I thought it would to really come to terms with.

As a lot of my usual, more technical posts generally get eaten up by what I’m doing at work, which limits my ability to talk about them. I’ve decided to write more on the other parts of the job, whatever you call them, whether it’s management, leadership, team-lead, or coordinator.

  • Technology (For the most part)
  • +Process
  • +People

My thoughts have always been that leadership is about removing friction. Digging out obstacles and other impediments to let the team push on. The best leaders I’ve worked with have always been honest, clear and generally dedicated to that thankless task.

Related to this, I’ve been enjoying reading more books about the people side recently, about leadership and the facet of psychology that comes into it, as well as finding some new opportunities for insight (mostly of my own inadequacy). I am continually surprised however that just reading a few (of the right) books on a subject can give you such a great leg up.

As well as new things, there’s also rediscovery of things I’ve read or immersed myself in before. A good example of this is Turn the Ship Around by L. David Marquet and intent (Leader/Leader) based interaction. I was reintroduced to it via a reference to the Radio Free XP podcast in a podcast from the absolutely excellent writer (and technologist) Nikhil Suresh. I haven’t linked to the specific episode, you should read (and listen to) all his stuff!

Turn the Ship Around

The biggest thing that stood out to me this time I reread the book was the sheer amount of respect for people’s time and experience in all the interactions. What sounds like a little thing, with requests going to all levels of approval at once, rather than serially.

I’ve lived through the serial approval points, with things taking literal weeks to see the other side of approval. Over time, it abrades away all respect for the process. I don’t know why this stuck out to me so much this time.

It might be related to the approach I take to self-service in the platforms and solutions I help build. I believe it’s a necessity in all areas and really relates to the deep respect of people’s time. Build the system and the guardrails, then get out of the way!

I originally came to the approach of self-service from the facet of building ownership, the respect realisation came later. Because it’s self-service, my aim was to attach the element of work added by the person, which then builds that feeling of ownership, by respecting their time and letting them build their way (or that’s the theory anyway). I’m not sure there’s been overwhelming evidence that’s working for me yet, but it seems to be building trust.

Anyways, great book and always worth a re-read to see what you missed last time around. Marquet’s site and his Leadership Nudges are well worth investing time into.

Test-Business (PSDay 2018)

Oh my, what a great day! I met loads of great people and in general had a great day with many many ideas to take back to work!

Things started well with the keynote by Richard Siddaway, on his birthday, no less! He gave a great session on where we are, what we’re here for and also a bit on where we’re going, as well as a great call to action, that consisted of contribute, contribute, contribute!

Right after a quick break, I gave my Test-Business session, which was nervewracking, but ultimately really good (i think?!). I had some great feedback afterward that really made my day. As long as someone is interested to try my code, I’m happy. Slides and content are on GitHub. I’m planning to also do a bit of cleanup and release the module soon as a preview module on the PowerShell Gallery.

The rest of the day was a blur of amazing sessions, speaking to amazing people and a quick jaunt to a pub for a bit more of a geek session! In no particular order, my takeaways were:

  • Some excellent tips on Pester mocking from Mark Wragg
  • Wonderful quotes from a DevOps culture presentation by Edward Pearson (plus loads of book recommendations!).
  • Very interesting ‘Notes from the field at Jaguar Land Rover’ from James O’Neill
  • Some great chats with a bunch of people including James Ruskin, Gael Colas and a quick chat with a guy that came all the way from Australia!

All in all, a magic day, many great people and one of the best days (and the best conference) I’ve ever been to!

Major thanks to the organisers Jonathan Medd, Daniel Krebs, Ryan Yates and all the other people that made this possible!

Working with Workday - The SOAP Opera (APIra?)

It’s been a couple of months and I’m back with new learning from the ever busy front where i’m at war with my own ignorance!

I’ve been having a look recently at quite a few web APIs (REST and SOAP and all that) and I’m here to share some slightly whacky odd script for getting Workday data from their SOAP API. For those that don’t know, Workday is a SaaS Human Capital Management (HCM) system with some grand ambitions around employee lifecycles, payroll, expenses and all those buzzwords and things you need to do when you employ people, pretty cool.

So looking at the Workday API pages, you might think SOAP? that’s old school!, use REST! For Workday, fortunately there is a REST API that’s easy to get to grips with, however it’s very difficult to get the security right and automate the login/authorization piece (at least with PowerShell, or my knowledge thereof). This means I’ve been concentrating on the SOAP API, which seems harder to set up the query, but a bit more usable from my position and security standpoint.

OK, so a SOAP API - New-WebServiceProxy to the rescue? Again, not much luck. For some reason I cannot get it to authenticate requests, so I had to fall back on good old Invoke-WebRequest for this one! The user account you use needs to be set up the same way as the system integration user defined here.

Essentially, I took a bunch of syntax for queries that already worked and amalgamated them in one string replacement hackfest. It’s not the cleanest piece of work in the world, but it gets the job done and allows me to keep trying New-WebServiceProxy in the background until I get that working. Any suggestions? Please let me know!

Here’s the function to get the data, you can delve into the returned object and get a load of good data out. I’ll clean up a function for exposing the object in a nice way too at some point soon, hopefully I can get to the point where I can release the entire module!

function GetWDWorkerData {
    <#
        .Synopsis
        A function to construct and send a SOAP request to Workday to retrieve worker data from the Workday API.

        .Description
        A function to construct and send a SOAP request to Workday to retrieve worker data from the Workday API.

        .Parameter Credential
        The credentials required to access the Workday API.

        .Parameter RequestType
        The type of data you would like to retrieve. Defaults to returning all data.

        RequestType can be one of the following:

            Contact
            Employment
            Management
            Organization
            Photo

        .Parameter WorkerID
        Limit your search data to a single worker. If your request encompasses multiple workers, use the pipeline.

        By default all worker information is returned.

        .Parameter Tenant

        The tenant to query for information.

        .Example
        GetWDWorker -Credential (Get-Credential) -Tenant TENANT -RequestType Contact

        Get contact data about all workers from the TENANT environment.

        .Notes
        Author: David Green
    #>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string]
        $Tenant,

        [Parameter(Mandatory)]
        [pscredential]
        $Credential,

        [Parameter(Mandatory)]
        [ValidateSet(
            'Contact',
            'Employment',
            'Management',
            'Organization',
            'Photo'
        )]
        [string[]]
        $RequestType,

        [Parameter()]
        [ValidateScript( {
                $_ -lt 1000 -and $_ -gt 0
            })]
        [int]
        $RecordsPerPage = 750,

        [Parameter(ValueFromPipeline)]
        [string]
        $WorkerID
    )

    Process {
        $page = 0

        do {
            $page++
            $Query = @{
                Uri             = "https://wd3-services1.myworkday.com/ccx/service/$Tenant/Human_Resources/v30.2"
                Method          = 'POST'
                UseBasicParsing = $true
                Body            = @"
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <env:Header>
        <wsse:Security env:mustUnderstand="1">
            <wsse:UsernameToken>
                <wsse:Username>$($Credential.UserName)</wsse:Username>
                <wsse:Password
                    Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">$($Credential.GetNetworkCredential().Password)</wsse:Password>
            </wsse:UsernameToken>
        </wsse:Security>
    </env:Header>
    <env:Body>
        <wd:Get_Workers_Request xmlns:wd="urn:com.workday/bsvc" wd:version="v30.2">
       $(if ($WorkerID) { @"
    <wd:Request_References wd:Skip_Non_Existing_Instances="true">
                <wd:Worker_Reference>
                    <wd:ID wd:type="Employee_ID">$($WorkerID)</wd:ID>
                </wd:Worker_Reference>
            </wd:Request_References>
"@ })
            <wd:Response_Filter>
                <wd:Page>$($page)</wd:Page>
                <wd:Count>$($RecordsPerPage)</wd:Count>
            </wd:Response_Filter>
            <wd:Response_Group>
                $(switch ($RequestType) {
                    'Contact' { "<wd:Include_Personal_Information>true</wd:Include_Personal_Information>" }
                    'Employment' { "<wd:Include_Employment_Information>true</wd:Include_Employment_Information>" }
                    'Management' { "<wd:Include_Management_Chain_Data>true</wd:Include_Management_Chain_Data>" }
                    'Organization' { "<wd:Include_Organizations>true</wd:Include_Organizations>" }
                    'Photo' { "<wd:Include_Photo>true</wd:Include_Photo>" }
                })
            </wd:Response_Group>
        </wd:Get_Workers_Request>
    </env:Body>
</env:Envelope>
"@
                ContentType     = 'text/xml; charset=utf-8'
            }

            [xml]$xmlresponse = Invoke-WebRequest @Query
            Write-Verbose -Message $Query.Uri

            if ($xmlresponse) {
                $xmlresponse
                $ResultStatus = $xmlresponse.Envelope.Body.Get_Workers_Response.Response_Results
                [int]$Records += [int]$ResultStatus.Page_Results
                Write-Verbose -Message "$Records/$($ResultStatus.Total_Results) records retrieved."
                Write-Verbose -Message "$($ResultStatus.Page_Results) records this page ($($ResultStatus.Page)/$($ResultStatus.Total_Pages))."
                $TotalPages = $ResultStatus.Total_Pages
            }
        }
        while ($page -lt $TotalPages)
    }
}

SCCM Cache size and client content management

Managing client content in SCCM is usually a zero touch affair. You set the content size and the data usually manages itself as it ages out over time and makes way for newer content. However, sometimes an application come along to break all those assumptions and make you cringe.

I recently had to deploy an application that required me to do just that. Two halves of an application, with the larger piece being a svelte 20GB of install content, even when compressed!

This required me to do two things to ensure the SCCM clients wouldn’t run into too many problems, especially if they’d been recently deployed.

One thing was to force clear the client cache before install, to use the minimal amount of space and free up space that may have been used for deployments that day, especially if the machine was freshly deployed, whee this is usually an issue. The second thing was to temporarily increase the cache size for the install with a silent required application, then reset/leave it to the SCCM policy afterward, since these apps are very rare.

For both of these things, I used my trusty friend PowerShell to load the SCCM client management COM object, then expand or clear the client content. Big thanks to both [OzThe2](https://fearthemonkey.co.uk/how-to-change-the-ccmcache-size-using-powershell and kittiah on reddit for the info, near enough just altered things into functions to make my deployment tidier.

Here’s the clean cache script merged into the function. For the cache size script, see here:

#Requires -RunAsAdministrator

Param (
    [Parameter()]
    [int]
    $OlderThan = 30
)

function Clear-CCMCache {
    <#
        .Synopsis
            Clears the content from the CCMCache.

        .DESCRIPTION
            Clears the content from the CCMCache.
            If no parameters are supplied then the cache is completely cleaned.

            To use this in Configuration Manager, on the Programs tab on the Deployment Type, set the 'Program' to be:

            Powershell -ExecutionPolicy Bypass -File .\Clear-CCMCache.ps1 -OlderThan 30

            The number after -OlderThan reflects the age of the CCM cache content to clear.
            If you leave out the -OlderThan parameter then all cache will be cleared.

        .EXAMPLE
            Clear-CCMCache -OlderThan 2

            Clears the cache data older than 2 days

        .NOTES
            Author: David Green
            Credit: https://www.reddit.com/r/SCCM/comments/4mx9h9/clean_ccmcache_on_a_regular_schedule/d3z8px0/?context=3
    #>

    [CmdletBinding()]
    Param (
        [Parameter()]
        [int]
        $OlderThan
    )

    if ($OlderThan) {
        $TargetDate = (Get-Date).AddDays($OlderThan * -1)
    }
    else {
        $TargetDate = Get-Date
    }

    # Create Cache COM object
    $CCM = New-Object -ComObject UIResource.UIResourceMgr
    $CCMCache = $CCM.GetCacheInfo()

    # Enumerate all cache items, remove any not referenced since $targetDate from the CCM Cache
    $CCMCache.GetCacheElements() | ForEach-Object {
        if ($_.LastReferenceTime -lt $TargetDate) {
            $CCMCache.DeleteCacheElement($_.CacheElementID)
        }
    }

    Write-Verbose -Message 'Updating registry.'

    # Create the registry key we will use for checking last run.
    $RegistryKey = 'HKLM:\System\CCMCache'
    New-Item -Path $RegistryKey -Force

    # Add size that CCMCache was changed to in MB
    $Item = @{
        Path         = $RegistryKey
        Name         = 'LastCleared'
        Value        = (Get-Date -Format 'dd/MM/yyyy HH:mm:ss').ToString()
        PropertyType = 'String'
        Force        = $true
    }

    New-ItemProperty @Item

    Write-Verbose -Message 'Done.'
}

# Script entry point
Clear-CCMCache -OlderThan $OlderThan

PowerShell Modules - Storing state for connections

Following on from my recent module writing work, i’ve been using a little trick in some of my PowerShell modules to store a small amount of state that persists as long as the module is loaded. I’ve been primarily using this strategy as a way to manage connections for SharePoint and REST services.

My example here is a small one derived loosely from a SharePoint Client-Side Object Model (CSOM) module I use and maintain. Essentially, in the module .psm1 file the variable is defined and then used by a couple of functions to set, get and clear the connection data using the $script:variable scoping, as shown below:

Note: You need the SharePoint Online PowerShell module for the SharePoint DLLs!

$Assemblies = @(
    "$env:ProgramFiles\SharePoint Online Management Shell\Microsoft.Online.SharePoint.PowerShell\Microsoft.SharePoint.Client.dll"
    "$env:ProgramFiles\SharePoint Online Management Shell\Microsoft.Online.SharePoint.PowerShell\Microsoft.SharePoint.Client.Runtime.dll"
    "$env:ProgramFiles\SharePoint Online Management Shell\Microsoft.Online.SharePoint.PowerShell\Microsoft.Online.SharePoint.Client.Tenant.dll"
)

Add-Type -Path $Assemblies -Verbose
# SharePoint connection states
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')]
$Connection = New-Object System.Collections.ArrayList

Function Connect-SharePoint {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [PSCredential]
        $Credential,

        [Parameter(Mandatory)]
        [string]
        $Uri,

        [parameter()]
        [switch]
        $PassThru
    )

    # Login
    $SPContext = New-Object Microsoft.SharePoint.Client.ClientContext($Uri)
    $SPContext.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials(
        $Credential.UserName,
        $Credential.Password
    )

    try {
        $SPContext.ExecuteQuery()
        $null = $Script:Connection.Add($SPContext)

        if ($PassThru) {
            $SPContext
        }
    }
    catch {
        Write-Error "Error connecting to SharePoint."
    }
}

Function Get-SharePointConnection {
    [CmdletBinding()]
    Param (
        [parameter()]
        [string]
        $Site
    )

    if ($Site) {
        $Script:Connection | Where-Object -Property URL -eq -Value $Site
    }
    else {
        $Script:Connection
    }
}

Function Disconnct-SharePoint {
    [CmdletBinding(DefaultParameterSetName = 'Site')]
    Param (
        [Parameter(
            Mandatory,
            ParameterSetName = 'Site'
        )]
        [string]
        $Site,

        [Parameter(
            ParameterSetName = 'All'
        )]
        [switch]
        $All
    )

    if ($PSCmdlet.ParameterSetName -eq 'Site') {
        $Disconnect = Get-SharePointConnection -Site $Site

        if ($Disconnect) {
            $Script:Connection.Remove($Disconnect)
        }
        else {
            throw "No connection for site $Site found."
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'All') {
        $Script:Connection = New-Object System.Collections.ArrayList
    }
}

Anyways, hopefully this helps for managing connections, REST auth tokens and a few other little scenarios. I’d be wary of using this approach too often for things like modifying function or module states, as it’s not really the one function for a job (the UNIX way) most PowerShell users might expect.