PowerShell script in VS Code

IT Automation with PowerShell

There are still a lot of IT administrators out there who have never taken the time to learn PowerShell (or any scripting language). PowerShell is a great tool that can save you a lot of time by automating those repetitive and/or tedious tasks that you’re always being asked to do.

It can take some time to learn the fundamentals of scripting when you are just getting started, but it’s well worth the time investment. Eventually, someone will give you what sounds like a very repetitive/tedious task (example to come) and your first response will no longer be an internal sigh. Instead, it will be excitement at the chance to write some PowerShell! Okay, maybe that’s a slight exaggeration.

A Real Life Example

This is a “real life” example. I had a requirement to create a folder structure within a network share. Each user in a specific Active Directory group required their own folder. The name of the folder was to match that user’s AD account username. The environment I work in is reasonably small, so that results in around 100 folders. However, the number of folders doesn’t really matter here, it could just as easily have been 1,000 or 10,000.

What would your initial reaction be if asked to perform that same task? Would you launch Excel, compile a list of the users who require a folder and then create 100 folders by hand in File Explorer, marking them off as you go? There are definitely IT admins out there who would take that approach.

It’s arguably a bad approach, but the logic behind the steps involved it isn’t so bad. You could use the steps in that process as the building blocks for a PowerShell script.

Break the problem down

Stop for a minute and think about the smaller steps involved in completing the bigger task. For most people, I imagine the thought process would be something like this:

  1. Get a list of all users in the required AD group
  2. Get the usernames of all those users
  3. For each user in the list:
    1. Check to see if they already have a folder
    2. If one doesn’t exist, create a new folder
    3. If there is already a folder, move on to the next user

Now that the problem is broken down into those smaller steps, you can almost translate it line for line into PowerShell. It is already starting to look more like a script!

Writing the PowerShell script

Even if you have no experience of PowerShell at all, hopefully you can look over the code below and it will make some kind of sense. At the very least it should somewhat resemble the steps listed above.

Import-Module ActiveDirectory

$groupName  = "AD Group Name"
$rootFolder = "\\SERVER\FileShare\"

$users = Get-ADGroupMember -Identity $groupName | `
Where-Object { $_.objectClass -eq 'user' } | `
Select-Object -Property samAccountName | `
Sort-Object

foreach($user in $users)
{
    $path = $rootFolder + $user.samAccountName

    if (-not (Test-Path $path))
    {
        New-Item -Path $path -ItemType Directory
    }
}

If you’ve never used PowerShell and have little or no experience of scripting, there will admittedly be parts of that code that aren’t very intuitive. You’ll likely be wondering what all the symbols mean. However, I bet if you spent some time working through a few PowerShell tutorials, most of it would make sense in no time.

How it works

Lets have a look through the script bit by bit and I’ll explain what it does.

Import-Module Active Directory

Because the script is going to be interacting with Active Directory, the first line tells the computer that we want to import all the Active Directory commands. These aren’t included as standard with PowerShell, which is why we need to import them. These actually need to be installed on the computer/server that will be running the script.

$groupName  = "AD Group Name"
$rootFolder = "\\SERVER\FileShare\"

These two lines are assigning values to variables. Think back to algebra class. If X = 4 then X + 2 = 6. X is just a placeholder for a value and that’s what $groupName and $rootFolder are. They’re just convenient names for values that we’ll be using later.

When I run this script for real I replace those dummy values with something useful. $groupName would equal the name of the AD group that contained the users. $rootFolder would contain the path to the file share that will contain all the sub-folders.

Getting the list of users

$users = Get-ADGroupMember -Identity $groupName | `
Where-Object { $_.objectClass -eq 'user' } | `
Select-Object { $_.samAccountName } | `
Sort-Object

This line, which spans multiple lines for readability, is probably the most intimidating looking bit of the script. Basically, I’m creating a list of all the usernames, sorted alphabetically, in the $users variable.

On the first line, after the “=”, I’m getting a list of all members of the AD group named $groupName. Now remember, $groupName is a placeholder that translates to the name of the AD group we’re interested in.

In the second line, “Where-Object” is acting as a filter. Because AD groups can contain more than just users as members (e.g. other groups, computers, etc.) we want to filter out everything that isn’t a user.

The third line, “Select-Object” is selecting only the attributes of the users I’m interested in. A user has many attributes, but in this case I only need the samAccountName (their username) so that’s all I pull through.

The last line “Sort-Object” is then just sorting the list of usernames alphabetically. This step isn’t really necessary. I just like processing things in order.

foreach ($user in $users)
{
    ....
}

Remember, the list of usernames lives in the $users variable. What I want to do is go through those users, one at a time, and set up their folder.

The foreach loop works it’s way through the list, user by user. The current user being processed is held in the $user variable. Notice it’s singular, rather than plural. That’s just how I like to name my variables. $users is the whole list and $user is the individual currently being processed.

