Different ways I have automated Processes using Powershell.

Using AzureAutomate to replace all Scheduled Tasks.

I found that scheduled tasks just suck. You cannot go back and see what happened to the scheduled tasks. When you have to change a password to say a service account you have to change each scheduled task. It also can keep your SQL creds and other creds out of your code out of the box. I’ll post some examples

You can implement Source control with your Azure DevOps. So, when you push changes from your repository, it will add to your runbooks.
Scheduling can be very customized
So, you can store anything. I store SQL info, APPIDs / Secret, this can also store certificates which you can call with some commands I’ll show below.
As you can see here you can assign the Credential to a Variable.
You can drill down in a job and see what happens. Instead of Write-Host you will have to use a Write-Output. This will effectively show you what the script did.
You can use the All Logs tab to see more granular information. It will show you any errors or warnings. There is also an option to run everything in verbose.

There is so many things you can use with AzureAutomate, just explore. There is also a feature to have a run AS account for your hybrid worker / Azure worker. So, you can run all the scripts as a single user. I forget to talk about pricing but for the most part I run close to 4K jobs daily and the cost is around 20 dollars a month.

Using Microsoft Forms / PowerApps + Flow + To Azure Automate.

This allows you to quickly make a form you can hand to the helpdesk team that they can use quickly. You can make this external as well if needed. I do have a form for a vendor we work with so they can add users to a group from their end so they can SSO.

Kurt always breaks things
What the flow looks like on the backend. Basically, takes the information on the form and creates a Job in Azure Automate. If you need to run the command on prem make sure you click advanced under the create job and add the name of your hybrid worker.
What the job looks like on the intake side. Just give it parameters, so when you select it in the flow it will have the parameters already in there, as you can see in the above picture.

Powerapps uses the same concept as above, but you can customize it more than a form. It’s not the prettiest thing in the world since I suck at UI, but it does what you need to do. This PowerApp is mainly given to our helpdesk. I give them access to this instead of direct access to AD / Exchange / O365.

Some quick actions our field technicians have, and you can access this all via Mobile APP which is a cool thing.

Using Universal Dashboard

https://universaldashboard.io This allows you to basically create a website on the frontend but it is all programmed with Powershell which is amazing, and easy to work with. It can use SSO, you can also build API endpoints that you can invoke via rest which is amazing.

This is just an example of a webpage I created for our SOC team. It has a Password never expires, Built-in Admin Accounts, ServiceAccount with Domain Admin Tabs. The following is a modal that allows a helpdesk user build a Admin account, they have to put in the ticket number with the approvals and for the most part it keeps our compliance team happy as all of this info is logged into a SQL database.

There is much more you can do with Universal Dashboard. It is very fun to use but does cost 500 a year for a single website.

Powershell Studios

https://www.sapien.com/software/powershell_studio This is the way I used to build UI Apps with Powershell. It is still relevant, but windows forms / WPF is just old school. I have built 2 apps that our company uses.

The main purpose of this app was to give helpdesk access to perform tasks in a safe manner. So, at this company, we are a hosting provider which means we handle multiple companies. That means that all of the customers sit on the same AD Forest. So, if we were to give helpdesk access to AD / Exchange. They could do something like Get-Mailbox | Set-mailbox which would affect all customers instead of one, that would be a bad day.
We manage a big exchange environment. We also do not have SCCM, but this tool allows you to update Exchange on multiple nodes at the same time, and with exchange you need to properly fail it over before you reboot and turn off bitlocker. This app has been pretty huge to the team, and we use it on a daily basis. Anything that happens in the app writes to a teams channel so everyone in the team is notified if someone is working on a server.

There are soo many ways you can automate with Powershell and I wanted to share my experiences and how these apps have changed my life to make it much easier in the long run. Most of these apps take a very long time to build, but when it is finished, you get soo much relief from the day to day / busy work tasks. If you have any questions on how I used these apps feel free to contact me on LinkedIn.


