Current section

Files

Jump to
eipmi src eipmi_trap.erl
Raw

src/eipmi_trap.erl

%%%=============================================================================
%%% Copyright (c) 2019-2020 Lindenbaum GmbH
%%%
%%% Permission to use, copy, modify, and/or distribute this software for any
%%% purpose with or without fee is hereby granted, provided that the above
%%% copyright notice and this permission notice appear in all copies.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%%%
%%% @doc
%%% A server that listens for IPMI Platform Event Trap (PET) messages forwarding
%%% these to the owner of all currently active and matching IPMI sessions.
%%% Matching is performed on the provided agent IP address. The first matching
%%% session is used to acknowledge the trap if it's sequence number is
%%% specified (not null).
%%% @end
%%%=============================================================================
-module(eipmi_trap).
-behaviour(gen_server).
%% API
-export([decode/1,
start_link/1]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include_lib("snmp/include/snmp_types.hrl").
-type trap() :: {trap, [property()]}.
-type trap_severity() :: monitor | information | ok | non_critical | critical |
non_recoverable.
-type trap_source() :: firmware | smi_handler | isv_system | alert_asic |
ipmi | bios_vendor | system_board | system_integrator |
third_party_add_in | osv | nic | system_management_card.
-type property() :: {type,
cold_start | warm_start |
authentication_failure |
egp_neighbor_loss |
enterprise_specific |
link_down | link_up} |
{enterprise, [non_neg_integer()]} |
{seq_nr, non_neg_integer()} |
{data, binary()} |
{event_severity, trap_severity()} |
{event_source, trap_source()} |
{event_source_raw, non_neg_integer()} |
{guid, binary()} |
{if_index, non_neg_integer()} |
{local_time, non_neg_integer()} |
{manufacturer_id, non_neg_integer()} |
{oid, [non_neg_integer()]} |
{sensor_device, non_neg_integer()} |
{sensor_number, non_neg_integer()} |
{sensor_type, eipmi_sensor:type()} |
{system_id, non_neg_integer()} |
{reading_type, eipmi_sensor:reading()} |
{time, non_neg_integer()} |
{trap_source, trap_source()} |
{utc_offset, non_neg_integer()} |
eipmi_sensor:entity() |
eipmi_sensor:value() |
#varbind{}.
-export_type([trap/0,
trap_severity/0,
trap_source/0,
property/0]).
-include("eipmi.hrl").
%%%=============================================================================
%%% API
%%%=============================================================================
%%------------------------------------------------------------------------------
%% @doc
%% Decodes a binary representing an SNMP (PET) trap message in its erlang
%% record representation.
%% @end
%%------------------------------------------------------------------------------
-spec decode(binary() | list()) ->
{ok, {inet:ip4_address(), trap()}} | {error, term()}.
decode(Binary) when is_binary(Binary) ->
decode(binary_to_list(Binary));
decode(List) when is_list(List) ->
C = application:get_env(eipmi, community, undefined),
try snmp_pdus:dec_message(List) of
#message{version = 'version-1',
community = Community,
data = Trap = #trappdu{}}
when C =:= undefined; C =:= Community ->
case trap_pdu(Trap) of
{ok, Properties} -> {ok, {agent_addr(Trap), Properties}};
Error = {error, _} -> Error
end;
#message{version = 'version-1',
community = Community,
data = #trappdu{}} ->
{error, {invalid_community, Community}};
#message{version = 'version-1'} ->
{error, not_snmp_trap};
#message{version = Version} ->
{error, {unsupported, Version}}
catch
exit:{error, Reason} ->
{error, Reason};
error:function_clause ->
{error, {not_snmp_pdu, List}};
C:E ->
{error, {decode_error, {C, E}, List}}
end.
%%------------------------------------------------------------------------------
%% @doc
%% Start a trap listener process on the given port number.
%% @end
%%------------------------------------------------------------------------------
-spec start_link(inet:port_number()) -> {ok, pid()} | {error, term()}.
start_link(Port) -> gen_server:start_link(?MODULE, [Port], []).
%%%=============================================================================
%%% gen_server Callbacks
%%%=============================================================================
-record(state, {socket :: inet:socket()}).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
init([Port]) ->
{ok, Socket} = gen_udp:open(Port, [binary]),
{ok, #state{socket = Socket}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
handle_call(_Request, _From, State) -> {reply, undef, State}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
handle_cast(_Request, State) -> {noreply, State}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
handle_info({udp_closed, Socket}, State = #state{socket = Socket}) ->
{stop, socket_closed, State};
handle_info({udp, Socket, SrcIP, _, Bin}, State = #state{socket = Socket}) ->
try decode(Bin) of
{ok, {AgentIP, Trap}} ->
case lists:filter(forward(AgentIP, Trap), eipmi:sessions()) of
[Session | _] -> acknowledge(Session, Trap);
_ -> ok
end;
{error, Reason} ->
eipmi_util:warn("Unsupported trap from ~w: ~w", [SrcIP, Reason])
catch
C:E ->
Reason = {decode_error, {C, E}},
eipmi_util:warn("Bad trap from ~w: ~w", [SrcIP, Reason])
end,
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
terminate(_Reason, _State) -> ok.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%%------------------------------------------------------------------------------
%% @private
%% Forward the trap to a matching sessions' owner.
%%------------------------------------------------------------------------------
forward(AgentIP, Trap) ->
fun(Session = {session, {IP, _}, {Owner, _}}) when AgentIP =:= IP ->
Owner ! {ipmi, Session, AgentIP, Trap},
true;
(_Session) ->
false
end.
%%------------------------------------------------------------------------------
%% @private
%% Acknowledge enterprise specific traps with a specified sequence number
%% (cookie).
%%------------------------------------------------------------------------------
acknowledge(Session, {trap, TrapProperties}) ->
case proplists:get_value(type, TrapProperties) of
enterprise_specific ->
case proplists:get_value(seq_nr, TrapProperties, 0) of
X when X > 0 ->
_ = eipmi:raw(Session,
?IPMI_NETFN_SENSOR_EVENT_REQUEST,
?PET_ACKNOWLEDGE,
[{retransmits, 0} | TrapProperties]);
_ ->
ok
end;
_ ->
ok
end.
%%------------------------------------------------------------------------------
%% @private
%% For some reason the `snmp_pdus' module may return IPv4 addresses as list of
%% integers.
%%------------------------------------------------------------------------------
agent_addr(#trappdu{agent_addr = Addr}) -> agent_addr(Addr);
agent_addr(List) when is_list(List) -> list_to_tuple(List).
%%------------------------------------------------------------------------------
%% @private
%% RFC 1157
%%------------------------------------------------------------------------------
trap_pdu(Trap = #trappdu{generic_trap = 0}) ->
{ok, {trap, [{type, cold_start} | trap_misc(Trap)]}};
trap_pdu(Trap = #trappdu{generic_trap = 1}) ->
{ok, {trap, [{type, warm_start} | trap_misc(Trap)]}};
trap_pdu(Trap = #trappdu{generic_trap = 2, varbinds = Vs}) ->
Ps = [{if_index, If} || #varbind{variabletype = 'INTEGER',
oid = [1,3,6,1,2,1,2,2,1,8|_],
value = If} <- Vs],
{ok, {trap, [{type, link_down} | trap_misc(Trap)] ++ Ps}};
trap_pdu(Trap = #trappdu{generic_trap = 3, varbinds = Vs}) ->
Ps = [{if_index, If} || #varbind{variabletype = 'INTEGER',
oid = [1,3,6,1,2,1,2,2,1,8|_],
value = If} <- Vs],
{ok, {trap, [{type, link_up} | trap_misc(Trap)] ++ Ps}};
trap_pdu(Trap = #trappdu{generic_trap = 4}) ->
{ok, {trap, [{type, authentication_failure} | trap_misc(Trap)]}};
trap_pdu(Trap = #trappdu{generic_trap = 5, varbinds = Vs}) ->
Ps = [{neighbor_addr, list_to_tuple(IP)}
|| #varbind{variabletype = 'IpAddress',
oid = [1,3,6,1,2,1,8,5,1,2|_],
value = IP} <- Vs],
{ok, {trap, [{type, egp_neighbor_loss} | trap_misc(Trap)] ++ Ps}};
trap_pdu(Trap = #trappdu{generic_trap = 6,
enterprise = Enterprise,
specific_trap = SpecificTrap,
varbinds = Vs}) ->
Ps = [trap_enterprise_specific(Enterprise, SpecificTrap, V) || V <- Vs],
{ok, {trap, [{type, enterprise_specific} | trap_misc(Trap)] ++ lists:append(Ps)}};
trap_pdu(Trap = #trappdu{}) ->
{error, {unsupported, Trap}}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_misc(#trappdu{enterprise = E, time_stamp = T}) ->
[{enterprise, E}, {time, T}].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_enterprise_specific([1,3,6,1,4,1,3183,1,1],
SpecificTrap,
#varbind{oid = [1,3,6,1,4,1,3183,1,1,1],
variabletype = 'OCTET STRING',
value = String}) ->
trap_ipmi_pet(<<SpecificTrap:32>>, list_to_binary(String));
trap_enterprise_specific([1,3,6,1,4,1,27768,1,1,2],
_SpecificTrap,
#varbind{oid = [1,3,6,1,4,1,27768,1,1,2,1,1],
variabletype = 'IpAddress',
value = IP}) ->
[{main_ip, list_to_tuple(IP)}];
trap_enterprise_specific([1,3,6,1,4,1,27768,1,1,2],
_SpecificTrap,
#varbind{oid = [1,3,6,1,4,1,27768,1,1,2,1,2],
variabletype = 'IpAddress',
value = IP}) ->
[{primary_ip, list_to_tuple(IP)}];
trap_enterprise_specific([1,3,6,1,4,1,27768,1,1,2],
_SpecificTrap,
#varbind{oid = [1,3,6,1,4,1,27768,1,1,2,1,3],
variabletype = 'OCTET STRING',
value = Mac}) ->
[{mac_address, lists:foldr(fun(0, Acc) -> Acc;
($-, Acc) -> [$: | Acc];
(C, Acc) -> [C | Acc]
end, "", Mac)}];
trap_enterprise_specific(_, _, Varbind) ->
[Varbind].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_ipmi_pet(
<<?EIPMI_RESERVED:8, SensorType:8, EventType:8,
Assertion:1, ?EIPMI_RESERVED:3, _Offset:4>>,
<<GUID:16/binary, SeqNr:16, Time:32, UtcOffset:16, TrapSource:8,
EventSource:8, Severity:8, Device:8, Number:8, Entity:8, Instance:8,
Data:3/binary, DataRest:5/binary, _LangCode:8, Manufacturer:32,
SystemId:16, _OEMCustomFields/binary>>) ->
{Reading, Type} = eipmi_sensor:get_type(EventType, SensorType),
lists:append(
[
[{event_source_raw, EventSource}],
trap_smbios_guid(GUID),
unspecified_if_0(seq_nr, SeqNr),
unspecified_if_0(local_time, Time),
unspecified_if_0(utc_offset, UtcOffset),
trap_source(trap_source, TrapSource),
trap_source(event_source, EventSource),
trap_severity(Severity),
unspecified_if_0(manufacturer_id, Manufacturer),
unspecified_if_0(system_id, SystemId),
unspecified_if_ff(sensor_device, Device),
unspecified_if_ff(unspecified_if_0(sensor_number, Number)),
unspecified_if_0(eipmi_sensor:get_entity(Entity, Instance)),
[{sensor_type, Type}, {reading_type, Reading}],
eipmi_util:decode_event_data(Reading, Type, Assertion, Data),
[{data, <<Data/binary, DataRest/binary>>}]
]).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_smbios_guid(<<0:16,0:16,0:16,0:16,0:16,0:16,0:16,0:16>>) ->
[];
trap_smbios_guid(<<A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P>>) ->
GUID = io_lib:format("~2.16.0B~2.16.0B~2.16.0B~2.16.0B-"
"~2.16.0B~2.16.0B-"
"~2.16.0B~2.16.0B-"
"~2.16.0B~2.16.0B-"
"~2.16.0B~2.16.0B~2.16.0B~2.16.0B~2.16B~2.16.0B",
[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P]),
[{guid, iolist_to_binary(GUID)}].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_source(T, X) when X < 16#7 -> [{T, firmware}];
trap_source(T, X) when X < 16#f -> [{T, smi_handler}];
trap_source(T, X) when X < 16#17 -> [{T, isv_system}];
trap_source(T, X) when X < 16#1f -> [{T, alert_asic}];
trap_source(T, X) when X < 16#27 -> [{T, ipmi}];
trap_source(T, X) when X < 16#2f -> [{T, bios_vendor}];
trap_source(T, X) when X < 16#37 -> [{T, system_board}];
trap_source(T, X) when X < 16#3f -> [{T, system_integrator}];
trap_source(T, X) when X < 16#47 -> [{T, third_party_add_in}];
trap_source(T, X) when X < 16#4f -> [{T, osv}];
trap_source(T, X) when X < 16#57 -> [{T, nic}];
trap_source(T, X) when X < 16#5f -> [{T, system_management_card}];
trap_source(_, _) -> [].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
trap_severity(16#1) -> [{event_severity, monitor}];
trap_severity(16#2) -> [{event_severity, information}];
trap_severity(16#4) -> [{event_severity, ok}];
trap_severity(16#8) -> [{event_severity, non_critical}];
trap_severity(16#10) -> [{event_severity, critical}];
trap_severity(16#20) -> [{event_severity, non_recoverable}];
trap_severity(_) -> [].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
unspecified_if_0(L) when is_list(L) ->
lists:flatmap(fun({T, Val}) -> unspecified_if_0(T, Val) end, L).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
unspecified_if_0(_, 0) -> [];
unspecified_if_0(T, Val) -> [{T, Val}].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
unspecified_if_ff(L) when is_list(L) ->
lists:flatmap(fun({T, Val}) -> unspecified_if_ff(T, Val) end, L).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
unspecified_if_ff(_, 16#ff) -> [];
unspecified_if_ff(T, Val) -> [{T, Val}].
%%%=============================================================================
%%% TESTS
%%%=============================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
trap_enterprise_specific_test() ->
MacAddress = {varbind,[1,3,6,1,4,1,27768,1,1,2,1,3],
'OCTET STRING',
[48,48,45,52,48,45,52,50,45,48,98,45,50,99,45,51,48,0],
3},
?assertEqual(
[{mac_address, "00:40:42:0b:2c:30"}],
trap_enterprise_specific([1,3,6,1,4,1,27768,1,1,2], 1337, MacAddress)).
-endif. %% TEST