En artículos anteriores comentábamos cómo automatizar despliegues de Integration Services así como de Reporting Services mediante scripts de PowerShell.
En esta ocasión veremos cómo podemos gestionar el procesado, la publicación y la gestión de la seguridad, también mediante la ejecución de un script de PowerShell. Por la cantidad de código vamos a dividir el post en 3 bloques.
Procesado con PowerShell
Haremos uso de 3 ficheros:
- El script principal de ejecución: PS_Lab01_Procesar.ps1.
- Fichero de módulo de script con diferentes funciones que usaremos en el script principal: Funciones.psm1.
- Un fichero de configuración donde están los grupos de medida a procesar: MeasureGroupsSlices.cfg.
Nuestro fichero de configuración tendrá el siguiente contenido:
SALES =[DATE].[MONTH].&[$YearParticion]&[$MonthParticion] STOCK=[DATE].[MONTH].&[$YearParticion]&[$MonthParticion] TICKETS=[ORIGINAL DATE].[MONTH].&[$YearParticion]&[$MonthParticion]
Se tratan de los grupos de medida que queremos procesar, por supuesto, podremos añadir/eliminar/modificar los mismos.
Nuestro fichero de funciones tendrá las siguientes funciones:
- Escritura en log:
Function LogWriteSeparador { LogWrite "---------------------------------------------------------------------------------------------------------------------------------" } Function LogWrite([String[]]$Text, [ConsoleColor[]]$Color = "White", [int]$StartTab = 0, [int] $LinesBefore = 0,[int] $LinesAfter = 0) { $DefaultColor = $Color[0] if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host "`n" -NoNewline } } # Add empty line before if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host "`t" -NoNewLine } } # Add TABS before text if ($Color.Count -ge $Text.Count) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host $Text[$i] -ForegroundColor $Color[$i] -NoNewLine } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host $Text[$i] -ForegroundColor $Color[$i] -NoNewLine } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host $Text[$i] -ForegroundColor $DefaultColor -NoNewLine } } $vFecha = Get-Date -format "yyyy-MM-dd HH:mm:ss" $logString = ($vFecha) + (" --> ") + ($logString) Add-content $vLog -value ($Text -join " ") Write-Host if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host "`n" } } # Add empty line after }
- Importamos los módulos con los cmdlets de Analysis Services:
Function ImportModulesAndAssemblies () { Try { LogWriteSeparador LogWrite "INICIO de carga de modulos Analysis Services..." Import-Module -DisableNameChecking sqlascmdlets LogWrite -Text " SqlASCmdLets.............................", "[Ok]" -Color Yellow, Green Import-Module -DisableNameChecking SQLPS LogWrite -Text " SQLPS....................................", "[Ok]" -Color Yellow, Green $ASsLoadStatus = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices") LogWrite -Text " Microsoft.AnalysisServices...............", "[Ok]" -Color Yellow, Green $xmlaLoadstatus =[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices.Xmla") LogWrite -Text " Microsoft.AnalysisServices.Xmla..........", "[Ok]" -Color Yellow, Green $xmlLoadStatus = [System.Reflection.Assembly]::LoadWithPartialName("System.Xml") LogWrite -Text " System.Xml...............................", "[Ok]" -Color Yellow, Green $SMOLoadStatus = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") LogWrite -Text " Microsoft.SqlServer.Smo..................", "[Ok]" -Color Yellow, Green $ISLoadStatus = [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices") LogWrite -Text " Microsoft.SqlServer.IntegrationServices..", "[Ok]" -Color Yellow, Green LogWrite "FIN de carga de modulos Analysis Services..." LogWrite "" } Catch { $ErrorMessage = $_.Exception.Message LogWrite "ERROR en importacion de modulos (cmdlets) Analysis Services. Salida del script con codigo 4 ($ErrorMessage)" Break } }
- Función para ajustar cadena retirando espacios en blanco, tabulador, salto de linea y salto de carro:
Function Trimmed { Param ([string]$stringToTrim) return $stringToTrim.Trim(" ","`t","`n","`r") }
- Función para montar array de strings eliminando cadenas vacías:
Function SplitTrimmed { Param ([string]$stringToArray) return $stringToArray.Split(";",[System.StringSplitOptions]::RemoveEmptyEntries) | foreach {(Trimmed($_))} | Where-Object { $_ -ne ([string]::IsNullOrWhiteSpace($_))} | ? {$_} }
- Comprueba la consistencia de una ruta a subdirectorio (creo no se usa):
Function EnsurePath([string] $path) { if(-not $path.EndsWith('\')) { $path=$path+'\' } return $path } Function NormalizePath([string] $fileName) { $fileName=$fileName.Replace('\\','\') return $fileName }
- Función para hacer detach de una base de datos multidimensional:
Function Detach-ASDatabase { Param ([string]$serverDetach, [string]$databaseDetach) #Instanciamos el objeto XmlaClient [Microsoft.AnalysisServices.xmla.xmlaclient]$xmlac = new-Object Microsoft.AnalysisServices.Xmla.XmlaClient $xmlaDetach = @" <Detach xmlns="http://schemas.microsoft.com/analysisservices/2003/engine"> <Object> <DatabaseID>$databaseDetach</DatabaseID> </Object> </Detach> "@ $xmlac.Connect($serverDetach) $response=$xmlac.send($xmlaDetach,$null) $xmlac.Disconnect() if($response.Contains('exception')) { Throw $response} }
- Función para hacer attach de una base de datos multidimensional:
Function Attach-ASDatabase { #Aunque el nombre de la base de datos siempre es el mismo, la dejamos preparada para que se pase como parametro Param ([string]$serverAttach, [string]$databasePath, [string]$Modo) #Instanciamos el objeto XmlaClient [Microsoft.AnalysisServices.xmla.xmlaclient]$xmlac = new-Object Microsoft.AnalysisServices.Xmla.XmlaClient $xmlaAttach = @" <Attach xmlns="http://schemas.microsoft.com/analysisservices/2003/engine"> <Folder>$databasePath</Folder> <ReadWriteMode xmlns="http://schemas.microsoft.com/analysisservices/2008/engine/100">$Modo</ReadWriteMode> </Attach> "@ $xmlac.Connect($serverAttach) $response=$xmlac.send($xmlaAttach,$null) $xmlac.Disconnect() if($response.Contains('exception')) { Throw $response} }
- Función para hacer backup de una base de datos Analysis Services:
Function Backup-ASDatabase ($Server, $dbName,$backupFolder) { #$BackupF=$Server+$backupFolder $svrt = new-Object Microsoft.AnalysisServices.Server $svrt.Connect($Server) #se hace el BACKUP si existe el modelo en ese servidor. if ($svrt.Databases.FindByName($dbName)) { $dbQ = $svrt.Databases.FindByName($dbName) [string]$fecha = date $fecha = ($fecha.Replace(':','').Replace('/','-').Replace(' ','-')).Substring(0,10) # obtiene la bbdd $fullpath = NormalizePath("$($backupFolder)\$dbName-$fecha.abf") #$fullpath=$queryserver.QueryServer+ "\$BackupFolder1\$dbName-$fecha.abf" $BackupFileName="$dbName-$fecha.abf" $dbQ.Backup($fullpath, $true) return $BackupFileName } else { $existsBackup1 = $false LogWrite " No existe el cubo en $Server. No es necesario el backup" } }
- Función que crea un XMLA Envelope:
Function AddXMLABatchEnvelop([string]$xmla) { $xmla=@" <Batch xmlns="http://schemas.microsoft.com/analysisservices/2003/engine"> <Parallel> $xmla </Parallel> </Batch> "@ return $xmla }
- Función que ejecuta un comando XMLA:
Function SendXMLA { Param ([string]$server, [string]$xmla) #Instanciamos el objeto XmlaClient [Microsoft.AnalysisServices.xmla.xmlaclient]$xmlac = new-Object Microsoft.AnalysisServices.Xmla.XmlaClient $xmlac.Connect($server) $response= $xmlac.send($xmla,$null) $xmlac.Disconnect() if($response.Contains('exception')) { Throw $response} }
- Comprueba la conectividad con un servidor AS y devuelve la conexión como resultado:
Function Check-ASServer ($qs) { try { LogWrite "INICIO Comprobacion estado servidor ($qs)" $svrt = new-Object Microsoft.AnalysisServices.Server $svrt.Connect($qs) LogWrite "FIN Comprobacion estado servidor ($qs)" return $svrt } catch { LogWrite "ERROR en Comprobacion estado servidor ($qs)" return $null } }
Veamos ahora paso por paso el fichero principal:
En primer lugar estableceremos las rutas de dónde leer los ficheros:
$FicheroModulo="C:\Users\Administrador\Documents\PowerShell\Funciones.psm1" $FicheroSlices = "C:\Users\Administrador\Documents\PowerShell\Config\MeasureGroupsSlices.cfg"
El contenido del fichero de configuración lo almacenamos en una tabla de hash:
$SlicesFormulas = @{} Get-Content $FicheroSlices | foreach { $line = $_.split("=") $SlicesFormulas.($line[0].Trim()) = $line[1]}
Cargamos el modulo que contiene las funciones auxiliares:
$LoadedModule = Import-Module $FicheroModulo -PassThru -force -DisableNameChecking
Establecemos los tipos de procesado:
$ProcessFull="ProcessFull" $ProcesDefault = "ProcessDefault"
Cargamos la librería de funciones de Analysis Services, como veréis empezamos a hacer uso de las funciones del módulo de script:
ImportModulesAndAssemblies
Creamos un objeto de tipo Server:
$ASObj = New-Object Microsoft.AnalysisServices.Server
Conectamos con la base de datos localhost o la que corresponda, esta información la podríamos también tener en un fichero de configuración, en la publicación veremos un ejemplo:
$ASObj.Connect("Localhost")
Seleccionamos una Base de Datos:
$SelectedDataBase = $ASObj.Databases | #Mostramos las propiedasdes Id, nombre, fecha de ultimo procesado y tipo de modelo (tabular o multidimensional) Select-Object -Property ID, Name, LastProcessed, ModelType | #Indicamos que se debe seleccionar Out-GridView -OutputMode Single $DataBase = $ASObj.Databases.Find($SelectedDataBase.ID)
Seleccionamos un Cubo:
$SelectedCube = $DataBase.Cubes | #Indicamos que se debe seleccionar Out-GridView -OutputMode Single $Cube = $DataBase.Cubes.FindByName($SelectedCube.Name)
Escribimos en el log que vamos a comenzar la ejecución:
LogWrite "Comienza la ejecución del procesado del Cubo" LogWriteSeparador
Ahora ya vamos con el procesado en sí, en primer lugar haremos el procesado de dimensiones. Para cada dimensión del cubo comprobamos si es de minería de datos y procesamos en función del tipo:
foreach ($dim in $Cube.Dimensions) { $DimToProcess = $DataBase.Dimensions.Find($Dim.DimensionID) LogWrite ($dim.Name + " :: MiningModel ---> " + $(if($dim.MiningModel.Algorithm -eq $null) { "None" } else { $dim.MiningModel.Algorithm})) $TStart = Get-Date #Si la dimension contiene una estructura de mineria de datos procesamos default si no full if ($DimToProcess.MiningModel.Algorithm -eq $null) { $DimToProcess.Process($ProcessFull) } else { $DimToProcess.Process($ProcesDefault) } $TEnd = Get-Date $Span = $TEnd - $TStart $TotalSpan += $Span LogWrite (" -Tiempo Proceso $dim.Name: " + $Span.ToString()) LogWrite "" } LogWrite ("Tiempo Total de Procesado dimensiones: " + $TotalSpan.ToString())
Asignamos la propiedad slice a todas las particiones (MOLAP y HOLAP) de los grupos de medidas:
LogWrite "" #Indicamos cual es la primera particion, en este caso es la del año 2011 $FirstPartitionYear=2011 # obtiene la bbdd $db = $ASObj.Databases.Find($SelectedDataBase.ID) # obtiene el cubo a procesar $cub = $db.Cubes.FindByName($SelectedCube.Name) #Para cada grupo de medidas del fichero de configuracion se establecen los slices #----------------------------------------------------------------------------- foreach($elem in $SlicesFormulas.Keys) #Obtiene cada una de las Measures del conjunto indicado como parametro { #Por cada una, procesa $mgMeasure = $cub.MeasureGroups.FindByName($elem) $mgPartitions = $mgMeasure.Partitions $Formula=$SlicesFormulas[$elem] #Para cada particion del grupo de medidas se le establece la propiedad slice #----------------------------------------------------------------------------- foreach ($mgPartition in $mgPartitions) { #Obtenemos el año qu ele corresponde a la particion actual $AnoParticion = $mgPartition.Name.Substring($mgPartition.Name.Length - 4) #Calculamos el slice en funcion de la particion #Teniendo en cuenta que en la particion de 2011 se almacenan 2010 y 2011 #----------------------------------------------------------------------------- if($AnoParticion -eq $FirstPartitionYear) { $miembroFecha = ("{" + $Formula.Replace('$AnoParticion',$AnoParticion) + "," + $Formula.Replace('$AnoParticion',$AnoParticion -1) + "}") } else { $miembroFecha = $Formula.Replace('$AnoParticion',$AnoParticion) } #----------------------------------------------------------------------------- LogWrite " Fijamos propiedad Slice para la particion $mgPartition con valor $miembroFecha" LogWrite "" #Asignamos la propiedad Slice $mgPartition.Slice = $miembroFecha #Actualizamos la particion $mgPartition.Update() } #----------------------------------------------------------------------------- }
Procesamos grupos de medidas:
LogWrite ("Comienza el procesado de los grupos de medidas") LogWriteSeparador #Procesamos los grupos de medidas uno a uno #----------------------------------------------------------------------------- foreach ( $MG in $Cube.MeasureGroups) { $TStart = Get-Date LogWrite ("Procesado del grupo de medidas: " + $MG.Name) #Si el grupo de medidas nunca ha sido procesado se procesa por completo if($MG.state -eq "Unprocessed") { LogWrite(" " + $MG.Name + " No ha sido procesado nunca, se procede a realizar un procesado completo") $MG.Process($ProcessFull) LogWrite " Fin Procesado $MG.Name" } #En caso de que ya se haya procesado se hace un procesado por defecto else { LogWrite (" Ultimo procesado: " + $MG.LastProcessed.ToString() + " Se procede a realizar un proceso por defecto") $MG.Process("ProcessDefault") LogWrite " Fin Procesado $MG.Name" } #Calculamos tiempo de proceso $TEnd = Get-Date $Span = $TEnd - $TStart $TotalSpan += $Span LogWrite (" Tiempo Proceso $MG.Name: " + $Span.ToString()) LogWrite "" $MG.Update() } LogWrite ("Tiempo Total de Procesado Grupos de Medidas: " + $TotalSpan.ToString())
Desconectamos y salimos:
$ASObj.Disconnect() $ASObj.Dispose() break
A modo de resumen de este hilo de código, veamos una diagrama de flujo de los pasos seguidos, por supuesto lo podemos complicar tanto como deseemos:
Con lo visto hasta aquí tendríamos lista la parte de procesado, continuamos en el siguiente post con la parte de automatización de la publicación de SSAS.