Prepping Servers for Dell Open Managed Enterprise

So at one of my companies we lease servers from a large hosting company (we only purchase Dell). When they set them up for us, the default name for the Drac DNS name is the server tag. When using Open Manged Essentials it wasn’t a big deal. However, in Enterprise since it actually connects to DRAC (you have to put the DRAC subnet and password to manage with Enterprise) If you don’t know about Open Managed Enterprise give it a shot! It runs on CentOS and the install is pretty easy to use, you just load up the VHD in VMWare or Hyper-V and roll out. The firmware updates also can be staged to run on reboot, so if you need to do certain fail overs it won’t start until the machine is rebooting. It also utilizes the LifeCycle controller to update the box, which makes it more stable, because I ran into issues running Intel NIC Firmware updates causing blue screens. Anyways, here is the powershell Script, I made to Prep all my servers for Open Manage Enterprise.

This Script will grab all the computers in AD (you can filter it how you want), Check to see if it can run racadm. If it can, it will run the following commands, I will explain what each one does. Each command has the legacy command also, I will keep it in there encase you aren’t running DRAC 7+

  • racadm config -g cfgLanNetworking -o cfgDNSRacName $computer;racadm set iDRAC.NIC.DNSRacName $computer Sets Name for DRAC
  • racadm config -g cfgUserAdmin -o cfgUserAdminPassword -i 1 supersecurepassword;racadm set iDRAC.Users.1.Password supersecurepassword Sets Admin password for index 1 and index 2 which should be root. I had issues with index 1 being root in the past, if anon is not there, so i just kept it there and let it error.
  • racadm racreset This just resets DRAC, takes about 10 minutes.
$computers = Get-ADComputer -filter *

foreach($computer in $computers)
	$cn = $computer.name 

	$rac = Invoke-Command -ComputerName $cn -scriptblock {racadm getsysinfo} -ErrorAction SilentlyContinue
		Write-Host "Updating DRAC info for $cn" -ForegroundColor Yellow
		Invoke-Command -ComputerName $cn -ScriptBlock {$Computer = hostname; racadm config -g cfgLanNetworking -o cfgDNSRacName $computer;racadm set iDRAC.NIC.DNSRacName $computer}
		Invoke-Command -ComputerName $cn -scriptblock {racadm config -g cfgUserAdmin -o cfgUserAdminPassword -i 1 supersecurepassword;racadm set iDRAC.Users.1.Password supersecurepassword}
		Invoke-Command -ComputerName $cn -scriptblock {racadm config -g cfgUserAdmin -o cfgUserAdminPassword -i 2 supersecurepassword;racadm set iDRAC.Users.2.Password supersecurepassword}
		Invoke-Command -ComputerName $cn -scriptblock {racadm racreset}
		Write-Host "DRAC is not Available on $CN" -ForegroundColor Red
$rac = $null

Creating an On Call Calendar Rotation with Powershell

Our company was using a Sharepoint Calendar for all of the on call stuff. Which was very hard to manage especially if we got a new employee, swap on call weeks, etc. So I came up with an idea to create a webpage as well as sending an email every Tuesday (Our swap day) to notify everyone who is on call. It also provided the accurate dates on when you would be on call.

Above is the sample HTML file created by my script. I created a scheduled task to run this script nightly to keep the remote days updated. It also will rotate the user when Tuesday comes around to the bottom. You can change it to what ever you need. To run this, just create a CSV with the headers “Name”,”Email”,”Cell”,”Area” in the folder you are running the Powershell Script. If you have any problems just let me know!

