Wstęp

Wraz z rozwojem naszej firmy, wzrasta zapotrzebowanie na stanowiska pracy. W dobie biurowców szkieletowych, w których to najemca określa układ piętra, nie stanowi to jakiegoś większego problemu. Kiedy pracownik zgłasza zapotrzebowanie na zmianę zagospodarowania wolnej przestrzeni pod nowe biuro, trzeba zadbać o infrastrukturę. O ile w przypadku podłączenia sprzętu nie jest to jakieś wielkie wyzwanie, tak z przygotowaniem infrastruktury pomieszczenia już pojawia się problem. W większych firmach istnieją odpowiednie działy – powiedzmy – remontowe, które właśnie się tym zajmują, ale funkcjonują jako odrębna spółka. ServiceDesk ma wszechstronne zastosowanie, także w środowiskach nie-IT, gdzie znacznie ułatwia pracę. W tym artykule, postaramy się przedstawić jak wykorzystać do tego dwie instancje ServiceDesk’ów – jedna dla zgłaszanego zapotrzebowania na nowe stanowiska dla danej spółki, a druga na przygotowanie odpowiedniego pomieszczenia przez spółkę remontową przedsiębiorstwa.

Założenia

Wiemy, że producent aplikacji wprowadził ESM, jednakże wielu klientom nie podoba się sposób w jaki zostało to rozwiązane (m. in. zmiana raportowania, numery zgłoszeń dla poszczególnych instancji SD itp.) i to właśnie dla takich osób kierowany jest artykuł. W naszym przykładzie będzie to firma, która ma w swojej strukturze dział remontowy zajmujący się właśnie przygotowaniami pomieszczeń. Użytkownicy są ci sami, a jeśli użytkownik nie istnieje na drugiej instancji zostanie on utworzony automatycznie (oczywiście bez loginu).

Co jest potrzebne?

Zanim zaczniemy pisać integrację, musimy się do niej odpowiednio przygotować. Będziemy potrzebować dwóch kont techników wykorzystywanych do integracji, po jednym dla każdej z instancji. Aby w pełni zautomatyzować proces, potrzebne jest również dodatkowe pole zawierające numer zgłoszenia pochodzącego z ServiceDesku źródłowego. W naszym przypadku jest to cały dedykowany szablon, na naszej testowej platformie – defaultowe wartości. Oraz oczywiście trochę wolnego czasu na napisanie skryptów 🙂

Skrypt transferujący zgłoszenie

Na początek zajmiemy się trudniejszym do napisania skryptem. W skrypcie kopiującym zgłoszenie należy uwzględnić takie cechy zgłoszenia jak zawartość obrazków inline (w tekście opisu zgłoszenia) oraz załączniki. Te wszystkie informacje będą potrzebne przy tworzeniu zgłoszenia na drugiej instancji. Także zabieramy się za pisanie 🙂

Zmienne
Zanim zaczniemy pisać funkcje i mechanikę właściwą skryptu, warto przygotować sobie kilka zmiennych, które będą użyte w skrypcie. Na samej górze znajduje się parametr, który przechowywać będzie plik JSON zgłoszenia. Z tego pliku będziemy w dalszej części skryptu potrzebować kilku informacji.

param ($workorderJson = $null)

#common variables
$Script:ScriptPath = $MyInvocation.MyCommand.Path
$Script:dir = Split-Path $ScriptPath
Set-Location $dir

$jsonData = Get-Content $workorderJson -Encoding UTF8 | ConvertFrom-Json

$Script:actualDate = Get-Date -Format "dd-MM-yyyy_HH-mm-ss"
$Script:log_path = "$dir\logs\Log_$($actualDate).log"
$Script:log = [System.IO.StreamWriter] $log_path

$Script:requestsApiUrl = "api/v3/requests"
$Script:attachmentsApiUrl = "api/v3/attachments"
$Script:requestTemplate = "New network infrastructure"
$Script:addMethod = "POST"
$Script:openStatus = "Open"

#specific variables for sdphost01
$srcSdpPath = $pwd.Drive.Name + ":\ManageEngine\ServiceDesk" #path to ServiceDesk folder
$tempAttchmntsFolder = "$dir\temp\attachments"

