aboutsummaryrefslogblamecommitdiffstats
path: root/lib/common_test/test/ct_netconfc_SUITE_data/ns.erl
blob: 09217f60a35f90ef8a49fa6607aa38b416101075 (plain) (tree)
1
2
3
4


                                                                      
                                                        















                                                                         
            











                                                                    
                  
                        
                        
                     
                     
                           
                           











































































                                                                      

                                                          




                                                                  

                                                      




                                                                    

                                                   




                                                                     


                                             






                                                                     
                               

                  



                                                    




                                                                    
                                  




















                                                                 





                                                         











                                                                        
                                                     














                                                   
                                                                         


















                                                                   
                                                                      





                                                                  

                                                       


                                            
                                                                   
































                                                                    



















                                                     
             


                                                     


        








                                            
























                                                                    



                                                          




                                                               


                                                                             









































































                                                                        
                                                               







                                                     
                                                             



                                                                       
                                    



                            
                                                         


                                                          
                                                               


                                                              
                                                               



                                                               
                                                                               





























































































                                                                                  




                                                                    
%%--------------------------------------------------------------------
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2012-2013. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%----------------------------------------------------------------------
%% A netconf server used for testing of netconfc
-module(ns).

%-compile(export_all).
-include_lib("common_test/src/ct_netconfc.hrl").


%%%-----------------------------------------------------------------
%%% API
-export([start/1,
	 stop/1,
	 hello/1,
	 hello/2,
	 expect/1,
	 expect/2,
	 expect_reply/2,
	 expect_reply/3,
	 expect_do/2,
	 expect_do/3,
	 expect_do_reply/3,
	 expect_do_reply/4,
	 hupp/1,
	 hupp/2]).

%%%-----------------------------------------------------------------
%%% ssh_channel callbacks
-export([init/1,
	 terminate/2,
	 handle_ssh_msg/2,
	 handle_msg/2]).

%%%-----------------------------------------------------------------
%% Server specifications
-define(SERVER_DATA_NAMESPACE, "ClientTest").
-define(CAPABILITIES,?CAPABILITIES_VSN("1.0")).
-define(CAPABILITIES_VSN(Vsn),
	[
	 ?NETCONF_BASE_CAP ++ Vsn,
	 "urn:ietf:params:netconf:capability:writable-running:1.0",
	 "urn:ietf:params:netconf:capability:candidate:1.0",
	 "urn:ietf:params:netconf:capability:confirmed-commit:1.0",
	 "urn:ietf:params:netconf:capability:rollback-on-error:1.0",
	 "urn:ietf:params:netconf:capability:startup:1.0",
	 "urn:ietf:params:netconf:capability:url:1.0",
	 "urn:ietf:params:netconf:capability:xpath:1.0",
	 "urn:ietf:params:netconf:capability:notification:1.0",
	 "urn:ietf:params:netconf:capability:interleave:1.0",
	 ?ACTION_NAMESPACE,
	 ?SERVER_DATA_NAMESPACE
	]).
-define(SSH_PORT, 2060).
-define(ssh_config(Dir),[{port, ?SSH_PORT},
			 {interface, {127,0,0,1}},
			 {system_dir, Dir},
			 {user_dir, Dir},
			 {user_passwords, [{"xxx","xxx"}]},
			 {password, "global-xxx"}]).

%% Some help for debugging
%-define(dbg(F,A),io:format(F,A)).
-define(dbg(F,A),ok).
-define(dbg_event(Event,Expect),
	?dbg("Event: ~p~nExpected: ~p~n",[Event,Expect])).

%% State
-record(session, {cb,
		  connection,
		  buffer = <<>>,
		  session_id}).


%%%-----------------------------------------------------------------
%%% API

%% Start the netconf server and use the given directory as system_dir
%% and user_dir
start(Dir) ->
    spawn(fun() -> init_server(Dir) end).

%% Stop the netconf server
stop(Pid) ->
    Pid ! {stop,self()},
    receive stopped -> ok end.

%% Set the session id for the hello message.
%% If this is not called prior to starting the session, no hello
%% message will be sent.
%% 'Stuff' indicates some special handling to e.g. provoke error cases
hello(SessionId) ->
    hello(SessionId,undefined).
hello(SessionId,Stuff) ->
    insert(hello,{SessionId,Stuff}).

%% Tell server to expect the given message without doing any further
%% actions. To be called directly before sending a request.
expect(Expect) ->
    expect_do_reply(Expect,undefined,undefined).
