Octopus Deploy - SQL - Execute Scripts Ordered step giving Exception - powershell

In Octopus deploy I have added a step in process to run the stored procedure with library script “SQL - Execute Scripts Ordered step”.
When I’m providing the script to execute the stored procedure it is throwing the below Exception:
Exception calling “ReadAllText” with “1” argument(s): “The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.”
Closing connection
I believe this is because of the large script as text I've provided to execute in field “SQL Script File”.
As shown in examples I can run script directly. So I’m providing the stored procedure execution script but in library's PowerShell scipt -
$content = [IO.File]::ReadAllText($OctopusParameters[‘SqlScriptFile’])
ReadAllText is expecting something less than 260 characters.
One solution I can think of is to provide the execution script as a file within package itself. But this will be the last resort.
How can I run the stored procedure directly from the step in process?

Apparantly [IO.File]::ReadAllText($OctopusParameters[‘SqlScriptFile’]) is expecting file path as SqlScriptFile. I updated the library's powershell script to take the full sql script from field "SQL Script File" as parameter and passed it directly to the function.
$content= $OctopusParameters['SqlScriptFile']
Execute-SqlQuery -query $content
providing below the full powershell script for reference:
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $OctopusParameters['ConnectionString']
Register-ObjectEvent -inputobject $connection -eventname InfoMessage -action {
write-host $event.SourceEventArgs
} | Out-Null
function Execute-SqlQuery($query) {
$queries = [System.Text.RegularExpressions.Regex]::Split($query, "^\s*GO\s*`$", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline)
$queries | ForEach-Object {
$q = $_
if ((-not [String]::IsNullOrWhiteSpace($q)) -and ($q.Trim().ToLowerInvariant() -ne "go")) {
$command = $connection.CreateCommand()
$command.CommandText = $q
$command.CommandTimeout = $OctopusParameters['CommandTimeout']
$command.ExecuteNonQuery() | Out-Null
}
}
}
Write-Host "Connecting"
try {
$connection.Open()
Write-Host "Executing script in" $OctopusParameters['SqlScriptFile']
# $content = [IO.File]::ReadAllText($OctopusParameters['SqlScriptFile'])
$content= $OctopusParameters['SqlScriptFile']
Execute-SqlQuery -query $content
}
catch {
if ($OctopusParameters['ContinueOnError']) {
Write-Host $_.Exception.Message
}
else {
throw
}
}
finally {
Write-Host "Closing connection"
$connection.Dispose()
}

Related

Set a variable equal to the output of a powershell script

