Pages

Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

Tuesday, December 30, 2025

Not Able to Add Article or Subscription After Upgrading

In SQL 2022, there is change in the linked server format used by replication for the listener subscriber with non default port, as shown below, the old format name is LISTENER,54321 and the new format is LISTENER and the port is in the provider string select name, provider_string from sys.servers
name              provider_string
LISTENER,54321    NULL
LISTENER          addr=tcp:LISTENER,54321
After the upgrade to SQL 2022 a couple of behaviors where observed: 

The first one is adding an article to a publication of a subscriber using the old format won’t generate a snapshot: A snapshot was not generated because no subscription needed initialization 

The second one is creating a new publication for an existing subscriber in the old format will get this error:

'SQL1' is not defined as a Subscriber for 'SQL2'. Could not update the distribution database subscription table. The subscription status could not be changed. The subscription could not be created. The subscription could not be found. Changed database context to 'dummy'. (Microsoft SQL Server, Error: 20032) 

Or 

Cannot insert the value NULL into column ‘freq_subday_interval’, table ‘distribution.dbo.MSrepl_agent_jobs’; column doen not allow nulls. UPDATE fails. Could not update the distribution database subscription table. The subscription status could not be changed. The subscription could not be created. The subscription could not be found. Changed database context to 'dummy'. (Microsoft SQL Server, Error: 20032) 

The workaround is to enable TF 15005 in both the publisher and distributor as well as creating aliases for the listener subscribers with non default port numbers, example LISTENER3 

If publisher and distributor are AG then aliases for the listener publisher and listener distributor are also needed, LISTENER1 and LISTENER2, respectively, in both 32 and 64-bit SQL Native Client, as shown below, in a PowerShell script
$aliases = @(
   @{name = 'LISTENER1'; value = 'DBMSSOCN,LISTENER1,54321'}
   @{name = 'LISTENER2'; value = 'DBMSSOCN,LISTENER2,54321'}
   @{name = 'LISTENER3'; value = 'DBMSSOCN,LISTENER3,54321'}
)

