%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1999-2019. All Rights Reserved.
%% 
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% 
%% %CopyrightEnd%
%%
-module(snmp_framework_mib).

-include("snmp_types.hrl").
-include("STANDARD-MIB.hrl").

-define(VMODULE,"FRAMEWORK-MIB").
-include("snmp_verbosity.hrl").

-ifndef(default_verbosity).
-define(default_verbosity,silence).
-endif.


%%%-----------------------------------------------------------------
%%% This module implements the init- configure- and instrumentation-
%%% functions for the SNMP-FRAMEWORK-MIB.
%%%
%%% We also keep internal datastructures here, e.g. a table
%%% over all known contexts.
%%%-----------------------------------------------------------------
%% External exports
%% Avoid warning for local function error/1 clashing with autoimported BIF.
-compile({no_auto_import,[error/1]}).
-export([init/0, configure/1]).
-export([intContextTable/1, intContextTable/3,
	 intAgentTransportDomain/1, intAgentTransports/1,
	 intAgentUDPPort/1, intAgentIpAddress/1,
	 snmpEngineID/1,
	 snmpEngineBoots/1,
	 snmpEngineTime/1,
	 snmpEngineMaxMessageSize/1,
	 get_engine_id/0, get_engine_max_message_size/0,
	 get_engine_boots/0, get_engine_time/0,
	 set_engine_boots/1, set_engine_time/1,
	 table_next/2, check_status/3]).
-export([add_context/1, delete_context/1]).
-export([check_agent/2, check_context/1, order_agent/2]).


%%-----------------------------------------------------------------
%% Func: init/0
%% Purpose: Creates the tables and variables necessary for the SNMP
%%          mechanism to work properly.
%%          Note that this function won't destroy any old values.
%%          This function should be called only once.
%%-----------------------------------------------------------------
init() ->
    maybe_create_table(intContextTable),
    init_engine().


%%-----------------------------------------------------------------
%% Func: configure/1
%% Args: Dir is the directory with trailing dir_separator where
%%       the configuration files can be found.
%% Purpose: Reads the config-files for the internal tables, and
%%          inserts the data.  Makes sure that all old data in
%%          the tables are deleted, and the new data inserted.
%%          This function makes sure that all (and only) 
%%          config-file-data are in the tables. 
%% Returns: ok
%% Fails: exit(configuration_error)
%% PRE: init/1 has been successfully called
%%-----------------------------------------------------------------
configure(Dir) ->
    set_sname(),
    case snmpa_agent:get_agent_mib_storage() of
        mnesia ->
            ok;
        _ ->
	    case (catch do_configure(Dir)) of
		ok ->
		    ok;
		{error, Reason} ->
		    ?vinfo("configure error: ~p", [Reason]),
		    config_err("configure failed: ~p", [Reason]),
		    exit(configuration_error);
		Error ->
		    ?vinfo("configure failed: ~p", [Error]),
		    config_err("configure failed: ~p", [Error]),
		    exit(configuration_error)
	    end
    end,
    ok.

do_configure(Dir) ->
    ?vdebug("read internal config files",[]),
    Contexts = read_internal_config_files(Dir),
    ?vdebug("read agent config files",[]),
    Agent = read_agent(Dir),
    ?vdebug("initiate vars",[]),
    init_vars(Agent),
    %% Add default context, if not present.
    NContexts = [{""} | lists:delete({""}, Contexts)],
    ?vdebug("initiate tables",[]),
    init_tabs(NContexts),
    ok.

read_internal_config_files(Dir) ->
    ?vdebug("read context config file",[]),
    Gen    = fun gen_context/2,
    Order  = fun snmp_conf:no_order/2,
    Filter = fun snmp_conf:no_filter/1,
    Check  = fun(Entry, State) -> {check_context(Entry), State} end,
    [Ctxs] =
	snmp_conf:read_files
	  (Dir, [{"context.conf", Gen, Order, Check, Filter}]),
    Ctxs.

read_agent(Dir) ->
    ?vdebug("read agent config file", []),
    FileName = "agent.conf",
    File     = filename:join(Dir, FileName), 
    Conf0    =
	try
	    snmp_conf:read(File, fun order_agent/2, fun check_agent/2)
	catch
	    throw:{error, Reason} ->
		error({failed_reading_config_file, Dir, FileName, Reason})
	end,
    Mand =
	[{intAgentTransports,       mandatory},
	 {snmpEngineMaxMessageSize, mandatory},
	 {snmpEngineID,             mandatory}],
    {ok, Conf} = snmp_conf:check_mandatory(Conf0, Mand),
    Conf.