expect(SessionId,Expect) ->
    expect_do_reply(SessionId,Expect,undefined,undefined).

%% Tell server to expect the given message and reply with the give
%% reply. To be called directly before sending a request.
expect_reply(Expect,Reply) ->
    expect_do_reply(Expect,undefined,Reply).
expect_reply(SessionId,Expect,Reply) ->
    expect_do_reply(SessionId,Expect,undefined,Reply).

%% Tell server to expect the given message and perform an action. To
%% be called directly before sending a request.
expect_do(Expect,Do) ->
    expect_do_reply(Expect,Do,undefined).
expect_do(SessionId,Expect,Do) ->
    expect_do_reply(SessionId,Expect,Do,undefined).

%% Tell server to expect the given message, perform an action and
%% reply with the given reply. To be called directly before sending a
%% request.
expect_do_reply(Expect,Do,Reply) ->
    add_expect(1,{Expect,Do,Reply}).
expect_do_reply(SessionId,Expect,Do,Reply) ->
    add_expect(SessionId,{Expect,Do,Reply}).

%% Hupp the server - i.e. tell it to do something -
%% e.g. hupp(send_event) will cause send_event(State) to be called on
%% the session channel process.
hupp(send_event) ->
    hupp(send,[make_msg(event)]);
hupp(kill) ->
    hupp(1,fun hupp_kill/1,[]).

hupp(send,Data) ->
    hupp(1,fun hupp_send/2,[Data]).

hupp(SessionId,Fun,Args) when is_function(Fun) ->
    [{_,Pid}] = lookup({channel_process,SessionId}),
    Pid ! {hupp,Fun,Args}.

%%%-----------------------------------------------------------------
%%% Main loop of the netconf server
init_server(Dir) ->
    register(main_ns_proc,self()),
    ets:new(ns_tab,[set,named_table,public]),
    Config = ?ssh_config(Dir),
    {_,Host} = lists:keyfind(interface, 1, Config),
    {_,Port} = lists:keyfind(port, 1, Config),
    Opts = lists:filter(fun({Key,_}) ->
				lists:member(Key,[system_dir,
						  password,
						  user_passwords,
						  pwdfun])
			end,
			Config),
    {ok, Daemon} =
	ssh:daemon(Host, Port,
		   [{subsystems,[{"netconf",{?MODULE,[]}}]}
		    |Opts]),
    loop(Daemon).

loop(Daemon) ->
    receive
	{stop,From} ->
	    ssh:stop_daemon(Daemon),
	    From ! stopped;
	{table_trans,Fun,Args,From} ->
	    %% Simple transaction mechanism for ets table
	    R = apply(Fun,Args),
	    From ! {table_trans_done,R},
	    loop(Daemon)
    end.

%%----------------------------------------------------------------------
%% Behaviour callback functions (ssh_channel)
%%----------------------------------------------------------------------
init([]) ->
    {ok, undefined}.

terminate(_Reason, _State) ->
    ok.

handle_ssh_msg({ssh_cm,CM,{data, Ch, _Type = 0, Data}}, State) ->
    %% io:format("~p~n",[{self(),Data,CM,Ch,State}]),
    data_for_channel(CM, Ch, Data, State);
handle_ssh_msg({ssh_cm,CM,{closed, Ch}}, State)  ->
    %% erlang:display({self(),closed,CM,Ch,State}),
    stop_channel(CM, Ch, State);
handle_ssh_msg({ssh_cm,CM,{eof, Ch}}, State) ->
    %% erlang:display({self(),eof,CM,Ch,State}),
    data_for_channel(CM,Ch, <<>>, State).


handle_msg({'EXIT', _Pid, _Reason}, State) ->
    {ok, State};
handle_msg({ssh_channel_up,Ch,CM},undefined) ->
    %% erlang:display({self(),up,CM,Ch}),
    ConnRef = {CM,Ch},
    SessionId = maybe_hello(ConnRef),
    insert({channel_process,SessionId},self()), % used to hupp the server
    {ok, #session{connection = ConnRef,
		  session_id = SessionId}};
handle_msg({hupp,Fun,Args},State) ->
    {ok,apply(Fun,Args ++ [State])}.