$registryPaths = @(
   'HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo'
   'HKLM:\SOFTWARE\WOW6432Node\Microsoft\MSSQLServer\Client\ConnectTo'
)
foreach ($alias in $aliases) {
   foreach ($registryPath in $registryPaths) {
      if (-not (Test-Path $registryPath)) {
          New-Item -Path $registryPath -Force
      }
      New-ItemProperty -Path $registryPath -Name $alias.name `
      -Value $alias.value -PropertyType String -Force
   }
}
Then you should be able to add the article or to create the subscription without specifying the port number

Wednesday, January 24, 2024

Automating Major SQL Version Upgrade with DBATools

You can easily automate a major version upgrade, for example to SQL 2022, using Install-DbaInstance from the DBATools PowerShell module. As of this writing, there was not a documented example, so I played around with it and figured it out. 

If you use the configuration parameter option
$config = @{
    ACTION="Upgrade"
}
You will get this error: The setting 'FEATURES' is not allowed when the value of setting 'ACTION' is 'Upgrade'. The workaround is to use a configuration file to override the FEATURES option that the configuration parameter adds by default.  The configuration file must have a least these three options: ACTION, INSTANCENAME, and QUIET

The following example does a remote upgrade of a named instance to SQL 2022 
$options = '
[OPTIONS]
ACTION="Upgrade"
INSTANCENAME="instance_name"
QUIET="True"
'
Set-Content -Path 'c:\temp\config.ini' -Value $options
 
$paramsUpgrade = @{
    ComputerName      = 'computer_name'
    Version           = '2022'
    Path              = 'sql_install_path’
    UpdateSourcePath  = 'sql_cu_path’
    ConfigurationFile = 'c:\temp\config.ini'
    Restart           = $true
    Credential        = Get-Credential
Confirm = $false } Install-DbaInstance @paramsUpgrade

Monday, November 6, 2023

Finding Modules that Depend on Az.Accounts

I needed to use new functionality in the Az.Compute module so I updated it to the last version 

Update-Module Az.Compute

But then my script started failing with this error: "Method 'get_SerializationSettings' does not have an implementation." This article suggested to downgrade the version of the Az.Accounts module to 2.12.1. That got me thinking how many modules will be affected by the downgrade of Az.Accounts?

You can find the answer by finding the dependencies of the modules using Find-Module. For example for the Az.Compute the current version at the time of writing this post is 6.3.0 and it has a dependency on Az.Accounts 2.13.0

Find-Module -Name Az.Compute
 
Version Name       Repository Description                                                                           
------- ----       ---------- -----------                                                                           
6.3.0   Az.Compute PSGallery  Microsoft Azure PowerShell...
 
$r = Find-Module -Name Az.Compute
$r.Dependencies
 
Name           Value                                                                                                                                
----           -----                                                                                                                                
Name           Az.Accounts                                                                                                                          
MinimumVersion 2.13.0                                                                                                                               
CanonicalId    powershellget:Az.Accounts/2.13.0#https://www...  

To find all the modules that have a dependency on Az.Accounts 2.12.1 you can use the same cmdlet Find-Module to first get a list of all modules that start with Az, then for each module ($module) get all the versions, next for each module version ($module2), check each dependency ($dep) if it matches the name and version you are looking for (Az.Accounts and 2.12.1), then show the the respective info. The variable $found is used as short circuit flag to break the loop once a match is found 

$depName = 'Az.Accounts'
$depVersion = '2.12.1'
$modules = Find-Module -Name 'az.*' |
Where-Object {$_.Name -ne $refName} |
Sort-Object -Property Name
 
foreach ($module in $modules) {
    $modules2 = Find-Module -Name $module.Name -AllVersions
    $found = $false
    foreach ($module2 in $modules2){
        foreach ($dep in $module2.Dependencies){
            if ($dep.Name -eq $depName -and $dep.MinimumVersion -eq $depVersion){
                [pscustomobject]@{
                    ModuleName = $module2.Name
                    ModuleVersion = $module2.Version
                    DependencyName = $depName
                    DependencyVersion = $depVersion
                }
                $found = $true
                break
            }
            else {
                $found = $false
            }
        }
        if ($found) {
            break
        }
    }
}
 
ModuleName           ModuleVersion DependencyName DependencyVersion
----------           ------------- -------------- -----------------
Az.Aks               5.3.2         Az.Accounts    2.12.1          
Az.ArcResourceBridge 0.1.0         Az.Accounts    2.12.1          
Az.Batch             3.4.0         Az.Accounts    2.12.1          
Az.Billing           2.0.1         Az.Accounts    2.12.1          
Az.CognitiveServices 1.13.1        Az.Accounts    2.12.1          
Az.Compute           5.7.0         Az.Accounts    2.12.1          
Az.ContainerRegistry 3.0.3         Az.Accounts    2.12.1          
Az.CosmosDB          1.10.0        Az.Accounts    2.12.1          
Az.CostManagement    0.3.1         Az.Accounts    2.12.1          
Az.DataProtection    1.2.0         Az.Accounts    2.12.1          
Az.EventGrid         1.6.0         Az.Accounts    2.12.1          
Az.EventHub          3.2.3         Az.Accounts    2.12.1          
Az.Kusto             2.2.0         Az.Accounts    2.12.1          
Az.Network           5.6.0         Az.Accounts    2.12.1          
Az.Reservations      0.12.0        Az.Accounts    2.12.1          
Az.Resources         6.6.0         Az.Accounts    2.12.1          
Az.Search            0.9.0         Az.Accounts    2.12.1          
Az.ServiceBus        2.2.1         Az.Accounts    2.12.1          
Az.Sql               4.5.0         Az.Accounts    2.12.1          
Az.SqlVirtualMachine 1.1.1         Az.Accounts    2.12.1          
Az.Storage           5.5.0         Az.Accounts    2.12.1          
Az.Websites          2.14.0        Az.Accounts    2.12.1          
Az.Workloads         0.1.0         Az.Accounts    2.12.1      

From the output you can see that in our example to use Az.Accounts 2.12.1 you require Az.Compute 5.6.0 instead of 6.3.0. Fortunately, Az.Compute 5.6.0 still had the new functionality I was looking. Thus, reinstalling the correct versions fixed the issue.

Uninstall-Module Az.Accounts -RequiredVersion 2.13.0
Install-Module Az.Accounts -RequiredVersion 2.12.1
Uninstall-Module Az.Compute -RequiredVersion 6.3.0
Install-Module Az.Compute -RequiredVersion 5.6.0

Saturday, January 14, 2023

An Exception Occurred in SMO While Trying to Manage a Service

One way to automate SQL tasks is by using SMO, but sometimes when there are multiple SQL versions installed or uninstalled it may be corrupted and you may get "An exception occurred in SMO while trying to manage a service..." when using PowerShell or “Cannot connect to WMI provider. You do not have permission or the server in unreachable…” when using SQL Server Configuration Manager. In this post I will show you how to easily fix it. 

To use SMO with PowerShell, first you need to load the assembly
   
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SqlWmiManagement') | Out-Null

Next, you can create an instance of the .Net object, providing the name of the server, e.g. PABLITO
   
$s = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer PABLITO
$s

ConnectionSettings : Microsoft.SqlServer.Management.Smo.Wmi.WmiConnectionInfo
Services           :
ClientProtocols    :
ServerInstances    :
ServerAliases      :
Urn                : ManagedComputer[@Name='PABLITO']
Name               : PABLITO
Properties         : {}
UserData           :
State              : Existing

Note that in the results above, services is blank, so when you reference it, you will get the error
   
$s.Services

The following exception occurred while trying to enumerate the collection:
"An exception occurred in SMO while trying to manage a service.".
At line:1 char:1
+ $s.Services
+ ~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], ExtendedTypeSystemException
    + FullyQualifiedErrorId : ExceptionInGetEnumerator

The fix is to compile the sqlmgmproviderxpsp2up.mof of the highest SQL version installed in the machine. You can quickly search the path of the .mof, order by Creation Date, and displaying the Directory name
   
Get-ChildItem "c:\program files (x86)\Microsoft SQL Server\*\Shared\sqlmgmproviderxpsp2up.mof" |
Sort-Object CreationTime |
Select-Object Directory
 
Directory
---------
C:\program files (x86)\Microsoft SQL Server\90\Shared
C:\program files (x86)\Microsoft SQL Server\100\Shared
C:\program files (x86)\Microsoft SQL Server\110\Shared

You can see that this machine has three SQL versions installed SQL 2005, 2008, and 2012 (Yes I still support those old versions but not by choice) To get the highest version, you just need to add -Last 1 and assign the result to the variable $i to execute mofcomp
   
$i = Get-ChildItem "c:\program files (x86)\Microsoft SQL Server\*\Shared\sqlmgmproviderxpsp2up.mof" |
Sort-Object CreationTime |
Select-Object Directory -Last 1

mofcomp "$($i.Directory)\sqlmgmproviderxpsp2up.mof"

Microsoft (R) MOF Compiler Version 6.1.7600.16385
Copyright (c) Microsoft Corp. 1997-2006. All rights reserved.
Parsing MOF file: C:\program files (x86)\Microsoft SQL Server\110\Shared\sqlmgmproviderxpsp2up.mof
MOF file has been successfully parsed
Storing data in the repository...
Done!

Now you can get the services with no errors
   
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SqlWmiManagement') | Out-Null
$s = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer PABLITO
$s.Services

Friday, December 30, 2022

Setting Delete On Termination in Attached AWS Volumes

By default, any additional EBS volumes that you attach to an EC2 instance persist even after the instance terminates, unless Delete On Termination is set on each of the attached volumes. This AWS doc shows how to set it in the console and cli but not in PowerShell. 

However, the cli example uses "aws ec2 modify-instance-attribute" and you can find in the AWS PowerShell reference doc a cmdlet with similar name Edit-EC2InstanceAttribute. So here is how to set it with PowerShell.


$bdm = New-Object Amazon.EC2.Model.InstanceBlockDeviceMappingSpecification
$ebs = New-Object Amazon.EC2.Model.EbsInstanceBlockDeviceSpecification

Next assign values similar to the cli json format example
     
[
  {
    "DeviceName": "device_name",
    "Ebs": {
      "DeleteOnTermination": true
    }
  }
]

The equivalent in PowerShell is the following
     
$ebs.DeleteOnTermination = $true
$bdm.DeviceName = 'device_name'
$bdm.Ebs = $ebs

Finally, call the cmdlet
     
Edit-EC2InstanceAttribute -InstanceId 'id' -BlockDeviceMapping $bdm -Region 'region'

Wednesday, September 8, 2021

Search-AzGraph for All Subscriptions

A while ago I wrote about Search-AzGraph here. Since then, I have used it quite a bit and learned that depending on how you connect you may not query all subscriptions

Connecting Specifying a Subscription

To illustrate this behavior, let's say you have a service principal with access to 3 subscriptions, so to connect you would use the cmdlet Connect-AzAccount specifying the first subscription context

$paramAz = @{
    ServicePrincipal = $true
    TenantId         = 'tttttttt-tttt-tttt-tttt-tttttttttttt'
    Credential       = Get-Credential
    Subscription     = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
}
Connect-AzAccount @paramAz

Then you can query the resource count in all subscriptions with this Kusto query

$query = "
Resources
| summarize count() by subscriptionId
"

As expected, the cmdlet Search-AzGraph returned the resource count in all 3 subscriptions

Search-AzGraph -Query $query 

subscriptionId                       count_
--------------                       ------
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx    577
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy    338
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz    111

However, if you change the context to the second subscription using Set-AzContext, you will see that now Search-AzGraph only shows the resource count for the second subscription

Set-AzContext -Subscription 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'

Search-AzGraph -Query $query 

subscriptionId                       count_
--------------                       ------
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy    338

Moreover, if you change the context to the third subscription, you will see that Search-AzGraph shows the resource count for the second and third subscriptions

Set-AzContext -Subscription 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz'

Search-AzGraph -Query $query 

subscriptionId                       count_
--------------                       ------
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy    338
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz    111

The reason is that Search-AzGraph uses the cumulative context when a subscription context is set in the connection, Get-Context shows the two subscriptions Search-AzGraph will use

(Get-AzContext).Account.ExtendedProperties.Subscriptions

yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz

A workaround is to pass to Search-AzGraph a list of all subscriptions using Get-AzSubscription

$subIds = (Get-AzSubscription).Id

Search-AzGraph -Query $query -Subscription $subIds

subscriptionId                       count_
--------------                       ------
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx    577
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy    338
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz    111

Connecting Without Specifying a Subscription

In this scenario, you would use the cmdlet Connect-AzAccount without specifying a subscription

$paramAz = @{
    ServicePrincipal = $true
    TenantId         = 'tttttttt-tttt-tttt-tttt-tttttttttttt'
    Credential       = Get-Credential
}
Connect-AzAccount @paramAz

Next, you change the context to the second subscription using Set-AzContext, you will see that Search-AzGraph shows the resource count for all subscriptions, unlike our previous case

Set-AzContext -Subscription 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'

Search-AzGraph -Query $query 

subscriptionId                       count_
--------------                       ------
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx    577
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy    338
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz    111

Because a subscription context is not set in the connection, Get-Context will show all the subscriptions Search-AzGraph will use

(Get-AzContext).Account.ExtendedProperties.Subscriptions

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx,
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,
zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz

Conclusion

If you specify a subscription context in the connection, Search-AzGraph will not look in all subscriptions when you change the context later. You can override this behavior by passing a subscription list. On the other hand, if you do not specify the subscription context in the connection, Search-AzGraph will look in all subscriptions regardless of changing the context.

Friday, June 11, 2021

Nesting Invoke-SqlCmd in the Pipeline

If you need to collect data from an list of SQL Servers stored in a table, you would write a PowerShell script that uses Invoke-SqlCmd to query the list from the table and then loop through each instance to execute another query. However, PowerShell 5 does not support nesting Invoke-SqlCmd in the pipeline. The workaround is to use an intermediate variable or use PowerShell 7. 

Scenario 

For example you have a table with the SQL instance names stored in SQL1
create table instances(name varchar(255))
insert instances values ('SQL1')
insert instances values ('SQL2')
insert instances values ('SQL3')

To query the list, you use Invoke-SqlCmd in PowerShell 5
$query = "select name from instances"
Invoke-Sqlcmd -ServerInstance 'SQL1' -Query $query

To loop through each instance, you pipe the results and use Invoke-SqlCmd to execute another query to let's say get the version
Invoke-Sqlcmd -ServerInstance 'SQL1' -Query $query |
ForEach-Object {
    $query2 = "select instance = serverproperty('InstanceName'), version = serverproperty('ProductVersion')"
    Invoke-Sqlcmd -ServerInstance $_.name -Query $query2
}

The script will fail with this error
# Invoke-Sqlcmd : The WriteObject and WriteError methods cannot be 
# called from outside the overrides of the BeginProcessing, ProcessRecord,     
# and EndProcessing methods, and they can only be called from within the same thread.
# Validate that the cmdlet makes these calls correctly, or contact Microsoft Customer 
# Support Services.

Workaround 

Use a variable $instances to hold the results of the first Invoke-SqlCmd and then pipe it
$instances = Invoke-Sqlcmd -ServerInstance 'SQL1' -Query $query 
$instances |
ForEach-Object {
    $query2 = "select instance = serverproperty('InstanceName'), version = serverproperty('ProductVersion')"
    Invoke-Sqlcmd -ServerInstance $_.name -Query $query2
}

or use PowerShell 7
Invoke-Sqlcmd -ServerInstance 'SQL1' -Query $query |
ForEach-Object {
    $query2 = "select instance = serverproperty('InstanceName'), version = serverproperty('ProductVersion')"
    Invoke-Sqlcmd -ServerInstance $_.name -Query $query2
}

# instance     version
# --------     -------
# SQL1         13.0.5865.1
# SQL2         13.0.5865.1
# SQL3         13.0.5492.2

Tuesday, May 5, 2020

Listing Azure Databases

If you are a data professional and need to list all the databases in your organization, across all subscriptions and resource providers, then you can easily accomplish this task by using Azure Resource Graph. We will start with writing the Kusto query in the Azure portal and later we will use PowerShell to execute the query programatically.

Azure Resource Graph Explorer

The easiest way to write a Kusto query is using the explorer. In the Azure portal search box, type Resource Graph Explorer and click on it. On the left pane search for sql, and you will see all the resource types that contain sql in the name. Click on resources, then click on the resource type microsoft.sql/servers/databases, both will show in the query editor in the right like this:

resources
 | where type == "microsoft.sql/servers/databases"

Next click on Run query and that's it, you wrote your first Kusto query!


In this example you will notice that master shows as a third database, you can filter it out by adding another where operator:

 | where name != "master"

It is a good practice to only show the columns needed (less data travel over the wire), you can select the columns with the project operator:

 | project type, name, kind, subscriptionId

Since the query will search in all subscriptions, you can create another query to find the subscriptions in your organizations using resourcecontainers of type microsoft.resources/subscriptions, and you will use the project operator to select what is needed such as subscriptionId and susbcriptionName:

 resourcecontainers
 | where type == "microsoft.resources/subscriptions"
 | project subscriptionId, subscriptionName = name

Now you can join both queries using the join operator with the column subscriptionId, and selecting again only the columns needed with the project operator:

resources
 | where type == "microsoft.sql/servers/databases"
 | where name != "master"
 | project type, name, kind, subscriptionId
 | join kind = inner (
   resourcecontainers
   | where type == "microsoft.resources/subscriptions"
   | project subscriptionId, subscriptionName = name
   ) on subscriptionId
 | project type, name, kind, subscriptionName

Azure PowerShell

Now that you have the Kusto query under your belt, you can call it from a PowerShell script using the cmdlet Search-AzGraph. But first you will need the Az modules, then connect to Azure:

Connect-AzAccount

Next you can run the same query from above. In addition, you can add other database resource types (MySQL and PostgreSQL) using the in operator

$query = '
resources
 | where type in (
    "microsoft.sql/servers/databases"
    ,"microsoft.sqlvirtualmachine/sqlvirtualmachines"
    ,"microsoft.sql/managedinstances"
    ,"microsoft.dbforpostgresql/servers"
    ,"microsoft.dbformysql/servers"
   )
 | where name != "master"
 | project type, name, kind, subscriptionId
 | join kind = inner (
   resourcecontainers
   | where type == "microsoft.resources/subscriptions"
   | project subscriptionId, subscriptionName = name
   ) on subscriptionId
 | project type, name, kind, subscriptionName
'
Search-AzGraph -Query $query

This was just a peek of the capabilities that Azure Resource Graph provides and hopefully it sparked your interest to learn more about it.


Monday, September 30, 2019

Using the .Net Oracle Data Access Client

There is a cmdlet for SQL Server databases called Invoke-Sqlcmd that will allow you to run TSQL commands but there is not a cmdlet for Oracle. This is where the .Net framework comes to the rescue.

Microsoft no longer provides the Oracle library. So we need to download it from the Oracle website link below

https://www.oracle.com/database/technologies/dotnet-odacmsi-vs2017-downloads.html

Then, we can use Add-Type to add the .Net class to a PowerShell session, and use a basic .Net framework syntax to execute database commands, as shown below.


Add-Type -Path 'C:\Program Files (x86)\Oracle Developer Tools for VS2017\odp.net\managed\common\Oracle.ManagedDataAccess.dll'

$conString = "User Id=myuser;Password=mypassword;Data Source=mydatabase"
$con = New-Object Oracle.ManagedDataAccess.Client.OracleConnection
$con.ConnectionString = $conString
$con.Open()

$cmd = New-Object Oracle.ManagedDataAccess.Client.OracleCommand
$cmd.Connection = $con
$cmd.CommandText = 'select banner from v$version'
$rs = $cmd.ExecuteReader()

while ($rs.Read()) 
{
    Write-Host "$($rs.GetValue(0))"
}

$con.Close()
$con.Dispose()


Friday, July 26, 2019

Azure DevOps Build Queue Wait Time

Azure DevOps is used to automate CI/CD pipelines. It uses an agent to execute one job at a time. If we have more jobs to execute than agents then some jobs have to wait in the queue. This post is about how to gather metrics about the wait time to decide when to add more agents.

Agents are organized into pools and the data needed is available via REST API.  The URI to list the pools is

https://dev.azure.com/{your organization}/_apis/distributedtask/pools

and the URI to list the jobs running on each pool is

https://dev.azure.com/{your organization}/_apis/distributedtask/pools/{pool id}/jobrequest

To authenticate to Azure DevOps, you will need your access token and replace it in the PowerShell script below

$token = "{your access token}"
$bytes = [System.Text.Encoding]::UTF8.GetBytes(":$($token)")
$base64bytes = [System.Convert]::ToBase64String($bytes)
$headers = @{ "Authorization" = "Basic $base64bytes"}

$uri = 'https://dev.azure.com/{your organization}/_apis/distributedtask/pools/{pool id}/jobrequests'

$r = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get -ContentType "application/json"


The result is a PSCustomObject with other nested PSCustomObject as Get-Member shows, so  expressions are needed to query the nested objects for example reservedAgent. The time fields such as queueTime are strings, so expressions are also required to convert to universal time

$r.value | gm

   TypeName: System.Management.Automation.PSCustomObject

Name                   MemberType   Definition
----                   ----------   ----------
Equals                 Method       bool Equals(System.Object obj)                     
GetHashCode            Method       int GetHashCode()                                  
GetType                Method       type GetType()                                     
ToString               Method       string ToString()                                  
agentDelays            NoteProperty Object[] agentDelays=System.Object[]               
assignTime             NoteProperty string assignTime=2019-07-19T21:40:26.8666667Z     
data                   NoteProperty System.Management.Automation.PSCustomObject
definition             NoteProperty System.Management.Automation.PSCustomObject
demands                NoteProperty Object[] demands=System.Object[]                   
hostId                 NoteProperty string hostId=22399273-0444-4322-aea9-2e628bac6c60 
jobId                  NoteProperty string jobId=12f1170f-54f2-53f3-20dd-22fc7dff55f9  
lockedUntil            NoteProperty string lockedUntil=2019-07-19T22:05:33.4833333Z    
matchesAllAgentsInPool NoteProperty bool matchesAllAgentsInPool=True                   
orchestrationId        NoteProperty string orchestrationId=d476f3d0-c2b4-41d5-9ebb
owner                  NoteProperty System.Management.Automation.PSCustomObject
planId                 NoteProperty string planId=d476f3d0-c2b4-41d5-9ebb-789eb9704ca7 
planType               NoteProperty string planType=Build                              
poolId                 NoteProperty int poolId=9                                       
queueTime              NoteProperty string queueTime=2019-07-19T21:40:26.4633333Z      
receiveTime            NoteProperty string receiveTime=2019-07-19T21:40:29.379468Z     
requestId              NoteProperty int requestId=1024                                 
reservedAgent          NoteProperty System.Management.Automation.PSCustomObject 
scopeId                NoteProperty string scopeId=0020fc51-3569-4cdc-8c2b-2affa5401996
serviceOwner           NoteProperty string serviceOwner=00025394-6065-48ca-87d


The final script parses the required info and filters out Pool Maintenance jobs and data older than 3 hours using Where-Object.

$r.value `
| Select-Object requestId, poolId, result,`
  @{Label=”name”; Expression={$_.definition.name}},`
  @{Label=”agent”; Expression={$_.reservedAgent.name}},`
  @{Label=”queued”; Expression={(get-date $_.queueTime).ToUniversalTime()}},` 
  @{Label=”assigned”; Expression={(get-date $_.assignTime).ToUniversalTime()}},` 
  @{Label=”received”; Expression={(get-date $_.receiveTime).ToUniversalTime()}},` 
  @{Label=”finished”; Expression={(get-date $_.finishTime).ToUniversalTime()}}`
| Where-Object {$_.name -ne 'PoolMaintenance' `
  -and $_.queued -gt (Get-Date).AddHours(-3).ToUniversalTime()}`
| Select-Object requestId, poolId, result, name, agent, queued,`
  @{Label=”wait”; Expression={$_.assigned-$_.queued}},`
  @{Label=”duration”; Expression={$_.finished-$_.received}}`
| Format-Table

requestId poolId result    name              agent   queued                wait     duration
--------- ------ ------    ----              -----   ------                ----     --------
     1023      9 succeeded project-for-demo2 AGENT01 7/19/2019 8:00:31 PM  00:33:40 00:00:25
     1022      9 succeeded project-for-demo1 AGENT01 7/19/2019 7:55:04 PM  00:00:00 00:38:03


From the results above, request id 1023 was waiting 33 minutes for request id 1022 to complete. If a 33 minute wait is not acceptable then a second agent can be added to process the job. Also this data can be collected and stored for trend analysis.

Friday, June 28, 2019

SqlServerDsc Account

The PowerShell SqlServerDsc module uses the account [NT AUTHORITY\SYSTEM] to login to SQL Server by default. However, I was puzzled by why it can execute admin commands since it was not in the sysadmin role. It turns out that [NT AUTHORITY\SYSTEM] is like a member of the group [NT SERVICE\Winmgmt] and it will use the group permissions.

For example, to enable CLR on the named instance SERVER01\SQL01, we can use this script

[DSCLocalConfigurationManager()]
Configuration LcmPush 
{    
    Node SERVER01
    {
        Settings 
        {
            AllowModuleOverwrite = $True
            ConfigurationMode = 'ApplyOnly'
            RefreshMode = 'Push'
        }
    }
}

Configuration SqlServerConfig
{
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName SqlServerDsc

    node SERVER01
    {
        SqlServerConfiguration clr
        {
            ServerName     = 'SERVER01'
            InstanceName   = 'SQL01'
            OptionName     = 'clr enabled'
            OptionValue    = 1
        }
    }
}

$MofPath = 'C:\DSC\LCM'
LcmPush -OutputPath $MofPath
Set-DscLocalConfigurationManager -Path $MofPath -Force 

SqlServerConfig -OutputPath $MofPath
Start-DscConfiguration -ComputerName 'SERVER01' -Path $MofPath -Force -Wait -Verbose 

A SQL trace will show that the account used is [NT AUTHORITY\SYSTEM]



If we remove [NT AUTHORITY\SYSTEM] from the sysadmin server role to and disable CLR

ALTER SERVER ROLE [sysadmin] DROP MEMBER [NT AUTHORITY\SYSTEM]
GO
sp_configure 'clr enabled', 0
reconfigure
GO

And execute the script again, it will still succeed

VERBOSE: [SERVER01]: LCM:  [ Start  Set      ]  [[SqlServerConfiguration]clr]
VERBOSE: [SERVER01]:                            [[SqlServerConfiguration]clr] Found PowerShell module SqlServer already imported in the session.
VERBOSE: [SERVER01]:                            [[SqlServerConfiguration]clr] Connected to SQL instance 'SERVER01\SQL01'.
VERBOSE: [SERVER01]:                            [[SqlServerConfiguration]clr] Configuration option 'clr enabled' has been updated to value '1'.
VERBOSE: [SERVER01]:                            [[SqlServerConfiguration]clr] The option was changed without the need to restart the SQL Server instance.
VERBOSE: [SERVER01]: LCM:  [ End    Set      ]  [[SqlServerConfiguration]clr]  in 0.3290 seconds.

If we remove [NT SERVICE\Winmgmt] from the sysadmin server role to and disable CLR

ALTER SERVER ROLE [sysadmin] DROP MEMBER [NT SERVICE\Winmgmt]
GO
sp_configure 'clr enabled', 0
reconfigure
GO

And execute the script again, it will error out

Exception calling "Alter" with "0" argument(s): "Alter failed. "
    + CategoryInfo          : NotSpecified: (:) [], CimException
    + FullyQualifiedErrorId : FailedOperationException
    + PSComputerName        : SERVER01
 
The PowerShell DSC resource '[SqlServerConfiguration]clr' with SourceInfo '::22::9::SqlServerConfiguration' threw one or more non-terminating errors while running the Set-TargetResource 
functionality. These errors are logged to the ETW channel called Microsoft-Windows-DSC/Operational. Refer to this channel for more details.
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : NonTerminatingErrorFromProvider
    + PSComputerName        : SERVER01
The SendConfigurationApply function did not succeed.
    + CategoryInfo          : NotSpecified: (root/Microsoft/...gurationManager:String) [], CimException
    + FullyQualifiedErrorId : MI RESULT 1

There is very little documentation of what [NT SERVICE\Winmgmt] is for, but the experiment above shows that [NT AUTHORITY\SYSTEM] behaves like a member of the group [NT SERVICE\Winmgmt] even though is not actually a group

Friday, May 24, 2019

Copy Data with PowerShell Benchmark

To copy a large amount of rows from one SQL Server (server1) to another (server2), I compared the .Net data provider I used back with PS 2.0, with the new sqlserver module -OutputAs DataTable. The results are shown below.

Using .Net, we need to write more code but it is faster. Using -OutputAs DataTable, we can write much less code but it takes longer since it need to load the table in memory. Note -OutputAs DataRows is the default which is even more slower since it will copy one row at a time

Repro

On server1, create a view to easily generate 6.4 million rows

create view my_spt_values
as
select c1.* from spt_values c1 cross join spt_values c2

On server2, create a stage table

select * into test_stage from spt_values where 1=2

Create the scripts below to copy from server1 to server2

Sqlclient.ps1

$conTargetString = "Data Source=server2;Initial Catalog=mydb;Integrated Security=True"

$conTarget = New-Object System.Data.SQLClient.SQLConnection($conTargetString)
$conTarget.Open()

$conBulk = New-Object System.Data.SQLClient.SQLBulkCopy($conTargetString, [System.Data.SQLClient.SqlBulkCopyOptions]::TableLock)
$conBulk.DestinationTableName = 'test_stage'
$conBulk.BatchSize = 10000000

$cmdTarget = New-Object System.Data.SQLClient.SQLCommand
$cmdTarget.Connection = $conTarget

$cmdTarget.CommandText = "truncate table test_stage"
$cmdTarget.ExecuteNonQuery()

$conSourceString = "Data Source=server1;Initial Catalog=mydb;Integrated Security=True"
$conSource = New-Object System.Data.SQLClient.SQLConnection($conSourceString)
$conSource.Open()

$cmdSource = New-Object System.Data.SQLClient.SQLCommand
$cmdSource.CommandText = "select * from my_spt_values"
$cmdSource.Connection = $conSource

$tabSource = $cmdSource.ExecuteReader()
$conBulk.WriteToServer($tabSource)

$conSource.Close()
$conSource.Dispose()

$conTarget.Close()
$conTarget.Dispose() 

Readcmd.ps1

Invoke-Sqlcmd -ServerInstance server2 -Database mydb -Query "truncate table test_stage"

Read-SqlViewData -ServerInstance server1 -Database master -SchemaName dbo -ViewName my_spt_values -OutputAs DataTable |
Write-SqlTableData -ServerInstance server2 -Database myfb -SchemaName dbo -TableName test_stage -Passthru 

Sqlcmd.ps1

Invoke-Sqlcmd -ServerInstance server2 -Database mydb -Query "truncate table test_stage"

Invoke-Sqlcmd -ServerInstance server1 -Database master -Query "select * from my_spt_values" -OutputAs DataTables |
Write-SqlTableData -ServerInstance server2 -Database mydb -SchemaName dbo -TableName test_stage -Passthru 

Measure

Measure-Command -Expression {.\sqlclient.ps1} | select seconds
Measure-Command -Expression {.\readcmd.ps1} | select seconds
Measure-Command -Expression {.\sqlcmd.ps1} | select seconds


Seconds
-------
     12
     36
     45

Reading the Data

While measuring, open task manager to see the memory consumption of each of the scrips, we will see that the first method did not load the entire result set to memory while the latter 2 methods did


On server1, a SQL trace will show the duration to select the same amount rows. The first method is the fasted with a 12 seconds. The last 2 methods take longer (18 and 24 seconds respectively) since they have to wait until the rows are loaded into memory, so they will hold a share lock on the table longer too, which may cause blocking in the source if other processes are writing to it



Also sp_WhoIsActive will show the wait type of ASYNC_NETWORK_IO which means the it is waiting for the client (PowerShell) to process the rows


Writing the Data

On server2, a SQL trace, will show the other benefit of using the .Net bulkcopy: the lock table hint, so the logical reads are much more less. In addition, bulkcopy allows to control the batch size to not fill up the transaction log if there is lots of concurrent transactions


We can see that to copy a large amount of rows, the .Net data provider is much faster because it does not load the entire result set into memory, and allows more control with hints such as table lock that saves IO

Tuesday, February 5, 2019

Monitoring Identity Column Values

Identity columns are often used for generating key values. Once create it, we often forget about it. However, once the max value allowed by the data type is reached, no more rows can be inserted.

To avoid unexpected outage, we can use sys.identity_columns to monitor if the values are approaching the max limit.  The 6 data types supported are: tinyint, smallint, int, bigint, numeric, and decimal.

For example, we can create 3 tables with identity columns, assign a seed value, and insert some data

create table paul_test1 (a tinyint identity, b char(1))
create table paul_test2 (a int identity, b char(1))
create table paul_test3 (a numeric(6,0) identity, b char(1))

dbcc checkident('paul_test1', reseed, 200)
dbcc checkident('paul_test2', reseed, 2000000000)
dbcc checkident('paul_test3', reseed, 999999)

insert paul_test1 (b) values ('x')
insert paul_test2 (b) values ('x')
insert paul_test3 (b) values ('x')

If we insert one more row on the third table

insert paul_test3 (b) values ('x')

We will get this error:

Msg 8115, Level 16, State 1, Line 62 Arithmetic overflow error converting IDENTITY to data type numeric. Arithmetic overflow occurred.

We can query sys.identity_columns to find the identity column last value of a table and calculate the percent used of the positive values. Then we can run the query on each database using sp_MSforeachdb, as shown on the script below (e.g. C:\TEMP\identity_values.sql)

create table #monitor_identity(
instance_name varchar(255)
,database_name varchar(255)
,table_name varchar(255) 
,schema_name varchar(255)
,column_name varchar(255) 
,current_value numeric(38)
,max_value numeric(38)
,pct_used numeric(38,2)
)
insert #monitor_identity
exec sp_MSforeachdb '
select 
@@servername
,''?''
,object_schema_name(object_id, db_id(''?''))
,object_name(object_id, db_id(''?''))
, name
, cast(last_value as numeric(38))
, max_value = case TYPE_NAME(system_type_id)
when ''tinyint'' then 255
when ''smallint'' then 32767
when ''int'' then 2147483647
when ''bigint'' then 9223372036854775807
when ''numeric'' then cast(replicate(''9'',precision) as numeric(38))
when ''decimal'' then cast(replicate(''9'',precision) as numeric(38))
end
, pct_used = cast(last_value as numeric(38)) / case TYPE_NAME(system_type_id)
when ''tinyint'' then 255
when ''smallint'' then 32767
when ''int'' then 2147483647
when ''bigint'' then 9223372036854775807
when ''numeric'' then cast(replicate(''9'',precision) as numeric(38))
when ''decimal'' then cast(replicate(''9'',precision) as numeric(38))
end * 100
from ?.sys.identity_columns with (nolock)
where last_value is not null
'
select
instance_name  
,database_name 
,schema_name 
,table_name 
,column_name 
,current_value 
,max_value 
,pct_used 
from #monitor_identity
where pct_used > 70
order by 
database_name 
,schema_name 
,table_name 
drop table #monitor_identity