%%-----------------------------------------------------------------
%% Generate a context.conf file.
%%-----------------------------------------------------------------
gen_context(Dir, _Reason) ->
    config_err("missing context.conf file => generating a default file", []),
    File = filename:join(Dir, "context.conf"),
    case file:open(File, [write]) of
	{ok, Fid} ->
	    ok = io:format(Fid, "~s\n", [context_header()]),
	    ok = io:format(Fid, "%% The default context\n\"\".\n", []),
	    file:close(Fid),
	    [];
	{error, Reason} ->
            file:delete(File),
	    error({failed_creating_file, File, Reason})
    end.

context_header() ->
    {Y,Mo,D} = date(),
    {H,Mi,S} = time(),
    io_lib:format("%% This file was automatically generated by "
                  "snmp_config v~s  ~w-~2.2.0w-~2.2.0w "
                  "~2.2.0w:~2.2.0w:~2.2.0w\n",
                  [?version, Y, Mo, D, H, Mi, S]).


%%-----------------------------------------------------------------
%%  Context
%%  Context.
%%-----------------------------------------------------------------
check_context(Context) ->
    ?vtrace("check_context -> entry with"
        "~n   Context: ~p", [Context]),
    case (catch snmp_conf:check_string(Context)) of
	ok ->
	    {ok, {Context}};
	_ ->
	    error({invalid_context, Context})
    end.


%%-----------------------------------------------------------------
%%  Agent
%%  {Name, Value}.
%%-----------------------------------------------------------------
check_agent(Entry, undefined) ->
    check_agent(Entry, {snmp_target_mib:default_domain(), undefined});
check_agent({intAgentTransportDomain, Domain}, {_, Port}) ->
    {snmp_conf:check_domain(Domain), {Domain, Port}};
check_agent({intAgentUDPPort, Port}, {Domain, _}) ->
    ok = snmp_conf:check_port(Port),
    {ok, {Domain, Port}};
check_agent({intAgentIpAddress, _}, {_, undefined}) ->
    error({missing_mandatory, intAgentUDPPort});
check_agent({intAgentIpAddress = Tag, Ip} = Entry, {Domain, Port} = State) ->
    {case snmp_conf:check_ip(Domain, Ip) of
	 ok ->
	     [Entry,
	      {intAgentTransports, [{Domain, {Ip, Port}}]}];
	 {ok, FixedIp} ->
	     [{Tag, FixedIp},
	      {intAgentTransports, [{Domain, {FixedIp, Port}}]}]
     end, State};
check_agent({intAgentTransports = Tag, Transports}, {_, Port} = State)
  when is_list(Transports) ->
    CheckedTransports =
	[case Transport of
	     {Domain, Address} ->
		 case
		     case Port of
			 undefined ->
			     snmp_conf:check_address(Domain, Address);
			 _ ->
			     snmp_conf:check_address(Domain, Address, Port)
		     end
		 of
		     ok ->
			 Transport;
		     {ok, FixedAddress} ->
			 {Domain, FixedAddress}
		 end;
	     _ ->
		 error({bad_transport, Transport})
	 end
	 || Transport <- Transports],
    {{ok, {Tag, CheckedTransports}}, State};
check_agent(Entry, State) ->
    {check_agent(Entry), State}.

%% This one is kept for backwards compatibility
check_agent({intAgentMaxPacketSize, Value}) -> 
    snmp_conf:check_packet_size(Value);
check_agent({snmpEngineMaxMessageSize, Value}) -> 
    snmp_conf:check_packet_size(Value);
check_agent({snmpEngineID, Value}) -> 
    snmp_conf:check_string(Value);
check_agent(X) -> 
    error({invalid_agent_attribute, X}).

%% Ordering function to sort intAgentTransportDomain first
%% hence before intAgentIpAddress.  Sort other entries on the key.
-dialyzer({nowarn_function, order_agent/2}).
order_agent(EntryA, EntryB) ->
    snmp_conf:keyorder(
      1, EntryA, EntryB,
      [intAgentTransportDomain, intAgentUDPPort | sort]).



