PowerShell creating a backup of a stored procedure results in a blank file - sql

I am trying to create a backup of a SQL stored procedure using PowerShell, but it produces a blank file. It's not throwing an error.
Here is my code:
param([String]$step='exeC dbo.test',[String]$sqlfile='',[String]$servename = 'test',[String]$dbname = 'test')
$step2=$step
$step3=$step2.Replace('[','')
$step4 = $step3.Replace(']','')
$step4 = $step4.Split(" ")[1]
$step5 = $step4.Split(".")
Write-Output $step5[0,1]
[System.Reflection.Assembly]::LoadWithPartialName(“Microsoft.SqlServer.SMO”) | out-null
$logfolder = 'C:\Users\fthoma15\Documents\sqlqueries\Logs'
$bkupfolder = 'C:\Users\fthoma15\Documents\sqlqueries\Backup'
$statsfolder = 'C:\Users\fthoma15\Documents\sqlqueries\stats'
$SMOserver = new-object ("Microsoft.SqlServer.Management.Smo.Scripter") #-argumentlist $server
$srv = new-Object Microsoft.SqlServer.Management.Smo.Server("$servename")
#Prompt for user credentials
$srv.ConnectionContext.LoginSecure = $false
$credential = Get-Credential
#Deal with the extra backslash character
$loginName = $credential.UserName -replace("\\","")
#This sets the login name
$srv.ConnectionContext.set_Login($loginName);
#This sets the password
$srv.ConnectionContext.set_SecurePassword($credential.Password)
$srv.ConnectionContext.ApplicationName="MySQLAuthenticationPowerShell"
#$srv.Databases | Select name
$db = New-Object Microsoft.SqlServer.Management.Smo.Database
$db = $srv.Databases.Item("$dbname")
#$db.storedprocedures | Select name
$Objects = $db.storedprocedures[$step5[1,0]]
#Write-Output $step5[1,0]
#Write-Output $Objects
$scripter = new-object ("$SMOserver") $srv
$Scripter.Script($Objects) | Out-File $bkupfolder\backup_$($step5[1]).sql
Please help

This was an issue with permission to the database. I gave the SQL id permission to the database and now it works.

Related

How to create service principal Azure SQL database user from service principal login (which is an AAD admin of the Azure SQL Server)

