Utkast: ruting av Scala-alarmer i RT

RT 6.0.3 har et egnet regelverktøy under Admin → Utløsere. Løsningen bør være én samlet, køspesifikk utløser med handlingen User Defined. Flere overlappende flytteregler kan konkurrere om samme sak.

Ingen regel er opprettet eller aktivert i RT. Kontoen som ble kontrollert, fikk en ShowScrips/ModifyScrips-feil under globale handlinger. RT-drift må bekrefte rettighetene og installere og teste utkastet.

Den tidligere saken RT 7127382 beskriver samme behov. Den angir it-hjelp som mottaks- og felleskø, SE* som it-sentrum og BL05 som it-ovre-blindern. Saken viser også til en CSV-tabell som nå gir 404. scala_koekart.csv er et foreløpig, rekonstruert køkart basert på denne saken og historiske RT-saker.

Foreslått RT-oppsett

  1. La Scala-meldingene komme direkte til it-hjelp. Bruk omdirigering som bevarer avsenderen, ikke vanlig videresending.
  2. Opprett en køspesifikk utløser med navnet Rute Scala-alarm etter byggkode på it-hjelp.
  3. Bruk disse innstillingene:
    • Betingelse: On Create Via Email, dersom RT-drift bekrefter at UiOs lokale betingelse er egnet. Ellers brukes den dokumenterte kjerne-betingelsen On Create; koden avgrenser videre på kø, avsender og innhold.
    • Handling: User Defined
    • Forberedelseskode: return 1;
    • «Custom action commit code»: scala_rt_ruting_action.pl
    • Mal: Blank
    • Nivå/Stage: Normal (internverdi TransactionCreate), ikke Batch
    • Status under test: deaktivert med utløserens egen aktiv/deaktivert-kontroll
    • Sortering: før eventuelle On Create-varsler dersom køen styrer mottakere, mal eller avsender
  4. Test med utgående e-post slått av. Aktiver først etter godkjent køkart og kontroll av faktiske Scala-meldinger.

Rutingskategorier

KategoriSignaturerKø
Lokal spiller eller maskinvareheartbeat overdue, RunICIsDown, ApplicationTerimatedAbnormally, PlayerEventWatchdogTimerExpiredGeografisk kø fra spillerkode
Lokal avspillingScriptPlayedIncorrectly med Error 24 eller 304.23Geografisk kø fra spillerkode
HelseopplastingSSHealthNonrecoverableErrorUploadingHealthLogsGeografisk kø når spillerkoden er kjent, ellers it-hjelp
Sentralt innhold eller planMediaFileMissing, NetworkClientPlanUnreadable, Error 314 eller teksten invalid code pointit-hjelp
LisensRunICInvalidLicenseit-hjelp
Ikke klassifisertError 824.1 og andre ukjente Scala-signaturerit-hjelp
Ukjent byggkodeKjent lokal feil uten bekreftet geografisk treffit-hjelp

ApplicationTerimatedAbnormally er skrevet slik av Scala og må beholdes i filteret.

Foreløpige kontrolltilfeller

Spiller og feilForventet kø
se27-01-1001.skilt.uio.no, lokal feilit-sentrum
bl16-01-102-hoyre, lokal feilit-ovre-blindern
bl18-03-3601.skilt.uio.no, lokal feilit-nedre-blindern
bl20-01-101.skilt.uio.no, lokal feilit-nedre-blindern
GA02 Domus Odontologica, lokal feilit-gaustad-geitmyrsveien
ga24-015n006.skilt.uio.no, lokal feilit-lvb
Kjent spiller med RunICInvalidLicenseit-hjelp
Kjent spiller med Error 314it-hjelp
DigEx_3.Etg Mellom C og Dit-hjelp

Før aktivering

  • Gjenopprett eller erstatt den tidligere, komplette byggkode–kø-tabellen.
  • Avklar blant annet GA06; den er ikke lagt inn fordi riktig geografisk kø ikke kunne bekreftes.
  • Bruk Player.BuildingID foran vertsnavnprefikset når feltet finnes og verdien er kvalitetssikret. Bruk eierarbeidsgruppe eller lokasjon først når en godkjent kobling til kø foreligger.
  • Bekreft at alle målkøene bruker kompatibel livssyklus og status.
  • Test at Exchange/MTA bevarer From: noreply_UiO@atea-signage.no. Koden avviser avvik og lar saken stå i it-hjelp.
  • La Exchange/MTA kontrollere betrodd transport og SPF, DKIM eller DMARC. RT-koden alene autentiserer ikke domenet.
  • Avklar de nøyaktige fleksifeltene før tema Maskinvare og fritekstverdien Scala legges til.
  • Legg deduplisering og terskler i en egen fase. Dette utkastet gjør bare første ruting.
    • Lokale feil: spiller-ID + alarmsignatur + feilkode innen én time.
    • Sentrale feil: alarmsignatur + feilkode + medie-/plan-/kanalnavn på tvers av spillere.
    • Error 304.23 og helseopplastingsfeil: oppfølging først ved minst to treff innen én time, dersom driftsdata bekrefter at enkeltfeil er forbigående.
    • Samme lokale signatur i minst to geografiske områder innen én time: samle eller eskaler til it-hjelp.