maybe_create_table(Name) ->
    case snmpa_local_db:table_exists(db(Name)) of
	true -> 
	    ok;
	_ -> 
	    ?vtrace("create table: ~w",[Name]),
	    snmpa_local_db:table_create(db(Name))
    end.

init_vars(Vars) ->
    lists:map(fun init_var/1, Vars).

init_var({Var, Val}) ->
    ?vtrace("init var: "
	    "~n   set ~w to ~w",[Var, Val]),
    snmp_generic:variable_set(db(Var), Val).    

init_tabs(Contexts) ->
    ?vdebug("create context table",[]),
    snmpa_local_db:table_delete(db(intContextTable)),
    snmpa_local_db:table_create(db(intContextTable)),
    init_context_table(Contexts).
    
init_context_table([Row | T]) ->
    Context = element(1, Row),
    Key = [length(Context) | Context],
    ?vtrace("create intContextTable table row for: ~w",[Key]),
    snmpa_local_db:table_create_row(db(intContextTable), Key, Row),
    init_context_table(T);
init_context_table([]) -> true.


table_cre_row(Tab, Key, Row) ->
    snmpa_mib_lib:table_cre_row(db(Tab), Key, Row).

table_del_row(Tab, Key) ->
    snmpa_mib_lib:table_del_row(db(Tab), Key).


%% FIXME: does not work with mnesia
add_context(Ctx) ->
    case (catch check_context(Ctx)) of
	{ok, Row} ->
	    Context = element(1, Row),
	    Key = [length(Context) | Context],
	    case table_cre_row(intContextTable, Key, Row) of
		true ->
		    {ok, Key};
		false ->
		    {error, create_failed}
	    end;
	{error, Reason} ->
	    {error, Reason};
	Error ->
	    {error, Error}
    end.

%% FIXME: does not work with mnesia
delete_context(Key) ->
    case table_del_row(intContextTable, Key) of
        true ->
            ok;
        false ->
            {error, delete_failed}
    end.

   
%%-----------------------------------------------------------------
%% Instrumentation functions
%% Retreive functions are also used internally by the agent, so
%% don't change the interface without changing those functions.
%% Note that if these functions implementations are changed,
%% an error can make the agent crash, as no error detection is
%% performed for the internal data.
%% These functions cannot use the default functions as is, because
%% the default functions rely on that the mib is loaded, and
%% these functions must work even if the OTP-FRAMEWORK-MIB isn't loaded.
%% So we hardcode the information necessary for the functions
%% called by the default functions in snmp_generic. This info is
%% normally provided by the mib compiler, and inserted into 
%% snmpa_symbolic_store at load mib time.
%%-----------------------------------------------------------------

%%-----------------------------------------------------------------
%% None if the int* objects are defined in any MIB.  
%%
%% intContextTable keeps all
%% known contexts internally, but is ordered as an SNMP table.  It
%% could be defined as:
%%   intContextTable OBJECT-TYPE
%%       SYNTAX       SEQUENCE OF IntContextEntry
%%       MAX-ACCESS   not-accessible
%%       STATUS       current
%%       DESCRIPTION "A table of locally available contexts."
%%       ::= { xx }
%%   
%%   intContextEntry OBJECT-TYPE
%%       SYNTAX       IntContextEntry
%%       MAX-ACCESS   not-accessible
%%       STATUS       current
%%       DESCRIPTION "Information about a particular context."
%%       INDEX       {
%%                     intContextName
%%                   }
%%       ::= { intContextTable 1 }
%%   
%%   IntContextEntry ::= SEQUENCE
%%       {
%%           intContextName SnmpAdminString
%%       }
%%   
%%   intContextName  OBJECT-TYPE
%%       SYNTAX       SnmpAdminString (SIZE(0..32))
%%       MAX-ACCESS   read-only
%%       STATUS       current
%%       DESCRIPTION "A human readable name identifying a particular
%%                    context at a particular SNMP entity.
%%   
%%                    The empty contextName (zero length) represents the
%%                    default context.
%%                   "
%%       ::= { intContextEntry 1 }
%%-----------------------------------------------------------------

%% Op == new | delete
intContextTable(Op) ->
    snmp_generic:table_func(Op, db(intContextTable)).

%% Op == get get_next  -- READ only table
intContextTable(get, RowIndex, Cols) ->
    get(intContextTable, RowIndex, Cols);
