I'm setting up a Powershell script to provide some user notifications. The notification is legal in nature and may be updated/changed from time to time so it must be fairly easy to locate. It also has a few 'fill in the blank' variables that depend on the person receiving the notification.
I wanted to have a secondary Powershell file that contained the copy (text) to be used, so something like...
$body = "By accessing this system, you agree that your name ($currentUserName) and IP address ($currentUserIPAddr) will be recorded and stored for up to ($currentUserRetentionPeriod)."
The file could be updated as needed without actually opening the script, finding the line to edit, and potentially messing up other items/just being difficult. However, I'm looping through several thousand users in a single execution, so all the $currentUser... variables will be re-used frequently. This poses a problem because $body tries to get the variables immediately and acts as a static string instead of evaluating the variable contents each time it's invoked.
Is there a clever way for me to define $body a single time (i.e. not inside a loop) but still allow for redefinition of internal variables? I'd also rather not split the string up into multiple parts so it became $part1 + $var1 + part2 + var2....n+1 times.
A simple approach would be to just dot-source the script containing the copy whenever you need the variable "re-compiled":
BodyDef.ps1:
$body = "By accessing this system, you agree that your name ($currentUserName) and IP address ($currentUserIPAddr) will be recorded and stored for up to ($currentUserRetentionPeriod)."
Send-Notification.ps1
$bodyDefPath = (Join-Path $PSScriptRoot BodyDef.ps1)
foreach($user in Get-Users){
$currentUserName = $user.UserName
$currentUserIPAddr = $user.IPAddress
$currentUserRetentionPeriod = $user.RententionPeriod
. $bodyDefPath
Send-MailMessage -Body $body
}
The above would work just fine, but it's not very powershell-idiomatic, and kind of silly, reading the file over and over again.
As suggested in the comments, you should define a second function (or just a scriptblock) if you want to reuse the same template with different values:
Send-Notification.ps1
# You could as well define this as a function, doesn't make much difference
$NotificationSender = {
param($User)
$body = "By accessing this system, you agree that your name ($($user.UserName)) and IP address ($($user.IPAddress)) will be recorded and stored for up to $($user.RetentionPeriod)."
Send-MailMessage -Body $body
}
foreach($user in Get-Users){
& $NotificationSender -user $user
}
Related
I have a Powershell script that will make a GET request to SonarQube's REST API to check if the Quality Gate passes or fails. If the Quality Gate fails, the pipeline will fail. I am able to make this work when only looking at the master branch however, I am trying to look at all branches and pull requests.
My pipeline Powershell script:
- job:
pool:
name: 'POEM-GBT-Agent'
variables:
- group: SonarQube
displayName: 'SonarQube API'
steps:
- checkout: none
- powershell: |
$token = [System.Text.Encoding]::UTF8.GetBytes("$(SONARQUBE_API_TOKEN)" + ":")
$base64 = [System.Convert]::ToBase64String($token)
$basicAuth = [string]::Format("Basic {0}", $base64)
$headers = #{ Authorization = $basicAuth }
if ($(System.PullRequest.PullRequestId)) {
$param = "pullRequest=$(System.PullRequest.PullRequestId)"
}
else {
$param = "branch=$env:$BRANCH_NAME"
}
$result = Invoke-RestMethod -Method Get -Uri https://sonarqube.tjx.com/api/qualitygates/project_status?projectKey=$(sonarProjectKey)"&"$param -Headers $headers
$result | ConvertTo-Json | Write-Host
if ($result.projectStatus.status -ne "OK") {
Write-Host "##vso[task.logissue type=error]Quality Gate Failed"
Write-Host "##vso[task.complete result=Failed]"
}
env:
BRANCH_NAME: replace('$(Build.SourceBranch)', 'refs/heads/', '')
This results in an error saying:
+ $param = "branch=$env:$BRANCH_NAME"
+ ~~~~~
Variable reference is not valid. ':' was not followed by a valid variable name
character. Consider using ${} to delimit the name.
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : InvalidVariableReferenceWithDrive
After receiving this error I changed my conditional statement to:
if ($(System.PullRequest.PullRequestId)) {
$param = "pullRequest=$(System.PullRequest.PullRequestId)"
}
else {
$param = "branch=${env}:$BRANCH_NAME"
}
After changing my conditional I get this error:
System.PullRequest.PullRequestId : The term 'System.PullRequest.PullRequestId'
is not recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included, verify
that the path is correct and try again.
At D:\GBT\agent\Workspace\_temp\0a756446-474a-4d58-94ff-ad25e38c3c7a.ps1:9
char:7
+ if ($(System.PullRequest.PullRequestId)) {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.PullRequest.PullRequestI
d:String) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : CommandNotFoundException
I am trying to set $param to pullRequest=1234 if the Git PR is affected, otherwise, I want to set $param to something like branch=feature/my-branch-name.
You have two problems here. I'll delve into each below.
The term 'System.PullRequest.PullRequestId' is not recognized
Under your job step, define some environment variables. I have limited experience with Azure DevOps (will reference as ADO from here on) but you will need two under your powershell environment variable definitions (you already have BRANCH_NAME but I will include it below):
- job:
steps:
- powershell: |
YOUR
CODE
HERE
env:
BRANCH_NAME: replace('$(Build.SourceBranch)', 'refs/heads/', '')
PULL_REQUEST_ID: $(System.PullRequest.PullRequestId)
You need to define PULL_REQUEST_ID as an env var because your rendered PowerShell script won't render $() as an inserted value from ADO. This is likely by design as $() is syntax used in other programming languages, including PowerShell. This is the crux of your issue where System.PullRequest.PullRequestId can't be found as a command; PowerShell literally tries to use that as a program name which it can't find.
Within your script, you can then reference the pull request ID with:
$env:PULL_REQUEST_ID
Variable reference is not valid. ':' was not followed by a valid variable name character
The issue is distinct from the issue above, but the solution is the same. Just reference the environment variable as:
$env:BRANCH_NAME
You don't need the extra $, as this just confuses the parser.
Now, you're not asking for this, but if you needed a conditional environment variable name (e.g. the name of the environment variable is sourced from some other ADO variable), you need to use a different syntax to access the environment variable, as the $ confuses the parser when accessed via the $env: syntax. Read on if you are curious about this.
If you did need to access an environment variable whose name is conditional
PowerShell doesn't allow special characters in variable names without specifying the variable name as a string. The notable exceptions are _ (no special meaning) and :, the latter of which is used as a separator for variables defined within a PSDrive. Environment variables are accessible via the Environment provider, which is accessed under the Env PSDrive. You can see this by running Get-PSDrive, there is an Environment provider exposed under the Env drive.
You can reference PSProvider variables in a few ways, the most common way is $drive:path (the variable name is technically considered a path node under the provider). So to reference the UserProfile variable, you can use:
$env:UserProfile # ====> Returns the path to your user profile directory
The problem with your code is you have the following:
$env:$VariableName
In this case the intent is to get an environment variable value whose name is based on another variable's value, but this confuses the parser for this syntax, as $ is now being translated as a literal portion of the variable name, which is invalid. Normally you would use the ${} syntax to avoid this... but you can't here, because the confused portion should be rendered as part of that same variable. In this case, you need to use an alternative approach to access the environment variable, go through the provider directly.
To use UserProfile as an example again:
$envVarName = 'UserProfile'
$envVarValue = (Get-ChildItem Env:/$envVarName).Value
This is the other way to get a value from a PSProvider; traverse its contents as a drive. This works somewhat like a filesystem, though Get-ChildItem will return different properties for different provider types than the FileInfo or DirectoryInfo objects you may be used to from the Filesystem provider. It's a bit cumbersome, but luckily it's not a scenario one often needs to account for.
I need to find a way to generate a random number each time the REST call is executed.
I have the following GET call:
exec(http("Random execution")
.get("/randomApi")
.queryParam("id", getRandomId()))
}
Obviously it doesn't work as the random number is only generated once and I end up with the same
number whenever this call is executed. I cant use the feeder option as my feeder is already huge and is generated by a 3rd party for each test.
.queryParam takes Expressions as its arguments, and since Expression is an alias for a session function, you can just do...
.queryParam("id", session => getRandomId())
You could also define a second feeder that uses a function to generate the values - no need to update your existing feeder or add another csv file. This would be useful if you had more complicated logic for getting / generating an Id
val idFeeder = Iterator.continually(Map("id" -> Random.nextInt(999999)))
//in your scenario...
.feed(idFeeder)
.exec(http("Random execution")
.get("/randomApi")
.queryParam("id", "${id}")
)
In the spirit of having options, another option you have is to store an object in the session that support toString, which generates whatever you need. It's a nifty trick that you can use for all kinds of things.
object RANDOM_ID {
toString() { return RandomId().toString() }
}
...
exec( _.set( "RANDOM_ID", RANDOM_ID ) )
...
.exec(
http("Random execution")
.get("/randomApi")
.queryParam( "id", "${RANDOM_ID}" )
)
You can apply the same principle to generating random names, addresses, telephone numbers, you name it.
So, which is the better solution? The feeder, or the object in session?
Most of the time, it'll be the feeder, because you control when it is updated. The object in session will be different every time, whereas the feeder solution, you control when the value updates, and then you can reference it multiple times before you change it.
But there may be instances where the stored object solution results in easier to read code, provided you are good with the value changing every time it is accessed. So it's good to know that it is an option.
I'm trying to add a custom header for AIP's msip_labels to a Powershell script that I'm writing. I've figured out how to do this with .Net.SMTP using:
$message.Headers.Add("msip_labels","MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Enabled=True; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SiteId=00000000-1111-2222-3333-444444444444; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Owner=user2#domain.tld; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SetDate=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")); MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Name=Internal; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Application Microsoft Azure Information Protection; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_ActionId ffffffff-5555-6666-7777-888888888888; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Extended_MSFT_Method Manual")
Based on research I've done this should work using Outlook 2016 in theory:
$Outlook = New-Object -ComObject Outlook.Application
$message = $Outlook.CreateItem(0)
$message.PropertyAccessor.SetProperty("http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/msip_labels", "MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Enabled=True; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SiteId=00000000-1111-2222-3333-444444444444; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Owner=user2#domain.tld; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SetDate=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")); MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Name=Internal; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Application=Microsoft Azure Information Protection; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_ActionId=ffffffff-5555-6666-7777-888888888888; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Extended_MSFT_Method=Manual")
$message.To = "user1#domain.tld"
$message.Cc = "user3#domain.tld"
$message.Subject = "Report"
$message.HTMLBody = #"
<p><font face = "Calibri" size = "3">Hello World</p></font>
"#
$reportMessage.Send()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Outlook) | Out-Null
I confirmed this using a MAPI viewer that this is what's used in Outlook itself with other emails I've sent using just Outlook. But, when I tried running this in my script I get this error:
Exception setting "SetProperty": Cannot convert the "MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Enabled=True; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SiteId=00000000-1111-2222-3333-444444444444;
MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Owner=user2#domain.tld; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_SetDate=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")); MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Name=Internal;
MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Application=Microsoft Azure Information Protection; MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_ActionId=ffffffff-5555-6666-7777-888888888888;
MSIP_Label_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_Extended_MSFT_Method=Manual" value of type "string" to type "Object".
At C:\emailtest.ps1:21 char:1
+ $message.PropertyAccessor.SetProperty("http://schemas.microsoft ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : RuntimeException
Which doesn't seem to make much sense it's suppose to be a string in the MAPI schema, so I'm not sure why it thinks it should be an object. I even tried converting those values to objects using ConvertFrom-String, but it didn't work. Any advice on this would be greatly appreciated.
Try to reduce the string length you pass as a value. Does it work correctly?
It seems you need to use a low-level API on which Outlook is based on - Extended MAPI. It doesn't have any restrictions on the string length unlike OOM if you use the OpenProperty method. Also, you may consider using any third-party wrapper around that API such as Redemption.
If you use PropertyAccessor, you must have good knowledge of exception handling logic. Below I list some roadblocks that you may run into:
The body and content of an attachment of an Outlook items are not accessible through PropertyAccessor.
The PropertyAccessor ignores any seconds of the date/time value
String properties are limited in size depending on the information store type.
Limitation: Personal Folders files (.pst) and Exchange offline folders files (.ost) cannot be more than 4,088 bytes.
Limitation: For direct online access to Exchange mailbox or Public Folders hierarchy, the limit is 16,372 bytes.
Binary properties only those whose values are under 4,088 byte can be retrieved or set. (If trying to use larger values, you get an out-of-memory error).
You may find the Don't stumble over a stone working with the PropertyAccessor and StorageItem classes in Outlook 2007 article helpful.
It worked for me by appending a null character at the end of the string ( "`0" ) This is required by type "PtypString", required by property.
https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/0c77892e-288e-435a-9c49-be1c20c7afdb
I have almost 20 resources in azure, 4 of them have been given Tags #
{"Office1work"="work"}
{"Office2practice"="Practice"}
{"Office3practice"="Practice"}
{"Office4practice"="Practice"}
Now I want to get the resources whose Tag names start with the keyword "Office".
I know to get a resource by a TagName,for example "hello", I simply use the following command,
get-azureRmResource -TagName "Hello"
How can I use the -Tagname property of get-azurermresource to give me all resources whose tags are starting with the keyword "Office" ?
Or is there any other good method to get all resources whose Tags start with a particular string?
Thanks :)
You can use this code snippet:
$resources = Get-AzureRmResources
$resources.foreach{ if ($PSItem.tags.keys -match '^Office') { $PSItem } }
First you get all the resources in the subscription, then you filter out all the resource whose tags do not match the 'Office' "expression".
as #LotPings points out, it would probably make more sense to filter without saving to a temporary variable:
$resources = Get-AzureRmResources|Where-Object {$_.tags.keys -match "^Office"}
Also, I didnt notice you were asking for a starts with filter, so you should use ^Office as a more strict filter (if you need to).
I've long had a bunch of VBS automations for IIS 6, including one that gets/sets complex server bindings on several farms of paired servers, each having dozens of apps, each app having 3-12 host headers. Each app has hostname, hostname-fullyqualified, and Disaster Recovery enabled hostname, so they can be a mess to maintain manually.
I did all my vbs stuff using ADSI, but I'm thinking WMI is probably more flexible than ADSI from a full server maintenance perspective. Please correct me if I'm wrong. So now I'm trying to move up to PowerShell + WMI to prepare for Windows 2008 + IIS 7.5. I'm enjoying the learning process, but I've hit a roadblock on this problem.
I can get/set all properties via WMI on my IIS 6 web servers, except ServerBindings. I feel like I'm close, but I'm missing some layer of containment, and I just can't get the objects I'm building to cast over to the right automation object.
The following code gets and reads the ServerBindings just fine. I simply can't figure out a way to write my changes back. Any advice is welcomed.
$objWMI = [WmiSearcher] "Select * From IISWebServerSetting"
$objWMI.Scope.Path = "\\" + $server + "\root\microsoftiisv2"
$objWMI.Scope.Options.Authentication = 6
$sites = $objWMI.Get()
foreach ($site in $sites)
{
$bindings = $site.psbase.properties | ? {$_.Name -contains "ServerBindings"}
foreach ($pair in $bindings.Value.GetEnumerator())
{
# The pair object is a single binding and contains the correct data
$pair
$pair.IP
$pair.Port
$pair.Hostname
# And this line will successfully erase the contents of
# the ServerBindings
$bindings.Value = #{}
# but I can't figure out what to do to update $bindings.Value
}
$site.Put()
}
I'm liking Powershell so far, so thanks for any help you're able to offer.
Alright. I got distracted with major disk failures. The fun never stops.
Anyway, the solution to this problem is simpler than I'd made it:
process
{
$bindings = $_.ServerBindings
foreach ($binding in $bindings)
{
$binding.IP = $ip
$binding.Port = $port
$binding.Hostname = $hostname
}
$_.ServerBindings = $bindings
$_.Put()
}
ServerBindings is an array, but it likes to be an array of its own kind. I was trying to build the array from scratch, but my home-rolled array didn't smell right to Powershell. So, pull the array out of ServerBindings into a new variable, manipulate the variable, then assign the manipulated variable back to the ServerBindings property. That keeps all the right typing in place. It's smooth as silk, and seems easier than ADSI.