Teknisk grunnlag

Koden er syntakskontrollert med Perl (syntax OK), kjørt mot ti lokale rutingstester og statisk kontrollert mot RT 6.0.3-API-et. Testene ligger i test_scala_rt_ruting_action.pl. Den må fortsatt funksjonstestes i et RT 6.0.3-testmiljø før aktivering.'

scala_koekart.csv

spillerregel,rt_koe,status,kilde,merknad
^SE,it-sentrum,dokumentert,RT 7127382,Tidligere bestilling sier at alle SE-koder skal hit
^BL05,it-ovre-blindern,dokumentert,RT 7127382,Eksplisitt eksempel i tidligere bestilling
^BL16,it-ovre-blindern,verifisert,RT 7441925,AV-sak for Georg Morgenstiernes hus
^BL18,it-nedre-blindern,verifisert,RT 7231766 og 7019636,Historiske lokale saker
^BL20,it-nedre-blindern,verifisert,RT 7401074,AV-sak for Helga Engs hus
^GA02,it-gaustad-geitmyrsveien,verifisert,RT 7479225,AV-sak for Domus Odontologica
^GA24,it-lvb,verifisert,RT 7472499 og 7633324,Digital skilting og lokal sak for Livsvitenskapsbygget
.*,it-hjelp,fallback,RT 7127382,Ukjent byggkode eller generisk feil

test_scala_rt_ruting_action.pl

use strict;
use warnings;
use utf8;

package MockAddress;

sub new     { bless { address => $_[1] }, $_[0] }
sub address { $_[0]->{address} }

package MockQueueObject;

sub new  { bless { ticket => $_[1] }, $_[0] }
sub Name { $_[0]->{ticket}->{queue} }

package MockTicket;

sub new      { bless $_[1], $_[0] }
sub Subject  { $_[0]->{subject} }
sub Id       { 1001 }
sub QueueObj { MockQueueObject->new($_[0]) }

sub IsWatcher {
    my ($self, %args) = @_;
    return $self->{requestor_ok}
        && $args{Type} eq 'Requestor'
        && lc($args{Email}) eq lc('noreply_UiO@atea-signage.no');
}

sub SetQueue {
    my ($self, $queue) = @_;
    $self->{queue} = $queue;
    return (1, 'ok');
}

package MockTransaction;

sub new { bless $_[1], $_[0] }

sub Addresses {
    my ($self) = @_;
    return { From => [ MockAddress->new($self->{from}) ] };
}

sub Content { $_[0]->{body} }

package MockScripAction;

sub new            { bless $_[1], $_[0] }
sub TicketObj      { $_[0]->{ticket} }
sub TransactionObj { $_[0]->{transaction} }

package RT;

sub SystemUser { 'system-user' }

package RT::Queue;

sub new { bless {}, $_[0] }

sub Load {
    my ($self, $name) = @_;
    $self->{name} = $name;
    return ($name, 'ok');
}

sub Id { $_[0]->{name} }

package RT::Logger;

sub warning { 1 }
sub error   { 1 }
sub info    { 1 }

package main;

my $action_path = 'rt-filterutkast/scala_rt_ruting_action.pl';
open my $action_file, '<:encoding(UTF-8)', $action_path
    or die "Kan ikke lese $action_path: $!\n";
local $/;
my $action_code = <$action_file>;
close $action_file;

eval 'sub run_action { my ($self) = @_;' . "\n"
    . $action_code . "\n}";
die "Kompileringsfeil i handlingen: $@" if $@;

my $scala_header = <<'SCALA';
Scala Content Manager on uio.atea-signage.no received an error from one of its players:
SCALA