For every user, it’s going to run the code between the { and } brackets.

Generating the file path

$path = $rootFolder + $user.samAccountName

I’m using a few variables in this line. I create a path variable and use it to hold the full file path of the current user’s folder, regardless of whether or not it exists. To create the path, I take the $rootFolder value, which was defined as “\\SERVER\FileShare\” and add the user’s samAccountName to the end of it. So it might end up looking like “\\SERVER\FileShare\Dean”.

Creating the folder (if needed)

if (-not (Test-Path $path))
{
    New-Item -Path $path -ItemType Directory
}

This is the part where I check if the file path already exists and, if not, create the folder. Test-Path is doing the work of checking if the folder already exists and New-Item is then creating a directory at that path if one does not already exist.

And that’s it.

Have you seen the light?

If you are one of the people who would have manually created the folders, hopefully this has given you a new perspective. Of course you’re not going to be able to sit down and start writing scripts just by having read this post, but maybe you’ll now have motivation to hunt down some PowerShell tutorials and start learning.

In my example I wrote a script that saved me from having to manually create 100 folders. It maybe took me 15-30 minutes to write and test that script. How long would it have taken to manually create the 100 folders? What if there were actually 1,000 folders to create? Well, the script can handle that too, without requiring any changes!

What if more users were added to that AD group next week and each required a new folder? Well, you could just run the script again and it would create the new folders.

But wait… having to run the script would be a manual task. Granted, it’s still much less time consuming than creating folders manually, but it’s still a little inconvenient.

Perhaps you could create a scheduled task to run the script every morning. That way any new users added to the AD group would automatically have a folder created. Plus, if anyone unintentionally deleted their folder it would be re-created (albeit without the contents).

Where to go from here?

  1. Look for online learning content
  2. Get familiar with the basics of PowerShell
  3. Bookmark the PowerShell documentation
  4. Put together a list of your simple, repetitive admin tasks that could be automated. Make these your first projects to tackle
  5. Start scripting!

Scripting is a skill and, like anything worth learning, it takes practice. You’re not going to become a master overnight. You’re not going to memorise every PowerShell cmdlet. What’s important is that you start changing the way you approach problems.

Look for manual tasks that you perform regularly. Break them down into steps. Translate those steps into PowerShell until you’ve built out the whole process. It might take you a while to get the script working, but once it is working that’s one less recurring manual task on your to do list!

Hopefully you see how much this can benefit you and those that are dependent on your work. Automate the mind-numbingly dull tasks out of your working life and spend more time focusing on the big picture projects that benefit everyone.

Any IT admin can create 1,000 folders. The smart admin will have a script do all the work for them!

PowerShell - my prompt

Customizing the PowerShell Prompt

By default, PowerShell comes with a prompt that can be annoying at times. Thankfully, customizing the PowerShell prompt is a relatively easy task.

The main reason that I dislike the default PowerShell prompt is because it displays the full path to your current directory. When working deep within nested folders, the prompt begins to take up a lot of the horizontal line space. This means that even short commands can run onto the next line, making it more awkward to read.

default powershell prompt

Customizing the PowerShell prompt is easy. You just need to define a function call “prompt” and store this in your PowerShell profile.

Getting Started

The first thing to do is find your PowerShell profile. You can do this by simply entering $profile into a PowerShell session and pressing return. This will give you the path to your profile file.

It’s worth noting that different terminal emulators can use different prompt files. For example, the profile file referred to by PowerShell ISE is not the same profile file that Windows Terminal uses. However you can create a prompt function in both if needed.

The profile file can be edited by running “notepad $prompt” as shown below:

PowerShell - displaying the location of your profile

Running the above command will launch notepad and will likely open an empty file. Within the file, you can define a function called “prompt”.

NotePad - example prompt function

The image above shows my PowerShell prompt function, which displays a few basic pieces of information:

  • Current date (not actually displayed, but used for getting the time)
  • The time in the format I want, using GetDateTimeFormats
  • Current working directory’s name (not the full path)
  • If the current directory is the root of a drive (e.g. C:\) then I format the string to make it display nicer. By default, it would just show as “C”

From there, the function then writes this information to the host, which creates the new PowerShell prompt.

For simplicity, I use the Write-Host cmdlet. This makes it easy to the change text colours and background colours of the prompt.

The Finished Result

Once the prompt function has been defined, save the changes and restart the PowerShell session. You should now see the prompt you have defined.

PowerShell - my prompt

My prompt might not be the most aesthetically pleasing, but it gets the job done. The use of colours makes it easy to see where commands begin when scrolling back through the session.

In the blue section, I display my current username and hostname. This can be a useful reminder of who you are and where you are if you have multiple user accounts on multiple machines.

The yellow section displays the name of the current working directory. Unlike the default prompt, it does not display the absolute path to this directory.