Output


To run the script in multiple instances, we can have a list of instances in a text file (e.g. C:\TEMP\instance_list.txt)

instance1
instance2

Then we can easily use a PowerShell script to pipe the instance list content to Invoke-Sqlcmd, and pipe to Out-GridView

Get-Content C:\TEMP\instance_list.txt | Invoke-Sqlcmd -InputFile C:\TEMP\identity_values.sql | Out-GridView

Saturday, August 29, 2015

Automatically Manage Server Registrations

To create, update, or drop server registrations, we can either use the system tables directly:

  • msdb.dbo.sysmanagement_shared_registered_servers_internal
  • msdb.dbo.sysmanagement_shared_server_groups_internal
Or we can use SMO and PowerShell to automatically maintain the Central Management Servers. For  example, three group levels will be created as shown below



The central SQL Server  is named SQL1. The database is named dba_utilities. The table called instances is used to store the instances that are automatically or manually discovered in your organization. The stored procedure called sel_registered_servers is used to show the list of servers and respective groups. Additionally, sel_registered_servers_to_drop is used to show the list of servers that are no longer in your organization.
 
-- table for instances
create table instances(
logical_name varchar(255) not null constraint instances_pk primary key
,physical_name varchar(255) not null
,support_code varchar(10) not null
,sql_version varchar(10) not null
)
go