my @tests = (
    {
        name     => 'Error 314 er sentral selv med kjent spiller',
        subject  => 'se27 Reported at Scala [ScriptPlayedIncorrectly]',
        body     => $scala_header . "Player Name: se27-04-4219.skilt.uio.no\nProblem 6: Script played incorrectly.\nError 314: Text contains an invalid code point\n",
        expected => 'it-hjelp',
    },
    {
        name     => 'Error 24 går til Nedre Blindern',
        subject  => 'bl18 Reported at Scala [ScriptPlayedIncorrectly]',
        body     => $scala_header . "Player Name: bl18-03-3601.skilt.uio.no\nProblem 6: Script played incorrectly.\nError 24: Not enough memory\n",
        expected => 'it-nedre-blindern',
    },
    {
        name     => 'DirectSound-feilkode finnes midt på linjen',
        subject  => 'bl20 Reported at Scala [ScriptPlayedIncorrectly]',
        body     => $scala_header . "Player Name: bl20-05-502.skilt.uio.no\nProblem 6: Script played incorrectly.\nDirectSound Error 304.23: DSERR_BUFFERLOST\n",
        expected => 'it-nedre-blindern',
    },
    {
        name     => 'Watchdog går til Nedre Blindern',
        subject  => 'bl20 Reported at Scala [PlayerEventWatchdogTimerExpired]',
        body     => $scala_header . "Player Name: bl20-01-101.skilt.uio.no\nWatchdog detected that Player failed to complete playing.\nError 702.37: timeout\n",
        expected => 'it-nedre-blindern',
    },
    {
        name     => 'Heartbeat i GA24 går til LVB',
        subject  => 'A heartbeat is overdue from a Scala player',
        body     => $scala_header . "Player Name: ga24-035s014.skilt.uio.no\n",
        expected => 'it-lvb',
    },
    {
        name     => 'Error 824.1 er ikke klassifisert',
        subject  => 'se27 Reported at Scala [ScriptPlayedIncorrectly]',
        body     => $scala_header . "Player Name: se27-04-4219.skilt.uio.no\nProblem 6: Script played incorrectly.\nError 824.1: ukjent\n",
        expected => 'it-hjelp',
    },
    {
        name     => 'Ukjent byggkode får fallback',
        subject  => 'ga06 Reported at Scala [RunICIsDown]',
        body     => $scala_header . "Player Name: ga06-01-001.skilt.uio.no\nRunICIsDown\n",
        expected => 'it-hjelp',
    },
    {
        name     => 'Ukjent signatur får fallback selv med kjent spiller',
        subject  => 'se27 Reported at Scala [UnknownAlarm]',
        body     => $scala_header . "Player Name: se27-04-4219.skilt.uio.no\nUnknownAlarm\n",
        expected => 'it-hjelp',
    },
    {
        name     => 'BuildingID brukes før spillerkode',
        subject  => 'ukjent spiller Reported at Scala [RunICIsDown]',
        body     => $scala_header . "Player Name: ukjent-spiller\nPlayer.BuildingID: GA02\nRunICIsDown\n",
        expected => 'it-gaustad-geitmyrsveien',
    },
    {
        name         => 'Feil avsender flytter ikke saken',
        subject      => 'se27 Reported at Scala [RunICIsDown]',
        body         => $scala_header . "Player Name: se27-04-4219.skilt.uio.no\nRunICIsDown\n",
        from         => 'annen@example.org',
        requestor_ok => 0,
        expected     => 'it-hjelp',
    },
);

my $failures = 0;

for my $test (@tests) {
    my $ticket = MockTicket->new({
        subject      => $test->{subject},
        queue        => 'it-hjelp',
        requestor_ok => $test->{requestor_ok} // 1,
    });
    my $transaction = MockTransaction->new({
        body => $test->{body},
        from => $test->{from} // 'noreply_UiO@atea-signage.no',
    });
    my $self = MockScripAction->new({
        ticket      => $ticket,
        transaction => $transaction,
    });

    run_action($self);

    if ($ticket->{queue} eq $test->{expected}) {
        print "OK: $test->{name}\n";
    }
    else {
        $failures++;
        print "FEIL: $test->{name}: fikk $ticket->{queue}, ventet $test->{expected}\n";
    }
}

die "$failures test(er) feilet\n" if $failures;
print scalar(@tests) . " tester bestått\n";

scala_rt_ruting_action.pl

# Utkast for RT 6.0.3.
# Bruk som «Custom action commit code» for «User Defined» i en utløser som bare kjøres:
# - i mottakskøen it-hjelp
# - med «On Create Via Email», eller «On Create» dersom RT-drift velger kjernen
#
# Ikke aktiver før køkartet og en ekte Scala-melding er testet av RT-drift.

my $ticket  = $self->TicketObj;
my $txn     = $self->TransactionObj;
my $subject = $ticket->Subject // '';
my $common_queue = 'it-hjelp';