[int]$DateCounter = 0 [int]$StartDateCounter = 0 [int]$CycleCount = 0 [String]$DayofWeek = (get-date).DayOfWeek [Switch]$SendMail = $False [String]$Runtime = get-date -Format g Set-Location $PSScriptRoot If ($DayOfWeek -ne "Tuesday") { $DoNotRotate = $True } $Website = @() $NewList = @() $Website += "<html>" $Website += "<head>" $Website += " <title>IT Engineering Schedule</title>" $Website += "<style>" $Website += "body {" $Website += " color: #666;" $Website += ' font-family: /*"Open Sans", */Arial, Helvetica, sans-serif;' $Website += " line-height: 1.5;" $Website += " font-size: 16px;" $Website += "}" $Website += ".container {" $Website += " max-width: 800px;" $Website += " width: 80%;" $Website += " margin: 50px auto 0;" $Website += "}" $Website += "" $Website += "a {" $Website += " color: #0053a0;" $Website += " text-decoration: none;" $Website += "" $Website += "}" $Website += ".notice {" $Website += " display: block;" $Website += " font-weight: bold;" $Website += " color: rgb(177, 39, 39);" $Website += " border: 1px solid #ccc;" $Website += " border-radius: 2px;" $Website += " padding: .5em 5%;" $Website += " background-color: #f1f1f1;" $Website += " text-align: center;" $Website += " font-size: 1.2em;" $Website += "}" $Website += ".calendar {" $Website += " display:block;" $Website += " text-align: left;" $Website += " font-size: 1.5em;" $Website += " font-weight: bold;" $Website += " color: black;" $Website += " background-color: #FFFFFFF;" $Website += " -webkit-transition: background-color .1s ease-in-out;" $Website += " -moz-transition: background-color .1s ease-in-out;" $Website += " -o-transition: background-color .1s ease-in-out;" $Website += " transition: background-color .1s ease-in-out;" $Website += "}" $Website += ".calendar:hover {" $Website += " color: white;" $Website += " background-color: #6F94B6;" $Website += " cursor: pointer;" $Website += " -webkit-transition: background-color .1s ease-in-out;" $Website += " -moz-transition: background-color .1s ease-in-out;" $Website += " -o-transition: background-color .1s ease-in-out;" $Website += " transition: background-color .1s ease-in-out;" $Website += "}" $Website += ".calendar:active {" $Website += " background-color: #A7BED2;" $Website += " color: white;" $Website += "}" $Website += "" $Website += "p, h3 span {" $Website += " color: #989898;" $Website += " font-size: 80%;" $Website += "}" $Website += "" $Website += "@media all and (max-width: 600px) and (min-width: 0px) {" $Website += " body {" $Website += " font-size: 14px;" $Website += " }" $Website += " .container {" $Website += " margin: 0 auto;" $Website += " width: 95%;" $Website += " }" $Website += " .notice {" $Website += " padding: .5em .5em;" $Website += " }" $Website += " .calendar {" $Website += " font-size: 1em;" $Website += " padding: 2%;" $Website += " }" $Website += "}" $Website += "" $Website += "li { background:white; }" $Website += "li.odd { background: #f1f1f1; }" $Website += "table {" $Website += " width:100%;" $Website += "}" $Website += "table, th, td {" $Website += " border-collapse: collapse;" $Website += "}" $Website += "th, td {" $Website += " padding: 1px;" $Website += " text-align: left;" $Website += "}" $Website += "table#t01 tr:nth-child(even)" $Website += "{" $Website += " background-color: #eee;" $Website += "}" $Website += "table#t01 tr:nth-child(odd)" $Website += "{" $Website += " background-color: #fff;" $Website += "}" $Website += "table#t01 th {" $Website += ' background-color: #989898;' $Website += " color: white;" $Website += "}" $Website += "</style>" $Website += "</head>" $Website += "<body>" $Website += '<div class="container">' $Website += '<p class="notice">IT Engineering Schedule. If you are Highlighted you are On Call :)</p>' $Website += "" $Website += "<h3>On Call List</h3>" $Website += "<table>" If ($DonotRotate) { $NewList = Import-Csv ".\OnCallList.csv" } Else { #Check if Already Rotated this Tuesday [string]$TuesdayDate = get-date -Format MM.dd.y $TuesdayCheck = Get-Content ".\rotatecheck.txt" $CheckTuesday = $TuesdayCheck | Select-String $tuesdaydate If ($CheckTuesday) { $NewList = Import-Csv ".\OnCallList.csv" } Else { #Append Date to File Out-File -FilePath ".\rotatecheck.txt" -Append -InputObject $TuesdayDate #Gather List of users $ListofUsers = Import-Csv ".\OnCallList.csv" foreach ($User in $ListofUsers) { #Move Top of the list to bottom If ($User -eq $ListofUsers[0]) { #Skip the user } Else { $NewList += $User } } #Add the First user to bottom $NewList += $ListofUsers[0] $NewList | Export-Csv -path ".\OnCallList.csv" -NoTypeInformation -Force $SendMail = $True $OnCallUser = $NewList[0].Name $Subject = "$OnCallUser is on call this week" $OnCallEmail = $NewList[0].Email } } Foreach ($OnCall in $NewList) { $Website += "<tr>" if ($DonotRotate -and $CycleCount -eq 0) { $i = 0 do { $LastTuesday = (Get-Date).AddDays(--$i).Date } until ($LastTuesday.DayofWeek -eq 'Tuesday') $StartDate = $LastTuesday.ToString('MM-dd-yyyy') $Plus6Days = $LastTuesday.AddDays(6).ToString('MM-dd-yyyy') $Website += '<td> <font color="Red">' + "$($OnCall.Name)" + "</td>" $Website += '<td> <font color="Red">' + "$($OnCall.Cell)" + "</td>" $Website += '<td> <font color="Red">' + "$($OnCall.Area)" + "</td>" $Website += '<td> <font color="Red">' + "$StartDate - $Plus6Days" + "</td></font>" [int]$Math = 7 + $i $StartDateCounter = $StartDateCounter + $Math } Else { $DateCounter = $StartDateCounter + 6 $StartDate = (Get-Date).AddDays($StartDateCounter).ToString('MM-dd-yyyy') $Plus6Days = (Get-Date).AddDays($DateCounter).ToString('MM-dd-yyyy') If ($CycleCount -eq 0) { $Website += '<td> <font color="Red">' + "$($OnCall.Name)" + "</td>" $Website += '<td> <font color="Red">' + "$($OnCall.Cell)" + "</td>" $Website += '<td> <font color="Red">' + "$($OnCall.Area)" + "</td>" $Website += '<td> <font color="Red">' + "$StartDate - $Plus6Days" + "</td></font>" } Else { $Website += "<td> $($OnCall.Name) </td>" $Website += "<td> $($OnCall.Cell) </td>" $Website += "<td> $($OnCall.Area) </td>" $Website += "<td> $StartDate - $Plus6Days </td>" } $StartDateCounter = $StartDateCounter + 7 } $Website += "</tr>" $CycleCount++ } $Website += "" $Website += " </ul>" $Website += " </table>" $Website2 = $Website $Website2 += "" $Website2 += "<h3>Who's Remote Today?</h3>" $Website2 += " <ul>" $Website2 += "" Switch ($DayofWeek) { "Tuesday" { $Website2 += "<li>Joe Shmoe</li>" $Website2 += "<li>Jon Smith</li>" $Website2 += "<li>Michael Jackson</li>" } "Wednesday" { $Website2 += "<li>R Kelly</li>" $Website2 += "<li>Alejandro Perez</li>" $Website2 += "<li>Anthony P</li>" $Website2 += "<li>Lewis Clarke</li>" $Website2 += "<li>Larry Bird</li>" $Website2 += "<li>Michael Jordan</li" } "Thursday" { $Website2 += "<li>Ken Streetfighter</li>" $Website2 += "<li>Cool Guy</li>" $Website2 += "<li>Funny Alvarez</li>" } "Friday" { $Website2 += "<li>R Kelly</li>" $Website2 += "<li>Alejandro Perez</li>" $Website2 += "<li>Anthony P</li>" } default { $Website2 += "<li> None </li>" } } $Website2 += "" $Website2 += " </ul>" $Website2 += "<p><span>Note: The List rotates every Tuesday Morning. If you need to swap with anyone ask a person close to your rotation.<br>" $Website2 += "An Email is Generated every Tuesday Morning. Script was Last Ran at $RunTime </span></p>" $Website2 += "" $Website2 += "" $Website2 += "<br>" $Website2 += "</body>" $Website2 += "</html>" $Website += "<p><span>Note: The List rotates every Tuesday Morning. If you need to swap with anyone ask a person close to your rotation.<br>" $Website += "An Email is Generated every Tuesday Morning. Script was Last Ran at $RunTime </span></p>" $Website += "" $Website += "" $Website += "<br>" $Website += "</body>" $Website += "</html>" $website2 | Out-File ".\index.html" -Force if ($SendMail) { $Body = $website | Out-String Send-MailMessage -From ITEngineeringoncall@contoso.com -To "itengineering@contoso.com" -BodyAsHtml -Body $Body -SmtpServer smtp.wellstar.org -Subject $Subject }

Shared Mailboxes in Hybrid Exchange

So, when I started my 2nd Job one of the main tasks was to migrate all the users from Exchange 2010 to the O365. Once all of that was completed, I planned on removing Exchange, however I found that it is recommended to keep at least 1 Exchange server to run Exchange commands to if you plan to keep your AD on Prem. If you live on the wild side. You can edit AD attributes manually / script to create users, and have DIRSync/ADConnect pick it up. But that would be a hassle, instead of just doing a New-RemoteMailbox command. So I built an Exchange 2016 Box using the Hybrid License, and decommed the others. At the time I was on CU7. The problem I was having is that the New-RemoteMailbox command did not have an option to create shared mailboxes directly to the cloud. So I had to create a normal mailbox and convert it or create the SharedMailbox on Prem and migrate it. However, I found out in the later CUs they added a parameter to allow Creation of Shared Mailbox directly to the cloud.

Note: This switch is available only in Exchange 2013 CU21 or later and Exchange 2016 CU10 or later. To use this switch, you also need to run setup.exe /PrepareAD. For more information, see KB4133605.

This is a Function I created for our O365 Admins to quickly create a mailbox, and also a group to assign the users that need Full/SendAS access to the mailbox. I might have some other proprietary code such as removing the internal domain, but you can comment that stuff out if needed, but it shouldn’t hurt if it stayed in there.

Function New-SharedMailbox { Param ( [Parameter(Mandatory = $true)] [String]$Name, [Parameter(Mandatory = $true)] [String]$DisplayName, [Parameter(Mandatory = $true)] [String]$OwnerEmail ) $EmailAddress = $Name + '@Tenant.org' $MBCheck = Get-Recipient $EmailAddress -ErrorAction SilentlyContinue $Replace = '@tenant.int' If ($MBCheck) { Write-Host "EmailAddress Is already in Use Please use a different Name" } Else { $RoutingAddress = $EmailAddress -replace ("Tenant.org", "Tenant.mail.onmicrosoft.com") Do { $RoutingAddressCheck = Get-Recipient $RoutingAddress -ErrorAction SilentlyContinue if ($RoutingAddressCheck) { [String]$RandomNumber = (10 .. 100) | Get-Random [String]$RoutingAddress = $Name + $RandomNumber + '@Tenant.mail.onmicrosoft.com' } $RoutingAddressCheck = Get-Recipient $RoutingAddress -ErrorAction SilentlyContinue } Until($RoutingAddressCheck -eq $Null) Write-Host "Enabling $Name to Cloud" New-RemoteMailbox -Alias $Name -OnPremisesOrganizationalUnit "OU=Email,OU=Accounts,DC=tenant,DC=int" -shared -RemoteRoutingAddress $RoutingAddress -userprincipalname $EmailAddress -PrimarySmtpAddress $EmailAddress -DisplayName $DisplayName -name $Name Write-Host "waiting on AD Replication 30 Seconds" Sleep 30 $ProxyAddresses = (Get-ADUser $Name -Properties ProxyAddresses).ProxyAddresses Foreach ($ProxyAddress in $ProxyAddresses) { If ($ProxyAddress -like "*tenant.int*") { Write-Host "Removing $Replace from $Name" Set-ADUser $Name -Remove @{ Proxyaddresses = $ProxyAddress } } } $GroupName = $Name + '-OW' $GroupEmail = $GroupName + '@Tenant.org' Write-Host "Creating Group $GroupName" New-DistributionGroup -Name $GroupName -PrimarySmtpAddress $GroupEmail -ManagedBy $OwnerEmail -RequireSenderAuthenticationEnabled:$True -CopyOwnerToMember:$True -OrganizationalUnit "OU=USG,OU=Email,OU=Accounts,DC=tenant,DC=int" -Type Security Write-Host "Hiding From Address List" Sleep 15 Set-DistributionGroup $GroupName -HiddenFromAddressListsEnabled:$True Write-Host "All Done.. Make sure to add the group with full / sendas on O365 after 30 min Sync" -ForegroundColor Green } }

Creation of Contacts, for Email to Text on Phone

At my healthcare organization a nursing leader needed to push alerts via email, to users Cell Phones. She had a list of phone numbers and names of the users (around 100). So when she sent me the list I asked if she could make another column with the providers next to them so I can automate it. Below is a script I created to create the Contacts. I also added comments on everything, to help a novice powershell user to better understand how everything works. Remember to Change the OU Location at the bottom of the script, Could probably make a new Parameter for that. Also, to run the command you would just copy and paste the function in Exchange Powershell then Run New-CellContact -FirstName John -LastName Doe -Provider Cricket -CellNumber 8774878777

Function New-CellContact
    Param (
        [parameter(Mandatory = $true)]
        #This means if you run New-CellContact, It will need this value to Continue
        [parameter(Mandatory = $true)]
        [parameter(Mandatory = $true)]
        [ValidateSet("ATT", "Sprint", "Cricket", "Tmobile", "Verizon", "StraightTalk", "MetroPCS")]
        #Validating Set means, when providing the cmdlet the data you will need to give it the one listed or it will fail
        [parameter(Mandatory = $true)]
        #Validating Pattern Uses RegEx This RegEx is saying there needs to be 10 Digits \d is Numbers anything more or less or with a letter will fail.
    #This switch is a faster IF statement Basically if $Provider = ATT then $CarrierEmail = @mms.att.net
    Switch ($Provider)
        "ATT" { $CarrierEmail = "@mms.att.net" }
        "Sprint" { $CarrierEmail = "@messaging.sprintpcs.com" }
        "Cricket" { $CarrierEmail = "@mms.mycricket.com" }
        "Tmobile" { $CarrierEmail = "@tmomail.net" }
        "Verizon" { $CarrierEmail = "@vtext.com" }
        "StraightTalk" { $CarrierEmail = "@vtext.com" }
        "MetroPCS" { $CarrierEmail = "@mymetropcs.com" }
    #This builds out the Alias for the AD Object
    $Alias = 'Cell-Alerts-' + $FirstName + "-" + $LastName
    #This builds out the email address so 7708278377@vtext.com
    $ForwardingEmail = $CellNumber + $CarrierEmail
    #Write-Host "Testing $Alias $ForwardingEmail" this is for testing
    #This is the command to actually build the object using the parameters given and throw it in the correct OU
    New-MailContact -Alias $Alias -DisplayName $Alias -Name $Alias -FirstName $FirstName -LastName $LastName -ExternalEmailAddress $ForwardingEmail -OrganizationalUnit "Contoso.int/Contacts"