-- insert instances
insert instances values('SQL1','COMPUTER3\SQL1','TEST','2012')
insert instances values('SQL2','COMPUTER1\SQL2','PROD','2008')
insert instances values('SQL3','COMPUTER1\SQL3','PROD','2008')
insert instances values('SQL4','COMPUTER2\SQL4','PROD','2012')
insert instances values('SQL5','COMPUTER3\SQL5','TEST','2012')
insert instances values('SQL6','COMPUTER3\SQL6','TEST','2014')
go

-- stored procedure to show registered servers
create proc sel_registered_servers
as
select
'Support Version' group1
,support_code group2
,sql_version group3
,logical_name
,physical_name
from instances
where logical_name <> 'SQL1' -- central server cannot be added 
union
select
'Version Support' group1
,sql_version group2
,support_code group3
,logical_name
,physical_name
from instances
where logical_name <> 'SQL1' -- central server cannot be added 
go

-- stored procedure to show obsolete servers
create proc sel_registered_servers_to_drop
as
select distinct name 
into #temp
from msdb.dbo.sysmanagement_shared_registered_servers_internal 

select t.name logical_name
from instances i
right join #temp t
on i.logical_name = t.name
where i.logical_name is null

drop table #temp
go

Output of sel_registered_servers



The PowerShell script can read the results of the stored procedures and hierarchically navigate through the directory tree and create, update, or drop server registrations
 