We are creating an Azure SQL Server and the database using ARM templates. We have set a AAD group as AAD admin on the SQL Server. That group contains a service principal which we have created under Application registrations.
The deployment succeeds and I can see the AAD admin set correctly for the SQL Server. However when I try to run scripts from the deployment pipeline from that service principal, like create user, it fails.
e.g. this powershell scripts successfully fetches the token and opens the connection, but fails with an error.
$tenant = "xxxx-xxxx-xxxxxx"
$applicationId = $(Get-AzADServicePrincipal -DisplayName "MyServicePrincipal").ApplicationId
$clientSecret = "yyyy-yyyyy-yyyy"
$subscriptionId = "xyxy-xyxy-xyxy-xyxy"
$secstr = New-Object -TypeName System.Security.SecureString
$clientSecret.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $applicationId, $secstr
Connect-AzAccount -Tenant $tenant -SubscriptionId $subscriptionId -ServicePrincipal -Credential $cred
$resourceAppIdURI = 'https://database.windows.net/'
$authority = ('https://login.windows.net/{0}' -f $tenant)
$ClientCred = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential]::new($applicationId, $clientSecret)
$authContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new($authority)
$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $ClientCred)
$Token = $authResult.Result.AccessToken
$conn = New-Object System.Data.SqlClient.SQLConnection
$SQLServerName = "sql-myApplicationDbServer-dev.database.windows.net"
$DatabaseName = 'sqldb-myApplicationDb-dev'
$conn.ConnectionString = "Data Source=$SQLServerName;Initial Catalog=$DatabaseName;Connect Timeout=30"
$conn.AccessToken = $($Token)
$conn.Open()
$query = #"
IF NOT EXISTS (
SELECT [name]
FROM sys.database_principals
WHERE [name] = 'myAppServiceName'
)
BEGIN
CREATE USER [myAppServiceName] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [myAppServiceName];
ALTER ROLE db_datawriter ADD MEMBER [myAppServiceName];
ALTER ROLE db_ddladmin ADD MEMBER [myAppServiceName];
END
"#
$command = $conn.CreateCommand()
$command.CommandText = $query
$result = $command.ExecuteNonQuery()
$result
$conn.Close()
the error
...
PS C:\Users\TJ> $conn.Open()
PS C:\Users\TJ> $command = $conn.CreateCommand()
PS C:\Users\TJ> $command.CommandText = $query
PS C:\Users\TJ> $result = $command.ExecuteNonQuery()
MethodInvocationException: Exception calling "ExecuteNonQuery" with "0" argument(s): "Principal 'myAppServiceName' could not be resolved. Error message: ''
Cannot add the principal 'myAppServiceName', because it does not exist or you do not have permission.
Cannot add the principal 'myAppServiceName', because it does not exist or you do not have permission.
Cannot add the principal 'myAppServiceName', because it does not exist or you do not have permission."
But, if I add my own AAD user in the SQL Server AAD Administrator group, which the Service principal is part of, I can successfully run the script.
I think service principal needs an associated user and proper rights before i run any create user scripts. how to do that from the service principal login(remember its an admin as its a member of the SQL Server AAD admin group). This would be part of a pipeline so I don't want to run any script with manual intervention.
update(May-2021):it seems that Microsoft has fixed this issue and the workaround is no longer necessary.
So it seems that a service principal does not have enough rights to create users. Microsoft states that they are planning to fix it. For now, there is a workaround.
the trick is to use the syntax
CREATE USER [myAppServiceName] WITH SID=<0xappServiceServicePrincipalSid>, TYPE=E
and it also turns out that the SID that database understands is different from the objectId we get in azure(like in case of MSI). It is a transform of the application service principal that gets created for the appService in my case.
so i need to run this first to get the app Service service principal
$appServiceServicePrincipal = (Get-AzADServicePrincipal -SearchString myAppServiceName).ApplicationId
now we have to convert it to a SID which the db can understand, so run the follwoing in the DB
SELECT CONVERT(VARCHAR(1000), CAST(CAST('$appServiceServicePrincipal' AS UNIQUEIDENTIFIER) AS VARBINARY(16)),1);
and use that value in the SID=<0xappServiceServicePrincipalSid> i mentioned previously
then it works.here is the full sample.
$tenant = "xxxx-xxxx-xxxxxx"
$applicationId = $(Get-AzADServicePrincipal -DisplayName "AspBuildApps").ApplicationId
$clientSecret = "yyyy-yyyyy-yyyy"
$subscriptionId = "xyxy-xyxy-xyxy-xyxy"
$appServiceName = 'myAppServiceName' #get it from ARM output
$secstr = New-Object -TypeName System.Security.SecureString
$clientSecret.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $applicationId, $secstr
Connect-AzAccount -Tenant $tenant -SubscriptionId $subscriptionId -ServicePrincipal -Credential $cred
$resourceAppIdURI = 'https://database.windows.net/'
$authority = ('https://login.windows.net/{0}' -f $tenant)
$ClientCred = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential]::new($applicationId, $clientSecret)
$authContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new($authority)
$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $ClientCred)
$Token = $authResult.Result.AccessToken
$conn = New-Object System.Data.SqlClient.SQLConnection
$SQLServerName = "sql-myApplicationDbServer-dev.database.windows.net"
$DatabaseName = 'sqldb-myApplicationDb-dev'
$conn.ConnectionString = "Data Source=$SQLServerName;Initial Catalog=$DatabaseName;Connect Timeout=30"
$conn.AccessToken = $($Token)
$appServiceServicePrincipal = (Get-AzADServicePrincipal -SearchString $appServiceName).ApplicationId
$conn.Open()
$query = #"
SELECT CONVERT(VARCHAR(1000), CAST(CAST('$($appServiceServicePrincipal)' AS UNIQUEIDENTIFIER) AS VARBINARY(16)),1);
"#
$command = $conn.CreateCommand()
$command.CommandText = $query
$appServiceServicePrincipalSid = $command.ExecuteScalar()
Write-Host "appServiceServicePrincipalSid="$appServiceServicePrincipalSid
$query = #"
IF NOT EXISTS (
SELECT [name]
FROM sys.database_principals
WHERE [name] = '$($appServiceName)'
)
BEGIN
CREATE USER [$($appServiceName)] WITH SID=$($appServiceServicePrincipalSid), TYPE=E
ALTER ROLE db_datareader ADD MEMBER [$($appServiceName)];
ALTER ROLE db_datawriter ADD MEMBER [$($appServiceName)];
ALTER ROLE db_ddladmin ADD MEMBER [$($appServiceName)];
END
"#
$command = $conn.CreateCommand()
$command.CommandText = $query
$result = $command.ExecuteNonQuery()
Write-Host $result
$conn.Close()
Thanks #LeonYue for pointing me in the right direction. I want to answer it with a bit of explanation.
Links that helped me:
https://blog.bredvid.no/handling-azure-managed-identity-access-to-azure-sql-in-an-azure-devops-pipeline-1e74e1beb10b
https://roadtoalm.com/2020/01/31/automatically-provision-a-azure-sql-db-with-a-managed-service-identity-msi/