#specific variables for sdphost02
$Script:sdpHost02 = "http://sdphost02.sdpadmins.pl/"
$Script:apiKey02 = "<api_key_02>"

Funkcje
Funkcja tworzenia logu – bardzo przydatna rzecz 🙂 Bardzo prosta funkcja zapisująca konkretną linię do pliku logu, definiowanego na początku skryptu w części zmiennych wspólnych

#logging function
function addToLog($message)
{
    $time = Get-Date -Format "HH:mm:ss"

    if($message.Contains("Błąd") -and $message.IndexOf("Błąd") -lt 2)
    {
        $Script:errorNum+=1
        $message = "[ERROR] " + $message
    }

    $message = $time + ": " + $message
    $log.WriteLine($message)
}

Funkcja tworzenia zgłoszenia na drugiej instancji ServiceDesk Plusa.

#create request function
function addRequest($method, $sdpHost, $apiUrl, $APIKey, $sourceRequestID, $requestTemplate, $requesterMail, $subject, $description, $status)
{
    $uri = $sdpHost+$apiUrl
    $apiHeaders = @{TECHNICIAN_KEY=$APIKey}
    $input = @{"request" = @{"subject" = "$subject"; "description" = "$description"; "requester"=@{"email_id"="$($requesterMail)"}; "status"=@{"name"="$status"}; "template"=@{"name"="$($requestTemplate)"}; "udf_fields"=@{"udf_long_601"="$($sourceRequestID)"}}} | ConvertTo-Json
    $body = @{input_data=$input;format='json'}

    try
        {
            Invoke-RestMethod -Method $method -uri $Uri -Headers $apiHeaders -Body $body -ContentType "application/x-www-form-urlencoded"
            addToLog -message "Zgłoszenie dodane pomyślnie!"
        }

    catch
        {
            addToLog -message "Błąd zapytania do API: $($_.Exception.Message)"
            return $false
        }
}

Funkcje kopiujące pliki. Pierwsza z nich kopiuje zawartość katalogu podanego w parametrze. Druga kopiuje załączniki ze zgłoszenia do katalogu tymczasowego. Ostatnia, podobnie jak funkcja kopiująca załączniki, kopiuje pliki obrazów inline do katalogu tymczasowego.

#copy files function
function copyFiles($source,$destination)
{
    try
    {
        if (!(Test-Path -path $destination)) {New-Item $destination -Type Directory}
        
        Copy-Item -Path $source -Destination $destination -Recurse -Force -ErrorAction Stop
        AddToLog -message "Skopiowano zawartość $source"
        return $true
    }

    catch 
    {
        AddToLog -message "Błąd podczas kopiowania $source"
        return $false
    }
}

#copy attachments function
function CopyAttach($woid, $dest)
{
    $requestAttachmntsList = $null
    $srcAttachmntsDir = $srcSdpPath + "\fileAttachments\Request\"
    $requestAttachmntsList = Get-ChildItem -Path $srcAttachmntsDir -Force -Recurse -Directory -ErrorAction Stop -Verbose | Where-Object {$_.Name -eq $woid}

    
    foreach($folder in $requestAttachmntsList.FullName)
    {
            $folder = $folder + "\*"
            CopyFiles -source $folder -dest $dest
    }   

}

#copy inline images function
function CopyInline($woid,$dest)
{
    $src = $srcSdpPath + "\inlineimages\WorkOrder\$woid\*"
    if(Test-Path -Path $src)
    {
        CopyFiles -source $src -dest $dest
    }

    else
    {
        AddToLog -message "[WARNING] Brak folderu $src - prawdopodobnie brak obrazków w treści zgłoszenia"
        return $true
    }
}

Aby pozbyć się niechcianych ramek zawierających informację o obrazie inline, musimy zmienić kod HTMLowy opisu tak, aby nie wyświetlał obrazów tylko na przykład tekst — plik w załączniku —. Użyjemy do tego wyrażenia regularnego w zaznaczonej linijce.