This is a little difficult to explain, but I will do my best. I am writing some code to import AD contacts to users' mailboxes through EWS using Powershell.
I have a Main.ps1 file that calls all the other scripts that do work in the background (for example 1 imports the AD modules) another imports O365 modules.
I have 1 script container that connect to EWS. The code looks like this:
#CONFIGURE ADMIN CREDENTIALS
$userUPN = "User#domain.com"
$AESKeyFilePath = ($pwd.ProviderPath) + "\ConnectToEWS\aeskey.txt"
$SecurePwdFilePath = ($pwd.ProviderPath) + "\ConnectToEWS\password.txt"
$AESKey = Get-Content -Path $AESKeyFilePath -Force
$securePass = Get-Content -Path $SecurePwdFilePath -Force | ConvertTo-SecureString -Key $AESKey
#create a new psCredential object with required username and password
$adminCreds = New-Object System.Management.Automation.PSCredential($userUPN, $securePass)
Try
{
[Reflection.Assembly]::LoadFile("\\MBX-Server\c$\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll") | Out-Null
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1)
$service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($userUPN,$adminCreds.GetNetworkCredential().Password)
$service.Url = new-object Uri("https://outlook.office365.com/EWS/Exchange.asmx");
return $service
}
Catch
{
Write-Output "Unable to connect to EWS. Make sure the path to the DLL or URL is correct"
}
The output of that code prints out the Service connection, but I want the information for that output stored in a variable such as $service.
Then I would pass that variable to another script that binds to the mailbox I want...
The problem I am having is $service doesn't seem to be storing that information. It only print it out once when I return it from the script above, but it doesn't append that information in the main script. When I print out $service it prints out once, but then it clears itself.
Here is my main script
CLS
#Root Path
$rootPath = $pwd.ProviderPath #$PSScriptRoot #$pwd.ProviderPath
Write-Host "Importing all necessary modules."
#******************************************************************
# PREREQUISITES
#******************************************************************
#Nuget - Needed for O365 Module to work properly
if(!(Get-Module -ListAvailable -Name NuGet))
{
#Install NuGet (Prerequisite) first
Install-PackageProvider -Name NuGet -Scope CurrentUser -Force -Confirm:$False
}
#******************************************************************
#Connect w\ Active Directory Module
& $rootPath\AD-Module\AD-module.ps1
#Load the O365 Module
& $rootPath\O365-Module\O365-module.ps1
#Clear screen after loading all the modules/sessions
CLS
#******************************************************************
# PUT CODE BELOW
#******************************************************************
#GLOBAL VARIABLES
$global:FolderName = $MailboxToConnect = $Service = $NULL
#Connect to EWS
& $rootPath\ConnectToEWS\ConnectToEWS.ps1
#Debug
$Service
#Create the Contacts Folder
& $rootPath\CreateContactsFolder\CreateContactsFolder.ps1
#Debug
$service
$ContactsFolder
#Clean up Sessions after use
if($NULL -ne (Get-PSSession))
{
Remove-PSSession *
}
[GC]::Collect()
The first time I output the $service variable, it prints fine. In the 2nd Debug output it doesn't print out anymore, and I believe that it why the script is failing when I launch "CreateContactsFolder.ps1"
Here is the content of "CreateContactsFolder.ps1"
CLS
Try
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxToConnect);
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
$RootFolder.Load()
#Check to see if they have a contacts folder that we want
$FolderView = new-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
$ContactsFolderSearch = $RootFolder.FindFolders($FolderView) | Where-Object {$_.DisplayName -eq $FolderName}
if($ContactsFolderSearch)
{
$ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service,$ContactsFolderSearch.Id);
#If folder exists, connect to it. Clear existing Contacts, and reupload new (UPDATED) Contact Info
Write-Output "Folder alreads exists. We will remove all contacts under this folder."
# Attempt to empty the target folder up to 10 times.
$tries = 0
$max_tries = 0
while ($tries -lt 2)
{
try
{
$tries++
$ErrorActionPreference='Stop'
$ContactsFolder.Empty([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true)
$tries++
}
catch
{
$ErrorActionPreference='SilentlyContinue'
$rnd = Get-Random -Minimum 1 -Maximum 10
Start-Sleep -Seconds $rnd
$tries = $tries - 1
$max_tries++
if ($max_tries -gt 100)
{
Write-Output "Error; Cannot empty the target folder; `t$EmailAddress"
}
}
}
}
else
{
#Contact Folder doesn't exist. Let's create it
try
{
Write-Output "Creating new Contacts Folder called $FolderName"
$ContactsFolder = New-Object Microsoft.Exchange.WebServices.Data.ContactsFolder($service);
$ContactsFolder.DisplayName = $FolderName
$ContactsFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
}
catch
{
Write-Output "Error; Cannot create the target folder; `t$EmailAddress"
}
}
}
Catch
{
Write-Output "Couldn't connect to the user's mailbox. Make sure the admin account you're using to connect to has App Impersonization permissions"
Write-Output "Check this link for more info: https://help.bittitan.com/hc/en-us/articles/115008098447-The-account-does-not-have-permission-to-impersonate-the-requested-user"
}
return $ContactsFolder
In the Main script, capture the returned variable from the EWS script like
$service = & $rootPath\ConnectToEWS\ConnectToEWS.ps1
Or dot-source that script into the Main script, so the variables from EWS.ps1 are local to the Main script, so you don't need to do return $service in there:
. $rootPath\ConnectToEWS\ConnectToEWS.ps1
and do the same for the CreateContactsFolder.ps1 script
OR
define the important variables in the called scripts with a global scope $global:service and $global:ContactsFolder
See About_Scopes

Code not being seen if I include it from a file