The magenta section displays the time. This just gives me a rough idea of when I executed a command and how long it took to run (if it took more than 1 minute).

If you ever want to revert back to the default prompt, simple edit the profile file again and delete your function. You will need to start a new PowerShell session for the changes to be applied.

At this point you should have enough information to start customizing your own PowerShell prompts. If you would like to play around with my prompt then feel free to copy the code below.

Example prompt function

function prompt {
    $date = Get-Date
    $time = $date.GetDateTimeFormats()[88]
    $curdir = $ExecutionContext.SessionState.Path.CurrentLocation.Path.Split("\")[-1]

    if($curdir.Length -eq 0) {
        $curdir = $ExecutionContext.SessionState.Drive.Current.Name+":\"
    }

    Write-Host ""$env:USERNAME"@"$env:COMPUTERNAME" " -BackgroundColor Cyan -ForegroundColor Black -NoNewline
    Write-Host " DIR:"$curdir" " -BackgroundColor Yellow -ForegroundColor Black -NoNewline
    Write-Host ""$time" " -BackgroundColor Magenta -ForegroundColor White
    "> "
}

Check out Microsoft’s documentation on the PowerShell prompt if you would like more detailed information.

zabbix 5.0 screenshot

Installing Zabbix Using Containers

I recently had a bit of an ordeal trying to upgrade Zabbix to version 5.0 on a CentOS 7 server. This led to me installing Zabbix using containers. The problem was that Zabbix 5.0 requires a newer version of PHP than CentOS 7 ships with. Somehow I managed to miss that note before I started working through the upgrade.

It got me thinking “wouldn’t this be so much easier if Zabbix just came bundled with all it’s dependencies?” Then it struck me; that’s what containers do! After a quick search on Dockerhub I could see that Zabbix containers were available. I’d been wanting to upgrade that server to CentOS 8, so seemed like a good excuse for a re-install.

Full disclaimer – I’d never worked with containers before this. My setup probably isn’t ideal for production use, but it worked as a learning exercise.

Getting started with Podman (or Docker) to manage the Zabbix containers

First up, have Podman (or Docker, the commands should be the same) download copies of the container images from Docker Hub:

podman pull docker.io/library/mariadb:latest
podman pull docker.io/zabbix/zabbix-server-mysql:centos-5.0-latest
podman pull docker.io/zabbix/zabbix-web-apache-mysql:centos-5.0-latest

Create a Dockerfile for each container. These will be used to create a customised container image. You’ll obviously want to set the passwords to something more sensible than the ones in my example. You’ll also need to change the IP addresses to fit your environment. You can get more info on what each parameter does on the Dockerhub pages.

# ~/dockerfiles/mariadb/Dockerfile

FROM docker.io/library/mariadb:latest
ENV MYSQL_ROOT_PASSWORD=password

# ~/dockerfiles/zabbix-server/Dockerfile 

FROM docker.io/zabbix/zabbix-server-mysql:centos-5.0-latest 
ENV DB_SERVER_HOST=192.168.179.128 
ENV DB_SERVER_PORT=33306 
ENV MYSQL_ROOT_PASSWORD=root 
ENV MYSQL_USER=zabbix 
ENV MYSQL_PASSWORD=password
ENV MYSQL_DATABASE=zabbix 

# ~/dockerfiles/zabbix-web/Dockerfile

FROM docker.io/zabbix/zabbix-web-apache-mysql:centos-5.0-latest 
ENV ZBX_SERVER_HOST=192.168.179.128 
ENV ZBX_SERVER_PORT=10051 
ENV DB_SERVER_HOST=192.168.179.128 
ENV DB_SERVER_PORT=33306 
ENV MYSQL_USER=zabbix 
ENV MYSQL_PASSWORD=password
ENV MYSQL_DATABASE=zabbix 
ENV PHP_TZ="Europe/London" 
ENV ZBX_SERVER_NAME=zabbix 

Create the custom container images using the Dockerfiles:

podman build –t mariadb ~/dockerfiles/mariadb/ 
podman build –t zabbix-server ~/dockerfiles/zabbix-server 
podman build –t zabbix-web ~/dockerfiles/zabbix-web 

Run the containers, mapping the host ports to the container ports. Because I’m running in rootless mode, I can’t use port 80 or 3306 on the host. Instead I’m using ports 8080 and 33306. If you were running as root I don’t believe the default port mappings would be an issue.

podman run –d –p 33306:3306 localhost/mariadb 
podman run –d –p 10051:10051 localhost/zabbix-server 
podman run –d –p 8080:8080 localhost/zabbix-web 

All I had left to do at this point was add some firewall rules to allow outside access to Zabbix:

firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --zone=public --add-port=10051/tcp --permanent
firewall-cmd --reload

And just like that, I had successfully finished installing Zabbix using containers!

A screenshot of the Zabbix 5.0 dashboard page after installing using containers.
Mastodon