#replace html code (show different text instead of lack of image frame)
function replaceInline($origDesc)
{
    $prepareDesc = @"
$origDesc
"@

    $descToSet = $prepareDesc -replace "<div><img src=`"/inlineimages/WorkOrder(.*?)png`" /><br /></div>", " -- plik w załączniku -- "

    return $descToSet
}

Funkcja dodająca załącznik. Jak widać na poniższym fragmencie skryptu, zapytanie API różni się znacznie od dotychczasowych. Jest to wymagane, ze względu na to, że nigdzie w zapytaniu nie jest podawana lokalizacja jaki plik należy wczytać. Stąd też wykorzystanie biblioteki .NET, która pozwala na upload pliku, w połączeniu z zapytaniem API i otrzymujemy taki jaki chcemy efekt – dołączony plik do zgłoszenia (trzeba zmienić &amp na &)

#add attachment to new/destination request - .NET library
function addAttachmntToRequest($method, $sdpHost, $apiUrl, $apiKey, $destRequestID, $filePath)
{
    $uri = $sdpHost+$apiUrl

    $sdLink = $sdpHost + $attachmentsApiUrl + "?input_data={'attachment': {'request': {'id': '$($destRequestID)'}}}&TECHNICIAN_KEY=$($apiKey)&format='json'"
    try
    {
        $wc = new-object System.Net.WebClient
        $result = $wc.UploadFile("$sdLink", $method, "$($filePath)")
        Start-Sleep -Seconds 5
        $resultTxt =  [System.Text.Encoding]::ASCII.GetString($result) 
        AddToLog -message $resultTxt
        $resultTxt = $resultTxt | ConvertFrom-Json
        $resultStatus = $resultTxt.response_status.status

        if($resultStatus -ne "Success")
        {
            addtolog -message "Błąd podczas dodawania załącznika: $($resultTxt.operation.result.message)"
            return $false
        }

        else
        {
            AddToLog -message "Pomyślnie dodano załącznik $filePath do zgłoszenia $destRequestID"
            return $resultStatus
        }
        
    }

    catch
    {
        AddToLog -message "Bład podczas ładowania pliku $filePath do zgłoszenia $destRequestID - $($_.Exception.Message) - $($_.Exception.ItemName)"
        return $false
    }
}

Mechanizm działania skryptu
Teraz przyszedł czas na właściwą część skryptu – mechanika i transfer zgłoszenia na docelową instancję ServiceDeska. Pracę rozpoczynamy od przygotowania pobranego opisu zgłoszenia z pliku JSON (usunięcie obrazów inline) oraz utworzenia zgłoszenia na drugiej instancji. W kolejnym kroku kopiujemy wszystkie wymagane pliki – inline oraz załączniki – do tymczasowej lokalizacji, z której będziemy wczytywać do docelowego zgłoszenia. Jeśli wszystko poszło zgodnie z planem, możemy rozpocząć dodawanie załączników do nowo utworzonego zgłoszenia dla remontowców. W tym celu pobieramy listę załączników do dodania (linijka 30) i dla każdego załącznika wykonujemy zapytanie API – pętla foreach. Po zakończonym procesie dodawania załączników, usuwamy katalog tymczasowy zgłoszenia, aby nie zaśmiecać serwera.

############################################## let's the party begin! :)

addToLog -message "Rozpocznym wykonywanie skryptu"

addToLog -message "Dodawanie nowego zgłoszenia na docelowej instancji ServiceDesk"

$requestDescription = replaceInline -origDesc $($jsonData.request.description)

$createTargetRequest = addRequest -method $addMethod -sdpHost $sdpHost02 -apiUrl $requestsApiUrl -APIKey $apiKey02 -sourceRequestID $($jsonData.request.id) -requestTemplate $Script:requestTemplate -requesterMail $($jsonData.request.requester.email_id) -subject $($jsonData.request.subject) -description $($requestDescription) -status $openStatus

if($createTargetRequest.response_status.status -eq "success")
{
    addToLog -message "Kopiowanie plików załączników"
    $copyInline = CopyInline -woid $($jsonData.request.id) -dest "$tempAttchmntsFolder\$($jsonData.request.id)"
    $copyAttach = CopyAttach -woid $($jsonData.request.id) -dest "$tempAttchmntsFolder\$($jsonData.request.id)"

    if($copyInline)
    {
        addToLog -message "-- inline images skopiowane pomyślnie"
    }

    if($copyAttach)
    {
        addToLog -message "-- załączniki skopiowane pomyślnie"
    }

    if(($copyInline -eq $true) -and ($copyAttach -eq $true))
    {
        addToLog -message "Dodawanie załączników do zgłoszenia na docelowym Servicedesk"
        $attachmentsList = Get-ChildItem -Path "$tempAttchmntsFolder\$($jsonData.request.id)" -Recurse | select FullName
        
        foreach($attachment in $attachmentsList)
        {
            addToLog -message "-- dodaję plik: $($attachment.FullName)"
            $addAttachment = addAttachmntToRequest -method $addMethod -sdpHost $sdpHost02 -apiUrl $attachmentsApiUrl -apiKey $apiKey02 -destRequestID $($createTargetRequest.request.id) -filePath $($attachment.FullName)

            if($addAttachment -eq "success")
            {
                addToLog -message "-- plik załącznika $($attachment) został dodany pomyślnie"
            }

            else
            {
                Write-Host "--"
                addToLog -message "Nie udało się dodać pliku $($attachment)"
                Remove-Item $($attachment.FullName) -Force -Confirm:$false
            }
        }

        Remove-Item $tempAttchmntsFolder\$($jsonData.request.id) -Recurse -Force -Confirm:$false
    }
}

else
{
    Write-Host "Coś poszło nie tak... :( sprawdź plik logu $Log"
}


if($ErrorNum -gt 0)
{
    addToLog -message "Liczba błędów w trakcie wykonywania skryptu: $($ErrorNum)"
}

else
{
    addToLog -message "-- brak błędów --"
}

addToLog -message "Koniec skryptu"

$Log.Close()

############################################## this is the end

Teraz wystarczy dodać wyzwalacz niestandardowy (custom trigger) jeśli chcemy aby skrypt był wyzwalany w momencie założenia konkretnego zgłoszenia lub przycisk do menu niestandardowego, używając polecenia

powershell.exe -WindowStyle Hidden -file <path_to_script_file>\simpleTransfer-create.ps1 $COMPLETE_V3_JSON_FILE

Skrypt otwierający źródłowe zgłoszenie

W naszym przykładzie, szablon służący do przygotowania nowego pomieszczenia biurowego od razu zakładane jest w statusie wstrzymanym – oczekujemy na informację z działu remontowego o gotowości infrastruktury – i wyzwalany jest skrypt tworzący zgłoszenie na instancji ServiceDesk działu remontowego. Po zamknięciu zgłoszenia (również custom triggerem) wyzwalany jest poniższy skrypt, który otwiera zgłoszenie i dodaje notatkę. Tak, wiemy 🙂 Nie jest opisane jak dodać notatkę do zgłoszenia w dokumentacji API ServiceDesk Plusa, ale dla nas odkrywców rzecz była prosta 🙂

Zmienne
Podobnie jak w pierwszym skrypcie, definiujemy sobie zmienne dla ułatwienia pracy przy pisaniu skryptu.

param ($workorderJson = "none" )

#common variables
$Script:ScriptPath = $MyInvocation.MyCommand.Path
$Script:dir = Split-Path $ScriptPath
Set-Location $dir

$jsonData = Get-Content $workorderJson -Encoding UTF8 | ConvertFrom-Json

$Script:actualDate = Get-Date -Format "dd-MM-yyyy_HH-mm-ss"
$Script:log_path = "$dir\logs\Log_$($actualDate).log"
$Script:log = [System.IO.StreamWriter] $log_path

$Script:requestsApiUrl = "api/v3/requests/"
$Script:setMethod = "PUT"
$Script:addMethod = "POST"
$Script:openStatus = "Open"
$Script:noteText = "Infrastruktura fizyczna przygotowana"

#specific variables for sdphost01
$Script:sdpHost01 = "http://sdphost01.sdpadmins.pl/"
$Script:apiKey01 = "<api_key_01>"

Funkcje
W tym skrypcie będzie zawrotna ilość funkcji – całe 3 🙂 Pierwsza z nich to standardowa funkcja logowania działań wykonywanych w trakcie skryptu. Drugą funkcją jest aktualizacja zgłoszenia źródłowego pod względem zmiany statusu z wstrzymanego na otwarty/w trakcie realizacji. Trzecia i ostatnia funkcja służy do dodania notatki z informacją o zakończonych pracach z infrastrukturą pomieszczenia, dająca zielone światło na wprowadzanie stanowisk pracy do danego biura. Podświetlone linijki przedstawiają kod skryptu dodającego notatkę – to dla tych którzy poszukują jak dodać notatkę i są tu tylko na chwilę 😉

#logging function
function addToLog($message)
{
    $time = Get-Date -Format "HH:mm:ss"

    if($message.Contains("Błąd") -and $message.IndexOf("Błąd") -lt 2)
    {
        $Script:errorNum+=1
        $message = "[ERROR] " + $message
    }

    $message = $time + ": " + $message
    $log.WriteLine($message)
}

#update request function
function updateRequest($method, $sdpHost, $apiUrl, $APIKey, $requestID, $statusToSet)
{
    $uri = $sdpHost+$apiUrl+$requestID
    $apiHeaders = @{TECHNICIAN_KEY=$APIKey}
    $input= @{"request"=@{"status"=@{"name"="$($statusToSet)"}}} | ConvertTo-Json
    $body = @{input_data=$input;format='json'}

    try
        {
            $updateRequest = Invoke-RestMethod -Method $method -uri $Uri -Headers $apiHeaders -Body $body -ContentType "application/x-www-form-urlencoded"
            addToLog -message "Zgłoszenie zaktualizowane pomyślnie"
        }

    catch
        {
            addToLog -message "Błąd zapytania do API: $($_.Exception.Message)"
            return "Błąd zapytania do API: $($_.Exception.Message)"
        }
}

#add note function
function addNote($method, $sdpHost, $apiUrl, $APIKey, $requestID, $noteTxt)
{
    $uri = $sdpHost+$apiUrl+$requestID+"/notes"
    $apiHeaders = @{TECHNICIAN_KEY=$APIKey}
    $input= @{"request_note"=@{"description"="$($noteTxt)"}} | ConvertTo-Json
    $body = @{input_data=$input;format='json'}

    try
        {
            $addNote = Invoke-RestMethod -Method $method -uri $Uri -Headers $apiHeaders -Body $body -ContentType "application/x-www-form-urlencoded"
            addToLog -message "Notatka dodana pomyślnie"
        }

    catch
        {
            addToLog -message "Błąd zapytania do API: $($_.Exception.Message)"
            return "Błąd zapytania do API: $($_.Exception.Message)"
        }
}

Mechanizm skryptu otwierającego zgłoszenie

Ok, gdy mamy już funkcje – czas na mechanikę. Całość opiera się tylko na zmianie statusu zgłoszenie źródłowego i dodaniu notatki – nie ma zbytnio co opisywać

############################################## let's the party begin! :)

addToLog -message "Rozpoczynam pracę skryptu"

addToLog -message "Aktualizuję zgłoszenie - zmiana statusu"

updateRequest -method $setMethod -sdpHost $sdpHost01 -apiUrl $requestsApiUrl -APIKey $apiKey01 -requestID $($jsonData.request.udf_fields.udf_long_601) -statusToSet $openStatus

addNote -method $addMethod -sdpHost $sdpHost01 -apiUrl $requestsApiUrl -APIKey $apiKey01 -requestID $($jsonData.request.udf_fields.udf_long_601) -noteTxt $noteText

if($ErrorNum -gt 0)
{
    addToLog -message "Liczba błędów w trakcie wykonywania skryptu: $($ErrorNum)"
}

else
{
    addToLog -message "-- brak błędów --"
}

addToLog -message "Koniec skryptu"

$Log.Close()

############################################## this is the end

Teraz tylko wystarczy ustawić wyzwalacz lub dodać przycisk do custom menu używając polecenia

powershell.exe -WindowStyle Hidden -file <path_to_script>\simpleTransfer-open-source-req.ps1 $COMPLETE_V3_JSON_FILE

I to wszystko. Całkowity czas pisania skryptu wraz z testowaniem i rozkminianiem nieopisanych operacji w dokumentacji to jakieś 4-5 godzin. Czas skrócony z zakładania dwóch zgłoszeń do jednego – nieporównywalny, podobnie jak udogodnienie dla Zgłaszających, że wszystko robią w jednym miejscu.