I'm writing some Powershell scripts to manage a SQL Server. When I create my SMO object, I do a test to see if I can list my databases. If so, then I assume I'm connected and call a function (via an included file) that has an Invoke-Sqlcmd that causes my database to disconnect or something.
However, if I run the invoke command directly in the script, it works fine.
Looking at the provided code, my output is as follows:
I'm connected
$SqlServer is not contactable
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | Out-Null
$ErrorActionPreference = "Stop"
Import-Module 'sqlps' -DisableNameChecking #load all of SMO
$ScriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -
Parent
try {
("$ScriptDirectory\_HRbackup.ps1")
("$ScriptDirectory\_HRprinter.ps1")
("$ScriptDirectory\_HRbody.ps1")
} catch {
Write-Host "Error while loading supporting PowerShell Scripts"
}
$ServerList = $env:COMPUTERNAME
$SqlServer = New-Object ('Microsoft.SqlServer.Management.Smo.Server')
$ServerList # add server list
try {
$databases = $SqlServer.Databases
if ($databases.Count -gt 0) {
Write-Host "I am connected"
Backup-Html
} else {
$login = Get-Credential -Message 'Please prov#ide your SQL
Credentials'
$SqlServer.ConnectionContext.LoginSecure = $false
$SqlServer.ConnectionContext.Login = $login.UserName
$SqlServer.ConnectionContext.set_SecurePassword($login.Password)
$SqlServer.ConnectionContext.connect()
}
} catch {
$WarningPreference = 'continue'
Write-Warning "$SqlServer is not contactable"
#$SqlServer.ConnectionContext.Disconnect()
} finally {
$SqlServer.ConnectionContext.Disconnect()
}
Here is the content of Backup-Html:
$Query = "select top 5 a.server_name, a.database_name,
backup_finish_date, a.backup_size,
CASE a.[type] -- Let's decode the three main types of
backup here
WHEN 'D' THEN 'Full'
WHEN 'I' THEN 'Differential'
WHEN 'L' THEN 'Transaction Log'
ELSE a.[type]
END as BackupType
-- Build a path to the backup
,'\\' +
-- lets extract the server name out of the recorded
server and instance name
CASE
WHEN patindex('%\%',a.server_name) = 0 THEN
a.server_name
ELSE
substring(a.server_name,1,patindex('%\%',a.server_name)-1)
END
-- then get the drive and path and file information
+ '\' + replace(b.physical_device_name,':','$') AS
'\\Server\Drive\backup_path\backup_file'
from msdb.dbo.backupset a join msdb.dbo.backupmediafamily
b
on a.media_set_id = b.media_set_id
where a.database_name Like 'Easy%'
order by a.backup_finish_date desc"
#Use SQLCmd to execute the query on the server
Invoke-Sqlcmd -ServerInstance $SQLServer -Query $Query

Using a try block to test if a file is locked in powershell

I want to know if its bad form to use try blocks to test if a file is locked. Here's the background.
I need to send text output of an application to two serial printers simultaneously. My solution was to use MportMon, and a Powershell script. The way it's supposed to work is the application default prints to the MportMon virtual printer port, which actually makes a uniquely named file in a "dropbox" folder. The powershell script uses a filesystemwatcher to monitor the folder and when a new file is created, it takes the textual content and pushes it out two serial printers, then deletes the file, so as not to fill up the folder. I was having a problem when trying to read the text from the file that the virtual printer created. I found that I was getting errors becasue the file was still locked. To fixed the problem, I used a FSM to impliment the logic and instead of checking for a lock everytime before attempting to get the content from the file, I used a try block that attempts to read content from the file, if it fails, the catch block just reaffirms the state that the FSM is in, and the process is repeated until successful. It seems to work fine, but I've read somewhere that its bad practice. Is there any danger in this method, or is it safe and reliable? Below is my code.
$fsw = New-Object system.io.filesystemwatcher
$q = New-Object system.collections.queue
$path = "c:\DropBox"
$fsw.path = $path
$state = "waitforQ"
[string]$tempPath = $null
Register-ObjectEvent -InputObject $fsw -EventName created -Action {
$q.enqueue( $event.sourceeventargs.fullpath )
}
while($true) {
switch($state)
{
"waitforQ" {
echo "waitforQ"
if ($q.count -gt 0 ) {$state = "retrievefromQ"}
}
"retrievefromQ" {
echo "retrievefromQ"
$tempPath = $q.dequeue()
$state = "servicefile"
}
"servicefile" {
echo " in servicefile "
try
{
$text = Get-Content -ErrorAction stop $tempPath
#echo "in try"
$text | out-printer db1
$text | out-printer db2
echo " $text "
$state = "waitforQ"
rm $tempPath
}
catch
{
#echo "in catch"
$state = "servicefile"
}
}
Default { $state = "waitforQ" }
}
}
I wouldn't say it's bad practice to test a file to see if it's locked, but it's not as clean as checking the handles used by other processes. Personally I'd test the file like you do, but I adjust a few parts to make it safer/better.
That switch-statement looks way to complicated (for me), I'd replace it with a simple if-test. "If files in queue, proceed, if not, wait".
You need to slow down.. You will try to read the file as many times as possible while it's locked. This is a waste of resources since it will take some time for the current application to let it go and save the data to a HDD. Add some pauses. You won't notice them, but your CPU will love them. The same applies when there are no files in the queue.
You might benefit from adding a timeout, like max 50 attempts to read the file, to avoid the script getting stuck if one specific file is never released.
Try:
$fsw = New-Object system.io.filesystemwatcher
$q = New-Object system.collections.queue
$path = "c:\DropBox"
$fsw.path = $path
$MaxTries = 50 #50times * 0,2s sleep = 10sec timeout
[string]$tempPath = $null
Register-ObjectEvent -InputObject $fsw -EventName created -Action {
$q.enqueue( $event.sourceeventargs.fullpath )
}
while($true) {
if($q.Count -gt 0) {
#Get next file in queue
$tempPath = $q.dequeue()
#Read file
$text = $null
$i = 0
while($text -eq $null) {
#If locked, wait and try again
try {
$text = Get-Content -Path $tempPath -ErrorAction Stop
} catch {
$i++
if($i -eq $MaxTries) {
#Max attempts reached. Stops script
Write-Error -Message "Script is stuck on locked file '$tempPath'" -ErrorAction Stop
} else {
#Wait
Start-Sleep -Milliseconds 200
}
}
}
#Print file
$text | Out-Printer db1
$text | Out-Printer db2
echo " $text "
#Remove temp-file
Remove-Item $tempPath
}
#Relax..
Start-Sleep -Milliseconds 500
}

