Author Archives: Sean Kearney

Automate Bootable USB creation with Powershell–Part 4


So we now have a pile of bootable USB keys.  Woohoo!

Now what we need to do is get ROBOCOPY to transfer data to them.   Automating THAT should be quite easy.   For that we have some very simple requirements.

  • All Data is in a folder called “C:\MDTImage\Content”
  • We are going to dump the data to all blank USB drives that fall into the same parameters as before (larger than 8GB, smaller than 64GB)

For this we COULD play with DISKPART some more, pull up the DETAIL on each drive and match the VOLUMES somehow, or I could cheat.

You see the keys we are going to use fit two key criteria.  They are USB and have been VERY recently formatted (like about 4 minutes) ago.  So they should have a very high free space count.   For this task we can leverage WMI and simply ask the computer the following question.

“Oh Great and Mighty Deep Thought!  Please show us all USB Drives with only One partition! We need to see the ones where the free space is over 99%, the drive is larger than 8gb and smaller than 64gb! And Please…. No.  Don’t tell us it will take a Billion Years in a computer called the Earth!”

Well had this been any OTHER giant computer, it might have.  But we can leverage a script similar to the first by accessing the “DETAIL” of each removable USB disk to find it’s correct Volume Letter.

But if I go into DISKPART and pull up the DETAIL DISK on a recently formatted drive I can see what I need is there already

image

Since we are already pulling information from DETAIL DISK, we can modify the original script and have it access the drive Letter in the final line and incorporate it into the original script.

In our original script (in part 1) we parsed DETAIL DISK with the following lines.

$Model=$detail[8]
$type=$detail[10].substring(9)

Since we are still accessing the output from $detail, we can add in one more PowerShell variable.  Specifically we want the Last line (which contains the volume information we are looking for) and a VERY specific element, the Driver letter.

$DriveLetter=$detail[-1].substring(15,1)

And we can just modify our [pscustomobject] to send that property back as an additional piece of info.

[pscustomobject]@{DiskNum=$d;Model=$model;Type=$type;DiskSize=$disktotal;DriveLetter=$DriveLetter}

Now armed with a more improved script we can ask Windows PowerShell to talk to his friend DISKPART to get the Drive Letter for the Single Volume on all Removable USB Drives larger than 8GB and smaller than 64GB.

$TYPE=’USB’
$MIN=8GB
$MAX=64GB
$letters=(./QUERYDISKPARTV2.PS1 | Where { $_.Type –eq $Type –and $_.Disksize –gt $Min –and $_.Disksize –lt $Max } | SELECT-Object DriveLetter)

Now armed with this information we can have Robocopy do the rest of the work for us.

$Source=”C:\MDTImage\Content”

