Schönen guten Tag,
da wir mitunter Probleme haben, dass die Pipeline Läufe auf dem neo42 MMS nicht aufrufbar sind ohne Absturz, möchte ich über das neo42 MMS eine Automatisierung einrichten, welche automatisch die Pipeline Läufe entfernt.
Das Skript aus dem Github Repository habe ich gefunden.
Ich möchte das aber in der Form erweitern/abändern, dass die Läufe nicht nur entfernt werden, sondern das Protokoll vorher heruntergeladen wird und gesichert werden kann.
Dafür möchte ich über “/api/apc/PipelineProtocol/GetMany/{runId}/{cultureCode}/{fromDateTicks}” mit das Protokol abholen.
Mein Problem dabei ist, dass ich nicht verstehe mit welchem Wert ich “fromDateTicks” befüllen muss.
RunID ist die ID des PipelineRuns und CultureCode “en” oder “de”
Mit freundlichen Grüßen
Alexander Schüler
Guten Tag @alexander.im
der Parameter fromDateTicks erwartet einen .NET DateTime-Ticks-Wert (UTC) als long. Das sind die Anzahl der 100-Nanosekunden-Intervalle seit dem 01.01.0001 00:00:00 UTC.
Um das komplette Protokoll eines Pipeline-Laufs abzurufen, übergib einfach 0:
GET /api/apc/PipelineProtocol/GetMany/{runId}/de/0
Der Wert 0 bewirkt intern, dass kein Datumsfilter angewendet wird und alle Protokolleinträge für die angegebene RunId zurückgegeben werden.
Falls nur Einträge ab einem bestimmten Zeitpunkt abgerufen werden sollen, kann der Ticks-Wert so ermittelt werden:
# Ticks für "jetzt" (UTC)
[DateTime]::UtcNow.Ticks
# Ticks für ein bestimmtes Datum
[DateTime]::Parse("2026-01-15 08:00:00").ToUniversalTime().Ticks
Grüße
Marco
Schönen guten Morgen,
herzlichen Dank für die schnelle Antwort.
Mit der 0 funktioniert es so wie erhofft.
Darüber hinaus ist mir noch aufgefallen, dass das “API/Housekeeping/Pipeline-Runs-Housekeeping.ps1” einen API-Endpunkt verwendet, der auf meinem Server nicht dokumentiert ist.
Im speziellen kann ich keine Dokumentation zu “apc/PipelineSpecificationItem” finden. Liegt es daran, dass sich im Laufe der Entwicklung der Endpunkt verschoben hat und in dem Skript noch eine alte Version verwendet wird? Oder gibt es versteckte Endpunkte?
Darüber hinaus finde ich die Dokumentation der API-Endpunkte an den meisten Stellen ausbaufähig.
Gibt es eine Möglichkeit da ggf. zu helfen und etwas beizusteuern?
Grüße
Alexander
Ja, es gibt versteckte API‑Endpunkte. Wir geben nur die Endpunkte frei, die wir als sicher betrachten. Alles, was in den Skripten auf GitHub steht, sollte jedoch freigeschaltet sein. Das werden wir prüfen.
Bezüglich der Dokumentation haben wir sicherlich Nachholbedarf. Derzeit verfügen wir lediglich über den API‑Browser und die PowerShell‑Skripte.
1 „Gefällt mir“
ich bekomme es doch noch nicht ans Laufen..
Ich bekomme zwar mit dem CultureCode “de” und für Ticks “0” einen HTTP Antwortcode von 200 aber der HTTP Body ist leer.
Ich bekomme also keine Daten von dem Lauf zurückgeliefert. Ich kann aber über die WebOberfläche sehen, dass die Pipeline bei dem Lauf etwas protokolliert hat und auch gelaufen ist.
Grüße
Alexander
Kannst du dein Skript einmal hier posten bitte?
das hier ist meine aktuelle Version:
# Requires -Version 7
<#
.SYNOPSIS
Deletes pipeline runs from the MMS database that exceed a given age or count per pipeline.
.DESCRIPTION
All pipeline runs that are older than the RemoveAfterDays or exceeds the count of
MaximumRuns per pipeline will be removed from the database. The product automation
history for these runs is also removed.
.PARAMETER RemoveAfterDays
The number of days after which pipeline runs are deleted in the database.
.PARAMETER MaximumRuns
The maximum number of run per pipeline to keep in the database.
.OUTPUTS
none
.EXAMPLE
.\Pipeline-Runs-Housekeeping.ps1 -RemoveAfterDays 30 -MaximumRuns 4
#>
[CmdletBinding()]
Param (
[parameter(Mandatory = $false)]
[int]
$RemoveAfterDays = 14,
[parameter(Mandatory = $false)]
[int]
$MaximumRuns = 50
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
[String] $Protocol = "https"
[String] $ServerName = "mms-server.example.com"
[String] $Port = "4242"
class DataConverter {
# Converts a single object to the specified class type
static [object] ConvertToClass([Type]$ClassType, [object]$DataSource) {
if (-not ($ClassType.IsSubclassOf([AbstractDataClass]) -and $ClassType.IsPublic)) {
throw "The provided data class type must be a public, derived class of AbstractDataClass."
}
[System.Reflection.PropertyInfo[]] $classProperties = $ClassType.GetProperties()
[System.Collections.Generic.List[string]] $inputProperties = $DataSource.PSObject.Properties | Select-Object -ExpandProperty Name
[object] $obj = $ClassType::new()
[System.Reflection.PropertyInfo] $Property = $null
foreach ($Property in $classProperties) {
[string] $propName = $Property.Name
if ($inputProperties -contains $propName) {
[object] $value = $DataSource.$propName
try {
$obj.$propName = $value
}
catch [System.Management.Automation.RuntimeException] {
throw "Failed to set property '$propName' with value '$value': $_"
}
}
}
$obj.DataSource = $DataSource
return $obj
}
# Converts an array or list of objects to the specified class type
static [System.Collections.Generic.List[object]] ConvertArrayToClass([Type]$ClassType, [object[]]$DataSources) {
[System.Collections.Generic.List[object]] $result = New-Object System.Collections.Generic.List[object]
[object] $data = $null
foreach ($data in $DataSources) {
[object] $converted = [DataConverter]::ConvertToClass($ClassType, $data)
$result.Add($converted)
}
return $result
}
}
class AbstractDataClass : System.IFormattable {
[object] $DataSource
[string] ToString([string] $Format, [System.IFormatProvider] $FormatProvider) {
$flags = [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Instance
$props = $this.GetType().GetProperties($flags) |
Where-Object { $_.DeclaringType -ne [System.Object] } |
ForEach-Object { "$($_.Name) = $($_.GetValue($this))" }
return "[{0}] {1}" -f $this.GetType().Name, ($props -join ' | ')
}
}
class Neo42MmsPipeline : AbstractDataClass, System.IComparable, System.IEquatable[Object] {
[string] $Id
[bool] $Enabled
[string] $Name
[string] $Note
[int] $TaskCount
[int] CompareTo([Object] $that) {
if ($null -eq $that) { return 1 }
$ThatPipeline = $That -as [Neo42MmsPipeline]
if ($null -eq $ThatPipeline) {
throw [System.ArgumentException]::new("unable to compare Neo42MmsPipeline to another type than Neo42MmsPipeline")
}
return $this.Name.CompareTo($ThatPipeline.Name)
}
[bool] Equals([Object] $that) {
if ($null -eq $that) { return $false }
$ThatPipeline = $that -as [Neo42MmsPipeline]
if ($null -eq $ThatPipeline) {
throw [System.ArgumentException]::new("unable to compare Neo42MmsPipeline to another type than Neo42MmsPipeline")
}
return $this.Id -eq $ThatPipeline.Id
}
}
class Neo42MmsPipelineRun : AbstractDataClass, System.IComparable, System.IEquatable[Object] {
[string] $Id
[string] $Name
[string] $PipelineId
[string] $RunId
[datetime] $StartTime
[datetime] $EndTime
[int] CompareTo([Object] $that) {
if ($null -eq $that) { return 1 }
$ThatPipelineRun = $That -as [Neo42MmsPipelineRun]
if ($null -eq $ThatPipelineRun) {
throw [System.ArgumentException]::new("unable to compare Neo42MmsPipelineRun to another type than Neo42MmsPipelineRun")
}
return $this.StartTime.CompareTo($ThatPipelineRun.StartTime)
}
[bool] Equals([Object] $that) {
if ($null -eq $that) { return $false }
$ThatPipeline = $that -as [Neo42MmsPipelineRun]
if ($null -eq $ThatPipeline) {
throw [System.ArgumentException]::new("unable to compare Neo42MmsPipelineRun to another type than Neo42MmsPipelineRun")
}
return $this.Id -eq $ThatPipeline.Id
}
}
function Get-Neo42MmsApiResponse {
[CmdletBinding()]
param (
[Parameter()]
[string] $ApiEndpoint
)
[string] $url = "${script:Protocol}://${script:ServerName}:${script:Port}/api/$ApiEndpoint"
[System.Collections.Generic.Dictionary[[String], [String]]] $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("X-Neo42-Auth", "Admin")
[PSObject] $ApiResponse = $null
try {
Write-Verbose "calling api:"
Write-Verbose "`turl: $url"
Write-Verbose "`theaders: $headers"
[PSObject] $ApiResponse = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -UseDefaultCredentials -ErrorAction Stop
}
catch [InvalidOperationException] {
$_.ErrorDetails = "Error while calling the api: $url"
$PSCmdlet.WriteError($_)
}
catch {
$_.ErrorDetails = "Error while calling the api: $url"
$PSCmdlet.WriteError($_)
}
return $ApiResponse
}
function Get-Pipelines {
Write-Verbose "retrieving all pipelines from neo42 MMS via api call"
[Object[]] $PipelinesWrapper = Get-Neo42MmsApiResponse -ApiEndpoint "apc/PipelineSpecificationItem?languageKey=en" -ErrorAction Stop
if ($null -eq $PipelinesWrapper.Error) {
return $PipelinesWrapper[0].Item | ForEach-Object -Process { [DataConverter]::ConvertArrayToClass([Neo42MmsPipeline], $_.PhaseSpecificationItem) }
}
else {
return $null
}
}
function Get-PipelineRuns {
param (
[Parameter()]
[Neo42MmsPipeline] $Pipeline
)
Write-Verbose "retrieving all runs for pipeline from neo42 MMS via api call"
Write-Verbose "`tpipeline: $Pipeline"
[Object[]] $Runs = Get-Neo42MmsApiResponse -ApiEndpoint "apc/PipelineRun/GetMany/$($pipeline.Id)"
return $Runs | ForEach-Object -Process { [DataConverter]::ConvertArrayToClass([Neo42MmsPipelineRun], $_) }
}
function Get-PipelineProtocol {
[CmdletBinding()]
param (
[Parameter()]
[Neo42MmsPipelineRun] $PipelineRun
)
[String] $runId = $PipelineRun.Id
[String] $cultureCode = "de"
[String] $fromDateTicks = "0"
[String] $ApiEndpoint = "apc/PipelineProtocol/GetMany/$runId/$cultureCode/$fromDateTicks"
Write-Debug "ApiEndpoint -> $ApiEndpoint"
Write-Debug "PipelineRun -> $PipelineRun"
Write-Debug "Pipeline -> $($PipelineRun.PipelineId)"
Get-Neo42MmsApiResponse -ApiEndpoint $ApiEndpoint | Out-Null
}
function Export-PipelineRuns {
[CmdletBinding()]
param (
[Parameter()]
[System.Collections.Generic.List[Neo42MmsPipelineRun]] $PipelineRuns
)
[Neo42MmsPipelineRun] $PipelineRun = $null
foreach ($PipelineRun in $PipelineRuns) {
Get-PipelineProtocol -PipelineRun $PipelineRun -Debug
}
}
function Remove-OldPipelineRuns {
Write-Verbose "get all pipelines.."
[System.Collections.Generic.List[Neo42MmsPipeline]] $Pipelines = Get-Pipelines
foreach ($Pipeline in $Pipelines) {
[System.Collections.Generic.List[Neo42MmsPipelineRun]] $PipelineRuns = Get-PipelineRuns -Pipeline $Pipeline
if ($null -eq $PipelineRuns) {
Continue
}
$PipelineRuns.Sort()
$PipelineRuns | ForEach-Object -Begin { Write-Verbose "Pipelineruns <$($Pipeline.Name)>:" } -Process { Write-Verbose "`t$_" } -End { Write-Verbose ('#' * 20) }
[System.Collections.Generic.List[Neo42MmsPipelineRun]] $RunsToArchive = [System.Collections.Generic.List[Neo42MmsPipelineRun]]::new()
while ($PipelineRuns.count -gt $MaximumRuns) {
Write-Verbose "mark pipelinerun for archive for count too high: $($PipelineRuns[0])"
$RunsToArchive.Add($PipelineRuns[0]) | Out-Null
$PipelineRuns.Remove($PipelineRuns[0]) | Out-Null
}
[System.TimeSpan] $cutoff = New-TimeSpan -Days $RemoveAfterDays
while (($PipelineRuns.Count -gt 0) -and ((New-TimeSpan -Start $PipelineRuns[0].StartTime -End ([datetime]::Now)) -gt $cutoff)) {
Write-Verbose "mark pipelinerun for archive for being too old: $($PipelineRuns[0])"
$RunsToArchive.Add($PipelineRuns[0]) | Out-Null
$PipelineRuns.Remove($PipelineRuns[0]) | Out-Null
}
#TODO: Retrieve protocols for runs via the MMS API and persist them.
Export-PipelineRuns -PipelineRuns $RunsToArchive
}
}
Remove-OldPipelineRuns
# $pipelines = Get-Pipelines
# if ($pipelines.Success) {
# foreach ($pipeline in $pipelines.Item) {
# # Get all runs of the pipeline from the MMS Server.
# $url = "$ServerName/api/apc/PipelineRun/GetMany/$($pipeline.PhaseSpecificationItem.Id)"
# $runs = Invoke-RestMethod -Method Get -Uri $url -Headers $headers -UseDefaultCredentials -ErrorAction Stop
# $count = 0;
# foreach ($run in ($runs | Sort-Object { Get-Date($_.StartTime) } -Descending)) {
# if ($null -ne $run.StartTime -and $null -ne $run.EndTime) {
# $count++
# $age = New-TimeSpan -Start (Get-Date $run.EndTime) -End (Get-Date)
# if ($count -gt $MaximumRuns -or $age.Days -gt $RemoveAfterDays) {
# # Delete the given pipeline run from the MMS Server.
# $url = "$ServerName/api/apc/PipelineRun/$($run.RunId)"
# $result = Invoke-RestMethod -Method Delete -Uri $url -Headers $headers -UseDefaultCredentials -ErrorAction Stop
# }
# }
# }
# }
# }
Ändere mal bitte folgende Zeile
[String] $runId = $PipelineRun.Id
zu
[String] $runId = $PipelineRun.RunId
Bei mir klappt es so 
Das hat es für mich gelöst.
Herzlichen Dank für die Hilfe