How to update datatable and commit back to sql server source - powershell

Alright so I have a table with some data in it. Here is a screenshot of the table def in SQL server:
And there is an app which populates some of the data in the table. Here is a screenshot of the sample data. All columns except for [emailSentCount] are populated by an external app.
Now my question is with with a Powershell script that I'm trying to build to consume this data and send email Notifications. I read all the content of the table in a DataTable. I go through each Row and decide if I have to send an email for that row. If so, then I send the email and I update the [emailSentCount] column by adding + 1 to it.
At the end of the script I'm trying to send these changes I made to the DataTable back to the table on SQL server. However I get an error:
Here is the script I'm working with.
param(
[string]$SQLServerName="SQLServerName\InstanceName"
,[string]$SQLDatabaseName="DBName"
,[string]$SQLTableName = "UserList"
,[string]$SQLSelectQuery="SELECT * FROM $SQLTableName"
)
cls
Function SendEmail
{
Param(
[string]$ToMailAddress,
[int]$MessageTemplate
)
[string]$MessageBody=$null
switch ($MessageTemplate)
{
1 {$MessageBody = Test new certificate issued. Please ignore!}
2 {$MessageBody = Test existing certificate renewed. Please ignore!}
}
$from = "EmailApplicationAccount#example.com"
$to = $ToMailAddress
$smtp = "smtp.example.net"
Send-MailMessage
-From $from
-To $to
-Subject $MessageBody
-SmtpServer $smtp
}
$sqlConn = New-Object System.Data.SqlClient.SqlConnection
$sqlConn.ConnectionString = “Server=$SQLServerName;Integrated Security=true;Initial Catalog=$SQLDatabaseName”
$sqlConn.Open()
$sqlCommand = $sqlConn.CreateCommand()
$sqlCommand.CommandText = $SQLSelectQuery
$dataAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $sqlCommand
$dataTable = New-Object System.Data.DataTable
$dataAdapter.Fill($dataTable) | Out-Null
foreach($dataRow in $dataTable.Rows)
{
write-host $dataRow["shouldSendEmail"]
if($dataRow["shouldSendEmail"] -eq $true)
{
# First send email depending on whether its a first time new cert or a cert renewal.
if($dataRow["certRenewal"] -eq $true)
{
SendEmail -ToMailAddress $dataRow["email"] -MessageTemplate 2
}
else
{
SendEmail -ToMailAddress $dataRow["email"] -MessageTemplate 1
}
# After you have sent the email, increase the emailSentCount value in the datatable.
$dataRow["emailSentCount"] = $dataRow["emailSentCount"] + 1
#Also reset the shouldSendEmail column to $false/0
$dataRow["shouldSendEmail"] = $false
}
}
$dataAdapter.Update($dataTable)
$sqlConn.Close()
It seems I need to include some Update command. But what will it look like in this context and wwhere does it need to be included?

Actually, I found the solution. We have to build an update command before we start messing with the data in the rows. Here is the bit I added just before the Foreach loop.
# command builder
$commandBuilder = new-object system.data.sqlclient.sqlcommandbuilder($dataAdapter)
$dataAdapter.UpdateCommand = $commandBuilder.GetUpdateCommand()
And that was it! No errors and I can see the data on columns [shouldSendEmail] and [emailSentCount] change on the source table in SQL server as intended in the script.

Related

Emails not sending when calling multiple scripts

Good afternoon -
I have a "trigger file" that calls several scripts to run some morning reports. Each called script contains code that attaches a file to an email and sends (via ComObject). If I run each script individually then there all emails send correctly. However, when i run the "trigger file", only a couple of the emails send and I am not receiving any error messages. Any ideas what is happening? I purposely made the trigger file run the scripts concurrently to save time. But is that overloading Outlook?
EDIT: Updated code to include Try/Catch block. There are 8 scripts that run using this template. All 8 successfully complete the excel open/run/save. However, only some of the emails send. And even with this Try/Catch block, no error message is being sent.
#Establish script file locale
$FullPath = “//fullpath/”
$SavePath = “//savepath/”
#Open Excel file
& {
$Excel = New-Object -ComObject excel.application
$Excel.Visible=$False
$Workbook = $Excel.Workbooks.Open($FullPath)
#Run Macro
$app=$Excel.Application
$app.Run("Macro1")
#Save and close Excel
$Excel.Application.DisplayAlerts=$False
$Workbook.SaveAs($SavePath,51)
$Workbook.Close()
$Excel.Quit()
}
#Send email with attachment
Try
{
$EmailSettings=#{
SMTPServer = "smtp"
From = "me#email.com"
To =
#(
"you#email.com"
)
Subject = "Subject"
Attachments = $SavePath
BodyAsHtml = $true
Body =
"<body><p>
Attached is the thing.
</b></p></body>"
}
Send-MailMessage #EmailSettings
}
Catch
{
$Subject = 'ERROR: '+$EmailSettings.Subject
$ErrorMessage = $_.Exception.Message+' '+$_.Exception.ItemName
Send-MailMessage -From me#email.com -To me#email.com -Subject $Subject -SmtpServer smtp -Body $ErrorMessage
}