# Forsvar i dybden: Handlingen skal bare kunne flytte saker fra mottakskøen.
return 1 unless lc($ticket->QueueObj->Name // '') eq lc($common_queue);

# Billig avgrensning før vi leser meldingsinnholdet.
return 1 unless $subject =~ /(?:heartbeat(?:\s+is)?\s+overdue|Reported at|Scala)/i;

# Kontroller både meldingsavsenderen og RTs registrerte innmelder.
my $expected  = 'noreply_UiO@atea-signage.no';
my $addresses = $txn->Addresses || {};
my @from      = @{ $addresses->{From} || [] };

my $from_ok = @from == 1
    && defined $from[0]->address
    && lc($from[0]->address) eq lc($expected);

my $requestor_ok = $ticket->IsWatcher(
    Type  => 'Requestor',
    Email => $expected,
);

unless ($from_ok && $requestor_ok) {
    my $seen = join ', ', map { $_->address // '' } @from;

    RT::Logger->warning(
        'Scala-ruting hoppet over sak ' . $ticket->Id
        . "; From='$seen'; forventet '$expected'"
    );

    return 1;
}

my $body = $txn->Content(Type => 'text/plain') // '';

return 1 unless $body
    =~ /Scala Content Manager on uio\.atea-signage\.no/i;

my ($player)     = $body =~ /^Player Name:\s*([^\r\n]+)/mi;
my ($building_id) = $body =~ /^(?:Player\.BuildingID|BuildingID):\s*([^\r\n]+)/mi;
my ($problem_no) = $body =~ /^Problem\s+(\d+):/mi;
my ($error_code) = $body =~ /\bError\s+([0-9]+(?:\.[0-9]+)?):/mi;

$player     //= '';
$building_id //= '';
$problem_no //= '';
$error_code //= '';
$player =~ s/^\s+|\s+$//g;
$building_id =~ s/^\s+|\s+$//g;

my $destination  = $common_queue;
my $signal_text  = "$subject\n$body";

# Sentrale feil skal ikke fordeles etter spillerens plassering.
my $is_generic =
       $signal_text =~ /\b(?:MediaFileMissing
                             |NetworkClientPlanUnreadable
                             |RunICInvalidLicense)\b/ix
    || $error_code eq '314'
    || $signal_text =~ /invalid code point/i;

# Bare dokumenterte, lokale signaturer kan flyttes til geografisk kø.
my $is_local =
       $signal_text =~ /heartbeat(?:\s+is)?\s+overdue/i
    || $signal_text =~ /\b(?:RunICIsDown
                            |ApplicationTerimatedAbnormally
                            |PlayerEventWatchdogTimerExpired
                            |SSHealthNonrecoverableErrorUploadingHealthLogs)\b/ix
    || $signal_text =~ /Watchdog detected that Player failed to complete/i
    || (
           $signal_text =~ /(?:\bScriptPlayedIncorrectly\b|played incorrectly)/i
        && ($error_code eq '24' || $error_code eq '304.23')
    );

my $category = $is_generic ? 'felles'
             : $is_local   ? 'lokal'
             :               'ukjent';

# Foreløpig køkart. Se scala_koekart.csv for kilde og status.
my @geographic_queues = (
    [ qr/^SE/i,             'it-sentrum' ],
    [ qr/^BL(?:05|16)\b/i,  'it-ovre-blindern' ],
    [ qr/^BL(?:18|20)\b/i,  'it-nedre-blindern' ],
    [ qr/^GA02\b/i,         'it-gaustad-geitmyrsveien' ],
    [ qr/^GA24\b/i,         'it-lvb' ],
);

if ($is_local && !$is_generic) {
    ROUTING_KEY:
    for my $routing_key (grep { length } ($building_id, $player)) {
        for my $rule (@geographic_queues) {
            if ($routing_key =~ $rule->[0]) {
                $destination = $rule->[1];
                last ROUTING_KEY;
            }
        }
    }
}

# Ukjent byggkode og generiske feil blir i felleskøen.
return 1 if lc($ticket->QueueObj->Name // '') eq lc($destination);

my $queue = RT::Queue->new(RT->SystemUser);
my ($queue_id, $queue_message) = $queue->Load($destination);

unless ($queue_id) {
    RT::Logger->error(
        "Scala-ruting: finner ikke kø '$destination': $queue_message"
    );
    return 1;
}

my ($ok, $message) = $ticket->SetQueue($queue->Id);

unless ($ok) {
    RT::Logger->error(
        'Scala-ruting: kunne ikke flytte sak ' . $ticket->Id
        . " til '$destination': $message"
    );
    return 1;
}

RT::Logger->info(
    'Scala-ruting: sak ' . $ticket->Id
    . "; kategori='$category'; spiller='$player'; bygg='$building_id'"
    . "; problem='$problem_no'; feil='$error_code'"
    . "; kø='$destination'"
);

return 1;
Publisert 3. sep. 2026 12:36 - Sist endret 3. sep. 2026 12:57