function Add-RegisteredServer ($group1, $group2, $group3, $logicalName, $physicalName)
{
    $serverGroups = $registeredServer.DatabaseEngineServerGroup
    $groupList = @($group1, $group2, $group3)
    
    # navigate down the hierarchy

    foreach ($group in $groupList) 
    {
        if ($serverGroups.ServerGroups[$group] -eq $null)
        {
         $newGroup = New-Object Microsoft.SqlServer.Management.RegisteredServers.ServerGroup($serverGroups, $group)
         $newGroup.create()
         $serverGroups.refresh()
        }
        $serverGroups = $serverGroups.ServerGroups[$group]
    }  

    # create or alter the registered server

    if($serverGroups.RegisteredServers.name -contains $logicalName)
    {
        $oldServer = $serverGroups.RegisteredServers[$logicalName]

        if ($oldServer.ServerName -ne $physicalName)
        {
            Write-Host "$group1 $group2 $group3 $logicalName Altered"
            $oldServer.ServerName = $physicalName
            $oldServer.Alter()
        }
    }
    else
    {
        Write-Host "$group1 $group2 $group3 $logicalName Created"
        $newServer = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServer($serverGroups, $logicalName)
        $newServer.ServerName = $physicalName
     $newServer.Create()
    }
}

function Drop-RegisteredServer ($logicalName)
{
    $serverGroups = $registeredServer.DatabaseEngineServerGroup
    $groupList1 = $serverGroups.ServerGroups.Name

    foreach ($group1 in $groupList1) 
    {
        $groupList2 = $serverGroups.ServerGroups[$group1].ServerGroups.Name
    
        foreach ($group2 in $groupList2) 
        {
            $groupList3 = $serverGroups.ServerGroups[$group1].ServerGroups[$group2].ServerGroups.Name

            foreach ($group3 in $groupList3) 
            {
                $oldGroup = $serverGroups.ServerGroups[$group1].ServerGroups[$group2].ServerGroups[$group3]

                if($oldGroup.RegisteredServers.Name -contains $logicalName)
                {
                    Write-Host "$group1 $group2 $group3 $logicalName Dropped"
                    $oldServer = $oldGroup.RegisteredServers[$logicalName]
                    $oldServer.Drop()
                }
            }
        }
    }
}