I am trying to update the data source of an oledb data adapter and nothing happens

I am attempting to update data in an oledb datasource. I pull data from a table that who's schema has been written out as an access create table script (i could probably find a better way to get that schema information) and stored in the AccessCreateScript field of the lookup table.
I am able get the following things to work:
pull the data out of the source table on sql server
put it into a dataset
verify its good
create a second dataset based off the created table on the access file
copy the data from the source dataset to the destination dataset
verify that the data has been copied
create the update command using a command builder
verify the commands look correct
But, then when it comes to using the update method on the ole data adapter, it just doesnt do anything.
The data contained in the lookup table has been validated to be correct thoroughly.
There currently is only one record in the lookup table.
$LookupServer = "Redacted"
$LookupDatabase = "redacted"
$LookupSQL = "Select [DBServer]
,[SourceDB]
,[SourceTableOrScript]
,[DestinationFile]
,[DestinationTable]
,[AccessDropScript]
,[AccessCreateScript]
,[Active]
FROM [AccessDataExport_Lookup_Data]"
$LookupConnection = New-Object System.Data.SQLClient.SQLConnection
$LookupCSBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$LookupCSBuilder['server'] = $LookupServer
$LookupCSBuilder['trusted_connection'] = $True
$LookupCSBuilder['database'] = $LookupDatabase
$LookupConnection.ConnectionString = $LookupCSBuilder.ConnectionString
$LookupConnection.Open()
$LookupCommand = New-Object System.Data.SQLClient.SQLCommand
$LookupCommand.Connection = $LookupConnection
$LookupCommand.CommandText = $LookupSQL
$LookupDataAdapter = new-object System.Data.SqlClient.SqlDataAdapter $LookupCommand
$LookupDataset = new-object System.Data.Dataset
$LookupDataAdapter.Fill($LookupDataset)
$LookupConnection.Close()
foreach( $r in $LookupDataset.Tables[0].Rows){
$SourceConnection = New-Object System.Data.SQLClient.SQLConnection
$SourceCSBuilder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$SourceCSBuilder['server'] = $r.DBServer
$SourceCSBuilder['trusted_connection'] = $true
$SourceCSBuilder['database'] = $r.SourceDB
$SourceConnection.ConnectionString = $SourceCSBuilder.ConnectionString
$SourceCmd = $SourceConnection.CreateCommand()
$DestConnection = New-Object System.Data.OleDb.OleDbConnection
$DestCSBuilder = New-Object System.Data.OleDb.OleDbConnectionStringBuilder
$DestCSBuilder['Provider'] = 'Microsoft.ACE.OLEDB.12.0'
$DestCSBuilder['Persist Security Info'] = $false
$DestCSBuilder['Data Source'] = $r.DestinationFile
$DestConnection.ConnectionString = $DestCSBuilder.ConnectionString
$DestCmd = $DestConnection.CreateCommand()
$DestConnection.Open()
$SourceConnection.Open()
$DestCmd.CommandText = $r.AccessDropScript
try
{
$DestCmd.ExecuteNonQuery()
}
catch
{
write-Output $_.Exception.Message
write-Output $_.InvocationInfo.ScriptLineNumber
}
$DestCmd.CommandText = $r.AccessCreateScript
try{
$DestCmd.ExecuteNonQuery()
$SourceCmd.CommandText = $r.SourceTableOrScript
$SourceDA = New-object System.Data.SqlClient.SqlDataAdapter $SourceCmd
$SourceData = new-object System.Data.Dataset
$DestCmd.CommandText = $r.SourceTableOrScript
$DestDA = New-Object System.Data.OleDb.OleDbDataAdapter $DestCmd
$DestData = new-object System.Data.Dataset
$result = $SourceDA.fill($SourceData)
Write-Output "Source filled $result rows"
$result = $DestDA.fill($DestData)
Write-Output "Dest filled $result rows"
$DestData.load($SourceData.CreateDataReader(),1,$SourceData.Tables[0].TableName)
$destRowCount = $DestData.Tables[0].Rows.Count
Write-Output "DestData Row Count $destrowcount"
$DestCb = New-Object System.Data.OleDb.OleDbCommandBuilder $DestDA
$DestDA.UpdateCommand = $DestCb.GetUpdateCommand()
$DestDA.InsertCommand = $DestCb.GetInsertCommand()
$DestDA.DeleteCommand = $DestCb.GetDeleteCommand()
$result = $DestDA.Update($DestData)
Write-Output "Copy data copied $result rows"
}
Catch
{
write-Output $_.Exception.Message
write-Output $_.InvocationInfo.ScriptLineNumber
}
finally{
$DestConnection.Close()
$SourceConnection.Close()
}
}
and the output is
PS C:\Users\****> & 'c:\Users\***\source\repos\Powershell Scripts\Modular-Export.ps1'
1
0
0
Source filled 1274 rows
Dest filled 0 rows
DestData Row Count 1274
Copy data copied 0 rows
Table data since this is pertinate, but with redacted sensitive information.
https://gist.github.com/KySoto/5a764f3fa3c0d139131056ed6d44529b
In the end a reddit user helped me figure it out, Their post was the thing that got everything rolling. What i needed to do was set the state of every datarow in my datatable to "Added" using the SetAdded method. Then i needed to set $DestCb.QuotePrefix = "[" and $DestCb.QuoteSuffix = "]" Then it properly filled.