Powershell Function SQL Stored Procedure with Parameters

The error received is "The SqlParameterCollection only accepts non-null SqlParameter type objects, not SqlCommand objects." & "Procedure or function 'usp__SingleUpdateServerBackupPath' expects parameter '#decServerName', which
was not supplied."
PowerShell code:
Set-StrictMode -Version 1.0
function update-serverbackuppath {
param(
[Parameter(Mandatory=$True,ValueFromPipeLine=$True)][object[]]$inputobject
)
BEGIN {
$connection = New-Object -TypeName System.Data.SqlClient.SqlConnection
$connection.ConnectionString = "server=servername;database=database;trusted_connection=yes"
$connection.Open()
}
PROCESS {
$UpdateBackupPath = New-Object -TypeName System.Data.SqlClient.SqlCommand
$UpdateBackupPath.Connection = $connection
$UpdateBackupPath.CommandText = "usp__SingleUpdateServerBackupPath"
$UpdateBackupPath.Commandtype = [System.Data.Commandtype]::StoredProcedure
$ParamUpdateBackupPath = New-Object -TypeName System.Data.SqlClient.SqlParameter
$ParamUpdateBackupPath.ParameterName = "#decBackupPath"
$ParamUpdateBackupPath.SqlDbType = [System.Data.SqlDbType]::VarChar
$ParamUpdateBackupPath.Direction = [System.Data.ParameterDirection]::Input
$ParamUpdateBackupPath.Value = $inputobject.paths
$ParamUpdateBackupPathServerName = New-Object -TypeName System.Data.SqlClient.SqlCommand
$ParamUpdateBackupPathServerName.ParameterName = "#decServerName"
$ParamUpdateBackupPathServerName.SqlDbType = [System.Data.SqlDbType]::VarChar
$ParamUpdateBackupPathServerName.Direction = [System.Data.ParameterDirection]::Input
$ParamUpdateBackupPathServerName.Value = $inputobject.names
$UpdateBackupPath.Parameters.Add($ParamUpdateBackupPath)
$UpdateBackupPath.Parameters.Add($ParamUpdateBackupPathServerName)
$reader = New-Object -TypeName System.Data.SqlClient.SqlDataReader = $UpdateBackupPath.ExecuteReader()
}
END {
$connection.Close()
}
}
SQL Procedure:
Create Procedure usp__SingleUpdateServerBackupPath
(
#decBackupPath AS varchar(50),
#decServerName AS varchar(50)
)
AS
UPDATE BCKP
SET PTH = #decBackupPath
FROM BCKP
INNER JOIN SRVR
ON SRVR.ID = BCKP.FK_SRVR
WHERE SRVR.NM = #decServerName
CSV File Format
Import-Csv -Path C:\Bin\Repos\Backup.csv | C:\Bin\Scripts\update-serverbackuppath.ps1
Names Paths
Server1 \\fileshare\server_name
The Powershell code has several syntax errors, like referring to enums in erroneus a way:
# Incorrect form
[System.Data.Commandtype.StoredProcedure]
# Correct form for referring to enumeration
[System.Data.Commandtype]::StoredProcedure
Later on, there is an undeclared object which's member method is called:
# $command is not set, so ExecuteReader method is available
$reader = $command.ExecuteReader()
It is highly recommended to use strict mode in Powershell. It helps catching typos by preventing access to non-existing properties an uninitialized variables.
Edit
After the updated code, there are still two errors:
# This doesn't make sense. The variable should be SqlParameter, not SqlCommand
$ParamUpdateBackupPathServerName = New-Object -TypeName System.Data.SqlClient.SqlCommand
# Like so:
$ParamUpdateBackupPathServerName = New-Object -TypeName System.Data.SqlClient.SqlParameter
# This is nonsense syntax
$reader = New-Object -TypeName System.Data.SqlClient.SqlDataReader = $UpdateBackupPath.ExecuteReader()
# Like so:
$reader = $UpdateBackupPath.ExecuteReader()