Multiple PowerShell Scripts Writing to Same File

I have a condition that kicks off a PowerShell script to append a short string to a text file. This condition can fire rapidly, so the file is being written multiple times by the same script. Additionally, a separate script is importing from that text file in batches (less frequently).
Whenever the condition fires very rapidly, I get the error: "The process can not access the file 'file_name' because it is being used by another process." When I do the same append in Python (my main language), I don't get the same error, but I could use some help fixing this in PowerShell.
$action = $args[0]
$output_filename = $args[1]
$item = $args[2]
if ($action -eq 'direct'){
$file_path = $output_filename
$sw = New-Object -typename System.IO.StreamWriter($file_path, "true")
$sw.WriteLine($item)
$sw.Close() }
I have also tried the following instead of StreamWriter, but apparently the performance is weak for Add-Content and Out-File (http://sqlblog.com/blogs/linchi_shea/archive/2010/01/04/add-content-and-out-file-are-not-for-performance.aspx):
out-file -Append -FilePath $file_path -InputObject $item }
Might try something like this:
while ($true)
{
Try {
[IO.File]::OpenWrite($file_path).close()
Add-Content -FilePath $file_path -InputObject $item
Break
}
Catch {}
}

Executing powershell script

I have a question i wrote a powershell script so i have name.ps1, but i have troubles for executing it, i mean i could debut it with windows powershell (ISE), by just adding the code to it and run ... but how do i execute it different?
When i open ordinary windows powershell (so NOT ISE) and i type there script.ps1 file.csv
i get this kind of error:
This is the code that i have, maybe im not proper initiating the script in my code i dont know:
param ([string]$Csv)
function GetHelp() {
$HelpText = #"
DESCRIPTION:
NAME: Add-STUser
Adds Users from the User Csv File
PARAMETERS:
-Csv The Csv file Used by the script (Required)
-help Prints the HelpFile (Optional)
The Csv File is built up in the following way:
Firstname, Surname, Email
"#
$HelpText
}
function Get-Csv ([string]$Csv) {
$CsvFile = Import-Csv $Csv
$CsvFile | ForEach {
Add-User -Firstname $_.Firstname -Surname $_.Surname -Email $_.Email
}
}
function Add-User ([string]$Firstname, [string]$Surname, [string]$Email) {
# Set up AD Connectionstring
# Get A Unique Password
[string]$Password = Generate-Password
$username=$Firstname.substring(0,1).toLower() + $Surname.toLower()
# Create User in AD
$container =[ADSI] $Connection
$User = $container.Create("User", "cn="+$username)
$User.Put("sAMAccountName", $username)
$User.Put("givenName", $Firstname)
$User.Put("sn", $Surname)
$User.Put("mail", $Email)
$User.Put("displayName", $Firstname + " "+$Surname)
$User.SetInfo()
# Set Random Pwd and Enable Account
$User.PsBase.Invoke("SetPassword", $Password)
$User.PsBase.InvokeSet("AccountDisabled", $false)
$User.pwdLastSet = 0
$User.SetInfo()
# Write Pwd to File
$FileName = "PasswordList " + (get-date -uformat "%Y-%m-%d") + ".txt"
"$Firstname, $Surname, $username, $email, $Password" | Add-Content $FileName
Write-Host "Added User: $username" -ForegroundColor Green
# Set Check Variable to False
$Password = $Null
#$Script:sAMAccountNameDoesntExist = $False
#$Script:distinguishedNameDoesntExist = $False
}
if ($help) {
GetHelp
} elseif ($Domain -AND $Csv) {
Get-Csv -Csv $Csv
} else {
GetHelp
}
So with other words i need to execute that script with only 1 param (path to csv file)
Thanks in advance
In Powershell, unlike cmd, current directory (.) is not in PATH.
So to run scripts or executables in the current directory, you have to prefix with ./
So you will have to do
.\script.ps1 file.csv
If you look carefully at the error message, at the bottom, Powershell is giving a suggestion that you have to do so.
You need to tell Powershell you want to execute a script from current location, like in *nix systems. So call the script like the error message hints:
./myScript.ps1
or
.\myScript.ps1
You can also provide full path to the script
c:\what\ever\is\the\path\myScript.ps1
The script/directory names are not case sensitive.