Powershell popup to list of usernames

I wish to have a powershell script that provides a popup message to a specific list of users. I need to use a list of relevant users in a spreadsheet with their network username and not use Active Directory. I have the powershell script to display a suitable message with a Warning icon and found to format the popup in a certain way I had to use a "FORM" and not a default popup messagebox. Although the following two lines for the Warning icon would not work in the form so had to use a picture box
$WarningIcon = New-Object ([System.Windows.MessageBoxImage]::Warning)
$Form.Controls.Add($WarningIcon)
The script for the form is as follows. There is probably a cleaner way of creating a form like this but I am quite new to Powershell!
Add-Type -AssemblyName System.Windows.Forms
$Form = New-Object system.Windows.Forms.Form
$Form.Width = 700
$Form.Height = 400
$Form.BackColor = "#DCDCDC"
$Form.Text = "System Restart Alert"
$Font = New-Object System.Drawing.Font("Ariel",30,
[System.Drawing.FontStyle]::Bold::Underline)
$FontB = New-Object System.Drawing.Font("Ariel",14,
[System.Drawing.FontStyle]::Bold)
$Picture = (get-item ("C:\FA_Files\Windows_Warning_Exclamation9.jpg"))
$img = [System.Drawing.Image]::Fromfile($Picture)
$pictureBox = new-object Windows.Forms.PictureBox
$pictureBox.Location = New-object System.Drawing.Size(160,30)
$pictureBox.Height = "100"
$pictureBox.Image = $img
$Form.controls.add($pictureBox)
$Label = New-Object System.Windows.Forms.Label
$Label.Location = "260,30"
$Label.Font = $Font
$Label.ForeColor = "Red"
$Label.Text = "WARNING!"
$Label.AutoSize = $True
$Form.Controls.Add($Label)
$LabelB = New-Object System.Windows.Forms.Label
$LabelB.Location = "100,130"
$LabelB.Font = $FontB
$LabelB.Text = "Due to essential maintenance system requires rebooting"
$LabelB.AutoSize = $True
$Form.Controls.Add($LabelB)
$LabelC = New-Object System.Windows.Forms.Label
$LabelC.Location = "100,160"
$LabelC.Font = $FontB
$LabelC.Text = "Please save all work immediately"
$LabelC.AutoSize = $True
$Form.Controls.Add($LabelC)
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = "300,280"
$okButton.Font = "$FontB"
$okButton.Size = "85,28"
$okButton.Text = "Okay"
$Form.Controls.Add($okButton)
$okButton.Add_Click = ({$Form.Close()})
$Form.ShowDialog()
I can retrieve a list of network usernames from the spreadsheet okay. Below is an image of the csv content
But I have googled a lot and cannot find a way of sending the popup to the list of usernames retrieved from the spreadsheet. So far I have tried the following where I have the form above set to the $msg varaiable and also tried having the form in another file and referencing that file in the $msg variable but it doesn't work
$csv = Import-csv "C:\FA_Files\NetNames.csv"
foreach($line in $csv)
{
$name = $line.("Name")
$netName = $line.("NetworkName")
#Echo "Name is $name and Network Name is $netName"
msg $netName $msg
}
It also has to be on the username and not the machine name.
How do I correct this please?
FWIW, msg.exe has replaced net /send in Windows, and is meant for sending messages to users, so I would begin by looking at the syntax for message.exe here. .
It has a /server: switch you can use to send a message to a remote host. So, take your user and computer name list and put them in a CSV file like this:
//MyInputFile.csv
ComputerName,UserName
Laptop01,BillG
Laptop02,StephenO
PC03,WayneH
Desktop04,JimA
You could use a short script like this to achieve your goal (or get you mostly there, at least 😁)
$Msg = "Put your message here"
$SpreadSheet = Import-CSV .\PathTo\MyInputFile.csv
ForEach ($row in $SpreadSheet){
"Sending message to \\$($row.ComputerName)\$($row.UserName) : $msg"
msg /server:$($row.ComputerName) $($row.UserName) $msg
}
You'll see an output like this in the console:
Sending message to \\Laptop01\BillG : Put your message here
Sending message to \\Laptop02\StephenO : Put your message here
Sending message to \\PC03\WayneH : Put your message here
Sending message to \\Desktop04\JimA : Put your message here
Sending message to \\localhost\* : Put your message here
Then your lucky user will see the following on their workstation.
If a computer can't be reached, the process will time out after five seconds or so and move on to the next one in the list.
All users that are a member of Active Directory by default receive read rights, this is by design as Active Directory is exactly the kind of system that should be relied on for the kind of functionality you desire... not sure why your ICT manager is so against the idea.
You cannot send a message to a user (with only the username), you must know the machine the user is logged into as well.
The only way I can think to achieve this is to iterate through every computer and query who is logged on and if the user matches your list then send a message to that computer. This would require reading a list of computers from AD or iterating through every available IP address on your local network(s).