function Drop-RegisteredServerGroup ()
{
    # Drop group3

    $serverGroups = $registeredServer.DatabaseEngineServerGroup
    $groupList1 = $serverGroups.ServerGroups.Name

    foreach ($group1 in $groupList1) 
    {
        $groupList2 = $serverGroups.ServerGroups[$group1].ServerGroups.Name
    
        foreach ($group2 in $groupList2) 
        {
            $groupList3 = $serverGroups.ServerGroups[$group1].ServerGroups[$group2].ServerGroups.Name

            foreach ($group3 in $groupList3) 
            {
                $oldGroup = $serverGroups.ServerGroups[$group1].ServerGroups[$group2].ServerGroups[$group3]
                
                if($oldGroup.RegisteredServers.Count -eq 0)
                {
                    Write-Host "$group1 $group2 $group3 Dropped"
                    $oldGroup.Drop()
                }
            }
        }
    }

    # Drop group2

    $serverGroups = $registeredServer.DatabaseEngineServerGroup
    $groupList1 = $serverGroups.ServerGroups.Name

    foreach ($group1 in $groupList1) 
    {
        $groupList2 = $serverGroups.ServerGroups[$group1].ServerGroups.Name
    
        foreach ($group2 in $groupList2) 
        {
            $oldGroup = $serverGroups.ServerGroups[$group1].ServerGroups[$group2]
                
            if($oldGroup.ServerGroups.Count -eq 0)
            {
                Write-Host "$group1 $group2 Dropped"
                $oldGroup.Drop()
            }
        }
    }

    # Drop group2

    $serverGroups = $registeredServer.DatabaseEngineServerGroup
    $groupList1 = $serverGroups.ServerGroups.Name

    foreach ($group1 in $groupList1) 
    {
        $oldGroup = $serverGroups.ServerGroups[$group1]
                
        if($oldGroup.ServerGroups.Count -eq 0)
        {
            Write-Host "$group1 Dropped"
            $oldGroup.Drop()
        }
    }
}

# Main Program

$tarSrv = "COMPUTER3\SQL1"
$tarDB  = "dba_utilities"

try
{
    $server = New-Object Microsoft.SqlServer.Management.Smo.Server($tarSrv)
    $registeredServer = New-Object Microsoft.SqlServer.Management.RegisteredServers.RegisteredServersStore($server.ConnectionContext.SqlConnectionObject)

    $tarCon = New-Object System.Data.SQLClient.SQLConnection("Data Source=$tarSrv;Initial Catalog=$tarDB; Integrated Security=True;")
    $tarCon.Open()

    $tarCmd = New-Object system.Data.SqlClient.SqlCommand("exec sel_registered_servers", $tarCon)   
    $instances = $tarCmd.ExecuteReader()

    while ($instances.Read()) 
    {
        $group1 = $instances.GetValue(0)
        $group2 = $instances.GetValue(1)
        $group3 = $instances.GetValue(2)
        $logicalName = $instances.GetValue(3)
        $physicalName= $instances.GetValue(4)
    
        Add-RegisteredServer $group1 $group2 $group3 $logicalName $physicalName
    }
    $instances.Close()

    $tarCmd.CommandText = "exec sel_registered_servers_to_drop"
    $instances = $tarCmd.ExecuteReader()

    while ($instances.Read()) 
    {
        $logicalName = $instances.GetValue(0)
    
        Drop-RegisteredServer $logicalName 
    }
    $instances.Close()

    Drop-RegisteredServerGroup

    $tarCon.Close()
    $tarCon.Dispose()

}
catch
{
    $e = $_ | select -ExpandProperty InvocationInfo
    $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
    throw $m
}