Add SQL Server Instances to Central Management Server Groups with Powershell

I am trying to create a script to automatically iterate through a text file of all our SQL Server instances and add each on if it doesn't already exist to the CMS. I want to try doing this through SMO instead of hardcoding sql strings in. Below is what I have so far but it doesn't seem to be working. Any help would be appreciated. Thanks.
Eventually I will add more If statements in to distribute the instances to certain groups but for now I'm just trying to get it to populate everything.
$CMSInstance = "cmsinstancename"
$ServersPath = "C:\Scripts\InPutFiles\servers.txt"
#Load SMO assemplies
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Management.RegisteredServers') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Management.Common') | out-null
$connectionString = "Data Source=$CMSINstance;Initial Catalog=master;Integrated Security=SSPI;"
$sqlConnection = new-object System.Data.SqlClient.SqlConnection($connectionString)
$conn = new-object Microsoft.SqlServer.Management.Common.ServerConnection($sqlConnection)
$CMSStore = new-object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($conn)
$CMSDBStore = $CMSStore.ServerGroups["DatabaseEngineServerGroup"]
$Servers = Get-Content $ServersPath;
foreach($Server in $Servers)
{
#Put this in loop to deal with duplicates in list itself
$AlreadyRegisteredServers = #()
$CMSDBStore.GetDescendantRegisteredServers()
$RegServerName = $Server.Name
$RegServerInstance = $Server.Instance
if($AlreadyRegisteredServers -notcontains $RegServerName)
{
Write-Host "Adding Server $RegServerName"
$NewServer = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServer($CMSDBStore, "$RegServerName")
$NewServer.SecureConnectionString = "server=$RegServerInstance;integrated security=true"
$NewServer.ConnectionString = "server=$RegServerInstance;integrated security=true"
$NewServer.ServerName = "$RegServerInstance"
$NewServer.Create()
}
else
{
Write-Host "Server $RegServerName already exists - cannot add."
}
}
I cut your script down to just the basics and it works for me. I did have to change the connection command to work in my environment but other than that and registering a default instance of SQL Server there were no errors. Once I did a refresh of the CMS server the newly registered server was visible and accessible.
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.Management.RegisteredServers') | Out-Null
$CMSInstance = 'CMS_ServerName'
$connectionString = "Data Source=$CMSInstance;Initial Catalog=master;Integrated Security=SSPI;"
$sqlConnection = new-object System.Data.SqlClient.SqlConnection($connectionString)
$conn = New-Object System.Data.SqlClient.SqlConnection("Server=$CMSInstance;Database=master;Integrated Security=True")
$CMSStore = new-object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($conn)
$CMSDBStore = $CMSStore.ServerGroups["DatabaseEngineServerGroup"]
$RegServerName = 'ServerToRegister'
$RegServerInstance = $RegServerName
$NewServer = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServer($CMSDBStore, "$RegServerName")
$NewServer.SecureConnectionString = "server=$RegServerInstance;integrated security=true"
$NewServer.ConnectionString = "server=$RegServerInstance;integrated security=true"
$NewServer.ServerName = "$RegServerInstance"
$NewServer.Create()

Powershell - User Mapping SQL Server 2012

I am trying to script User Mapping for different Login accounts. I have scripted the creation of users and individual server roles, but I can't figure out how to set User Mapping with Powershell, I will also need to set the Database Role membership, in Particular, db_backupoperator
Anyone know how to do this with Powershell?
Supposing your login is created
## Creating database user and assigning database role
#get variables
$instanceName = "yourInstance"
$loginName = "testLogin"
$dbUserName = "testUserName"
$databasename = "tempdb"
$roleName = "db_backupoperator"
$server = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server -ArgumentList $instanceName
#add a database mapping
$database = $server.Databases[$databasename]
$login = $server.Logins[$loginName]
if ($database.Users[$dbUserName])
{
$database.Users[$dbUserName].Drop()
}
$dbUser = New-Object `
-TypeName Microsoft.SqlServer.Management.Smo.User `
-ArgumentList $database, $dbUserName
$dbUser.Login = $loginName
$dbUser.Create()
#assign database role for a new user
$dbrole = $database.Roles[$roleName]
$dbrole.AddMember($dbUserName)
$dbrole.Alter