data_for_channel(CM, Ch, Data, State) ->
    try data(Data, State) of
	{ok, NewState} ->
	    case erase(stop) of
		true ->
		    stop_channel(CM, Ch, NewState);
		_ ->
		    {ok, NewState}
	    end
    catch
	Class:Reason ->
	    Stacktrace = erlang:get_stacktrace(),
	    error_logger:error_report([{?MODULE, data_for_channel},
				       {request, Data},
				       {buffer, State#session.buffer},
				       {reason, {Class, Reason}},
				       {stacktrace, Stacktrace}]),
	    stop_channel(CM, Ch, State)
    end.

data(Data, State = #session{connection = ConnRef,
			    buffer = Buffer,
			    session_id = SessionId}) ->
    AllData = <<Buffer/binary,Data/binary>>,
    case find_endtag(AllData) of
	{ok,Msgs,Rest} ->
	    [check_expected(SessionId,ConnRef,Msg) || Msg <- Msgs],
	    {ok,State#session{buffer=Rest}};
	need_more ->
	    {ok,State#session{buffer=AllData}}
    end.

stop_channel(CM, Ch, State) ->
    ssh:close(CM),
    {stop, Ch, State}.


%%%-----------------------------------------------------------------
%%% Functions to trigg via hupp/1:

%% Send data spontaneously - e.g. an event
hupp_send(Data,State = #session{connection = ConnRef}) ->
    send(ConnRef,Data),
    State.
hupp_kill(State = #session{connection = ConnRef}) ->
    kill(ConnRef),
    State.

%%%-----------------------------------------------------------------
%%% Internal functions


%%% Send ssh data to the client
send({CM,Ch},Data) ->
    ssh_connection:send(CM, Ch, Data).

%%% Kill ssh connection
kill({CM,_Ch}) ->
    ssh:close(CM).

add_expect(SessionId,Add) ->
    table_trans(fun do_add_expect/2,[SessionId,Add]).

table_trans(Fun,Args) ->
    S = self(),
    case whereis(main_ns_proc) of
	S ->
	    apply(Fun,Args);
	Pid ->
	    Pid ! {table_trans,Fun,Args,self()},
	    receive
		{table_trans_done,Result} ->
		    Result
	    after 5000 ->
		    exit(table_trans_timeout)
	    end
    end.

do_add_expect(SessionId,Add) ->
    case lookup({expect,SessionId}) of
	[] ->
	    insert({expect,SessionId},[Add]);
	[{_,First}] ->
	    insert({expect,SessionId},First ++ [Add])
    end,
    ok.

do_get_expect(SessionId) ->
    case lookup({expect,SessionId}) of
	[{_,[{Expect,Do,Reply}|Rest]}] ->
	    insert({expect,SessionId},Rest),
	    {Expect,Do,Reply};
	_ ->
	    error
    end.

insert(Key,Value) ->
    ets:insert(ns_tab,{Key,Value}).
lookup(Key) ->
    ets:lookup(ns_tab,Key).

maybe_hello(ConnRef) ->
    case lookup(hello) of
	[{hello,{SessionId,Stuff}}] ->
	    %% erlang:display({SessionId,Stuff}),
	    ets:delete(ns_tab,hello),
	    insert({session,SessionId},ConnRef),
	    reply(ConnRef,{hello,SessionId,Stuff}),
	    SessionId;
	[] ->
	    undefined
    end.

find_endtag(Data) ->
    case binary:split(Data,[?END_TAG],[global]) of
	[Data] ->
	    need_more;
	Msgs ->
	    {ok,lists:sublist(Msgs,length(Msgs)-1),lists:last(Msgs)}
    end.

check_expected(SessionId,ConnRef,Msg) ->
    %% io:format("~p~n",[{check_expected,SessionId,Msg}]),
    case table_trans(fun do_get_expect/1,[SessionId]) of
	{Expect,Do,Reply} ->
	    %% erlang:display({got,io_lib:format("~s",[Msg])}),
	    %% erlang:display({expected,Expect}),
	    match(Msg,Expect),
	    do(ConnRef, Do),
	    reply(ConnRef,Reply);
	error ->
	    timer:sleep(1000),
	    exit({error,{got_unexpected,SessionId,Msg,ets:tab2list(ns_tab)}})
    end.

match(Msg,Expect) ->
    ?dbg("Match: ~p~n",[Msg]),
    {ok,ok,<<>>} = xmerl_sax_parser:stream(Msg,[{event_fun,fun event/3},
						{event_state,Expect}]).

event(Event,_Loc,Expect) ->
    ?dbg_event(Event,Expect),
    event(Event,Expect).

event(startDocument,Expect) -> match(Expect);
event({startElement,_,Name,_,Attrs},[{se,Name}|Match]) ->
    msg_id(Name,Attrs),
    Match;
event({startElement,_,Name,_,Attrs},[ignore,{se,Name}|Match]) ->
    msg_id(Name,Attrs),
    Match;
event({startElement,_,Name,_,Attrs},[{se,Name,As}|Match]) ->
    msg_id(Name,Attrs),
    match_attrs(Name,As,Attrs),
    Match;
event({startElement,_,Name,_,Attrs},[ignore,{se,Name,As}|Match]) ->
    msg_id(Name,Attrs),
    match_attrs(Name,As,Attrs),
    Match;
event({startPrefixMapping,_,Ns},[{ns,Ns}|Match]) -> Match;
event({startPrefixMapping,_,Ns},[ignore,{ns,Ns}|Match]) -> Match;
event({endPrefixMapping,_},Match) -> Match;
event({endElement,_,Name,_},[{ee,Name}|Match]) -> Match;
event({endElement,_,Name,_},[ignore,{ee,Name}|Match]) -> Match;
event(endDocument,Match) when Match==[]; Match==[ignore] -> ok;
event(_,[ignore|_]=Match) -> Match;
event(Event,Match) -> throw({nomatch,{Event,Match}}).

msg_id("rpc",Attrs) ->
    case lists:keyfind("message-id",3,Attrs) of
	{_,_,_,Str} -> put(msg_id,Str);
	false -> erase(msg_id)
    end;
msg_id(_,_) ->
    ok.

match_attrs(Name,[{Key,Value}|As],Attrs) ->
    case lists:keyfind(atom_to_list(Key),3,Attrs) of
	{_,_,_,Value} -> match_attrs(Name,As,Attrs);
	false -> throw({missing_attr,Key,Name,Attrs});
	_ -> throw({faulty_attr_value,Key,Name,Attrs})
    end;
match_attrs(_,[],_) ->
    ok.

do(ConnRef, close) ->
    ets:match_delete(ns_tab,{{session,'_'},ConnRef}),
    put(stop,true);
do(_ConnRef, {kill,SessionId}) ->
    case lookup({session,SessionId}) of
	[{_,Owner}] ->
	    ets:delete(ns_tab,{session,SessionId}),
	    kill(Owner);
	_ ->
	    exit({no_session_to_kill,SessionId})
    end;
do(_, undefined) ->
    ok.

reply(_,undefined) ->
    ?dbg("no reply~n",[]),
    ok;
reply(ConnRef,Reply) ->
    ?dbg("Reply: ~p~n",[Reply]),
    send(ConnRef, make_msg(Reply)).

from_simple(Simple) ->
    unicode_c2b(xmerl:export_simple_element(Simple,xmerl_xml)).

xml(Content) ->
    <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
      Content/binary,"\n",?END_TAG/binary>>.

rpc_reply(Content) when is_binary(Content) ->
    MsgId = case erase(msg_id) of
		undefined -> <<>>;
		Id -> unicode_c2b([" message-id=\"",Id,"\""])
	    end,
    <<"<rpc-reply xmlns=\"",?NETCONF_NAMESPACE,"\"",MsgId/binary,">\n",
      Content/binary,"\n</rpc-reply>">>;
rpc_reply(Content) ->
    rpc_reply(unicode_c2b(Content)).

session_id(no_session_id) ->
    <<>>;
session_id(SessionId0) ->
    SessionId = unicode_c2b(integer_to_list(SessionId0)),
    <<"<session-id>",SessionId/binary,"</session-id>\n">>.

capabilities(undefined) ->
    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"]
			      || C <- ?CAPABILITIES]),
    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
capabilities({base,Vsn}) ->
    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"]
			      || C <- ?CAPABILITIES_VSN(Vsn)]),
    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