Having problems reading in data from file and using it on the fly using PowerShell

I am trying to create a PowerShell script that will send an email if a service goes into a stopped state. I would like to be able to read the email configuration from another file.
Email configuration file:
.\emailconfig.conf
$emailSmtpServer = "smtp.company.com"
$emailSmtpServerPort = "587"
$emailSmtpUser = "usera"
$emailSmtpPass = "passwordb"
$emailFrom = "userA#company.com"
$emailTo = "userB#company.com"
$emailcc= "userC#company.com"
And this is what I have so far in the PowerShell script:
.\emailservicecheck.ps1
$A = Get-Service "Service B"
if ($A.Status -eq "Stopped") {
Get-Content emailconfig.conf | Out-String
$emailMessage = New-Object System.Net.Mail.MailMessage($emailFrom, $emailTo)
$emailMessage.Cc.Add($emailcc)
$emailMessage.Subject = "subject"
#$emailMessage.IsBodyHtml = $true # true or false depends
$emailMessage.Body = Get-Service "Service B" | Out-String
$SMTPClient = New-Object System.Net.Mail.SmtpClient($emailSmtpServer, $emailSmtpServerPort)
$SMTPClient.EnableSsl = $False
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailSmtpUser, $emailSmtpPass);
$SMTPClient.Send($emailMessage)
}
The script works if I enter the text from the email config file into the script but I cannot seem to be able to read in the data from the file on the fly and get the script to work. It errors out and says that my variables are empty.
What you are searching for, (I think) are .psd1 files. I personally prefer them (along with JSON) over the other configuration formats. The link I'm referring to also describes other well-known formats and how to use them in PowerShell.
In short, module manifests work as follows:
configuration.psd1
#{
SmtpServer = "";
MailFrom = "";
Auth = #{
User = "";
Pass = "";
};
}
Script.ps1
$mailConfig = Import-LocalizedData -BaseDirectory C:\ -FileName configuration.psd1
$emailMessage = New-Object System.Net.Mail.MailMessage( $$mailConfig.mailFrom , $mailConfig.mailTo )
As Mark already pointed out, Get-Content emailconfig.conf | Out-String will just output the content of the file, it won't define the variables in your code. For that you'd need to dot-source the file, which requires a file with the extension ".ps1".
If you want to stick with a simple config file format I'd recommend changing the file to something like this:
emailSmtpServer = smtp.company.com
emailSmtpServerPort = 587
emailSmtpUser = usera
emailSmtpPass = passwordb
emailFrom = userA#company.com
emailTo = userB#company.com
emailcc = userC#company.com
And importing it into a hashtable via ConvertFrom-StringData:
$cfg = Get-Content emailconfig.conf | Out-String | ConvertFrom-StringData
The data in the hashtable can be accessed via dot-notation ($cfg.emailFrom) as well as via the index operator ($cfg['emailFrom']), so your code would have to look somewhat like this:
$msg = New-Object Net.Mail.MailMessage($cfg.emailFrom, $cfg.emailTo)
$msg.Cc.Add($cfg.emailcc)
$msg.Subject = 'subject'
$msg.Body = Get-Service 'Service B' | Out-String
$smtp = New-Object Net.Mail.SmtpClient($cfg.emailSmtpServer, $cfg.emailSmtpServerPort)
$smtp.EnableSsl = $false
$smtp.Credentials = New-Object Net.NetworkCredential($cfg.emailSmtpUser, $cfg.emailSmtpPass)
$smtp.Send($msg)
It looks like what you're trying to do is include some script from another file. This can be done by dot sourcing, however the file needs to be saved as a .ps1 file, you can't use .conf.
You'd do it as follows (in place of your existing Get-Content) line:
. .\emailconfig.ps1
Assuming the file is kept in the current working directory of the script.
Your script wasn't working because
get-content emailconfig.conf | Out-String
Was returning the contents of that file to the output pipeline, rather than including it in the script and executing it.
I'm not sure i understood correctly what you want.
If you want to use variables from external file, you need to dot source your external script, for example, create a file named variables.ps1 and put in the same folder
In the beginning of the main script use
. .\variables.ps1
If you are after expanding variables that are in external file to ues as an email template please do as following:
$HTMLBody = get-content "yourfilepath" | Foreach-Object {$ExecutionContext.InvokeCommand.ExpandString($_)}
This will expand all variables and put it in the $HTMLBody variable
Then use:
$emailMessage.Body = (ConvertTo-Html -body $HTMLBody)