Scripting out individual objects from SQL using SMO

For my job I often have to script out a table with all its keys, constraints and Triggers (basically a full script to recreate the table) from a Microsoft SQL 2008 server.I also have to do this for procedures and triggers.
What I do now is open SSMS right click the object and select script to and select to script it to a file. So if I have 3 procedures to do and 10 tables and 1 trigger I end up doing this 14 times .
What I would like is a powershell script that I could feed a list of objects to and then it would go and use SMO to script each on out to an individual file.
Thanks for the help
Here is a PowerShell function I use whenever I have to script a database. It should be easy to modify just to scripts the objects you need.
function SQL-Script-Database
{
<#
.SYNOPSIS
Script all database objects for the given database.
.DESCRIPTION
This function scripts all database objects (i.e.: tables, views, stored
procedures, and user defined functions) for the specified database on the
the given server\instance. It creates a subdirectory per object type under
the path specified.
.PARAMETER savePath
The root path where to save object definitions.
.PARAMETER database
The database to script (default = $global:DatabaseName)
.PARAMETER DatabaseServer
The database server to be used (default: $global:DatabaseServer).
.PARAMETER InstanceName
The instance name to be used (default: $global:InstanceName).
.EXAMPLE
SQL-Script-Database c:\temp AOIDB
#>
param (
[parameter(Mandatory = $true)][string] $savePath,
[parameter(Mandatory = $false)][string] $database = $global:DatabaseName,
[parameter(Mandatory = $false)][string] $DatabaseServer = $global:DatabaseServer,
[parameter(Mandatory = $false)][string] $InstanceName = $global:InstanceName
)
try
{
if (!$DatabaseServer -or !$InstanceName)
{ throw "`$DatabaseServer or `$InstanceName variable is not properly initialized" }
$ServerInstance = SQL-Get-Server-Instance $DatabaseServer $InstanceName
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null
$s = New-Object Microsoft.SqlServer.Management.Smo.Server($ServerInstance)
$db = $s.databases[$database]
$objects = $db.Tables
$objects += $db.Views
$objects += $db.StoredProcedures
$objects += $db.UserDefinedFunctions
$scripter = New-Object ('Microsoft.SqlServer.Management.Smo.Scripter') ($s)
$scripter.Options.AnsiFile = $true
$scripter.Options.IncludeHeaders = $false
$scripter.Options.ScriptOwner = $false
$scripter.Options.AppendToFile = $false
$scripter.Options.AllowSystemobjects = $false
$scripter.Options.ScriptDrops = $false
$scripter.Options.WithDependencies = $false
$scripter.Options.SchemaQualify = $false
$scripter.Options.SchemaQualifyForeignKeysReferences = $false
$scripter.Options.ScriptBatchTerminator = $false
$scripter.Options.Indexes = $true
$scripter.Options.ClusteredIndexes = $true
$scripter.Options.NonClusteredIndexes = $true
$scripter.Options.NoCollation = $true
$scripter.Options.DriAll = $true
$scripter.Options.DriIncludeSystemNames = $false
$scripter.Options.ToFileOnly = $true
$scripter.Options.Permissions = $true
foreach ($o in $objects | where {!($_.IsSystemObject)})
{
$typeFolder=$o.GetType().Name
if (!(Test-Path -Path "$savepath\$typeFolder"))
{ New-Item -Type Directory -name "$typeFolder"-path "$savePath" | Out-Null }
$file = $o -replace "\[|\]"
$file = $file.Replace("dbo.", "")
$scripter.Options.FileName = "$savePath\$typeFolder\$file.sql"
$scripter.Script($o)
}
}
catch
{
Util-Log-Error "`t`t$($MyInvocation.InvocationName): $_"
}
}
Here's a script to backup an individual object. Simply pass the object name to the function:
http://sev17.com/2012/04/backup-database-object/