$letters { FOREACH { $Destination=$_.DriveLetter+”`:”; START-PROCESS ROBOCOPY.EXE –ArgumentList “$Source $Destination /E /S”} }

So now if we tie all these pieces together we could build some kinda of cool crazy automatic USB key creator for MDT that does NOT involve showing somebody the inner workings of DISKPART and ROBOCOPY.

We can turn it into a “Click Here” solution.

Kinda cool eh?

Remember, the Power of Shell is in YOU

Sean
The Energized Tech

Automate Bootable USB creation with Powershell–Part 3


And now the fun begins.   Getting DISKPART and Powershell to do all the dirty work for us.  Cue the “Muah ha ha haaaaa!”

So automating DISKPART is actually very easy.   You have a text file that contains all the commands you need to run and you execute it in the following manner.

DISKPART /S Script.txt

That’s it!

But the challenge when creating a bootable USB key (at least for automating) is the the Disk number for removable devices may not always be the same number, or that you MIGHT have in an external hard drive when you do this.

So now that we have the PowerShell script from the previous articles which pulls out this data from Diskpart, we just need to build a text file.

Now we need to put some thought into this.  If I were just building ONE USB key everytime, I’d just ‘Go for broke’ and target a single key.  

But my goal is I would like to drop between one (1) and four (4) keys into a computer and have the following occur.

  • Identify the USB key
  • ERASE and CLEAN it within DISKPART
  • Make the USB key ACTIVE and as such bootable
  • Eventually tie in Robocopy to transfer a specified “CONTENT” folder from MDT directly onto said keys.

So we know I can use the new script QUERYDISKPART.PS1 to pull out all of the data.  But let’s imagine I have a bunch of USB keys like on this computer.  I would like to have a DISKPART script that will automatically format all of them as bootable USB devices.

image

So if you don’t already know the sequence to create a bootable USB key in DISKPART it is.

SELECT DISK Num (Where Num is the indexed numbered assigned the Disk for DiskPart)

CLEAN (Which wipes out the Partition table)

CREATE PARTITION PRIMARY (Which creates a New Primary Partition on the Selected Disk)

FORMAT FS=FAT32 QUICK (Which creates a FAT32 partition)

ASSIGN (which assigns a drive letter to the partition)

ACTIVE (which makes the partition active and bootable)

so a simple text file script for DISKPART to make a bootable USB key could look like this

SELECT DISK 1
CLEAN
CREATE PARTITION PRIMARY
FORMAT FS=FAT32 QUICK
ASSIGN
ACTIVE

So really all we will need to do is loop through the information, create a text file with this sequence of commands for each disk, and change the “SELECT DISK 1” with the appropriate value from DiskNum.

Now I could do things really fancy (and if you’d like to improve, by all means dive in!) but I’m going to be very brutish and manual in how I create the text file.

First we launch the “QUERYDISKPARTv2.PS1” script and capture the objects that are USB and within the Size we’d like.

$TYPE=’USB’

$MIN=8GB

$MAX=64GB

$DRIVELIST=(.\QueryDiskpartv2.ps1 | where { $_.Type –eq $Type –and $_.Size –gt $Min and $Size –lt $Max })

Now that we have a list of drives we can Build a single script to handle them all

NEW-ITEM -Path bootemup.txt -ItemType file -force | OUT-NULL

$DRIVELIST | FOREACH {

$DISKNUM=$_.DiskNum
ADD-CONTENT –path bootemup.txt –Value “SELECT DISK $DISKNUM”
ADD-CONTENT –path bootemup.txt –Value “CLEAN”
ADD-CONTENT –path bootemup.txt –Value “CREATE PARTITION PRIMARY”
ADD-CONTENT –path bootemup.txt –Value “FORMAT FS=FAT32 QUICK”
ADD-CONTENT –path bootemup.txt –Value “ASSIGN”
ADD-CONTENT –path bootemup.txt –Value “ACTIVE”

}

Once this process is complete we will have the script called ‘bootemup.txt’ which we can launch from DISKPART like this

DISKPART /S bootemup.txt

Let that run through and all the keys attached will become bootable.  But our next challenge, having Robocopy do the transfers to all those drives!

Well that we’ll talk about next time.

Remember, the Power of Shell is in YOU.

Sean
The Energized Tech

Automate Bootable USB creation with Powershell–Part 2


So the first challenge was the trickiest part.   Now for the fun!  Making some bootable USB keys!

Well not QUITE yet.   We want to be smart about this.  We’re going to define some parameters for a bootable USB key.

Think about it.  If I had a script that could just arbitrarily go out and take every disk attached and WIPE it as a bootable device, wouldn’t that be a little dangerous?  So we’re going to be smart about this.

First let’s define SOME parameters.   For this to be useful and somewhat safe we’re going to use the following parameters

  • USB device
  • No Smaller than 8 gigabytes
  • No Larger than 64 gigabytes

Now this may not define all possible combinations for safety, perhaps we’ll leverage using the “-whatif” parameter later on.  But this will be a start.  Anything smaller than 8gb won’t be a good candidate for and MDT Deployment key.   Anything larger than that is more than likely a removable hard disk.

So from our last script ‘QueryDiskPart.ps1’ we saw the output looked like this

image

And if you run a GET-MEMBER against the output you can see the types of useful data now, instead of just “Console output” from DISKPART

image

So we can now do a filter to show only Devices which are attached by USB, which is pretty cool

.\QueryDiskpartv2.ps1 | where { $_.Type –eq ‘USB’ }

image

This is not surprising.  In fact we COULD ask Windows for similar information from WMI using Win32_disk.

get-wmiobject -query “SELECT * from win32_diskdrive where Interfacetype = ‘USB’” | Select-object Model,InterfaceType,Size

image

What this is MISSING is the “Disk Number” which is only produced when the Disks are enumerated in DISKPART.

So this is why we must parse DISKPART.  It *IS* a very slow process but now that we have the “DiskNum” we can build the script.

Now of course I said the next thing we’ll want to do is Identify disks which are between a certain size.    At this point let’s define some PowerShell variables for the sizing and the Interface type.

$TYPE=’USB’

$MIN=8GB

$MAX=64GB

This will allow us to run the query like so.

.\QueryDiskpartv2.ps1 | where { $_.Type –eq $Type –and $_.Size –gt $Min and $Size –lt $Max }

Now we can think about our next task.   Since we can identify Removable USB keys and even identify the enumerated number it contains in DISKPART, we should be able to build the DISKPART script.

And we will…. next time

Remember the Power of Shell is in You

Sean
The Energized Tech

Automate Bootable USB creation with Powershell–Part 1


So I sat down the other day and decided I wanted to be able to do something really cool.

I wanted to be able to automatically create a bootable USB key, regardless of where it was in the computer.

Creating a Bootable USB key is actually not difficult in Windows.   It’s a well documented process that follows the lines of.

  • Insert USB Flash drive into a port
  • Launch DISKPART as Administrator
  • Execute some Commands in DISKPART
  • Use Robocopy to mirror your boot Media or Bootable content to the key.

Now in Truth I could write most all of this as a script I could launch in DISKPART with the /s parameter, then perhaps run this as a batch file.

But I wanted a far more dynamic solution.   I wanted to be able to plug in as many keys as I wanted on ANY computer and just have all the work done for me.

So this is very achievable by using DISKPART to pull up the data and have Windows PowerShell parse the output for the information we need.

Within DISKPART the first thing we are going to run is the Command LIST DISK to show us which disks are attached to the system.   The output will look somewhat similar to this.

image

Then within the application we can SELECT a particular Disk and access it’s DETAILS by doing something similar to this.

SELECT DISK 2

DETAIL DISK

This will give us the following output as an example

image

Now to automate any of these operations for DISKPART I would normally just create a TEXT file with the Commands I wish to run and execute something like

DISKPART /s thingstodo.txt

The process is easy to work with but the challenge is building a DISKPART script to pick the correct key every time.   You’re dealing with scenarios where it might NOT be DISK 2 for the USB, or perhaps there are removable hard disks.   There’s even a chance that you can’t USE some of the keys for a bootable deployment (the Operating system usually needs an 8gb key, bigger MDT deployments may cross into a 16gb key)

So Ideally what I would like to do (and will achieve by the end of this series) is have PowerShell Parse the output from DISKPART and BUILD the scripts I need for the application.   I will have Powershell identify the Disk number and produce an object return the details about the attached Disks (whether they are physical or removable)

So first I am going to have PowerShell build a DISKPART script to execute LIST DISK for me.   Why not have the file exist already?  Let’s go back to “Dynamic”.  I want the SCRIPT to be the environment and build whatever parts it needs.

new-item -Name listdisk.txt -Itemtype file -force | out-null
add-content -path listdisk.txt “list disk”

I will then execute DISKPART inside PowerShell and capture the output as an Object.

$listdisk=(diskpart /s listdisk.txt)

Now the output of this command is consistent in not only the format, but also in how much information it returns relevant to the physical disk count.  So one of the things I figured out was the number of lines in the output was EXACTLY 9 longer than the number of disks.   So with a little math I can have Powershell tell me how many disks DISKPART sees.

$totaldisk=$listdisk.count-9

Now for the fun part, we can build some simple DISKPART scripts to pull up the DETAIL on each individual DISK, including data on the partitions and Parse that out.

for ($d=0;$d -le $totaldisk;$d++)
{
new-item -Name detail.txt -ItemType file -force | out-null
add-content -Path detail.txt “select disk $d”
add-content -Path detail.txt “detail disk”

$Detail=(diskpart /s detail.txt)

Now from the output from the DETAIL DISK I want to pull out two key pieces of information, the Model of whatever disk we are examining and whether it is a USB key or not.  I COULD play with Regular Expressions but honestly this is simple enough to pull out with a Substring method and grabbing the output directly since the information is consistently in the same location.

$Model=$detail[8]
$type=$detail[10].substring(9)

Now the next challenge is to determine the SIZE of the removable key.   For that I can parse the data I already stored in $LISTDISK as I pulled it from LIST DISK which shows the size of each key on each row.

$size=$listdisk[8+$d].substring(25,9).replace(” “,”")

$length=$size.length
$multiplier=$size.substring($length-2,2)
$intsize=$size.substring(0,$length-2)

However let’s re-examine the output from LIST DISK

image

In some cases we will find the size of the disk is in Gigabytes, and sometimes reported in Megabytes.  Other times it’s even in Kilobytes!  So we’ll need a way to “Switch” those numbers with real values.   So the answer is simple.   Parse the numbers on the left, parse the MB/GB/KB on the right and figure out the multiplier it should be.   This sounds like a job …. for POWERSHELL!

We can use the Switch statement to easily swap out those letters for real values.   PowerShell has some built in shortcuts for things like Memory sizing, so we’ll leverage those.

switch($multiplier)
    {
        KB { $mult=1KB }
        MB { $mult=1MB }
        GB { $mult=1GB }
    }

Now we can do a little simple math to figure out the actual size of the Removable Key

$disktotal=([convert]::ToInt16($intsize,10))*$mult

Now we can just return this data as an Object with PowerShell’s new feature in Version 3 called “[pscustomobject]”

[pscustomobject]@{DiskNum=$d;Model=$model;Type=$type;DiskSize=$disktotal}

}

We now run just run a script that (although this is slow) will produce the data from DISKPART as an Object like the following.

image

Now if you’ve EVER had to create a Bootable USB key, I’ll bet you can see where I am going with this next… Automatically creating the Scripts for Diskpart!

Next time.  We’ll look at that next time.

Remember, the Power of Shell is in You

Sean
The Energized Tech

Get a List of all Windows TimeZones with Windows Powershell


There is an excellent post on getting and setting the TimeZone done by Pat Richard using Powershell and TZUtil which I thought was absolutely cool.

The only thing I noticed was the Script had a defined list for the Time Zones.   Knowing much of this information is contained directly in Windows I was wondering if there was a way to pull the list in and use it as the master.

Within .NET there is System.TimeZoneInfo which has a nice simple method called (are you ready?) GetSystemTimeZones

So to leverage this method in Windows Powershell all we need to do it

[System.TimeZoneInfo]::GetSystemTimeZones()

Poof! now we have a big funky list on our screen! But now to make it more useful as no ITPro likes to keep typing in .NET (Well some of us do, but we’re more classified as half dev/half itpro hobbits)

So we can make this a nice simple advanced Function

function get-timezone
{
[cmdletbinding()]
param([string]$Name)
([system.timezoneinfo]::GetSystemTimeZones() | where { $_.ID –like “*$Name*” }) 
}

or possibly a Function to only pull up a matching one?

function get-timezonematch
{
[cmdletbinding()]
param([string]$Name)
if ([system.timezoneinfo]::GetSystemTimeZones() | where { $_.ID -eq $Name })
{ Return $TRUE }
else
{ Return $FALSE }
 
}

Now I can do something like THIS

GET-TIMEZONE | Format-table –autosize

or

GET-TIMEZONE *EAST*

or

GET-TIMEZONEMATCH “Eastern Standard Time”

Now we can take Pat’s original script and give it a slight improvement to pull from the Internal list from Windows.  If we have already defined the “GET-TimeZone” Advanced function and executed it first, we can do this Pat’s function.

function Set-TimeZone {
[CmdletBinding(SupportsShouldProcess = $True)]
param(
    [Parameter(ValueFromPipeline = $False, ValueFromPipelineByPropertyName = $True, Mandatory = $False)]
    [ValidateNotNullOrEmpty()]
    [string]$TimeZone = “Eastern Standard Time”
  )

If (GET-TIMEZONEMATCH $TimeZone) {  
  $process = New-Object System.Diagnostics.Process
  $process.StartInfo.WindowStyle = “Hidden”
  $process.StartInfo.FileName = “tzutil.exe”
  $process.StartInfo.Arguments = “/s `”$TimeZone`”"
  $process.Start() | Out-Null }

ELSE { WRITE-ERROR “InvalidTimeZone” }

} # end function Set-TimeZone

Pretty cool what you can do with Powershell and .NET and STILL be classified as an ITPro.  Also Thanks to Pat Richard, Lync MVP Extraordinaire for writing this cool Powershell Script Smile

The 2013 Scripting Games–Release the Hero Within !


It’s time to release the Inner Hero within!  Compete online and show your skills!

The 2013 Scripting Games are here! Novice Users, Experts, Hobbyists!  ALL ARE WELCOME!

Thrills!

Chills!

the Agony of the feet (Oh! Sorry! Socks!)

REGISTER NOW at Powershell.Org and join in the fun!

The 2013 Scripting Games–See http://powershell.org/games/ for Details

The Inner Hero within you is calling.  REGISTER and COMPETE TODAY!

Powershell–How do I find the Mac Addresses of my Server Network cards?


Here’s a quick one.  A friend was working on a Hyper-V server and couldn’t find the MAC address of a network card.  “It’s not showing up under IPCONFIG as it’s not bound to TCP/IP”

Welcome to Powershell.   With Windows Powershell we can query the Win32_NetworkAdapter in WMI.

GET-WMIOBJECT Win32_NetworkAdapter

This would produce a list of ALL Network Adapters (Physical and Virtual) and their corresponding properties.

If you want, you can break that down to “All Physical Network Adapters” which should make your task easy.  

GET-WMIOBJECT –query ‘SELECT * from Win32_NetworkAdapter Where PhysicalAdapter=”TRUE”’

This however will still return some phantoms.   So we’ll filter this down to only Network Adapters which actually have a MacAddress

GET-WMIOBJECT –query ‘SELECT * from Win32_NetworkAdapter Where PhysicalAdapter=”TRUE” and MacAddress <>””’

Which gets an even SMALLER list.   But the Hyper-V virtual adapters still popup.   We can drill further down and get a more refined list by removing them.

GET-WMIOBJECT –query ‘SELECT * from Win32_NetworkAdapter Where PhysicalAdapter=”TRUE” and MacAddress <>”” and ServiceName <>”VMSMP”’

We can then take this output and sort it out by MacAddress and display it on screen in a more useful format.

GET-WMIOBJECT –query ‘SELECT * from Win32_NetworkAdapter Where PhysicalAdapter=”TRUE” and MacAddress <>”” and ServiceName <>”VMSMP”’ | SORT-OBJECT MacAddress | Format-Table

Life …. it’s always easier with a little Powershell Smile

Determining Language Locales and values for MDT 2012 with Powershell


Within MDT 2012 (and previous versions) you can pass along to the operating system certain definitions.  One of these is the default keyboard and language locales.   These are defined by the Variables in CustomSettings.ini or your “Rules” Tab in MDT 2012.

The Variables you need to modify are

UILanguage=en-US
UserLocale=0409:00000409
InputLocale= 0409:00000409
KeyboardLocale= 0409:00000409

You will also need to supply the following line as well

SkipLocaleSelection=Yes

To find out the LCID for a French version you can run this line in Windows Powershell

[system.globalization.cultureinfo]::getcultures(“AllCultures”) | where { $_.Displayname –like ‘*french*’ }

the Returned object in your Console which show you ALL Cultures with the name “French” in them.

image

To switch the French Canadian one for example we first alter UILanguage

UILanguage=fr-CA

Our next challenge is figuring out the UserLocale numbers.   What this ACTUALLY is, is a direct reference to the LCID.   The Displayed LCID is in Decimal.   We can test this by taking the first 4 hexadecimal digits from  “UserLocale” and converting them back to decimal with Powershell

[convert]::toint16(“0409”,16)

This returns the decimal value 1033 which if we query the Language locales will confirm this as a en-US configuration.

[system.globalization.cultureinfo]::getcultures(“AllCultures”) | where { $_.LCID –eq 1033 }

image

So all we need to do is convert the LCID value for a French Canadian entry into Hexadecimal and swap in the new values.

[convert]::tostring(3084,16)

This returns a value of c0c.   We just change the hexadecimal values of UserLocale, InputLocale and KeyboardLocale with the updated value for fr-CA

UserLocale=0c0c:00000c0c
InputLocale= 0c0c:00000c0c
KeyboardLocale= 0c0c:00000c0c

Feeling the Power of Automation? I AM !

Sean
The Energized Tech

Getting a list of Windows Language Locales with Windows Powershell


I shall never profess to be a developer.   I just like finding answers.  

One of the challenges I had today was finding a list of Language locales in Windows.   I poked about and found an online list posted which could help me out with MDT and programming in the Locales for Non english versions of Windows and Office 365 cached downloads.

The problem was the list links from Microsoft weren’t current.   I’m certain there are updated versions (if you have them, email them please or add them to comments) but I went for a programmatic solution.     But I wanted to ask WINDOWS what list it had available, and I didn’t want to sit down with a Hex editor playing with an NLS file. 

Granted, there is probably a BETTER way, but this was quick and simple.   I went to MSDN.com and did a search for “Locale Table” online.   One of the very first links I found was for the “DataTable.Locale” property in .NET.

image

I could see it was a part of “System.Globalization.Cultureinfo” and decided to try something quick and simple in Windows Powershell, see if this could be natively accessed.

So into Windows Powershell I tried it as an accelerator

[System.Globalization.Cultureinfo]

image

A smile crossed my face at this point, and I started to play.      I tacked on a ‘::’ and played with Tab Autocompletion to see what was available to play with out of the box.   First out of the list was ‘CurrentCulture’

[System.Globalization.Cultureinfo]::CurrentCulture

image

“Oh Cool!” my eyes lit up.  I then wondered if there was a “Get” method available.   A little more online poking on MSDN confirmed this as part of the base Methods.

image

So I tested with a simple numeric id,  Number 1 (Really, I just took a shot to see if it would work)

[System.Globalization.Cultureinfo]::GetCultureInfo(1)

image

Then i tried with a bad one, (again I was guessing at values)

[System.Globalization.Cultureinfo]::GetCultureInfo(999)

image

Aha! So all I had to do to get a list of Locales was

Make Errors silent temporarily
Loop through all possible locale values
Capture the list

$olderr=$ErrorActionPreference
$ErrorActionPreference=’SilentlyContinue’

$locales=For ($lcid=0; $lcid –lt 20500; $lcid++) {  [System.Globalization.Cultureinfo]::GetCultureInfo($lcid)}
$ErrorActionPreference=$olderr

Granted this is a REALLY bad example of code.  I am tripping off a CRAP load of errors just to get what I want.  There might even be an easier way to find the information, i just find something “Quick and Dirty” (watching all my Developer friends cringe as overload the stack with errors)

Now after having gone through this process my good friend and fellow Windows Powershell MVP Shay Levy mentioned to me there was an easier way! 

$locales=[System.Globalization.Cultureinfo]::GetCultures(“AllCultures”)

Boy THAT was a treat to that as solution! I could even make it into a cheap and dirty Cmdlet (And I do mean Cheap and Dirty) by doing this

Function global:GET-CULTURE { [System.Globalization.Cultureinfo]::GetCultures(“AllCultures”) }

$locales=GET-CULTURE

But now what I had was a simple object with the data captured.  I could Sort on it

$locales | SORT-OBJECT

Or export it out as a CSV file for easy reference later

$locales | EXPORT-CSV C:\Powershell\locales.csv

With this information finding the names of locales for my Office 365 and MDT deployments was now a piece of cake!

….and All thanks to the Power of Shell!

Listing the MDT Variables with Windows Powershell


I was trying to find out Where MDT got all of it’s lovely variables like %SERIALNUMBER% when I stumbled across this Great post from Andrew Barnes on Variables.DAT.

So I popped over and swiped a VARIABLES.DAT file from the WinPE environment and looked at it.   As Andrew stated, it was XML data.  I find reading XML data about as exciting as painting a bathroom wall with a toothbrush.

Yes, not all the enticed about it, am I?

But I DO love playing with Windows Powershell.  And since it can NATIVELY deal with XML files (unless my poor fuzzy human brain) I choose it!

 

So if you’d like an EASIER way to see what variables you have in VARIABLES.DAT to see how you could play about with them, here is an easier way.

[XML]$MDTVars=GET-CONTENT Variables.DAT

$MDTVars.MediaVarList.var.Name

This will dump a list of all the Variable names.  Would you like them sorted?  Sure!

$MDTVars.MediaVarList.var.Name | SORT-OBJECT

“Wait! I want to see the values too!”

(Fussy fussy fussy!)

*poof!*

$MDTVars.MediaVarList.var | SORT-OBJECT

Not anything truly amazing in Powershell but it’s something I found particularly handy today Smile

By the way, Thanks Andrew on that post! Made MY day easier!

Sean
”The Energized Tech”