Powershell - If SQL query returns a result, then run function

Within PowerShell, I'm trying to trigger an email to be sent only if there are any results from a SQL query. Email functionality and DB connection is working fine, but the "if then" statement based on SQL results isn't working - I don't think the $result = [bool] is valid.
End result would be the email only being sent if there are any records returned from the SQL statement, and the email would contain the SQL results.
Here's what I have so far:
$result = [bool]("
SELECT *
FROM table
WHERE condition")
If ($result -eq $true) {
function Invoke-SQL ($dataset) {
$connectionString = "server=servername;uid=valuehere;pwd=passwordhere;database=dbnamehere;"
$sqlCommand = "
SELECT *
FROM table
WHERE condition"
"
$connection = new-object system.data.SqlClient.SQLConnection($connectionString)
$command = new-object system.data.sqlclient.sqlcommand($sqlCommand,$connection)
$connection.Open()
$adapter = New-Object System.Data.sqlclient.sqlDataAdapter $command
$adapter.Fill($dataSet) #| Out-Null
$connection.Close()
}
function Invoke-Email ($dataset) {
foreach ($Row in $dataset.Tables[0].Rows)
{
write-host "value is : $($Row[0])"
}
$From = "email"
$To = "email"
$Subject = "subject here"
$Body = echo 'email body here' $dataset.tables[ 0 ] | Out-String
$SMTPServer = "server here"
Send-MailMessage -From $From -to $To -Subject $Subject -Body $Body -SmtpServer $SMTPServer
#-Port $Port
}
$dataset = New-Object System.Data.DataSet
Invoke-SQL $dataset
$dataset.Tables
Invoke-Email $dataset
$result
There are at least 2 things that you can do to accomplish this based on your current method of executing the query. For a simple SELECT query executed with the 'fill' method of the dataAdapter the return value should be the number of rows returned by the query. You could do this:
$resultCount = $adapter.fill($dataSet)
Then just check if $resultCount is greater than 0.
if($resultCount -gt 0){
#invoke your email function here
}
You could also just check the row count of your data table like this:
$dataSet.Tables[your table name or index].Rows.Count
Again simply checking for greater than 0.
if($dataSet.Tables[your table name or index].Rows.Count -gt 0){
#invoke your email function here
}
I recommend this second approach of counting the number of rows in the data table, because you do not have to worry about differences in the different data adapter objects that are available.