Microsoft Azure SQL Database Cloud Service Base Scripted Discovery Data Provider

Microsoft.SqlServer.Azure.Module.BaseScriptedDiscoveryDataSource (DataSourceModuleType)

This module allows execution of PowerShell scripts to discover objects of Microsoft Azure SQL Database cloud service.

Element properties:

TypeDataSourceModuleType
IsolationAny
AccessibilityInternal
RunAsMicrosoft.SqlServer.Azure.Profile.Workflow
OutputTypeSystem.Discovery.Data

Member Modules:

ID Module Type TypeId RunAs 
DS DataSource Microsoft.Windows.TimedPowerShell.DiscoveryProvider Default

Overrideable Parameters:

IDParameterTypeSelectorDisplay NameDescription
IntervalSecondsint$Config/IntervalSeconds$Interval (seconds)The recurring interval of time in seconds in which to run the workflow.
SyncTimestring$Config/SyncTime$Synchronization TimeThe synchronization time specified by using a 24-hour format. May be omitted.
TimeoutSecondsint$Config/TimeoutSeconds$Timeout (seconds)Specifies the time the workflow is allowed to run before being closed and marked as failed.

Source Code:

<DataSourceModuleType ID="Microsoft.SqlServer.Azure.Module.BaseScriptedDiscoveryDataSource" RunAs="Microsoft.SqlServer.Azure.Profile.Workflow" Accessibility="Internal">
<Configuration>
<IncludeSchemaTypes>
<SchemaType>Windows!Microsoft.Windows.PowerShellSchema</SchemaType>
</IncludeSchemaTypes>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="IntervalSeconds" type="xsd:integer"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" minOccurs="0" maxOccurs="1" name="SyncTime" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="TimeoutSeconds" type="xsd:int"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="DiscoveryName" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Script" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Parameters" type="NamedParametersType"/>
</Configuration>
<OverrideableParameters>
<OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int"/>
<OverrideableParameter ID="SyncTime" Selector="$Config/SyncTime$" ParameterType="string"/>
<OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int"/>
</OverrideableParameters>
<ModuleImplementation Isolation="Any">
<Composite>
<MemberModules>
<DataSource ID="DS" TypeID="Windows!Microsoft.Windows.TimedPowerShell.DiscoveryProvider">
<IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
<SyncTime>$Config/SyncTime$</SyncTime>
<ScriptName>$Config/DiscoveryName$DataSource.ps1</ScriptName>
<ScriptBody><Script>
$Config/Script$

function With($object, [ScriptBlock] $scriptBlock) {
try {
&amp;$scriptBlock
} catch {
if (-not ($object -is [System.Data.SqlClient.SqlConnection])) { throw }
$exception = $_.Exception.GetBaseException()
if (-not ($exception -is [System.Data.SqlClient.SqlException])) { throw }

# Hide some errors
if ($object.Database -eq 'master') {
# 40613: 'Database is not currently available'
# For 'master' this error will be covered by Server State monitor.
# For user databases - seems like it's not always reflected in 'state' column of sys.databases, so error should be propagated.
if ($exception.Number -eq 40613) { return }
} else {
# 4060: 'Login failed' could mean unexisting database.
# If this is the case, then we shouldn't do anything, but wait for next discovery.
# But if database is here, then, most likely, wrong credentials provided and error should be propagated.
if ($exception.Number -eq 4060 -and -not (IsDatabaseExists $object.DataSource $object.Database $Username $Password)) { return }
}

throw
} finally {
foreach ($disposeCandidate in $object) {
if ($disposeCandidate -is [IDisposable]) { $disposeCandidate.Dispose() }
}
}
}

