Documentation
¶
Overview ¶
Package parser decodes CEE/CEPA XML payloads from Dell PowerStore into strongly-typed CEPAEvent slices.
Two payload shapes are supported:
- Single-event: <CEEEvent>…</CEEEvent>
- VCAPS bulk batch: <EventBatch><CEEEvent>…</CEEEvent>…</EventBatch>
Two handshakes are detected and handled separately — callers must check for both before calling Parse, because neither is an event payload:
<RegisterRequest /> PowerStore, via Dell CEE <CheckFileRequest>… PowerScale (OneFS), which speaks CEPA directly
Index ¶
- Constants
- func CheckFileAction(body []byte) string
- func CheckFileActionDecoded(decoded []byte) string
- func DecodeBody(body []byte) ([]byte, error)
- func EncodeUTF16LE(b []byte) []byte
- func IsCheckEventRequest(body []byte) bool
- func IsCheckFileRequest(body []byte) bool
- func IsHeartBeatRequest(body []byte) bool
- func IsRegisterRequest(body []byte) bool
- func IsUTF16(body []byte) bool
- func IsUnmappedCEEEventType(eventType string) bool
- func IsUnmappedOneFSEventType(eventType string) bool
- type CEPAEvent
- func Parse(body []byte, receiveTime time.Time) ([]CEPAEvent, error)
- func ParseCheckEventRequest(body []byte, receiveTime time.Time) ([]CEPAEvent, error)
- func ParseCheckEventRequestDecoded(decoded []byte, receiveTime time.Time) ([]CEPAEvent, error)
- func ParseDecoded(decoded []byte, receiveTime time.Time) ([]CEPAEvent, error)
- func ParseOneFSEvent(body []byte, receiveTime time.Time) ([]CEPAEvent, error)
- func ParseOneFSEventDecoded(decoded []byte, receiveTime time.Time) ([]CEPAEvent, error)
- type Dialect
Constants ¶
const OneFSEventAction = "11"
OneFSEventAction is the Args/@action value OneFS uses for a file event, as opposed to OneFSHeartbeatAction for its heartbeat. Both arrive in the same <CheckFileRequest> element and both need a CheckFileResponse back; only this attribute separates them.
const OneFSHeartbeatAction = "9"
OneFSHeartbeatAction is the Args/@action value OneFS uses for its heartbeat. Events use other values — action 11 was measured for NFS file events.
Variables ¶
This section is empty.
Functions ¶
func CheckFileAction ¶
CheckFileAction returns the Args/@action attribute of a CheckFileRequest, or "" if the payload cannot be decoded or carries no action.
This distinction is load-bearing. A CheckFileRequest with action 11 holds a real audit event:
<CheckFileRequest><Args action="11" … name="<base64 UTF-16LE UNC path>"
protocol="1"><Cluster …/><Zone …/></Args>
<NFSEventArgs eventType="8" desiredAccess="0x100106" createDispo="0x3"
userSid="S-1-22-1-1000" clientIP="10.26.1.222" userId="1000"
timeStamp="1786563708" inode="4295432746" fsId="1"/></CheckFileRequest>
Answering that with a heartbeat response and nothing else makes OneFS advance its forwarding cursor — the event is acknowledged and gone. Treating every CheckFileRequest as a heartbeat therefore loses events *silently*, which is worse than rejecting them.
func CheckFileActionDecoded ¶
CheckFileActionDecoded is CheckFileAction for already-decoded input.
func DecodeBody ¶
DecodeBody converts a UTF-16 CEPA payload to UTF-8, returning the input unchanged if it is not UTF-16.
Dell CEE sends UTF-16LE without a BOM. Measured on the wire against CEE 9.2.0.0 (2026-08-11), the RegisterRequest handshake is 38 bytes for a 19-character document, with `Accept-Charset: utf-16` in the request headers:
<.R.e.g.i.s.t.e.r.R.e.q.u.e.s.t. ./.>.
Before this existed, IsRegisterRequest and Parse compared raw ASCII bytes, so every handshake fell through to the parse-error path and every UTF-16 event payload was dropped after being ACKed with HTTP 200 (see issue #32).
Decoding happens once, at the head of both entry points, so the two cannot disagree about what a payload says.
It is exported so a caller asking several questions about the same body can decode once and pass the result on. That only matters on the VCAPS path: OneFS sends plain UTF-8, for which this is a no-op returning the input untouched, while CEE sends UTF-16LE, for which every call allocates a []uint16 of len(body)/2 and a []byte of up to 4 bytes per rune. A batch of thousands of events routed through IsRegisterRequest, IsCheckFileRequest and Parse would otherwise be transcoded and discarded three times per PUT.
The result is safe to hand back to any of them: decoded UTF-8 still starts with '<' followed by a non-zero byte, so a second call falls through untouched and no caller has to know whether it holds raw or decoded bytes.
func EncodeUTF16LE ¶
EncodeUTF16LE converts UTF-8 to UTF-16LE without a BOM, which is how Dell CEE puts XML on the wire — measured against CEE 9.2.0.0 and 9.3.0.0, whose own replies carry no BOM either.
func IsCheckEventRequest ¶
IsCheckEventRequest reports whether the body is a Dell CEE event delivery.
This is CEE's own dialect, and it is a third thing — neither the <RegisterRequest /> handshake CEE opens with, nor the <CheckFileRequest> OneFS uses for both its heartbeat and its events. Shape recovered from CCheckEventRequest::GetXmlRequest() in libCEPPAPIWrapper.so:
<CheckEventRequest><EventList count="1">
<Event event="0x…" path="…" flag="0x…" server="…" share="…"
clientIP="…" serverIP="…" timeStamp="…" userSid="…" ownerSid="…"
fileSize="0x…" newName="…" desiredAccess="0x…" createDispo="0x…"
ntStatus="0x…" relativePath="…" encodingType="…" encodedPath="…"
encodedRelativePath="…" encodedNewName="…"/>
</EventList></CheckEventRequest>
func IsCheckFileRequest ¶
IsCheckFileRequest returns true if the body is the OneFS (PowerScale) heartbeat, which is a <CheckFileRequest> rather than PowerStore's <RegisterRequest>.
Measured on the wire from a 4-node OneFS 9.13.0.0 cluster (2026-08-12), 229 bytes of plain UTF-8 — not the 38-byte UTF-16LE that CEE sends:
<CheckFileRequest><Args action="9" sourceIP="10.26.1.150" sourceID="2" name="cABvAHcAZQByAHMAYwBhAGwAZQAxAA=="><Cluster id="00505692…" name="cABvAHcAZQByAHMAYwBhAGwAZQAxAA=="/></Args></CheckFileRequest>
`action="9"` is the heartbeat; `name` is the cluster name as base64 of UTF-16LE.
The element alone does not tell you what the payload is: OneFS carries its *events* in the same CheckFileRequest element, distinguished only by the action attribute. Use CheckFileAction to tell them apart — both need a CheckFileResponse back, but only action 9 is a heartbeat.
func IsHeartBeatRequest ¶
IsHeartBeatRequest reports whether the body is CEE's post-registration liveness probe.
This is the third thing CEE sends over HTTP, after <RegisterRequest /> and alongside <CheckEventRequest>. Its literal sits in libCEPPAPIWrapper.so immediately beside CHttpClient's other request bodies:
<RegisterRequest /> <HeartBeatRequest /> CHttpClient hbStatus= ntStatus=
It had never been seen on the wire because registration never succeeded, so CEE never got as far as sending one.
func IsRegisterRequest ¶
IsRegisterRequest returns true if the body is the CEPA handshake payload. Matches a <RegisterRequest> root element — guards against event payloads whose content (e.g. a file path) happens to contain the word.
This is the PowerStore/CEE dialect. OneFS opens with a different element entirely — see IsCheckFileRequest.
func IsUTF16 ¶
decodeUTF16 converts a UTF-16 CEPA payload to UTF-8, returning the input unchanged if it is not UTF-16.
Dell CEE sends UTF-16LE without a BOM. Measured on the wire against CEE 9.2.0.0 (2026-08-11), the RegisterRequest handshake is 38 bytes for a 19-character document, with `Accept-Charset: utf-16` in the request headers:
<.R.e.g.i.s.t.e.r.R.e.q.u.e.s.t. ./.>.
Before this existed, IsRegisterRequest and Parse compared raw ASCII bytes, so every handshake fell through to the parse-error path and every UTF-16 event payload was dropped after being ACKed with HTTP 200 (see issue #32).
Decoding happens once, at the head of both entry points, so the two cannot disagree about what a payload says. IsUTF16 reports whether a payload arrived as UTF-16, using the same detection decodeUTF16 uses so the two cannot disagree.
Callers need this to answer in the encoding they were addressed in. The two publishers differ: PowerStore sends UTF-16LE and CEE answers it in UTF-16LE, while OneFS sends plain UTF-8 and is answered in UTF-8 — measured on the wire from both. Replying in the wrong one produces a body the publisher cannot parse, which for OneFS is fatal (STATUS_DATA_ERROR) and for PowerStore means the CEPP session never establishes.
func IsUnmappedCEEEventType ¶
IsUnmappedCEEEventType reports whether an event carries a CEE event code this package has no established meaning for, so the caller can log the gap.
func IsUnmappedOneFSEventType ¶
IsUnmappedOneFSEventType reports whether an event carries a OneFS eventType this package has no established meaning for. Such events are still parsed and still flow to the writers — losing them would be worse, because the CheckFileResponse has already advanced the cluster's forwarding cursor by the time anyone could decide to drop them — but the caller should log them so the gap stays visible.
Types ¶
type CEPAEvent ¶
type CEPAEvent struct {
// Raw CEPA identifier, e.g. "CEPP_FILE_WRITE"
EventType string
// Filesystem path of the affected object
FilePath string
// User context
Username string
Domain string
UserSID string
LogonID string
// Network context
ClientAddr string
// Protocol the operation used, resolved from the wire's numeric code:
// CIFS, NFS, FTP or Unknown. Never empty — an unmapped code renders
// Unknown rather than blank, so it cannot merge with a missing value when
// used as a metric label.
Protocol string
// Server is the NAS server the operation happened on, as the array reports
// it. Bounded by the size of the estate, unlike ClientAddr, which is every
// workstation that ever touched a share — which is why this is the one
// safe to carry as a metric label and that one is not.
Server string
// Event timestamp (parsed from the XML or synthesised from receive time)
Timestamp time.Time
// I/O statistics — only meaningful for CEPP_CLOSE_MODIFIED
BytesRead int64
BytesWritten int64
NumberOfReads int64
NumberOfWrites int64
}
CEPAEvent is the normalised representation of a single CEPA audit event.
func Parse ¶
Parse decodes one or more CEPA events from a raw XML body. The receiveTime is used as a fallback when the XML payload contains no timestamp.
func ParseCheckEventRequest ¶
ParseCheckEventRequest decodes a CEE <CheckEventRequest> into the same CEPAEvent values the OneFS and VCAPS paths produce, so all three feed one mapper and one set of writers.
receiveTime is the fallback when an event carries no usable timestamp.
func ParseCheckEventRequestDecoded ¶
ParseCheckEventRequestDecoded is ParseCheckEventRequest for already-decoded input.
func ParseDecoded ¶
ParseDecoded is Parse for input Classify has already decoded.
func ParseOneFSEvent ¶
ParseOneFSEvent decodes a OneFS <CheckFileRequest action="11"> into the same CEPAEvent the PowerStore path produces, so both feed one mapper and one set of writers rather than a parallel pipeline.
receiveTime is the fallback when the payload carries no usable timestamp.
type Dialect ¶
type Dialect int
Dialect names one of the payload shapes that arrive on the CEPA URL.
All of them are POSTed or PUT to the same path by different publishers, and answering one with another's document is a silent, fatal failure — see pkg/server. Recognition lives here; the reply documents live in pkg/server.
const ( // DialectUnknown is a payload whose root element matches none of the // others. It is not an error on its own: the VCAPS event shapes are // recognised by Parse rather than by root element. DialectUnknown Dialect = iota DialectRegisterRequest DialectHeartBeatRequest DialectCheckFileRequest // OneFS: heartbeat and event share this element DialectCheckEventRequest // Dell CEE's event delivery )
func Classify ¶
Classify transcodes the body once and reports which dialect it is, returning the decoded bytes for the caller to hand to the matching parser.
This exists because transcoding is the dominant cost of handling a request. Every Is* predicate calls DecodeBody on the whole body just to read its root element, so dispatching through four predicates and then parsing decoded the same payload five times. Measured on a 1000-event UTF-16LE batch (698 KB): 41.2 ms and 47.6 MB per request, of which 25.5 ms and 43.7 MB — 62% of the time and 92% of the allocations — was redundant transcoding. DecodeBody allocates roughly 12.5x the body size each time it runs, and pkg/server accepts bodies up to 64 MiB against a ~3 s CEPA deadline.
The Is* predicates remain for callers holding a raw body; they are unchanged and still decode. Classify is what the dispatcher should use.