Friday, May 1, 2015

Enabling Additional Tracing for Data Collection (MDW)

If sysssislog or syscollector_execution_log_internal_message don’t give us a clue of what is the issue, we can enable additional tracing by adding a registry key as posted here

To enable tracing in a remote computer, we can execute this PowerShell script

 
$computer = "mycomputer"
$cred = Get-Credential mydomain\myadminaccount
Enter-PSSession $computer -Credential $cred

Push-Location 
if (-not(Test-Path("HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SQLTools")))
{
    Set-Location "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server"
    New-Item -Name SQLTools
}
if (-not(Test-Path("HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SQLTools\dcexec")))
{
    Set-Location "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SQLTools"
    New-Item -Name dcexec
    New-ItemProperty -Name Components -PropertyType string -Path dcexec -Value "DCEXEC,TxDataCollector,DataCollectorController,DataCollectorTasks,Microsoft.SqlServer.Management.CollectorTasks.dll"
    New-ItemProperty -Name Tracelvl -PropertyType dword -Path dcexec -Value "4294967287"
    New-ItemProperty -Name Traceloc -PropertyType dword -Path dcexec -Value 3
    New-ItemProperty -Name Prefix -PropertyType string -Path dcexec -Value "date,time,pid,tid"
    New-ItemProperty -Name LogDir -PropertyType string -Path dcexec -Value "c:\temp\tracing"
    New-ItemProperty -Name LogFileMode -PropertyType string -Path dcexec -Value "Unique"
}
Pop-Location
Exit  

Then restart data collection using the script of a previous post

A trace file per process id will be created on c:\temp\tracing, once the error in question occurs, we can open the respective file to see the information logged. File name example:

c:\Temp\Tracing\dcexec_04_27_2015_09_22_28_PID30316_n.log

To disable tracing in a remote computer, we can execute this PowerShell script

 
$computer = "mycomputer"
$cred = Get-Credential mydomain\myadminaccount
Enter-PSSession $computer -Credential $cred

Push-Location 
if (Test-Path("HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SQLTools"))
{
    Remove-Item "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\SQLTools" -Recurse 
}
Pop-Location  
Exit  

Then restart data collection using the script of a previous post

Restarting Data Collection (MDW)

Most data collection errors can be resolved by restarting data collection. A PowerShell script can be used to:

  1. Stop data collection in all the SQL Server instances running on the machine
  2. Kill dcexec.exe processes
  3. Delete all cache files (our cache directory is c:\temp)
  4. Start data collection in all the SQL Server instances running on the machine

 
function RestartDataCollection($SrcSrv)
{
    try
    {
        $SrcCon  = New-Object System.Data.SqlClient.SQLConnection("Data Source=$SrcSrv;Initial Catalog=master;Integrated Security=True;Connection Timeout=60") 
        $SrcCon.Open() 
                
        $SrcCmd = New-Object system.Data.SqlClient.SqlCommand("select @@version", $SrcCon)  
        $SrcCmd.CommandTimeout = 300

        $query = "
        declare @instances table( 
        value nvarchar(100)
        ,instance_name nvarchar(100)
        ,data nvarchar(100)
        );

        declare @count int;

        with a as(
        select *, ROW_NUMBER() over(partition by l.collection_set_id order by l.log_id desc) row_num
        from msdb.dbo.syscollector_execution_log_internal l with(nolock)
        )
        select @count = count(*)
        from a
        where a.row_num = 1
        and a.failure_message is not null

        --if @count > 0 

        insert into @instances
        exec xp_regread
        @rootkey = 'HKEY_LOCAL_MACHINE'
        ,@key = 'SOFTWARE\Microsoft\Microsoft SQL Server'
        ,@value_name = 'InstalledInstances'

        select 
        case when instance_name = 'MSSQLSERVER' then SERVERPROPERTY('ServerName') else instance_name end instance_name 
        ,SERVERPROPERTY('ComputerNamePhysicalNetBIOS') computer
        from @instances
        "

        $SrcCmd.CommandText = $query
        $instances = $SrcCmd.ExecuteReader()
        $a = New-Object System.Collections.ArrayList
        $i = 0

        $query = "
        exec msdb.dbo.sp_syscollector_stop_collection_set @name = 'Server Activity'
        exec msdb.dbo.sp_syscollector_stop_collection_set @name = 'Query Statistics'
        exec msdb.dbo.sp_syscollector_stop_collection_set @name = 'Disk Usage'
        "
        
        "stop collection"

        $computer = ""

        while ($instances.Read()) 
        {
            $SrcSrv = $instances.GetValue(0) 
            $computer = $instances.GetValue(1)
            $r = $a.Add($i) 
            $a[$i] = $SrcSrv
            $i = $i + 1
            $SrcSrv
            try
            {
                Invoke-Sqlcmd -ServerInstance $SrcSrv -Query $query -QueryTimeout 300
            }
            catch
            {
                $e = $_ | select -ExpandProperty InvocationInfo
                $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
                $m                
            }
        }
        $instances.Close()

        "killing dcexec processes"

        $processes = Get-Process -ComputerName $computer -Name "DCEXEC" -ErrorAction SilentlyContinue
        
        foreach ($process in $processes)
        {
            try
            {
                taskkill /F /T /S $computer /PID $process.Id
            }
            catch
            {
                $e = $_ | select -ExpandProperty InvocationInfo
                $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
                $m                
            }
        }

        "deleting cache files"

        $files = get-childitem "\\$computer\c$\temp\*.cache" -ErrorAction SilentlyContinue

        foreach ($file in $files)
        {
            try
            {
                $file.Delete()
            }
            catch
            {
                $e = $_ | select -ExpandProperty InvocationInfo
                $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
                $m                
            }
        }

        $query = "
        exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Server Activity'
        exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Query Statistics'
        exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Disk Usage'
        "
        
        "start collection"

        foreach ($SrcSrv in $a)
        {
            $SrcSrv
            try
            {
                Invoke-Sqlcmd -ServerInstance $SrcSrv -Query $query -QueryTimeout 300
            }
            catch
            {
                $e = $_ | select -ExpandProperty InvocationInfo
                $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
                $m                
            }
        }

        $SrcCon.Close() 
        $SrcCon.Dispose() 
    }
    catch
    {
        $e = $_ | select -ExpandProperty InvocationInfo
        $m = $_.Exception.Message.TrimEnd().Replace("'","") + ", " + $e.ScriptLineNumber.ToString() + ", " + $e.OffsetInLine.ToString()
        $m
    }
}

("Server1", "Server2", "Server3") | foreach{RestartDataCollection $_} 


Friday, January 30, 2015

Installing the MDW automatically

If we have hundreds of SQL Servers, a centralized MDW may not scale, see my previous post here. So we will need to install it on each SQL Server.

The following PowerShell script loops through each SQL Server name stored in a table named msmdw_central.core.instances, and for each instance does the following:
  1. Create the MDW database called msmdw
  2. Install the supporting objects
  3. Compress the tables if Enterprise edition is used
  4. Create an index to speed up purges
  5. Enable data collection
  6. Grant access to the reports to guest, so users can have access to it
  7. Ignore some waits, from Paul Randal’s blog

Note that the T-SQL for the supporting objects is located in the SQL Server install directory e.g.

C:\Program Files\Microsoft SQL Server\MSSQL10.SQLEXPRESS\MSSQL\Install\instmdw.sql