function IsDatabaseExists($serverName, $testDatabaseName, $username, $password) {
# Assuming that database should be there if anything goes wrong
if (-not ($serverName -and $testDatabaseName -and $username -and $password)) { return $true }
With ($testConnection = Create-SqlConnection $serverName 'master' $username $password) {
try {
With ($testCommand = $testConnection.CreateCommand()) {
$testCommand.CommandText = 'SELECT database_id FROM sys.databases WHERE name = @name'
$null = $testCommand.Parameters.Add('@name', $testDatabaseName)
$testConnection.Open()

With (,($testReader = $testCommand.ExecuteReader())) {
if ($testReader.Read()) { return $true }
}
}
} catch { return $true }
}
return $false
}

function Retry([int] $retryCount, [ScriptBlock] $scriptBlock) {
while ($retryCount-- -ge 0) {
try {
&amp;$scriptBlock
break
} catch {
$exception = $_.Exception.GetBaseException()
if (-not (IsTransientError $exception) -or ($retryCount -lt 0)) { throw $exception }
}
Start-Sleep -s 10
}
}

function IsTransientError($exception) {
# Since this list may change in the feature - for now only consider 'Login failed' as non-transient.
# As soon as all SCOM agents will be guaranteed to run on .NET 4.0 - use Microsoft Azure CAT library with ReliableSqlConnection.
$exception -is [System.Data.SqlClient.SqlException] -and (4060, 18456) -notcontains $exception.Number
}

function WithRetry([ScriptBlock] $objectInitialization, [ScriptBlock] $_withRetryScriptBlock) {
# Seems like it's not possible to make it "safe" with closures,
# because then you will lose $objectInitialization execution results in parent scope
Retry 2 {
With (Invoke-Expression "($objectInitialization)") $_withRetryScriptBlock
}
}

function ExtractAzureTracingID($errorMessage) {
$AzureTracingIdRegex = "(?m)'[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}'"
if ($errorMessage -match $AzureTracingIdRegex) {
@{ 'TracingId' = $matches[0]; 'Error' = ($errorMessage -replace $AzureTracingIdRegex, "'***'") }
} else { @{ 'Error' = $errorMessage } }
}

function PrepareErrorMessage($error, $localDatabaseName) {
$message = "Error: $error"
if ($localDatabaseName) { $message = "Database: $localDatabaseName`n$message" }
elseif ($DatabaseName) { $message = "Database: $DatabaseName`n$message" }
if ($ServerName) { $message = "Server name: $ServerName`n$message" }
$message
}

function LogMessage($workflowName, $severity, [string] $message, $localDatabaseName = $null) {
$azureMessage = ExtractAzureTracingID $message
$header = "SQLAzure MP. $workflowName"
if ($azureMessage.TracingId) {
$header += ". Tracing ID: $($azureMessage.TracingId)"
}
$ScriptApi.LogScriptEvent($header, 701, $severity, (PrepareErrorMessage $azureMessage.Error $localDatabaseName))
}
function Create-SqlConnection($serverName, $databaseName, $username, $password) {
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder.psbase.NetworkLibrary = "dbmssocn";
$builder.psbase.DataSource = $serverName
$builder.psbase.InitialCatalog = $databaseName
$builder.psbase.IntegratedSecurity = $false
$builder.psbase.UserID = $username
$builder.psbase.Password = $password
$builder.psbase.PersistSecurityInfo = $false
$builder.psbase.ConnectTimeout = 60

New-Object System.Data.SqlClient.SqlConnection -ArgumentList @($builder.ToString())
}


$ScriptApi = New-Object -ComObject "MOM.ScriptAPI"
try {
Main
} catch {
LogMessage '$Config/DiscoveryName$' 1 $_
$DiscoveryData = $ScriptApi.CreateDiscoveryData(0, '$MPElement$', '$Target/Id$')
$DiscoveryData.IsSnapshot = $false
$DiscoveryData
}
</Script></ScriptBody>
<Parameters>$Config/Parameters$</Parameters>
<TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds>
</DataSource>
</MemberModules>
<Composition>
<Node ID="DS"/>
</Composition>
</Composite>
</ModuleImplementation>
<OutputType>System!System.Discovery.Data</OutputType>
</DataSourceModuleType>