capabilities(no_base) ->
    [_|Caps] = ?CAPABILITIES,
    CapsXml = unicode_c2b([["<capability>",C,"</capability>\n"] || C <- Caps]),
    <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>;
capabilities(no_caps) ->
    <<>>.

%%%-----------------------------------------------------------------
%%% Match received netconf message from the client.  Add a new clause
%%% for each new message to recognize. The clause argument shall match
%%% the Expect argument in expect/1, expect_reply/2 or
%%% expect_do_reply/3.
%%%
%%% match(term()) -> [Match].
%%% Match = ignore | {se,Name} | {se,Name,Attrs} | {ee,Name} | {ns,Namespace}
%%% Name = string()
%%% Attrs = [{atom(),string()}]
%%% Namespace = string()
%%%
%%% 'se' means start element, 'ee' means end element - i.e. to match
%%% an XML element you need one 'se' entry and one 'ee' entry with the
%%% same name in the match list.
match(hello) ->
    [ignore,{se,"hello"},ignore,{ee,"hello"},ignore];
match('close-session') ->
    [ignore,{se,"rpc"},{se,"close-session"},
     {ee,"close-session"},{ee,"rpc"},ignore];
match('edit-config') ->
    [ignore,{se,"rpc"},{se,"edit-config"},{se,"target"},ignore,{ee,"target"},
     {se,"config"},ignore,{ee,"config"},{ee,"edit-config"},{ee,"rpc"},ignore];
