1%%---------------------------------------------------------------------- 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2012-2021. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20%%---------------------------------------------------------------------- 21%% File: ct_netconfc.erl 22%% 23%% Description: 24%% This file contains the Netconf client interface 25%% 26%% Netconf servers can be configured by adding the following statement 27%% to a configuration file: 28%% 29%% {server_id(), [option()]}. 30%% 31%% The server_id() or an associated ct:target_name() shall then be 32%% used in calls to open/2 connect/2. 33%% 34%% If no configuration exists for a server, use open/1 and connect/1. 35%% 36%% == Logging == 37%% 38%% The netconf server uses the `error_logger' for logging of netconf 39%% traffic. A special purpose error handler is implemented in 40%% `ct_conn_log_h'. To use this error handler, add the `cth_conn_log' 41%% hook in your test suite, e.g. 42%% 43%% suite() -> 44%% [{ct_hooks, [{cth_conn_log, [{ct:conn_log_mod(),ct:conn_log_options()}]}]}]. 45%% 46%% For example: 47%% 48%% suite() -> 49%% [{ct_hooks, 50%% [{cth_conn_log,[{ct_netconfc,[{log_type,pretty}, 51%% {hosts,[my_configured_server]}]}]} 52%% 53%% == Notifications == 54%% 55%% The netconf client is also compliant with RFC5277 NETCONF Event 56%% Notifications, which defines a mechanism for an asynchronous 57%% message notification delivery service for the netconf protocol. 58%% Functions supporting this are create_subscription/3 59%% get_event_streams/3. 60%% 61%%---------------------------------------------------------------------- 62-module(ct_netconfc). 63 64-dialyzer(no_improper_lists). 65 66-include("ct_netconfc.hrl"). 67-include("ct_util.hrl"). 68-include_lib("xmerl/include/xmerl.hrl"). 69 70%%---------------------------------------------------------------------- 71%% External exports 72%%---------------------------------------------------------------------- 73-export([connect/1, 74 connect/2, 75 disconnect/1, 76 session/1, 77 session/2, 78 session/3, 79 open/1, 80 open/2, 81 only_open/1, 82 only_open/2, 83 hello/1, 84 hello/2, 85 hello/3, 86 close_session/1, 87 close_session/2, 88 kill_session/2, 89 kill_session/3, 90 send/2, 91 send/3, 92 send_rpc/2, 93 send_rpc/3, 94 lock/2, 95 lock/3, 96 unlock/2, 97 unlock/3, 98 get/2, 99 get/3, 100 get_config/3, 101 get_config/4, 102 edit_config/3, 103 edit_config/4, 104 edit_config/5, 105 delete_config/2, 106 delete_config/3, 107 copy_config/3, 108 copy_config/4, 109 action/2, 110 action/3, 111 create_subscription/2, 112 create_subscription/3, 113 get_event_streams/1, 114 get_event_streams/2, 115 get_event_streams/3, 116 get_capabilities/1, 117 get_capabilities/2, 118 get_session_id/1, 119 get_session_id/2]). 120 121%% historic, no longer documented 122-export([create_subscription/1, 123 create_subscription/4, 124 create_subscription/5, 125 create_subscription/6]). 126 127%%---------------------------------------------------------------------- 128%% Exported types 129%%---------------------------------------------------------------------- 130-export_type([client/0, 131 handle/0, 132 notification/0]). 133 134%%---------------------------------------------------------------------- 135%% Internal exports 136%%---------------------------------------------------------------------- 137%% ct_gen_conn callbacks 138-export([init/3, 139 handle_msg/3, 140 handle_msg/2, 141 terminate/2, 142 close/1]). 143 144%% ct_conn_log callback 145-export([format_data/2]). 146 147%%---------------------------------------------------------------------- 148%% Internal defines 149%%---------------------------------------------------------------------- 150-define(APPLICATION,?MODULE). 151-define(DEFAULT_STREAM,"NETCONF"). 152 153-define(error(ConnName,Report), 154 error_logger:error_report([{ct_connection,ConnName}, 155 {client,self()}, 156 {module,?MODULE}, 157 {line,?LINE} | 158 Report])). 159 160-define(is_timeout(T), (is_integer(T) orelse T==infinity)). 161-define(is_filter(F), 162 (?is_simple_xml(F) 163 orelse (F==[]) 164 orelse (is_list(F) andalso ?is_simple_xml(hd(F))))). 165-define(is_simple_xml(Xml), 166 (is_atom(Xml) orelse (is_tuple(Xml) andalso is_atom(element(1,Xml))))). 167-define(is_string(S), (is_list(S) andalso is_integer(hd(S)))). 168 169%% Keys into the process dictionary. 170-define(KEY(T), {?MODULE, T}). 171 172%%---------------------------------------------------------------------- 173%% Records 174%%---------------------------------------------------------------------- 175%% Client state 176-record(state, {host, 177 port, 178 connection, % #connection 179 capabilities, 180 session_id, 181 msg_id = 1, 182 hello_status, % undefined | received | #pending{} 183 % string() | {error, Reason} 184 buf = false, % binary() | list() | boolean() 185 pending = [], % [#pending] 186 receivers = [] :: list() | pid()}).% notification destinations 187 188%% Run-time client options. 189-record(options, {ssh = [], % Options for the ssh application 190 host, 191 port = ?DEFAULT_PORT, 192 timeout = ?DEFAULT_TIMEOUT, 193 receivers = [], 194 name, 195 type}). 196 197%% Connection reference 198-record(connection, {reference, % {CM,Ch} 199 host, 200 port, 201 name, 202 type}). 203 204%% Pending replies from server 205-record(pending, {tref :: false | reference(), % timer reference 206 msg_id, 207 op, 208 caller}).% pid which sent the request 209 210%%---------------------------------------------------------------------- 211%% Type declarations 212%%---------------------------------------------------------------------- 213-type client() :: handle() | server_id() | ct:target_name(). 214-opaque handle() :: pid(). 215 216-type option() :: {host | ssh, host()} 217 | {port, inet:port_number()} 218 | {timeout, timeout()} 219 | {capability, string() | [string()]} 220 | {receiver, term()} 221 | ssh:client_option(). 222 223-type session_option() :: {timeout,timeout()} 224 | {receiver, term()} 225 | {capability, string() | [string()]}. 226 227-type host() :: inet:hostname() | inet:ip_address(). 228 229-type notification() :: {notification, xml_attributes(), [simple_xml()]}. 230 231-type stream_name() :: string(). 232-type streams() :: [{stream_name(),[stream_data()]}]. 233-type stream_data() :: {description,string()} | 234 {replaySupport,string()} | 235 {replayLogCreationTime,string()} | 236 {replayLogAgedTime,string()}. 237%% See XML Schema for Event Notifications found in RFC5277 for further 238%% detail about the data format for the string values. 239 240-type error_reason() :: term(). 241 242-type server_id() :: atom(). 243 244-type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} | 245 {xml_tag(), xml_content()} | 246 xml_tag(). 247-type xml_tag() :: atom(). 248-type xml_attributes() :: [{xml_attribute_tag(),xml_attribute_value()}]. 249-type xml_attribute_tag() :: atom(). 250-type xml_attribute_value() :: string(). 251-type xml_content() :: [simple_xml() | iolist()]. 252-type xpath() :: {xpath,string()}. 253 254-type netconf_db() :: running | startup | candidate. 255-type xs_datetime() :: string(). 256%% This date and time identifyer has the same format as the XML type 257%% dateTime and compliant to RFC3339. The format is 258%% "[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]" 259 260%%---------------------------------------------------------------------- 261%% External interface functions 262%%---------------------------------------------------------------------- 263 264%%---------------------------------------------------------------------- 265%% Open an SSH connection to a Netconf server 266%% If the server options are specified in a configuration file, use 267%% open/2. 268 269%% connect/1 270 271-spec connect(Options) -> Result when 272 Options :: [option()], 273 Result :: {ok, handle()} | {error, error_reason()}. 274connect(Options) -> 275 connect(Options, #options{type = connection}, []). 276 277%% connect/2 278 279-spec connect(KeyOrName, ExtraOptions) -> Result when 280 KeyOrName :: ct:key_or_name(), 281 ExtraOptions :: [option()], 282 Result :: {ok, handle()} | {error, error_reason()}. 283 284connect(KeyOrName, ExtraOptions) -> 285 connect(make_opts(KeyOrName, ExtraOptions), 286 #options{name = KeyOrName, type = connection}, 287 [{name, KeyOrName}]). 288 289%% connect/3 290 291connect(Opts, InitRec, NameOpt) -> 292 case make_options(Opts, InitRec) of 293 #options{} = Rec -> 294 start(Rec, NameOpt, false); 295 {error, _} = No -> 296 No 297 end. 298 299%% make_opts/2 300 301make_opts(KeyOrName, ExtraOptions) -> 302 SortedExtra = lists:keysort(1, ExtraOptions), 303 SortedConfig = lists:keysort(1, ct:get_config(KeyOrName, [])), 304 lists:ukeymerge(1, SortedConfig, SortedExtra). 305 306%%---------------------------------------------------------------------- 307%% Close the given SSH connection. 308-spec disconnect(Conn) -> ok | {error,error_reason()} when 309 Conn :: handle(). 310disconnect(Conn) -> 311 case call(Conn,get_ssh_connection) of 312 {ok,_} -> 313 ct_gen_conn:stop(Conn); 314 Error -> 315 Error 316 end. 317 318%%---------------------------------------------------------------------- 319%% Open a netconf session as a channel on the given SSH connection, 320%% and exchange `hello' messages. 321 322%% session/1 323 324-spec session(Conn) -> Result when 325 Conn :: handle(), 326 Result :: {ok, handle()} | {error, error_reason()}. 327 328session(Conn) -> 329 session(Conn, [], #options{type = channel}, []). 330 331%% session/2 332 333-spec session(Conn, Options) -> Result when 334 Conn :: handle(), 335 Options :: [session_option()], 336 Result :: {ok, handle()} | {error, error_reason()}; 337 (KeyOrName, Conn) -> Result when 338 KeyOrName :: ct:key_or_name(), 339 Conn :: handle(), 340 Result :: {ok, handle()} | {error, error_reason()}. 341 342session(Conn, Options) when is_list(Options) -> 343 session(Conn, Options, #options{type = channel}, []); 344 345session(KeyOrName, Conn) -> 346 session(Conn, 347 [], 348 #options{name = KeyOrName, type = channel}, 349 [{name, KeyOrName}]). 350 351%% session/3 352 353-spec session(KeyOrName, Conn, Options) -> Result when 354 Conn :: handle(), 355 Options :: [session_option()], 356 KeyOrName :: ct:key_or_name(), 357 Result :: {ok, handle()} | {error, error_reason()}. 358 359session(KeyOrName, Conn, ExtraOptions) -> 360 session(Conn, 361 make_opts(KeyOrName, ExtraOptions), 362 #options{name = KeyOrName, type = channel}, 363 [{name, KeyOrName}]). 364 365%% session/4 366 367session(Conn, Opts, InitRec, NameOpt) -> 368 T = make_ref(), 369 try 370 [_ | {ok, SshConn}] = [T | call(Conn, get_ssh_connection)], 371 [_ | #options{} = Rec] = [T | make_session_options(Opts, InitRec)], 372 [_ | {ok, Client} = Ok] = [T | start(SshConn, Rec, NameOpt, true)], 373 [_ | ok] = [T | hello(Client, caps(Opts), Rec#options.timeout)], 374 Ok 375 catch 376 error: {badmatch, [T | Error]} -> 377 Error 378 end. 379 380%% caps/1 381 382caps(Opts) -> 383 [T || {capability, _} = T <- Opts]. 384 385%%---------------------------------------------------------------------- 386%% Open a netconf session and exchange 'hello' messages. 387%% If the server options are specified in a configuration file, use 388%% open/2. 389 390%% open/1 391 392-spec open(Options) -> Result when 393 Options :: [option()], 394 Result :: {ok, handle()} | {error, error_reason()}. 395 396open(Options) -> 397 open(Options, 398 #options{type = connection_and_channel}, 399 [], 400 true). 401 402-spec open(KeyOrName, ExtraOption) -> Result when 403 KeyOrName :: ct:key_or_name(), 404 ExtraOption :: [option()], 405 Result :: {ok, handle()} | {error, error_reason()}. 406 407open(KeyOrName, ExtraOpts) -> 408 open(KeyOrName, ExtraOpts, true). 409 410%% open/3 411 412open(KeyOrName, ExtraOptions, Hello) -> 413 open(make_opts(KeyOrName, ExtraOptions), 414 #options{name = KeyOrName, type = connection_and_channel}, 415 [{name, KeyOrName}], 416 Hello). 417 418%% open/4 419 420open(Opts, InitRec, NameOpt, Hello) -> 421 T = make_ref(), 422 try 423 [_, #options{} = Rec] = [T, make_options(Opts, InitRec)], 424 [_, {ok, Client} = Ok | true] = [T, start(Rec, NameOpt, true) | Hello], 425 [_, ok] = [T, hello(Client, caps(Opts), Rec#options.timeout)], 426 Ok 427 catch 428 error: {badmatch, [T, Res | _]} -> 429 Res 430 end. 431 432%% start/3 433 434start(#options{host = undefined}, _, _) -> 435 {error, no_host_address}; 436 437start(#options{port = undefined}, _, _) -> 438 {error, no_port}; 439 440start(#options{host = Host, port = Port} = Opts, NameOpt, Fwd) -> 441 start({Host, Port}, Opts, NameOpt, Fwd). 442 443%% start/4 444 445start(Ep, Opts, NameOpt, Fwd) -> 446 ct_gen_conn:start(Ep, Opts, ?MODULE, [{reconnect, false}, 447 {use_existing_connection, false}, 448 {forward_messages, Fwd} 449 | NameOpt]). 450 451%%---------------------------------------------------------------------- 452%% Like open/1,2, but no 'hello' message is sent. 453 454-spec only_open(Options) -> Result when 455 Options :: [option()], 456 Result :: {ok, handle()} | {error, error_reason()}. 457 458only_open(Options) -> 459 open(Options, #options{type = connection_and_channel}, [], false). 460 461-spec only_open(KeyOrName, ExtraOptions) -> Result when 462 KeyOrName :: ct:key_or_name(), 463 ExtraOptions :: [option()], 464 Result :: {ok, handle()} | {error, error_reason()}. 465 466only_open(KeyOrName, ExtraOpts) -> 467 open(KeyOrName, ExtraOpts, false). 468 469%%---------------------------------------------------------------------- 470%% Send a 'hello' message. 471 472%% hello/1 473 474-spec hello(Client) -> Result when 475 Client :: handle(), 476 Result :: ok | {error, error_reason()}. 477 478hello(Client) -> 479 hello(Client, [], ?DEFAULT_TIMEOUT). 480 481%% hello/2 482 483-spec hello(Client, Timeout) -> Result when 484 Client :: handle(), 485 Timeout :: timeout(), 486 Result :: ok | {error, error_reason()}. 487 488hello(Client, Timeout) -> 489 hello(Client, [], Timeout). 490 491%% hello/3 492 493-spec hello(Client, Options, Timeout) -> Result when 494 Client :: handle(), 495 Options :: [{capability, [string()]}], 496 Timeout :: timeout(), 497 Result :: ok | {error, error_reason()}. 498 499hello(Client, Options, Timeout) -> 500 call(Client, {hello, Options, Timeout}). 501 502 503%%---------------------------------------------------------------------- 504%% Get the session id for the session specified by Client. 505-spec get_session_id(Client) -> Result when 506 Client :: client(), 507 Result :: pos_integer() | {error,error_reason()}. 508get_session_id(Client) -> 509 get_session_id(Client, ?DEFAULT_TIMEOUT). 510 511-spec get_session_id(Client, Timeout) -> Result when 512 Client :: client(), 513 Timeout :: timeout(), 514 Result :: pos_integer() | {error,error_reason()}. 515get_session_id(Client, Timeout) -> 516 call(Client, get_session_id, Timeout). 517 518%%---------------------------------------------------------------------- 519%% Get the server side capabilities. 520-spec get_capabilities(Client) -> Result when 521 Client :: client(), 522 Result :: [string()] | {error,error_reason()}. 523get_capabilities(Client) -> 524 get_capabilities(Client, ?DEFAULT_TIMEOUT). 525 526-spec get_capabilities(Client, Timeout) -> Result when 527 Client :: client(), 528 Timeout :: timeout(), 529 Result :: [string()] | {error,error_reason()}. 530get_capabilities(Client, Timeout) -> 531 call(Client, get_capabilities, Timeout). 532 533%%---------------------------------------------------------------------- 534%% Send an XML document to the server. 535-spec send(Client, SimpleXml) -> Result when 536 Client :: client(), 537 SimpleXml :: simple_xml(), 538 Result :: simple_xml() | {error,error_reason()}. 539send(Client, SimpleXml) -> 540 send(Client, SimpleXml, ?DEFAULT_TIMEOUT). 541 542-spec send(Client, SimpleXml, Timeout) -> Result when 543 Client :: client(), 544 SimpleXml :: simple_xml(), 545 Timeout :: timeout(), 546 Result :: simple_xml() | {error,error_reason()}. 547send(Client, SimpleXml, Timeout) -> 548 call(Client,{send, Timeout, SimpleXml}). 549 550%%---------------------------------------------------------------------- 551%% Wrap the given XML document in a valid netconf 'rpc' request and 552%% send to the server. 553-spec send_rpc(Client, SimpleXml) -> Result when 554 Client :: client(), 555 SimpleXml :: simple_xml(), 556 Result :: [simple_xml()] | {error,error_reason()}. 557send_rpc(Client, SimpleXml) -> 558 send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT). 559 560-spec send_rpc(Client, SimpleXml, Timeout) -> Result when 561 Client :: client(), 562 SimpleXml :: simple_xml(), 563 Timeout :: timeout(), 564 Result :: [simple_xml()] | {error,error_reason()}. 565send_rpc(Client, SimpleXml, Timeout) -> 566 call(Client,{send_rpc, SimpleXml, Timeout}). 567 568%%---------------------------------------------------------------------- 569%% Send a 'lock' request. 570-spec lock(Client, Target) -> Result when 571 Client :: client(), 572 Target :: netconf_db(), 573 Result :: ok | {error,error_reason()}. 574lock(Client, Target) -> 575 lock(Client, Target,?DEFAULT_TIMEOUT). 576 577-spec lock(Client, Target, Timeout) -> Result when 578 Client :: client(), 579 Target :: netconf_db(), 580 Timeout :: timeout(), 581 Result :: ok | {error,error_reason()}. 582lock(Client, Target, Timeout) -> 583 call(Client,{send_rpc_op,lock,[Target],Timeout}). 584 585%%---------------------------------------------------------------------- 586%% Send a 'unlock' request. 587-spec unlock(Client, Target) -> Result when 588 Client :: client(), 589 Target :: netconf_db(), 590 Result :: ok | {error,error_reason()}. 591unlock(Client, Target) -> 592 unlock(Client, Target,?DEFAULT_TIMEOUT). 593 594-spec unlock(Client, Target, Timeout) -> Result when 595 Client :: client(), 596 Target :: netconf_db(), 597 Timeout :: timeout(), 598 Result :: ok | {error,error_reason()}. 599unlock(Client, Target, Timeout) -> 600 call(Client, {send_rpc_op, unlock, [Target], Timeout}). 601 602%%---------------------------------------------------------------------- 603%% Send a 'get' request. 604-spec get(Client, Filter) -> Result when 605 Client :: client(), 606 Filter :: simple_xml() | xpath(), 607 Result :: {ok,[simple_xml()]} | {error,error_reason()}. 608get(Client, Filter) -> 609 get(Client, Filter, ?DEFAULT_TIMEOUT). 610 611-spec get(Client, Filter, Timeout) -> Result when 612 Client :: client(), 613 Filter :: simple_xml() | xpath(), 614 Timeout :: timeout(), 615 Result :: {ok,[simple_xml()]} | {error,error_reason()}. 616get(Client, Filter, Timeout) -> 617 call(Client,{send_rpc_op, get, [Filter], Timeout}). 618 619%%---------------------------------------------------------------------- 620%% Send a 'get-config' request. 621-spec get_config(Client, Source, Filter) -> Result when 622 Client :: client(), 623 Source :: netconf_db(), 624 Filter :: simple_xml() | xpath(), 625 Result :: {ok,[simple_xml()]} | {error,error_reason()}. 626get_config(Client, Source, Filter) -> 627 get_config(Client, Source, Filter, ?DEFAULT_TIMEOUT). 628 629-spec get_config(Client, Source, Filter, Timeout) -> Result when 630 Client :: client(), 631 Source :: netconf_db(), 632 Filter :: simple_xml() | xpath(), 633 Timeout :: timeout(), 634 Result :: {ok,[simple_xml()]} | {error,error_reason()}. 635get_config(Client, Source, Filter, Timeout) -> 636 call(Client, {send_rpc_op, get_config, [Source, Filter], Timeout}). 637 638%%---------------------------------------------------------------------- 639%% Send a 'edit-config' request. 640-spec edit_config(Client, Target, Config) -> Result when 641 Client :: client(), 642 Target :: netconf_db(), 643 Config :: simple_xml() | [simple_xml()], 644 Result :: ok | {error,error_reason()}. 645edit_config(Client, Target, Config) -> 646 edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT). 647 648-spec edit_config(Client, Target, Config, OptParams) -> Result when 649 Client :: client(), 650 Target :: netconf_db(), 651 Config :: simple_xml() | [simple_xml()], 652 OptParams :: [simple_xml()], 653 Result :: ok | {error,error_reason()}; 654 (Client, Target, Config, Timeout) -> Result when 655 Client :: client(), 656 Target :: netconf_db(), 657 Config :: simple_xml(), 658 Timeout :: timeout(), 659 Result :: ok | {error,error_reason()}. 660edit_config(Client, Target, Config, Timeout) when ?is_timeout(Timeout) -> 661 edit_config(Client, Target, Config, [], Timeout); 662edit_config(Client, Target, Config, OptParams) when is_list(OptParams) -> 663 edit_config(Client, Target, Config, OptParams, ?DEFAULT_TIMEOUT). 664 665-spec edit_config(Client, Target, Config, OptParams, Timeout) -> Result when 666 Client :: client(), 667 Target :: netconf_db(), 668 Config :: simple_xml() | [simple_xml()], 669 OptParams :: [simple_xml()], 670 Timeout :: timeout(), 671 Result :: ok | {error,error_reason()}. 672edit_config(Client, Target, Config, OptParams, Timeout) when not is_list(Config)-> 673 edit_config(Client, Target, [Config], OptParams, Timeout); 674edit_config(Client, Target, Config, OptParams, Timeout) -> 675 call(Client, {send_rpc_op, edit_config, [Target,Config,OptParams], Timeout}). 676 677 678%%---------------------------------------------------------------------- 679%% Send a 'delete-config' request. 680-spec delete_config(Client, Target) -> Result when 681 Client :: client(), 682 Target :: startup | candidate, 683 Result :: ok | {error,error_reason()}. 684delete_config(Client, Target) -> 685 delete_config(Client, Target, ?DEFAULT_TIMEOUT). 686 687-spec delete_config(Client, Target, Timeout) -> Result when 688 Client :: client(), 689 Target :: startup | candidate, 690 Timeout :: timeout(), 691 Result :: ok | {error,error_reason()}. 692delete_config(Client, Target, Timeout) when Target == startup; 693 Target == candidate -> 694 call(Client,{send_rpc_op, delete_config, [Target], Timeout}). 695 696%%---------------------------------------------------------------------- 697%% Send a 'copy-config' request. 698-spec copy_config(Client, Target, Source) -> Result when 699 Client :: client(), 700 Target :: netconf_db(), 701 Source :: netconf_db(), 702 Result :: ok | {error,error_reason()}. 703copy_config(Client, Source, Target) -> 704 copy_config(Client, Source, Target, ?DEFAULT_TIMEOUT). 705 706-spec copy_config(Client, Target, Source, Timeout) -> Result when 707 Client :: client(), 708 Target :: netconf_db(), 709 Source :: netconf_db(), 710 Timeout :: timeout(), 711 Result :: ok | {error,error_reason()}. 712copy_config(Client, Target, Source, Timeout) -> 713 call(Client,{send_rpc_op, copy_config, [Target, Source], Timeout}). 714 715%%---------------------------------------------------------------------- 716%% Execute an action. 717-spec action(Client, Action) -> Result when 718 Client :: client(), 719 Action :: simple_xml(), 720 Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}. 721action(Client,Action) -> 722 action(Client,Action,?DEFAULT_TIMEOUT). 723 724-spec action(Client, Action, Timeout) -> Result when 725 Client :: client(), 726 Action :: simple_xml(), 727 Timeout :: timeout(), 728 Result :: ok | {ok,[simple_xml()]} | {error,error_reason()}. 729action(Client,Action,Timeout) -> 730 call(Client,{send_rpc_op, action, [Action], Timeout}). 731 732%%---------------------------------------------------------------------- 733%% Send a 'create-subscription' request 734%% See RFC5277, NETCONF Event Notifications 735 736%% create_subscription/2 737 738-spec create_subscription(Client, Values) -> Result when 739 Client :: client(), 740 Values :: #{stream => Stream, 741 filter => Filter, 742 start => StartTime, 743 stop => StopTime}, 744 Stream :: stream_name(), 745 Filter :: simple_xml() | [simple_xml()], 746 StartTime :: xs_datetime(), 747 StopTime :: xs_datetime(), 748 Result :: ok | {error,error_reason()}; 749 %% historic, no longer documented 750 (Client, list() | timeout()) -> Result when 751 Client :: client(), 752 Result :: ok | {error,error_reason()}. 753 754create_subscription(Client, #{} = Values) -> 755 create_subscription(Client, Values, ?DEFAULT_TIMEOUT); 756 757%% historic clauses 758create_subscription(Client, Timeout) 759 when ?is_timeout(Timeout) -> 760 create_subscription(Client, #{}, Timeout); 761create_subscription(Client, Stream) 762 when ?is_string(Stream) -> 763 create_subscription(Client, #{stream => Stream}); 764create_subscription(Client, Filter) 765 when ?is_filter(Filter) -> 766 create_subscription(Client, #{filter => Filter}). 767 768-spec create_subscription(Client, Values, Timeout) -> Result when 769 Client :: client(), 770 Values :: #{stream => Stream, 771 filter => Filter, 772 start => StartTime, 773 stop => StopTime}, 774 Stream :: stream_name(), 775 Filter :: simple_xml() | [simple_xml()], 776 StartTime :: xs_datetime(), 777 StopTime :: xs_datetime(), 778 Timeout :: timeout(), 779 Result :: ok | {error,error_reason()}; 780 %% historic, no longer documented 781 (Client, list(), list() | timeout()) -> Result when 782 Client :: client(), 783 Result :: ok | {error,error_reason()}. 784 785create_subscription(Client, #{} = Values, Timeout) -> 786 Keys = [{stream, ?DEFAULT_STREAM}, 787 {filter, undefined}, 788 {start, undefined}, 789 {stop, undefined}], 790 call(Client, {send_rpc_op, {create_subscription, self()}, 791 [maps:get(K, Values, D) || {K,D} <- Keys], 792 Timeout}); 793 794%% historic clauses, arity 3 795create_subscription(Client, Stream, Timeout) 796 when ?is_string(Stream), ?is_timeout(Timeout) -> 797 create_subscription(Client, #{stream => Stream}, Timeout); 798create_subscription(Client, StartTime, StopTime) 799 when ?is_string(StartTime), ?is_string(StopTime) -> 800 create_subscription(Client, #{start => StartTime, stop => StopTime}); 801create_subscription(Client, Filter, Timeout) 802 when ?is_filter(Filter), ?is_timeout(Timeout) -> 803 create_subscription(Client, #{filter => Filter}, Timeout); 804create_subscription(Client, Stream, Filter) 805 when ?is_string(Stream), ?is_filter(Filter) -> 806 create_subscription(Client, #{stream => Stream, filter => Filter}). 807 808%% historic clauses, arity 1,4-5 809create_subscription(Client) -> 810 create_subscription(Client, #{}). 811create_subscription(Client, StartTime, StopTime, Timeout) 812 when ?is_string(StartTime), ?is_string(StopTime), ?is_timeout(Timeout) -> 813 Values = #{start => StartTime, 814 stop => StopTime}, 815 create_subscription(Client, Values, Timeout); 816create_subscription(Client, Stream, StartTime, StopTime) 817 when ?is_string(Stream), ?is_string(StartTime), ?is_string(StopTime) -> 818 create_subscription(Client, #{stream => Stream, 819 start => StartTime, 820 stop => StopTime}); 821create_subscription(Client, Filter, StartTime, StopTime) 822 when ?is_filter(Filter), ?is_string(StartTime), ?is_string(StopTime) -> 823 create_subscription(Client, #{filter => Filter, 824 start => StartTime, 825 stop => StopTime}); 826create_subscription(Client, Stream, Filter, Timeout) 827 when ?is_string(Stream), ?is_filter(Filter), ?is_timeout(Timeout) -> 828 Values = #{stream => Stream, 829 filter => Filter}, 830 create_subscription(Client, Values, Timeout). 831create_subscription(Client, Stream, StartTime, StopTime, Timeout) 832 when ?is_string(Stream), ?is_string(StartTime), ?is_string(StopTime), 833 ?is_timeout(Timeout) -> 834 Values = #{stream => Stream, 835 start => StartTime, 836 stop => StopTime}, 837 create_subscription(Client, Values, Timeout); 838create_subscription(Client, Stream, Filter, StartTime, StopTime) 839 when ?is_string(Stream), ?is_filter(Filter), ?is_string(StartTime), 840 ?is_string(StopTime) -> 841 create_subscription(Client, #{stream => Stream, 842 filter => Filter, 843 start => StartTime, 844 stop => StopTime}). 845create_subscription(Client, Stream, Filter, StartTime, StopTime, Timeout) -> 846 Values = #{stream => Stream, 847 filter => Filter, 848 start => StartTime, 849 stop => StopTime}, 850 create_subscription(Client, Values, Timeout). 851 852%%---------------------------------------------------------------------- 853%% Send a request to get the given event streams 854%% See RFC5277, NETCONF Event Notifications 855-spec get_event_streams(Client) 856 -> Result when 857 Client :: client(), 858 Result :: {ok,streams()} | {error,error_reason()}. 859get_event_streams(Client) -> 860 get_event_streams(Client,[],?DEFAULT_TIMEOUT). 861 862-spec get_event_streams(Client, Timeout) 863 -> Result when 864 Client :: client(), 865 Timeout :: timeout(), 866 Result :: {ok,streams()} | {error,error_reason()}; 867 (Client, Streams) -> Result when 868 Client :: client(), 869 Streams :: [stream_name()], 870 Result :: {ok,streams()} | {error,error_reason()}. 871get_event_streams(Client,Timeout) when is_integer(Timeout); Timeout==infinity -> 872 get_event_streams(Client,[],Timeout); 873get_event_streams(Client,Streams) when is_list(Streams) -> 874 get_event_streams(Client,Streams,?DEFAULT_TIMEOUT). 875 876-spec get_event_streams(Client, Streams, Timeout) 877 -> Result when 878 Client :: client(), 879 Streams :: [stream_name()], 880 Timeout :: timeout(), 881 Result :: {ok,streams()} | {error,error_reason()}. 882get_event_streams(Client,Streams,Timeout) -> 883 call(Client,{get_event_streams,Streams,Timeout}). 884 885 886%%---------------------------------------------------------------------- 887%% Send a 'close-session' request 888-spec close_session(Client) -> Result when 889 Client :: client(), 890 Result :: ok | {error,error_reason()}. 891close_session(Client) -> 892 close_session(Client, ?DEFAULT_TIMEOUT). 893 894-spec close_session(Client, Timeout) -> Result when 895 Client :: client(), 896 Timeout :: timeout(), 897 Result :: ok | {error,error_reason()}. 898close_session(Client, Timeout) -> 899 call(Client,{send_rpc_op, close_session, [], Timeout}, true). 900 901 902%%---------------------------------------------------------------------- 903%% Send a 'kill-session' request 904-spec kill_session(Client, SessionId) -> Result when 905 Client :: client(), 906 SessionId :: pos_integer(), 907 Result :: ok | {error,error_reason()}. 908kill_session(Client, SessionId) -> 909 kill_session(Client, SessionId, ?DEFAULT_TIMEOUT). 910 911-spec kill_session(Client, SessionId, Timeout) -> Result when 912 Client :: client(), 913 SessionId :: pos_integer(), 914 Timeout :: timeout(), 915 Result :: ok | {error,error_reason()}. 916kill_session(Client, SessionId, Timeout) -> 917 call(Client,{send_rpc_op, kill_session, [SessionId], Timeout}). 918 919 920%%---------------------------------------------------------------------- 921%% Callback functions 922%%---------------------------------------------------------------------- 923 924%% init/3 925 926init(_KeyOrName,{CM,{Host,Port}},Options) -> 927 case ssh_channel(#connection{reference=CM,host=Host,port=Port},Options) of 928 {ok,Connection} -> 929 {ok, CM, #state{connection = Connection, 930 receivers = Options#options.receivers}}; 931 {error,Reason}-> 932 {error,Reason} 933 end; 934init(_KeyOrName,{_Host,_Port},Options) when Options#options.type==connection -> 935 case ssh_connect(Options) of 936 {ok, Connection} -> 937 ConnPid = Connection#connection.reference, 938 {ok, ConnPid, #state{connection = Connection}}; 939 Error -> 940 Error 941 end; 942init(_KeyOrName,{_Host,_Port},Options) -> 943 case ssh_open(Options) of 944 {ok, Connection} -> 945 {ConnPid,_} = Connection#connection.reference, 946 {ok, ConnPid, #state{connection = Connection, 947 receivers = Options#options.receivers}}; 948 {error,Reason}-> 949 {error,Reason} 950 end. 951 952%% terminate/2 953 954terminate(_, #state{connection=Connection}) -> 955 ssh_close(Connection), 956 ok. 957 958%% handle_msg/3 959 960%% Send hello and return to the caller only after reception of the 961%% server's hello. 962handle_msg({hello, Options, Timeout}, 963 From, 964 #state{connection = Connection, 965 hello_status = HelloStatus} 966 = State) -> 967 case do_send(Connection, client_hello(Options)) of 968 ok when HelloStatus == undefined -> %% server hello not yet received 969 TRef = set_request_timer(Timeout, hello), 970 {noreply, State#state{hello_status = #pending{tref = TRef, 971 caller = From}}}; 972 ok -> %% or yes: negotiate version 973 handle_capx(State); 974 Error -> 975 {stop, Error, State} 976 end; 977 978handle_msg(get_ssh_connection, _From, #state{connection=Connection}=State) -> 979 Reply = 980 case Connection#connection.reference of 981 {_,_} -> {error,not_an_ssh_connection}; 982 CM -> {ok,{CM,{Connection#connection.host, 983 Connection#connection.port}}} 984 end, 985 {reply, Reply, State}; 986 987%% Request before server hello. Possible with only_open, since a 988%% handle is then returned without waiting for the server. 989handle_msg(_, _From, #state{session_id = undefined} = State) -> 990 {reply, {error, waiting_for_hello}, State}; 991 992handle_msg(get_capabilities, _From, #state{capabilities = Caps} = State) -> 993 {reply, Caps, State}; 994 995handle_msg(get_session_id, _From, #state{session_id = Id} = State) -> 996 {reply, Id, State}; 997 998handle_msg({send, Timeout, SimpleXml}, 999 From, 1000 #state{connection = Connection, 1001 pending = Pending} 1002 = State) -> 1003 case do_send(Connection, SimpleXml) of 1004 ok -> 1005 TRef = set_request_timer(Timeout, send), 1006 {noreply, State#state{pending = [#pending{tref = TRef, 1007 caller = From} 1008 | Pending]}}; 1009 Error -> 1010 {reply, Error, State} 1011 end; 1012 1013handle_msg({send_rpc, SimpleXml, Timeout}, From, State) -> 1014 do_send_rpc(undefined, SimpleXml, Timeout, From, State); 1015 1016handle_msg({send_rpc_op, Op, Data, Timeout}, From, State) -> 1017 SimpleXml = encode_rpc_operation(Op,Data), 1018 do_send_rpc(Op, SimpleXml, Timeout, From, State); 1019 1020handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) -> 1021 Filter = {netconf,?NETMOD_NOTIF_NAMESPACE_ATTR, 1022 [{streams,[{stream,[{name,[Name]}]} || Name <- Streams]}]}, 1023 SimpleXml = encode_rpc_operation(get,[Filter]), 1024 do_send_rpc(Op, SimpleXml, Timeout, From, State). 1025 1026handle_msg({ssh_cm, CM, {data, Ch, _Type, Data}}, State) -> 1027 ssh_connection:adjust_window(CM,Ch,size(Data)), 1028 log(State#state.connection, recv, Data), 1029 handle_data(Data, State); 1030 1031handle_msg({ssh_cm, _CM, _SshCloseMsg}, State) -> 1032 %% _SshCloseMsg can probably be one of 1033 %% {eof,Ch} 1034 %% {exit_status,Ch,Status} 1035 %% {exit_signal,Ch,ExitSignal,ErrorMsg,LanguageString} 1036 %% {signal,Ch,Signal} 1037 1038 %% This might e.g. happen if the server terminates the connection, 1039 %% as in kill-session (or if ssh:close is called from somewhere 1040 %% unexpected). 1041 1042 %%! Log this?? 1043 %%! Currently the log will say that the client closed the 1044 %%! connection - due to terminate/2 1045 1046 {stop, State}; 1047 1048handle_msg({timeout, TRef, hello}, 1049 #state{hello_status = #pending{tref = TRef, 1050 caller = From}} 1051 = State) -> 1052 ct_gen_conn:return(From, {error, {hello_session_failed, timeout}}), 1053 {stop, State#state{hello_status = {error,timeout}}}; 1054 1055handle_msg({timeout, TRef, Op}, #state{pending = Pending} = State) -> 1056 case lists:keytake(TRef, #pending.tref, Pending) of 1057 {value, #pending{caller = From}, Rest} -> 1058 ct_gen_conn:return(From, {error, timeout}), 1059 %% Discard received bytes in hope that the server has sent 1060 %% an incomplete message. Otherwise this is doomed to 1061 %% leave the connection in an unusable state. 1062 {if Op == close_session -> stop; true -> noreply end, 1063 State#state{pending = Rest, 1064 buf = is_binary(State#state.buf)}}; 1065 false -> 1066 {noreply, State} 1067 end. 1068 1069%% close/1 1070 1071%% Called by ct_util_server to close registered connections before terminate. 1072close(Client) -> 1073 case get_handle(Client) of 1074 {ok,Pid} -> 1075 case ct_gen_conn:stop(Pid) of 1076 {error,{process_down,Pid,noproc}} -> 1077 {error,already_closed}; 1078 Result -> 1079 Result 1080 end; 1081 Error -> 1082 Error 1083 end. 1084 1085 1086%%---------------------------------------------------------------------- 1087%% Internal functions 1088%%---------------------------------------------------------------------- 1089call(Client, Msg) -> 1090 call(Client, Msg, infinity, false). 1091call(Client, Msg, Timeout) when is_integer(Timeout); Timeout==infinity -> 1092 call(Client, Msg, Timeout, false); 1093call(Client, Msg, WaitStop) when is_boolean(WaitStop) -> 1094 call(Client, Msg, infinity, WaitStop). 1095call(Client, Msg, Timeout, WaitStop) -> 1096 case get_handle(Client) of 1097 {ok,Pid} -> 1098 case ct_gen_conn:call(Pid,Msg,Timeout) of 1099 {error,{process_down,Pid,noproc}} -> 1100 {error,no_such_client}; 1101 {error,{process_down,Pid,normal}} when WaitStop -> 1102 %% This will happen when server closes connection 1103 %% before client received rpc-reply on 1104 %% close-session. 1105 ok; 1106 {error,{process_down,Pid,normal}} -> 1107 {error,closed}; 1108 {error,{process_down,Pid,Reason}} -> 1109 {error,{closed,Reason}}; 1110 Other when WaitStop -> 1111 MRef = erlang:monitor(process,Pid), 1112 receive 1113 {'DOWN',MRef,process,Pid,Normal} when Normal==normal; 1114 Normal==noproc -> 1115 Other; 1116 {'DOWN',MRef,process,Pid,Reason} -> 1117 {error,{{closed,Reason},Other}} 1118 after Timeout -> 1119 erlang:demonitor(MRef, [flush]), 1120 {error,{timeout,Other}} 1121 end; 1122 Other -> 1123 Other 1124 end; 1125 Error -> 1126 Error 1127 end. 1128 1129get_handle(Client) when is_pid(Client) -> 1130 {ok,Client}; 1131get_handle(Client) -> 1132 case ct_util:get_connection(Client, ?MODULE) of 1133 {ok,{Pid,_}} -> 1134 {ok,Pid}; 1135 {error,no_registered_connection} -> 1136 {error,{no_connection_found,Client}}; 1137 Error -> 1138 Error 1139 end. 1140 1141%% make_options/2 1142 1143make_options(Opts, Rec) -> 1144 make_options(Opts, Rec#options{port = undefined}, fun opt/2). 1145 1146opt({T, Host}, Rec) 1147 when T == ssh; 1148 T == host -> 1149 Rec#options{host = Host}; 1150 1151opt({port, Port}, Rec) -> 1152 Rec#options{port = Port}; 1153 1154opt({timeout, Tmo}, Rec) 1155 when is_integer(Tmo); 1156 Tmo == infinity -> 1157 Rec#options{timeout = Tmo}; 1158 1159opt({timeout, _} = T, _) -> 1160 throw(T); 1161 1162opt({capability, _}, Rec) -> 1163 Rec; 1164 1165opt({receiver, Dest}, #options{receivers = T} = Rec) -> 1166 Rec#options{receivers = [Dest | T]}; 1167 1168opt(Opt, #options{ssh = Opts} = Rec) -> %% option verified by ssh 1169 Rec#options{ssh = [Opt | Opts]}. 1170 1171%% make_session_options/2 1172 1173make_session_options(Opts, Rec) -> 1174 make_options(Opts, Rec, fun session_opt/2). 1175 1176session_opt({receiver, Dest}, #options{receivers = T} = Rec) -> 1177 Rec#options{receivers = [Dest | T]}; 1178 1179session_opt({capability, _}, Rec) -> 1180 Rec; 1181 1182session_opt({timeout, Tmo}, Rec) 1183 when is_integer(Tmo); 1184 Tmo == infinity -> 1185 Rec#options{timeout = Tmo}; 1186 1187session_opt(T, _Rec) -> 1188 throw(T). 1189 1190%% make_options/3 1191 1192make_options(Opts, Rec, F) -> 1193 try 1194 #options{} = lists:foldl(F, Rec, Opts) 1195 catch 1196 T -> 1197 {error, {invalid_option, T}} 1198 end. 1199 1200%%%----------------------------------------------------------------- 1201 1202set_request_timer(infinity, _) -> 1203 false; 1204 1205set_request_timer(Tmo, Op) -> 1206 erlang:start_timer(Tmo, self(), Op). 1207 1208%%%----------------------------------------------------------------- 1209 1210cancel_request_timer(false) -> 1211 ok; 1212 1213cancel_request_timer(TRef) -> 1214 erlang:cancel_timer(TRef). 1215 1216%%%----------------------------------------------------------------- 1217 1218%% client_hello/1 1219%% 1220%% Prepend the 1.0 base capability only if none is specified by the 1221%% user. Store the versions in the process dictionary until they're 1222%% examined upon reception of server capabilities in handle_capx/1. 1223 1224client_hello(Opts) 1225 when is_list(Opts) -> 1226 UserCaps = [{T, cap(lists:flatten(Cs))} || {capability = T, Cs} <- Opts], 1227 Vsns = versions(UserCaps), 1228 put(?KEY(protocol_vsn), Vsns), 1229 {hello, 1230 ?NETCONF_NAMESPACE_ATTR, 1231 [{capabilities, [{capability, [?NETCONF_BASE_CAP, ?NETCONF_BASE_CAP_VSN]} 1232 || [] == Vsns] 1233 ++ UserCaps}]}. 1234 1235%% cap/1 1236%% 1237%% Let NETCONF capabilities be specified in the shorthand documented in 1238%% RFC 6241. 1239 1240%% This shorthand is documented in RFC 6241 10.4 NETCONF Capabilities 1241%% URNS, but not in 8 Capabilities. 1242cap(":base:" ++ _ = Str) -> 1243 ["urn:ietf:params:netconf", Str]; 1244 1245cap([$:|_] = Str) -> 1246 ["urn:ietf:params:netconf:capability", Str]; 1247 1248cap(Str) -> 1249 [Str]. 1250 1251%% versions/1 1252%% 1253%% Extract base protocol versions from capability options. 1254 1255versions(Opts) -> 1256 [V || {capability, L} <- Opts, 1257 S <- L, 1258 ?NETCONF_BASE_CAP ++ X <- [lists:flatten(S)], 1259 V <- [lists:takewhile(fun(C) -> C /= $? end, X)]]. 1260 1261%% handle_capx/1 1262%% 1263%% Ignore parameters as RFC 6241 (NETCONF 1.1) requires in 8.1 1264%% Capabilities Exchange. Be overly lenient with whitespace since RFC 1265%% 6241 gives examples with significant trailing whitespace. 1266 1267handle_capx(#state{hello_status = received, capabilities = Caps} = S) -> 1268 Remote = [V || ?NETCONF_BASE_CAP ++ X <- Caps, 1269 [V|_] <- [string:lexemes(X, "? \t\r\n")]], 1270 Local = erase(?KEY(protocol_vsn)), 1271 case protocol_vsn(Local, Remote) of 1272 false when Remote == [] -> 1273 Reason = {incorrect_hello, no_base_capability_found}, 1274 {stop, {error, Reason}, S}; 1275 false -> 1276 Reason = {incompatible_base_capability_vsn, lists:min(Remote)}, 1277 {stop, {error, Reason}, S}; 1278 Vsn -> 1279 put(?KEY(chunk), Vsn /= "1.0"), 1280 {reply, ok, rebuf(Vsn, S#state{hello_status = Vsn})} 1281 end; 1282 1283handle_capx(#state{hello_status = {error, _} = No} = S) -> 1284 {stop, No, S}. 1285 1286%% rebuf/2 1287%% 1288%% Turn the message buffer into a list for 1.1 chunking if the 1289%% negotiated protocol version is > 1.0. 1290 1291rebuf("1.0", S) -> 1292 S; 1293 1294rebuf(_, #state{buf = Bin} = S) -> 1295 S#state{buf = [Bin, 3]}. 1296 1297%% protocol_vsn/2 1298 1299protocol_vsn([], Vsns) -> 1300 protocol_vsn(["1.0"], Vsns); 1301 1302protocol_vsn(Local, Remote) -> 1303 lists:max([false | [V || V <- Remote, lists:member(V, Local)]]). 1304 1305%%%----------------------------------------------------------------- 1306 1307encode_rpc_operation(Lock,[Target]) when Lock==lock; Lock==unlock -> 1308 {Lock,[{target,[Target]}]}; 1309encode_rpc_operation(get,[Filter]) -> 1310 {get,filter(Filter)}; 1311encode_rpc_operation(get_config,[Source,Filter]) -> 1312 {'get-config',[{source,[Source]}] ++ filter(Filter)}; 1313encode_rpc_operation(edit_config,[Target,Config,OptParams]) -> 1314 {'edit-config',[{target,[Target]}] ++ OptParams ++ [{config,Config}]}; 1315encode_rpc_operation(delete_config,[Target]) -> 1316 {'delete-config',[{target,[Target]}]}; 1317encode_rpc_operation(copy_config,[Target,Source]) -> 1318 {'copy-config',[{target,[Target]},{source,[Source]}]}; 1319encode_rpc_operation(action,[Action]) -> 1320 {action,?ACTION_NAMESPACE_ATTR,[{data,[Action]}]}; 1321encode_rpc_operation(kill_session,[SessionId]) -> 1322 {'kill-session',[{'session-id',[integer_to_list(SessionId)]}]}; 1323encode_rpc_operation(close_session,[]) -> 1324 'close-session'; 1325encode_rpc_operation({create_subscription,_}, 1326 [Stream,Filter,StartTime,StopTime]) -> 1327 {'create-subscription',?NETCONF_NOTIF_NAMESPACE_ATTR, 1328 [{stream,[Stream]}] ++ 1329 filter(Filter) ++ 1330 maybe_element(startTime,StartTime) ++ 1331 maybe_element(stopTime,StopTime)}. 1332 1333filter(undefined) -> 1334 []; 1335filter({xpath,Filter}) when ?is_string(Filter) -> 1336 [{filter,[{type,"xpath"},{select, Filter}],[]}]; 1337filter(Filter) when is_list(Filter) -> 1338 [{filter,[{type,"subtree"}],Filter}]; 1339filter(Filter) -> 1340 filter([Filter]). 1341 1342maybe_element(_,undefined) -> 1343 []; 1344maybe_element(Tag,Value) -> 1345 [{Tag,[Value]}]. 1346 1347%%%----------------------------------------------------------------- 1348%%% Send XML data to server 1349do_send_rpc(Op, SimpleXml, Timeout, Caller, #state{connection = Connection, 1350 msg_id = MsgId, 1351 pending = Pending} 1352 = State) -> 1353 Msg = {rpc, 1354 [{'message-id', MsgId} | ?NETCONF_NAMESPACE_ATTR], 1355 [SimpleXml]}, 1356 Next = MsgId + 1, 1357 case do_send(Connection, Msg) of 1358 ok -> 1359 TRef = set_request_timer(Timeout, Op), 1360 Rec = #pending{tref = TRef, 1361 msg_id = MsgId, 1362 op = Op, 1363 caller = Caller}, 1364 {noreply, State#state{msg_id = Next, 1365 pending = [Rec | Pending]}}; 1366 Error -> 1367 {reply, Error, State#state{msg_id = Next}} 1368 end. 1369 1370do_send(Connection, Simple) -> 1371 ssh_send(Connection, frame(to_xml(Simple))). 1372 1373to_xml(Simple) -> 1374 Prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", 1375 Chars = xmerl:export_simple([Simple], 1376 xmerl_xml, 1377 [#xmlAttribute{name = prolog, 1378 value = Prolog}]), 1379 unicode:characters_to_binary(Chars). 1380 1381%% frame/1 1382 1383frame(Bin) -> 1384 case get(?KEY(chunk)) of 1385 true -> %% 1.1 chunking 1386 [chunk(Bin) | "\n##\n"]; 1387 _ -> %% 1.0 framing 1388 [Bin | ?END_TAG] 1389 end. 1390 1391%% chunk/1 1392%% 1393%% Chunk randomly to exercise the server. 1394 1395chunk(<<>>) -> 1396 []; 1397 1398chunk(Bin) -> 1399 Sz = min(rand:uniform(1024), size(Bin)), 1400 <<B:Sz/binary, Rest/binary>> = Bin, 1401 ["\n#", integer_to_list(Sz), $\n, B | chunk(Rest)]. 1402 1403%%%----------------------------------------------------------------- 1404%%% Parse and handle received XML data 1405 1406handle_data(Bin, #state{buf = Head} = S) -> 1407 case recv(Bin, Head) of 1408 {error, Reason} -> 1409 Conn = S#state.connection, 1410 ?error(Conn#connection.name, [{receive_error, Reason}, 1411 {buffer, Head}, 1412 {bytes, Bin}]), 1413 {stop, S}; 1414 {Bytes, Rest} -> 1415 handle_more(Rest, handle_xml(Bytes, S)); 1416 Buf -> 1417 {noreply, S#state{buf = Buf}} 1418 end. 1419 1420%% handle_more/2 1421 1422handle_more(_, {stop, _} = No) -> 1423 No; 1424 1425handle_more(Bin, {noreply, State}) -> 1426 handle_data(Bin, State#state{buf = true == get(?KEY(chunk))}). 1427 1428%% handle_xml/2 1429 1430handle_xml(Bytes, State) -> 1431 case parse(Bytes) of 1432 {ok, Simple, _Rest} -> %% ignore trailing bytes 1433 decode(Simple, State); 1434 {fatal_error,_Loc,Reason,_EndTags,_EventState} -> 1435 Conn = State#state.connection, 1436 ?error(Conn#connection.name, [{parse_error, Reason}, 1437 {message, Bytes}]), 1438 {noreply, handle_error(Reason, State)} 1439 end. 1440 1441%% parse/1 1442 1443parse(Bytes) -> 1444 xmerl_sax_parser:stream(<<>>, [{event_fun, fun sax_event/3}, 1445 {event_state, []}, 1446 {continuation_fun, fun cont/1}, 1447 {continuation_state, Bytes}]). 1448 1449%% cont/1 1450 1451cont([] = No) -> 1452 {<<>>, No}; 1453 1454cont([Bin | Rest]) -> 1455 {Bin, Rest}; 1456 1457cont(Bin) -> 1458 {Bin, <<>>}. 1459 1460%% handle_error/2 1461 1462handle_error(_Reason, #state{pending = []} = State) -> 1463 State; 1464 1465handle_error(Reason, #state{pending = Pending} = State) -> 1466 %% Assuming the first request gets the first answer. 1467 Rec = #pending{tref = TRef, 1468 caller = Caller} 1469 = lists:last(Pending), 1470 cancel_request_timer(TRef), 1471 ct_gen_conn:return(Caller,{error, {failed_to_parse_received_data, Reason}}), 1472 State#state{pending = lists:delete(Rec, Pending)}. 1473 1474%% Event function for the sax parser. It builds a simple XML structure. 1475%% Care is taken to keep namespace attributes and prefixes as in the original XML. 1476sax_event(Event,_Loc,State) -> 1477 sax_event(Event,State). 1478 1479sax_event({startPrefixMapping, Prefix, Uri},Acc) -> 1480 %% startPrefixMapping will always come immediately before the 1481 %% startElement where the namespace is defined. 1482 [{xmlns,{Prefix,Uri}}|Acc]; 1483sax_event({startElement,_Uri,_Name,QN,Attrs},Acc) -> 1484 %% Pick out any namespace attributes inserted due to a 1485 %% startPrefixMapping event.The rest of Acc will then be only 1486 %% elements. 1487 {NsAttrs,NewAcc} = split_attrs_and_elements(Acc,[]), 1488 Tag = qn_to_tag(QN), 1489 [{Tag,NsAttrs ++ parse_attrs(Attrs),[]}|NewAcc]; 1490sax_event({endElement,_Uri,_Name,_QN},[{Name,Attrs,Cont},{Parent,PA,PC}|Acc]) -> 1491 [{Parent,PA,[{Name,Attrs,lists:reverse(Cont)}|PC]}|Acc]; 1492sax_event(endDocument,[{Tag,Attrs,Cont}]) -> 1493 {Tag,Attrs,lists:reverse(Cont)}; 1494sax_event({characters,String},[{Name,Attrs,Cont}|Acc]) -> 1495 [{Name,Attrs,[String|Cont]}|Acc]; 1496sax_event(_Event,State) -> 1497 State. 1498 1499split_attrs_and_elements([{xmlns,{Prefix,Uri}}|Rest],Attrs) -> 1500 split_attrs_and_elements(Rest,[{xmlnstag(Prefix),Uri}|Attrs]); 1501split_attrs_and_elements(Elements,Attrs) -> 1502 {Attrs,Elements}. 1503 1504xmlnstag([]) -> 1505 xmlns; 1506xmlnstag(Prefix) -> 1507 list_to_atom("xmlns:"++Prefix). 1508 1509qn_to_tag({[],Name}) -> 1510 list_to_atom(Name); 1511qn_to_tag({Prefix,Name}) -> 1512 list_to_atom(Prefix ++ ":" ++ Name). 1513 1514parse_attrs([{_Uri, [], Name, Value}|Attrs]) -> 1515 [{list_to_atom(Name),Value}|parse_attrs(Attrs)]; 1516parse_attrs([{_Uri, Prefix, Name, Value}|Attrs]) -> 1517 [{list_to_atom(Prefix ++ ":" ++ Name),Value}|parse_attrs(Attrs)]; 1518parse_attrs([]) -> 1519 []. 1520 1521 1522%%%----------------------------------------------------------------- 1523 1524%% decode/2 1525%% 1526%% Decode parsed (incoming) XML. 1527 1528decode({Tag, _, _} = E, #state{} = State) -> 1529 case decode(get_local_name_atom(Tag), E, State) of 1530 #state{} = S -> 1531 {noreply, S}; 1532 {stop, #state{}} = T -> 1533 T 1534 end. 1535 1536%% decode/3 1537 1538decode('rpc-reply', {_, Attrs, _} = E, State) -> 1539 decode_rpc_reply(get_msg_id(Attrs), E, State); 1540 1541%% Incoming hello, outgoing not yet sent. 1542decode(hello, E, #state{hello_status = undefined} = State) -> 1543 case decode_hello(E) of 1544 {ok, SessionId, Capabilities} -> 1545 State#state{session_id = SessionId, 1546 capabilities = Capabilities, 1547 hello_status = received}; 1548 {error, _Reason} = No -> 1549 State#state{hello_status = No} 1550 end; 1551 1552%% Incoming hello, outgoing already sent: negotiate protocol version. 1553decode(hello, E, #state{hello_status = #pending{tref = TRef, 1554 caller = From}} 1555 = State) -> 1556 cancel_request_timer(TRef), 1557 case decode_hello(E) of 1558 {ok, SessionId, Capabilities} -> 1559 reply(From, handle_capx(State#state{session_id = SessionId, 1560 capabilities = Capabilities, 1561 hello_status = received})); 1562 {error, _Reason} = No -> 1563 ct_gen_conn:return(From, No), 1564 {stop, State#state{hello_status = No}} 1565 end; 1566 1567%% Duplicate hello: ignore. 1568decode(hello, E, #state{hello_status = Other} = State) -> 1569 ConnName = (State#state.connection)#connection.name, 1570 ?error(ConnName, [{got_unexpected_hello, E}, 1571 {hello_status, Other}]), 1572 State; 1573 1574decode(notification, E, State) -> 1575 notify(State, E), 1576 State; 1577 1578decode(Other, E, State) -> 1579 decode_send({got_unexpected_msg, Other}, E, State). 1580 1581%% notify/2 1582 1583notify(#state{receivers = []} = State, E) -> 1584 Name = (State#state.connection)#connection.name, 1585 ?error(Name, [{got_unexpected_notification, E}]); 1586 1587%% Sending can fail with an atom-valued destination, but it's up to 1588%% the user. 1589notify(#state{receivers = T}, E) -> 1590 lists:foreach(fun(D) -> D ! E end, if is_pid(T) -> [T]; true -> T end). 1591 1592%% reply/2 1593%% 1594%% Explicitly send a reply that can't be returned. 1595 1596reply(From, {T, Res, State}) -> 1597 ct_gen_conn:return(From, Res), 1598 case T of 1599 reply -> 1600 State; 1601 stop -> 1602 {T, State} 1603 end. 1604 1605%% get_msg_id/1 1606 1607get_msg_id(Attrs) -> 1608 case find('message-id', Attrs) of 1609 {_,Str} -> 1610 list_to_integer(Str); 1611 false -> 1612 undefined 1613 end. 1614 1615%% recode_rpc_reply/3 1616 1617decode_rpc_reply(undefined, E, #state{pending = [#pending{msg_id = MsgId}]} 1618 = State) 1619 when MsgId /= undefined -> 1620 ConnName = (State#state.connection)#connection.name, 1621 ?error(ConnName, [{warning, rpc_reply_missing_msg_id}, 1622 {assuming, MsgId}]), 1623 decode_rpc_reply(MsgId, E, State); 1624 1625decode_rpc_reply(undefined, _, State) -> 1626 ConnName = (State#state.connection)#connection.name, 1627 ?error(ConnName, [{error, rpc_reply_missing_msg_id}]), 1628 State; 1629 1630decode_rpc_reply(MsgId, 1631 {_, Attrs, Content0} 1632 = E, 1633 #state{pending = Pending} 1634 = State) -> 1635 case lists:keytake(MsgId, #pending.msg_id, Pending) of 1636 {value, Rec, Rest} -> 1637 #pending{tref = TRef, op = Op, caller = From} 1638 = Rec, 1639 cancel_request_timer(TRef), 1640 Content = forward_xmlns_attr(Attrs, Content0), 1641 {Reply, T} = do_decode_rpc_reply(Op, 1642 Content, 1643 State#state{pending = Rest}), 1644 ct_gen_conn:return(From, Reply), 1645 T; 1646 false -> %% not a send_rcp or server has sent wrong id 1647 decode_send({got_unexpected_msg_id, MsgId}, E, State) 1648 end. 1649 1650%% decode_send/2 1651%% 1652%% Result of send/2,3. Only handle one at a time there since all 1653%% pendings have msg_id = undefined. 1654 1655decode_send(ErrorT, Elem, #state{pending = Pending} = State) -> 1656 case [P || #pending{msg_id = undefined} = P <- Pending] of 1657 [Rec] -> 1658 #pending{tref = TRef, 1659 caller = From} 1660 = Rec, 1661 cancel_request_timer(TRef), 1662 ct_gen_conn:return(From, Elem), 1663 State#state{pending = lists:delete(Rec, Pending)}; 1664 _ -> 1665 Conn = State#state.connection, 1666 ?error(Conn#connection.name, [ErrorT, {expecting, Pending}]), 1667 State 1668 end. 1669 1670%% do_decode_rpc_reply/3 1671 1672do_decode_rpc_reply(Op, Result, State) 1673 when Op == lock; 1674 Op == unlock; 1675 Op == edit_config; 1676 Op == delete_config; 1677 Op == copy_config; 1678 Op == kill_session -> 1679 {decode_ok(Result), State}; 1680 1681do_decode_rpc_reply(Op, Result, State) 1682 when Op == get; 1683 Op == get_config; 1684 Op == action -> 1685 {decode_data(Result), State}; 1686 1687do_decode_rpc_reply(close_session, Result, State) -> 1688 case decode_ok(Result) of 1689 ok -> 1690 {ok, {stop, State}}; 1691 Other -> 1692 {Other, State} 1693 end; 1694 1695%% Only set a new destination if one (or more) hasn't been set with a 1696%% receiver option(), to allow more than calls to create_subscription 1697%% to order notifications. 1698do_decode_rpc_reply({create_subscription, Pid}, Result, #state{receivers = T} 1699 = State) -> 1700 case decode_ok(Result) of 1701 ok when T == []; 1702 is_pid(T) -> 1703 {ok, State#state{receivers = Pid}}; 1704 Other -> 1705 {Other, State} 1706 end; 1707 1708do_decode_rpc_reply(get_event_streams, Result, State) -> 1709 {decode_streams(decode_data(Result)), State}; 1710 1711do_decode_rpc_reply(undefined, Result, State) -> 1712 {Result, State}. 1713 1714 1715 1716decode_ok([{Tag,Attrs,Content}]) -> 1717 case get_local_name_atom(Tag) of 1718 ok -> 1719 ok; 1720 'rpc-error' -> 1721 {error,forward_xmlns_attr(Attrs,Content)}; 1722 _Other -> 1723 {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}} 1724 end; 1725decode_ok(Other) -> 1726 {error,{unexpected_rpc_reply,Other}}. 1727 1728decode_data([{Tag,Attrs,Content}]) -> 1729 case get_local_name_atom(Tag) of 1730 ok -> 1731 %% when action has return type void 1732 ok; 1733 data -> 1734 %% Since content of data has nothing from the netconf 1735 %% namespace, we remove the parent's xmlns attribute here 1736 %% - just to make the result cleaner 1737 {ok,forward_xmlns_attr(remove_xmlnsattr_for_tag(Tag,Attrs),Content)}; 1738 'rpc-error' -> 1739 {error,forward_xmlns_attr(Attrs,Content)}; 1740 _Other -> 1741 {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}} 1742 end; 1743decode_data(Other) -> 1744 {error,{unexpected_rpc_reply,Other}}. 1745 1746get_qualified_name(Tag) -> 1747 case string:lexemes(atom_to_list(Tag),":") of 1748 [TagStr] -> {[],TagStr}; 1749 [PrefixStr,TagStr] -> {PrefixStr,TagStr} 1750 end. 1751 1752get_local_name_atom(Tag) -> 1753 {_,TagStr} = get_qualified_name(Tag), 1754 list_to_atom(TagStr). 1755 1756 1757%% Remove the xmlns attr that points to the tag. I.e. if the tag has a 1758%% prefix, remove {'xmlns:prefix',_}, else remove default {xmlns,_}. 1759remove_xmlnsattr_for_tag(Tag,Attrs) -> 1760 {Prefix,_TagStr} = get_qualified_name(Tag), 1761 lists:keydelete(xmlnstag(Prefix), 1, Attrs). 1762 1763%% Prepend xmlns attributes from parent to children, omitting those 1764%% the child sets. 1765forward_xmlns_attr(ParentAttrs, Children) -> 1766 Namespace = lists:filter(fun is_xmlns/1, ParentAttrs), 1767 [{T, Ns ++ A, C} || {T, A, C} <- Children, 1768 F <- [fun({K,_}) -> not lists:keymember(K, 1, A) end], 1769 Ns <- [lists:filter(F, Namespace)]]. 1770 1771is_xmlns({Key, _}) -> 1772 Key == xmlns orelse lists:prefix("xmlns:", atom_to_list(Key)). 1773 1774%% Decode server hello to pick out session id and capabilities 1775decode_hello({hello, _Attrs, Hello}) -> 1776 U = make_ref(), 1777 try 1778 [{'session-id', _, [SessionId]}, _ | _] 1779 = [find('session-id', Hello), no_session_id_found | U], 1780 [{ok, Id}, _ | _] 1781 = [catch {ok, list_to_integer(SessionId)}, invalid_session_id | U], 1782 [true, _ | _] 1783 = [0 < Id, invalid_session_id | U], 1784 [{capabilities, _, Capabilities}, _ | _] 1785 = [find(capabilities, Hello), capabilities_not_found | U], 1786 [{ok, Caps}, _ | _] 1787 = [decode_caps(Capabilities, [], false), false | U], 1788 {ok, Id, Caps} 1789 catch 1790 error: {badmatch, [Error, false | U]} -> 1791 Error; 1792 error: {badmatch, [_, Reason | U]} -> 1793 {error, {incorrect_hello, Reason}} 1794 end. 1795 1796find(Key, List) -> 1797 lists:keyfind(Key, 1, List). 1798 1799decode_caps([{capability, [], [?NETCONF_BASE_CAP ++ _ = Cap]} | Caps], 1800 Acc, 1801 _) -> 1802 decode_caps(Caps, [Cap|Acc], true); 1803decode_caps([{capability, [], [Cap]} | Caps], Acc, Base) -> 1804 decode_caps(Caps, [Cap|Acc], Base); 1805decode_caps([H|_], _, _) -> 1806 {error, {unexpected_capability_element, H}}; 1807decode_caps([], _, false) -> 1808 {error, {incorrect_hello, no_base_capability_found}}; 1809decode_caps([], Acc, true) -> 1810 {ok, lists:reverse(Acc)}. 1811 1812 1813%% Return a list of {Name,Data}, where data is a {Tag,Value} list for each stream 1814decode_streams({error,Reason}) -> 1815 {error,Reason}; 1816decode_streams({ok,[{netconf,_,Streams}]}) -> 1817 {ok,decode_streams(Streams)}; 1818decode_streams([{streams,_,Streams}]) -> 1819 decode_streams(Streams); 1820decode_streams([{stream,_,Stream} | Streams]) -> 1821 {name,_,[Name]} = find(name, Stream), 1822 [{Name,[{Tag,Value} || {Tag,_,[Value]} <- Stream, Tag /= name]} 1823 | decode_streams(Streams)]; 1824decode_streams([]) -> 1825 []. 1826 1827 1828%%%----------------------------------------------------------------- 1829%%% Logging 1830 1831log(Connection,Action) -> 1832 log(Connection,Action,<<>>). 1833log(#connection{reference=Ref,host=Host,port=Port,name=Name},Action,Data) -> 1834 Address = 1835 case Ref of 1836 {_,Ch} -> {Host,Port,Ch}; 1837 _ -> {Host,Port} 1838 end, 1839 error_logger:info_report(#conn_log{client=self(), 1840 address=Address, 1841 name=Name, 1842 action=Action, 1843 module=?MODULE}, 1844 Data). 1845 1846 1847%% Log callback - called from the error handler process 1848format_data(How,Data) -> 1849 %% Assuming that the data is encoded as UTF-8. If it is not, then 1850 %% the printout might be wrong, but the format function will not 1851 %% crash! 1852 %% FIXME: should probably read encoding from the data and do 1853 %% unicode:characters_to_binary(Data,InEncoding,utf8) when calling 1854 %% log/3 instead of assuming utf8 in as done here! 1855 do_format_data(How,unicode:characters_to_binary(Data)). 1856 1857do_format_data(raw,Data) -> 1858 io_lib:format("~n~ts~n",[hide_password(Data)]); 1859do_format_data(pretty,Data) -> 1860 maybe_io_lib_format(indent(Data)); 1861do_format_data(html,Data) -> 1862 maybe_io_lib_format(html_format(Data)). 1863 1864maybe_io_lib_format(<<>>) -> 1865 []; 1866maybe_io_lib_format(String) -> 1867 io_lib:format("~n~ts~n",[String]). 1868 1869%%%----------------------------------------------------------------- 1870%%% Hide password elements from XML data 1871hide_password(Bin) -> 1872 re:replace(Bin,<<"(<password[^>]*>)[^<]*(</password>)">>,<<"\\1*****\\2">>, 1873 [global,{return,binary},unicode]). 1874 1875%%%----------------------------------------------------------------- 1876%%% HTML formatting 1877html_format(Bin) -> 1878 binary:replace(indent(Bin),<<"<">>,<<"<">>,[global]). 1879 1880%%%----------------------------------------------------------------- 1881%%% Indentation of XML code 1882indent(Bin) -> 1883 String = normalize(hide_password(Bin)), 1884 IndentedString = 1885 case erase(part_of_line) of 1886 undefined -> 1887 indent1(String,[]); 1888 Part -> 1889 indent1(lists:reverse(Part)++String,erase(indent)) 1890 end, 1891 unicode:characters_to_binary(IndentedString). 1892 1893%% Normalizes the XML document by removing all space and newline 1894%% between two XML tags. 1895%% Returns a list, no matter if the input was a list or a binary. 1896normalize(Bin) -> 1897 re:replace(Bin,<<">[ \r\n\t]+<">>,<<"><">>,[global,{return,list},unicode]). 1898 1899 1900indent1("<?"++Rest1,Indent1) -> 1901 %% Prolog 1902 {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$?,$<]), 1903 Line++indent1(Rest2,Indent2); 1904indent1("</"++Rest1,Indent1) -> 1905 %% Stop tag 1906 case indent_line1(Rest1,Indent1,[$/,$<]) of 1907 {[],[],_} -> 1908 []; 1909 {Line,Rest2,Indent2} -> 1910 "\n"++Line++indent1(Rest2,Indent2) 1911 end; 1912indent1("<"++Rest1,Indent1) -> 1913 %% Start- or empty tag 1914 put(tag,get_tag(Rest1)), 1915 case indent_line(Rest1,Indent1,[$<]) of 1916 {[],[],_} -> 1917 []; 1918 {Line,Rest2,Indent2} -> 1919 "\n"++Line++indent1(Rest2,Indent2) 1920 end; 1921indent1([H|T],Indent) -> 1922 [H|indent1(T,Indent)]; 1923indent1([],_Indent) -> 1924 []. 1925 1926indent_line("?>"++Rest,Indent,Line) -> 1927 %% Prolog 1928 {lists:reverse(Line)++"?>",Rest,Indent}; 1929indent_line("/></"++Rest,Indent,Line) -> 1930 %% Empty tag, and stop of parent tag -> one step out in indentation 1931 {Indent++lists:reverse(Line)++"/>","</"++Rest,Indent--" "}; 1932indent_line("/>"++Rest,Indent,Line) -> 1933 %% Empty tag, then probably next tag -> keep indentation 1934 {Indent++lists:reverse(Line)++"/>",Rest,Indent}; 1935indent_line("></"++Rest,Indent,Line) -> 1936 LastTag = erase(tag), 1937 case get_tag(Rest) of 1938 LastTag -> 1939 %% Start and stop tag, but no content 1940 indent_line1(Rest,Indent,[$/,$<,$>|Line]); 1941 _ -> 1942 %% Stop tag completed, and then stop tag of parent -> one step out 1943 {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "} 1944 end; 1945indent_line("><"++Rest,Indent,Line) -> 1946 %% Stop tag completed, and new tag comming -> keep indentation 1947 {Indent++lists:reverse(Line)++">","<"++Rest," "++Indent}; 1948indent_line("</"++Rest,Indent,Line) -> 1949 %% Stop tag starting -> search for end of this tag 1950 indent_line1(Rest,Indent,[$/,$<|Line]); 1951indent_line([H|T],Indent,Line) -> 1952 indent_line(T,Indent,[H|Line]); 1953indent_line([],Indent,Line) -> 1954 %% The line is not complete - will be continued later 1955 put(part_of_line,Line), 1956 put(indent,Indent), 1957 {[],[],Indent}. 1958 1959indent_line1("></"++Rest,Indent,Line) -> 1960 %% Stop tag completed, and then stop tag of parent -> one step out 1961 {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "}; 1962indent_line1(">"++Rest,Indent,Line) -> 1963 %% Stop tag completed -> keep indentation 1964 {Indent++lists:reverse(Line)++">",Rest,Indent}; 1965indent_line1([H|T],Indent,Line) -> 1966 indent_line1(T,Indent,[H|Line]); 1967indent_line1([],Indent,Line) -> 1968 %% The line is not complete - will be continued later 1969 put(part_of_line,Line), 1970 put(indent,Indent), 1971 {[],[],Indent}. 1972 1973get_tag("/>"++_) -> 1974 []; 1975get_tag(">"++_) -> 1976 []; 1977get_tag([H|T]) -> 1978 [H|get_tag(T)]; 1979get_tag([]) -> 1980 %% The line is not complete - will be continued later. 1981 []. 1982 1983 1984%%%----------------------------------------------------------------- 1985%%% SSH stuff 1986ssh_connect(#options{host=Host,timeout=Timeout,port=Port, 1987 ssh=SshOpts,name=Name,type=Type}) -> 1988 case ssh:connect(Host, Port, 1989 [{user_interaction,false}, 1990 {silently_accept_hosts, true}|SshOpts], 1991 Timeout) of 1992 {ok,CM} -> 1993 Connection = #connection{reference = CM, 1994 host = Host, 1995 port = Port, 1996 name = Name, 1997 type = Type}, 1998 log(Connection,connect), 1999 {ok,Connection}; 2000 {error,Reason} -> 2001 {error,{ssh,could_not_connect_to_server,Reason}} 2002 end. 2003 2004ssh_channel(#connection{reference=CM}=Connection0, 2005 #options{timeout=Timeout,name=Name,type=Type}) -> 2006 case ssh_connection:session_channel(CM, Timeout) of 2007 {ok,Ch} -> 2008 case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of 2009 success -> 2010 Connection = Connection0#connection{reference = {CM,Ch}, 2011 name = Name, 2012 type = Type}, 2013 log(Connection,open), 2014 {ok, Connection}; 2015 failure -> 2016 ssh_connection:close(CM,Ch), 2017 {error,{ssh,could_not_execute_netconf_subsystem}}; 2018 {error,timeout} -> 2019 ssh_connection:close(CM,Ch), 2020 {error,{ssh,could_not_execute_netconf_subsystem,timeout}} 2021 end; 2022 {error, Reason} -> 2023 {error,{ssh,could_not_open_channel,Reason}} 2024 end. 2025 2026 2027ssh_open(Options) -> 2028 case ssh_connect(Options) of 2029 {ok,Connection} -> 2030 case ssh_channel(Connection,Options) of 2031 {ok,_} = Ok -> 2032 Ok; 2033 Error -> 2034 ssh_close(Connection), 2035 Error 2036 end; 2037 Error -> 2038 Error 2039 end. 2040 2041ssh_send(#connection{reference = {CM,Ch}}=Connection, Data) -> 2042 case ssh_connection:send(CM, Ch, Data) of 2043 ok -> 2044 log(Connection,send,Data), 2045 ok; 2046 {error,Reason} -> 2047 {error,{ssh,failed_to_send_data,Reason}} 2048 end. 2049 2050ssh_close(Connection=#connection{reference = {CM,Ch}, type = Type}) -> 2051 _ = ssh_connection:close(CM,Ch), 2052 log(Connection,close), 2053 case Type of 2054 connection_and_channel -> 2055 ssh_close(Connection#connection{reference = CM}); 2056 _ -> 2057 ok 2058 end, 2059 ok; 2060ssh_close(Connection=#connection{reference = CM}) -> 2061 _ = ssh:close(CM), 2062 log(Connection,disconnect), 2063 ok. 2064 2065%% =========================================================================== 2066 2067%% recv/1 2068%% 2069%% Extract incoming messages using either NETCONF 1.0 framing or 2070%% NETCONF 1.1 chunking. 2071 2072recv(Bin, true) -> 2073 recv(Bin, [<<>>, 3]); 2074recv(Bin, false) -> 2075 recv(Bin, <<>>); 2076 2077recv(Bin, [Head, Len | Chunks]) -> %% 1.1 chunking 2078 chunk(<<Head/binary, Bin/binary>>, Chunks, Len); 2079 2080%% Start looking for the terminating end-of-message sequence ]]>]]> 2081%% 5 characters from the end of the buffered head, since this binary 2082%% has already been scanned. 2083recv(Bin, Head) when is_binary(Head) -> %% 1.0 framing 2084 frame(<<Head/binary, Bin/binary>>, max(0, size(Head) - 5)). 2085 2086%% frame/2 2087%% 2088%% Extract a message terminated by the ]]>]]> end-of-message sequence. 2089%% Don't need to extract characters as UTF-8 since matching byte-wise 2090%% is unambiguous: the high-order bit of every byte of a multi-byte 2091%% UTF character is 1, while the end-of-message sequence is ASCII. 2092 2093frame(Bin, Start) -> 2094 Sz = size(Bin), 2095 Scope = {Start, Sz - Start}, 2096 case binary:match(Bin, pattern(), [{scope, Scope}]) of 2097 {Len, 6} -> 2098 <<Msg:Len/binary, _:6/binary, Rest/binary>> = Bin, 2099 {trim(Msg), Rest}; 2100 nomatch -> 2101 Bin 2102 end. 2103 2104%% pattern/0 2105 2106pattern() -> 2107 Key = ?KEY(pattern), 2108 case get(Key) of 2109 undefined -> 2110 CP = binary:compile_pattern(<<"]]>]]>">>), 2111 put(Key, CP), 2112 CP; 2113 CP -> 2114 CP 2115 end. 2116 2117%% trim/1 2118%% 2119%% Whitespace before an XML declaration is an error, but be somewhat 2120%% lenient and strip line breaks since the RFC's are unclear on what's 2121%% allowed following a ]]>]]> delimiter. Typical seems to be a single 2122%% $\n, but strip any of " \t\r\n", and regardless of NETCONF version. 2123 2124trim(<<C, Bin/binary>>) 2125 when C == $\n; 2126 C == $\r; 2127 C == $\t; 2128 C == $ -> 2129 trim(Bin); 2130 2131trim(Bin) -> 2132 Bin. 2133 2134%% chunk/3 2135%% 2136%% The final argument is either 0 to indicate that a specified number 2137%% of bytes of chunk data should be consumed, or at least 3 to 2138%% indicate an offset at which to look for a newline following a chunk 2139%% size. 2140 2141%% Accumulating chunk-data ... 2142chunk(Bin, [Sz | Chunks] = L, 0) -> 2143 case Bin of 2144 <<Chunk:Sz/binary, Rest/binary>> -> 2145 chunk(Rest, acc(Chunk, Chunks), 3); %% complete chunk ... 2146 _ -> 2147 [Bin, 0 | L] %% ... or not 2148 end; 2149 2150%% ... or a header. 2151 2152chunk(Bin, Chunks, Len) 2153 when size(Bin) < 4 -> 2154 [Bin, 3 = Len | Chunks]; 2155 2156%% End of chunks. 2157chunk(<<"\n##\n", Rest/binary>>, Chunks, _) -> 2158 case Chunks of 2159 [] -> 2160 {error, "end-of-chunks unexpected"}; %% must be at least one 2161 Bins -> 2162 {lists:reverse(Bins), Rest} 2163 end; 2164 2165%% Matching each of the 10 newline possibilities is faster than 2166%% searching. 2167chunk(<<"\n#", Head:1/binary, $\n, Rest/binary>>, Chunks, _) -> 2168 acc(Head, Rest, Chunks); 2169chunk(<<"\n#", Head:2/binary, $\n, Rest/binary>>, Chunks, _) -> 2170 acc(Head, Rest, Chunks); 2171chunk(<<"\n#", Head:3/binary, $\n, Rest/binary>>, Chunks, _) -> 2172 acc(Head, Rest, Chunks); 2173chunk(<<"\n#", Head:4/binary, $\n, Rest/binary>>, Chunks, _) -> 2174 acc(Head, Rest, Chunks); 2175chunk(<<"\n#", Head:5/binary, $\n, Rest/binary>>, Chunks, _) -> 2176 acc(Head, Rest, Chunks); 2177chunk(<<"\n#", Head:6/binary, $\n, Rest/binary>>, Chunks, _) -> 2178 acc(Head, Rest, Chunks); 2179chunk(<<"\n#", Head:7/binary, $\n, Rest/binary>>, Chunks, _) -> 2180 acc(Head, Rest, Chunks); 2181chunk(<<"\n#", Head:8/binary, $\n, Rest/binary>>, Chunks, _) -> 2182 acc(Head, Rest, Chunks); 2183chunk(<<"\n#", Head:9/binary, $\n, Rest/binary>>, Chunks, _) -> 2184 acc(Head, Rest, Chunks); 2185chunk(<<"\n#", Head:10/binary, $\n, Rest/binary>>, Chunks, _) -> 2186 acc(Head, Rest, Chunks); 2187 2188chunk(<<"\n#", Bin:11/binary, _/binary>>, _, _) -> 2189 {error, {"chunk-size too long", Bin}}; %% 32-bits = max 10 digits 2190 2191chunk(<<"\n#", _/binary>> = Bin, Chunks, _) -> 2192 [Bin, size(Bin) | Chunks]; 2193 2194chunk(Bin, Chunks, 3 = Len) -> 2195 case drop(Bin) of 2196 <<>> -> 2197 [Bin, Len | Chunks]; 2198 <<"\n#", _/binary>> = B -> 2199 chunk(B, Chunks, Len); 2200 _ -> 2201 {error, {"not a chunk", Bin}} 2202 end. 2203 2204%% drop/1 2205 2206drop(<<"\n#", _/binary>> = Bin) -> 2207 Bin; 2208 2209drop(<<C, Bin/binary>>) 2210 when C == $\n; 2211 C == $\r; 2212 C == $\t; 2213 C == $ -> 2214 drop(Bin); 2215 2216drop(Bin) -> 2217 Bin. 2218 2219%% acc/2 2220 2221acc(Chunk, []) -> 2222 [B || B <- [trim(Chunk)], <<>> /= B]; 2223 2224acc(Chunk, Chunks) -> 2225 [Chunk | Chunks]. 2226 2227%% acc/3 2228 2229acc(Head, Rest, Chunks) -> 2230 case chunk_size(Head) of 2231 {error, _Reason} = No -> 2232 No; 2233 Sz -> 2234 chunk(Rest, [Sz | Chunks], 0) 2235 end. 2236 2237%% chunk_size/1 2238 2239chunk_size(<<C, _/binary>> = Bin) -> 2240 try true = $0 < C, binary_to_integer(Bin) of 2241 Sz when 0 < Sz bsr 32 -> 2242 {error, {"chunk-size too large", Sz}}; 2243 Sz -> 2244 Sz 2245 catch 2246 error: _ -> 2247 {error, {"chunk-size invalid", Bin}} 2248 end. 2249 2250%%---------------------------------------------------------------------- 2251%% END OF MODULE 2252%%---------------------------------------------------------------------- 2253