So I just copy it to a local path C:\scripts along with the stored procedure script for rpt_query_stats, see my post here.

 
function MDWInstall($SrcSrv)
{

    $SrcCon  = New-Object System.Data.SqlClient.SQLConnection("Data Source=$SrcSrv;Initial Catalog=master;Integrated Security=True;Connection Timeout=60") 
    $SrcCon.Open() 
                
    $SrcCmd = New-Object system.Data.SqlClient.SqlCommand("select @@version", $SrcCon)  
    $SrcCmd.CommandTimeout = 300

    $query = "
    begin try
    if not exists(select name from sys.databases where name = 'msmdw')
    begin
    create database msmdw
    alter database msmdw modify file (name='msmdw', size = 5000MB, maxsize = UNLIMITED, filegrowth = 100MB)
    alter database msmdw modify file (name='msmdw_log', size = 500MB, maxsize = UNLIMITED, filegrowth = 100MB)
    alter database msmdw set recovery simple
    exec msmdw.dbo.sp_changedbowner @loginame = N'sa', @map = false
    select compatibility_level from sys.databases where name = 'master'
    end
    "

    Start-Sleep -s 1

    $r = Invoke-Sqlcmd -ServerInstance "$SrcSrv" -ConnectionTimeOut 10 -Query "$query" -AbortOnError -QueryTimeout 300

        
    if ($r.compatibility_level -eq 100)
    {
        "install mdw 2008"
        sqlcmd -S $SrcSrv -d msmdw -i "C:\scripts\instmdw2008.sql" -x > null
    }
    elseif ($r.compatibility_level -eq 110)
    {
        "install mdw 2012"
        sqlcmd -S $SrcSrv -d msmdw -i "C:\scripts\instmdw2012.sql" -x > null
        sqlcmd -S $SrcSrv -d msmdw -i "C:\scripts\rpt_query_stats.sql" > null
    }
    elseif ($r.compatibility_level -eq 120)
    {
        "install mdw 2014"
        sqlcmd -S $SrcSrv -d msmdw -i "C:\scripts\instmdw2014.sql" -x > null
        sqlcmd -S $SrcSrv -d msmdw -i "C:\scripts\rpt_query_stats.sql" > null
    }

    $query = "
        
    if cast(serverproperty('Edition') as varchar) like 'Enterprise%'
    begin

    -- enable page compression

    exec msmdw.sys.sp_MSforeachtable 'alter index all on ? rebuild with (data_compression=page)'

    -- create index to speed up purge job

    create index IDX_query_stats on msmdw.snapshots.query_stats(sql_handle) with (data_compression=page)
    
    end

    else

    -- create index to speed up purge job

    create index IDX_query_stats on msmdw.snapshots.query_stats(sql_handle)


    -- set up data collection

    exec msdb.dbo.sp_syscollector_set_warehouse_database_name @database_name = 'msmdw'
    exec msdb.dbo.sp_syscollector_set_warehouse_instance_name @instance_name = @@servername
    exec msdb.dbo.sp_syscollector_set_cache_directory @cache_directory = 'c:\temp'

    exec msdb.dbo.sp_syscollector_update_collection_set @name = 'Disk Usage', @days_until_expiration = 2
    exec msdb.dbo.sp_syscollector_update_collection_set @name = 'Server Activity', @days_until_expiration = 2
    exec msdb.dbo.sp_syscollector_update_collection_set @name = 'Query Statistics', @days_until_expiration = 2

    -- start collection creates the jobs needed before enabling the collector

    exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Server Activity'
    exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Query Statistics'
    exec msdb.dbo.sp_syscollector_start_collection_set @name = 'Disk Usage'

    exec msdb.dbo.sp_syscollector_enable_collector

    -- change purge schedule to avoid blocking with collection sets

    exec msdb.dbo.sp_update_schedule @name='mdw_purge_data_schedule', @active_start_time=20500

    -- grant access on reports to guest

    use msmdw
    grant connect to guest
    grant view definition to mdw_reader
    exec msmdw.sys.sp_addrolemember N'mdw_reader', N'guest'

    use msdb
    create role dc_report_reader authorization dbo
    grant select on dbo.syscollector_collection_sets to dc_report_reader
    grant select on dbo.syscollector_execution_log to dc_report_reader
    grant select on dbo.syscollector_config_store to dc_report_reader
    grant connect to guest
    exec msdb.sys.sp_addrolemember N'dc_report_reader', N'guest'

    -- waits to ignore

    delete msmdw.core.wait_types where wait_type IN (
        N'BROKER_EVENTHANDLER',             N'BROKER_RECEIVE_WAITFOR',
        N'BROKER_TASK_STOP',                N'BROKER_TO_FLUSH',
        N'BROKER_TRANSMITTER',              N'CHECKPOINT_QUEUE',
        N'CHKPT',                           N'CLR_AUTO_EVENT',
        N'CLR_MANUAL_EVENT',                N'CLR_SEMAPHORE',
        N'DBMIRROR_DBM_EVENT',              N'DBMIRROR_EVENTS_QUEUE',
        N'DBMIRROR_WORKER_QUEUE',           N'DBMIRRORING_CMD',
        N'DIRTY_PAGE_POLL',                 N'DISPATCHER_QUEUE_SEMAPHORE',
        N'EXECSYNC',                        N'FSAGENT',
        N'FT_IFTS_SCHEDULER_IDLE_WAIT',     N'FT_IFTSHC_MUTEX',
        N'HADR_CLUSAPI_CALL',               N'HADR_FILESTREAM_IOMGR_IOCOMPLETION',
        N'HADR_LOGCAPTURE_WAIT',            N'HADR_NOTIFICATION_DEQUEUE',
        N'HADR_TIMER_TASK',                 N'HADR_WORK_QUEUE',
        N'KSOURCE_WAKEUP',                  N'LAZYWRITER_SLEEP',
        N'LOGMGR_QUEUE',                    N'ONDEMAND_TASK_QUEUE',
        N'PWAIT_ALL_COMPONENTS_INITIALIZED',
        N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
        N'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
        N'REQUEST_FOR_DEADLOCK_SEARCH',     N'RESOURCE_QUEUE',
        N'SERVER_IDLE_CHECK',               N'SLEEP_BPOOL_FLUSH',
        N'SLEEP_DBSTARTUP',                 N'SLEEP_DCOMSTARTUP',
        N'SLEEP_MASTERDBREADY',             N'SLEEP_MASTERMDREADY',
        N'SLEEP_MASTERUPGRADED',            N'SLEEP_MSDBSTARTUP',
        N'SLEEP_SYSTEMTASK',                N'SLEEP_TASK',
        N'SLEEP_TEMPDBSTARTUP',             N'SNI_HTTP_ACCEPT',
        N'SP_SERVER_DIAGNOSTICS_SLEEP',     N'SQLTRACE_BUFFER_FLUSH',
        N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
        N'SQLTRACE_WAIT_ENTRIES',           N'WAIT_FOR_RESULTS',
        N'WAITFOR',                         N'WAITFOR_TASKSHUTDOWN',
        N'WAIT_XTP_HOST_WAIT',              N'WAIT_XTP_OFFLINE_CKPT_NEW_LOG',
        N'WAIT_XTP_CKPT_CLOSE',             N'XE_DISPATCHER_JOIN',
        N'XE_DISPATCHER_WAIT',              N'XE_TIMER_EVENT')

    "

    if ($r.compatibility_level -ge 100)
    {
        "enable data collection"
        $SrcCmd.CommandText = $query
        $r = $SrcCmd.ExecuteNonQuery()
    }

    $SrcCon.Close() 
    $SrcCon.Dispose() 
}

# execute on a list of servers

("Server1", "Server2", "Server3") | foreach{MDWInstall $_}