match('get') ->
    match({get,subtree});
match({'get',FilterType}) ->
    [ignore,{se,"rpc"},{se,"get"},{se,"filter",[{type,atom_to_list(FilterType)}]},
     ignore,{ee,"filter"},{ee,"get"},{ee,"rpc"},ignore];
match('get-config') ->
    match({'get-config',subtree});
match({'get-config',FilterType}) ->
    [ignore,{se,"rpc"},{se,"get-config"},{se,"source"},ignore,{ee,"source"},
     {se,"filter",[{type,atom_to_list(FilterType)}]},ignore,{ee,"filter"},
     {ee,"get-config"},{ee,"rpc"},ignore];
match('copy-config') ->
    [ignore,{se,"rpc"},{se,"copy-config"},{se,"target"},ignore,{ee,"target"},
     {se,"source"},ignore,{ee,"source"},{ee,"copy-config"},{ee,"rpc"},ignore];
match('delete-config') ->
    [ignore,{se,"rpc"},{se,"delete-config"},{se,"target"},ignore,{ee,"target"},
     {ee,"delete-config"},{ee,"rpc"},ignore];
match('lock') ->
    [ignore,{se,"rpc"},{se,"lock"},{se,"target"},ignore,{ee,"target"},
     {ee,"lock"},{ee,"rpc"},ignore];
match('unlock') ->
    [ignore,{se,"rpc"},{se,"unlock"},{se,"target"},ignore,{ee,"target"},
     {ee,"unlock"},{ee,"rpc"},ignore];
match('kill-session') ->
    [ignore,{se,"rpc"},{se,"kill-session"},{se,"session-id"},ignore,
     {ee,"session-id"},{ee,"kill-session"},{ee,"rpc"},ignore];
match(action) ->
    [ignore,{se,"rpc"},{ns,?ACTION_NAMESPACE},{se,"action"},{se,"data"},ignore,
     {ee,"data"},{ee,"action"},{ee,"rpc"},ignore];
match({'create-subscription',Content}) ->
    [ignore,{se,"rpc"},{ns,?NETCONF_NOTIF_NAMESPACE},
     {se,"create-subscription"}] ++
	lists:flatmap(fun(X) ->
			      [{se,atom_to_list(X)},ignore,{ee,atom_to_list(X)}]
		      end, Content) ++
	[{ee,"create-subscription"},{ee,"rpc"},ignore];
match(any) ->
    [ignore].



%%%-----------------------------------------------------------------
%%% Make message to send to the client.
%%% Add a new clause for each new message that shall be sent. The
%%% clause shall match the Reply argument in expect_reply/2 or
%%% expect_do_reply/3.
make_msg({hello,SessionId,Stuff}) ->
    SessionIdXml = session_id(SessionId),
    CapsXml = capabilities(Stuff),
    xml(<<"<hello xmlns=\"",?NETCONF_NAMESPACE,"\">\n",CapsXml/binary,
	  SessionIdXml/binary,"</hello>">>);
make_msg(ok) ->
    xml(rpc_reply("<ok/>"));
make_msg({data,Data}) ->
    xml(rpc_reply(from_simple({data,Data})));
make_msg(event) ->
    xml(<<"<notification xmlns=\"",?NETCONF_NOTIF_NAMESPACE,"\">"
	  "<eventTime>2012-06-14T14:50:54+02:00</eventTime>"
	  "<event xmlns=\"http://my.namespaces.com/event\">"
	  "<severity>major</severity>"
	  "<description>Something terrible happened</description>"
	  "</event>"
	  "</notification>">>);
make_msg(Xml) when is_binary(Xml) ->
    xml(Xml);
make_msg(Simple) when is_tuple(Simple) ->
    xml(from_simple(Simple)).

%%%-----------------------------------------------------------------
%%% Convert to unicode binary, since we use UTF-8 encoding in XML
unicode_c2b(Characters) ->
    unicode:characters_to_binary(Characters).