intContextTable(get_next, RowIndex, Cols) ->
    next(intContextTable, RowIndex, Cols);
intContextTable(Op, Arg1, Arg2) ->
    snmp_generic:table_func(Op, Arg1, Arg2, db(intContextTable)).

%% FIXME: exported, not used by agent, not documented - remove?
table_next(Name, RestOid) ->
    snmp_generic:table_next(db(Name), RestOid).

%% FIXME: exported, not used by agent, not documented - remove?
%% FIXME: does not work with mnesia
check_status(Name, Indexes, StatusNo) ->
    case snmpa_local_db:table_get_element(db(Name), Indexes, StatusNo) of
	{value, ?'RowStatus_active'} -> true;
	_ -> false
    end.

db(intContextTable) -> {intContextTable, volatile};
db(X) -> snmpa_agent:db(X).

fa(intContextTable) -> 1.
 
foi(intContextTable) -> 1.

noc(intContextTable) -> 1.
 
next(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_next(db(Name), RowIndex, Cols,
                                   fa(Name), foi(Name), noc(Name)).
 
get(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_get(db(Name), RowIndex, Cols, foi(Name)).

%% Op == new | delete | get
intAgentUDPPort(Op) ->
    snmp_generic:variable_func(Op, db(intAgentUDPPort)).

intAgentIpAddress(Op) ->
    snmp_generic:variable_func(Op, db(intAgentIpAddress)).

intAgentTransportDomain(Op) ->
    snmp_generic:variable_func(Op, db(intAgentTransportDomain)).

intAgentTransports(Op) ->
    snmp_generic:variable_func(Op, db(intAgentTransports)).



snmpEngineID(print) ->
    VarAndValue = [{snmpEngineID, snmpEngineID(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
snmpEngineID(Op) ->
    snmp_generic:variable_func(Op, db(snmpEngineID)).

snmpEngineMaxMessageSize(print) ->
    VarAndValue = [{snmpEngineMaxMessageSize, snmpEngineMaxMessageSize(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
snmpEngineMaxMessageSize(Op) ->
    snmp_generic:variable_func(Op, db(snmpEngineMaxMessageSize)).

snmpEngineBoots(print) ->
    VarAndValue = [{snmpEngineBoots, snmpEngineBoots(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
snmpEngineBoots(Op) ->
    snmp_generic:variable_func(Op, db(snmpEngineBoots)).

snmpEngineTime(print) ->
    VarAndValue = [{snmpEngineTime, snmpEngineTime(get)}],
    snmpa_mib_lib:print_variables(VarAndValue);
snmpEngineTime(get) ->
    {value, get_engine_time()}.

init_engine() ->
    case snmp_generic:variable_get(db(snmpEngineBoots)) of
	{value, Val} when Val < 2147483647 ->
	    snmp_generic:variable_set(db(snmpEngineBoots), Val+1);
	{value, _} ->
	    ok;
	undefined ->
	    snmp_generic:variable_set(db(snmpEngineBoots), 1)
    end,
    reset_engine_base().
    

reset_engine_base() ->
    ets:insert(snmp_agent_table, {snmp_engine_base, snmp_misc:now(sec)}).

get_engine_id() ->
    {value, EngineID} = snmpEngineID(get),
    EngineID.

get_engine_max_message_size() ->
    {value, MPS} = snmpEngineMaxMessageSize(get),
    MPS.

get_engine_time() ->
    [{_, EngineBase}] = ets:lookup(snmp_agent_table, snmp_engine_base),
    snmp_misc:now(sec) - EngineBase.
    
get_engine_boots() ->
    {value, Val} = snmpEngineBoots(get),
    Val.
    
set_engine_boots(Boots) ->
    snmp_generic:variable_func(set, Boots, db(snmpEngineBoots)).
    
set_engine_time(Time) ->
    Base = snmp_misc:now(sec) - Time,
    ets:insert(snmp_agent_table, {snmp_engine_base, Base}).


set_sname() ->
    set_sname(get(sname)).

set_sname(undefined) ->
    put(sname,conf);
set_sname(_) -> %% Keep it, if already set.
    ok.

%% ------------------------------------------------------------------

error(Reason) ->
    throw({error, Reason}).

config_err(F, A) ->
    snmpa_error:config_err("[FRAMEWORK-MIB]: " ++ F, A).

