diff --git a/.gitignore b/.gitignore index d962fd67..9964c596 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ ergw.config data.ergw* log.ergw* apps/ergw_core/priv* +data +ergw@* diff --git a/apps/ergw/src/ergw_config.erl b/apps/ergw/src/ergw_config.erl index 8df8d4a8..bafa550f 100644 --- a/apps/ergw/src/ergw_config.erl +++ b/apps/ergw/src/ergw_config.erl @@ -13,7 +13,7 @@ -compile({no_auto_import,[put/2]}). %% API --export([load/0, apply/1, serialize_config/1]). +-export([load/0, apply/1, serialize_config/1, reload_config/1, ergw_core_init/2]). -ifdef(TEST). -export([config_meta/0, @@ -45,6 +45,14 @@ %%% API %%%=================================================================== +reload_config(#{} = Config) -> + load_typespecs(), + do([error_m || + load_schemas(), + validate_config_with_schema(Config), + return(coerce_config(Config)) + ]). + load() -> load_typespecs(), do([error_m || @@ -129,8 +137,9 @@ ergw_aaa_init(apps, #{apps := Apps0}) -> Apps = ergw_aaa_config:validate_options(fun ergw_aaa_config:validate_app/2, Apps0, []), maps:map(fun ergw_aaa:add_application/2, Apps), Apps; -ergw_aaa_init(_, _) -> - ok. +ergw_aaa_init(K, _) -> + ?LOG(warning, "The key ~p is missed in config of erGW-AAA", [K]), + {error, unhandled}. ergw_sbi_client_init(Opts) -> ergw_sbi_client_config:validate_options(fun ergw_sbi_client_config:validate_option/2, Opts). @@ -185,8 +194,9 @@ ergw_core_init(proxy_map, #{proxy_map := Map}) -> ok = ergw_core:setopts(proxy_map, Map); ergw_core_init(http_api, #{http_api := Opts}) -> ergw_http_api:init(Opts); -ergw_core_init(_K, _) -> - ok. +ergw_core_init(K, _) -> + ?LOG(warning, "The key ~p is missed in config of erGW", [K]), + {error, unhandled}. ergw_charging_init(rules, #{rules := Rules}) -> maps:map(fun ergw_core:add_charging_rule/2, Rules); diff --git a/apps/ergw/src/ergw_http_api.erl b/apps/ergw/src/ergw_http_api.erl index 02b2894c..e841a2fc 100644 --- a/apps/ergw/src/ergw_http_api.erl +++ b/apps/ergw/src/ergw_http_api.erl @@ -39,6 +39,8 @@ start_http_listener(#{enabled := true} = Opts) -> {"/status/[...]", http_status_handler, []}, %% 5G SBI APIs {"/sbi/nbsf-management/v1/pcfBindings", sbi_nbsf_handler, []}, + %% HTTP controller + {"/api/v1/controller", http_controller_handler, []}, %% serves static files for swagger UI {"/api/v1/spec/ui", swagger_ui_handler, []}, {"/api/v1/spec/ui/[...]", cowboy_static, {priv_dir, ergw_core, "static"}}]} diff --git a/apps/ergw/src/http_controller_handler.erl b/apps/ergw/src/http_controller_handler.erl new file mode 100644 index 00000000..e4721be4 --- /dev/null +++ b/apps/ergw/src/http_controller_handler.erl @@ -0,0 +1,154 @@ +%% Copyright 2021, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(http_controller_handler). + +-behavior(cowboy_rest). + +-export([init/2, content_types_provided/2, handle_request_json/2, + allowed_methods/2, delete_resource/2, content_types_accepted/2]). + +-ignore_xref([handle_request_json/2]). + +-include_lib("kernel/include/logger.hrl"). + +-define(CONTENT_TYPE_PROBLEM_JSON, #{<<"content-type">> => "application/problem+json"}). + +init(Req0, State) -> + case cowboy_req:version(Req0) of + 'HTTP/2' -> + {cowboy_rest, Req0, State}; + _ -> + Body = jsx:encode(#{ + title => <<"HTTP/2 is mandatory.">>, + status => 505, + cause => <<"UNSUPPORTED_HTTP_VERSION">> + }), + Req = cowboy_req:reply(505, ?CONTENT_TYPE_PROBLEM_JSON, Body, Req0), + {ok, Req, done} + end. + +allowed_methods(Req, State) -> + {[<<"POST">>], Req, State}. + +content_types_provided(Req, State) -> + {[{<<"application/json">>, handle_request_json}], Req, State}. + +content_types_accepted(Req, State) -> + {[{'*', handle_request_json}], Req, State}. + +delete_resource(Req, State) -> + Path = cowboy_req:path(Req), + Method = cowboy_req:method(Req), + handle_request(Method, Path, Req, State). + +handle_request_json(Req, State) -> + Path = cowboy_req:path(Req), + Method = cowboy_req:method(Req), + handle_request(Method, Path, Req, State). + +%%%=================================================================== +%%% Handler of request +%%%=================================================================== + +handle_request(<<"POST">>, <<"/api/v1/controller">>, Req0, State) -> + {ok, Body, Req} = read_body(Req0), + case validate_json_req(Body) of + {ok, Response} -> + reply(200, Req, jsx:encode(Response), State); + {error, invalid_json} -> + Response = rfc7807(<<"Invalid JSON">>, <<"INVALID_JSON">>, []), + reply(400, Req, Response, State); + {error, InvalidParams} -> + Response = rfc7807(<<"Invalid JSON params">>, <<"INVALID_JSON_PARAM">>, InvalidParams), + reply(400, Req, Response, State) + end. + +%%%=================================================================== +%%% Helper functions +%%%=================================================================== + +validate_json_req(JsonBin) -> + case json_to_map(JsonBin) of + {ok, Map} -> + apply_config(Map); + Error -> + Error + end. + +% Jesse errors: https://github.com/for-GET/jesse/blob/1.5.6/src/jesse_error.erl#L40 +apply_config(Map) -> + case catch ergw_config:reload_config(Map) of + {ok, Config} -> + _ = maps:fold(fun add_config_part/3, #{}, Config), + % @TODO for response collect all keys of config what was successfully applied + {ok, #{type => <<"success">>}}; + {error, [_|_] = Errors} = Reason -> + ?LOG(warning, "~p", [Reason]), + Params = lists:map(fun build_error_params/1, Errors), + {error, Params}; + Error -> + ?LOG(warning, "Unhandled error ~p~nfor config ~p", [Error, Map]), + {error, []} + end. + +build_error_params({Type, Schema, Error, Data, Path}) -> + #{ + type => atom_to_binary(Type), + schema => Schema, + error => atom_to_binary(Error), + data => Data, + path => Path + }. + +add_config_part(K, V, Acc) -> + Result = ergw_config:ergw_core_init(K, #{K => V}), + ?LOG(info, "The ~p added with result ~p", [K, Result]), + maps:merge(Acc, Result). + +json_to_map(JsonBin) -> + case catch jsx:decode(JsonBin) of + #{} = Map -> + {ok, Map}; + _ -> + {error, invalid_json} + end. + +read_body(Req) -> + read_body(Req, <<>>). + +read_body(Req0, Acc) -> + case cowboy_req:read_body(Req0) of + {ok, Data, Req} -> + {ok, <>, Req}; + {more, Data, Req} -> + read_body(Req, <>) + end. + +reply(StatusCode, Req0, Body, State) -> + Req = case StatusCode of + 200 -> + cowboy_req:reply(StatusCode, #{}, Body, Req0); + 400 -> + cowboy_req:reply(StatusCode, ?CONTENT_TYPE_PROBLEM_JSON, Body, Req0) + end, + {stop, Req, State}. + +%% https://datatracker.ietf.org/doc/html/rfc7807 +rfc7807(Title, Cause, []) -> + jsx:encode(#{ + title => Title, + status => 400, + cause => Cause + }); +rfc7807(Title, Cause, InvalidParams) -> + jsx:encode(#{ + title => Title, + status => 400, + cause => Cause, + 'invalid-params' => InvalidParams + }). diff --git a/apps/ergw/test/http_controller_handler_SUITE.erl b/apps/ergw/test/http_controller_handler_SUITE.erl new file mode 100644 index 00000000..c3cf2483 --- /dev/null +++ b/apps/ergw/test/http_controller_handler_SUITE.erl @@ -0,0 +1,180 @@ +%% Copyright 2021, Travelping GmbH + +%% This program is free software; you can redistribute it and/or +%% modify it under the terms of the GNU General Public License +%% as published by the Free Software Foundation; either version +%% 2 of the License, or (at your option) any later version. + +-module(http_controller_handler_SUITE). + +-compile(export_all). + +-include("smc_test_lib.hrl"). +-include("smc_ggsn_test_lib.hrl"). +-include_lib("ergw_core/include/ergw.hrl"). +-include_lib("gtplib/include/gtp_packet.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(HUT, ggsn_gn). + +all() -> + [http_controller_handler_post_ip_pools, + http_controller_handler_post_ip_pools_invalid, + http_controller_handler_post_apns, + http_controller_handler_post_apns_invalid, + http_controller_handler_post_upf_nodes, + http_controller_handler_post_upf_nodes_invalid, + http_controller_handler_post_invalid_json, + http_controller_handler_check_http2_support]. + +%%%=================================================================== +%%% Tests +%%%=================================================================== + +init_per_suite(Config0) -> + Config1 = smc_test_lib:init_ets(Config0), + Config2 = [{handler_under_test, ?HUT}|Config1], + Config = smc_test_lib:group_config(ipv4, Config2), + + [application:load(App) || App <- [cowboy, ergw_core, ergw_aaa]], + smc_test_lib:meck_init(Config), + + Dir = ?config(data_dir, Config), + application:load(ergw), + CfgSet = #{type => json, file => filename:join(Dir, "ggsn.json")}, + application:set_env(ergw, config, CfgSet), + {ok, Started} = application:ensure_all_started(ergw), + ct:pal("Started: ~p", [Started]), + + %ergw:wait_till_running(), %% @TODO ... + inets:start(), + + {ok, JsonBin} = file:read_file(filename:join(Dir, "post_test_data.json")), + TestsPostData = {test_post_data, jsx:decode(JsonBin)}, + + [TestsPostData|Config]. + +end_per_suite(Config) -> + smc_test_lib:meck_unload(Config), + ?config(table_owner, Config) ! stop, + [application:stop(App) || App <- [ergw_core, ergw_aaa, ergw_cluster, ergw]], + inets:stop(), + ok. + +http_controller_handler_post_ip_pools() -> + [{doc, "Check /api/v1/controller success POST API ip_pools"}]. +http_controller_handler_post_ip_pools(Config) -> + Body = prepare_json_body(<<"ip_pools">>, Config), + Resp = gun_post(Body), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 200}, Resp). + +http_controller_handler_post_ip_pools_invalid() -> + [{doc, "Check /api/v1/controller invalid POST API ip_pools"}]. +http_controller_handler_post_ip_pools_invalid(_Config) -> + Body = <<"{\"ip_pools\": \"test\"}">>, + Resp = gun_post(Body), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + #{headers := Headers} = Resp, + ?match(<<"application/problem+json">>, proplists:get_value(<<"content-type">>, Headers)), + ?match(#{status := 400}, Resp). + +http_controller_handler_post_apns() -> + [{doc, "Check /api/v1/controller success POST API apns"}]. +http_controller_handler_post_apns(Config) -> + Body = prepare_json_body(<<"apns">>, Config), + Resp = gun_post(Body), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 200}, Resp). + +http_controller_handler_post_apns_invalid() -> + [{doc, "Check /api/v1/controller invalid POST API apns"}]. +http_controller_handler_post_apns_invalid(_Config) -> + Body = <<"{\"apns\": \"test\"}">>, + Resp = gun_post(Body), + #{headers := Headers} = Resp, + ?match(<<"application/problem+json">>, proplists:get_value(<<"content-type">>, Headers)), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 400}, Resp). + +http_controller_handler_post_upf_nodes() -> + [{doc, "Check /api/v1/controller success POST API UPF nodes"}]. +http_controller_handler_post_upf_nodes(Config) -> + Body = prepare_json_body(<<"upf_nodes">>, Config), + Resp = gun_post(Body), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 200}, Resp). + +http_controller_handler_post_upf_nodes_invalid() -> + [{doc, "Check /api/v1/controller invalid POST API UPF nodes"}]. +http_controller_handler_post_upf_nodes_invalid(_Config) -> + Body = <<"{\"upf_nodes\": \"test\"}">>, + Resp = gun_post(Body), + #{headers := Headers} = Resp, + ?match(<<"application/problem+json">>, proplists:get_value(<<"content-type">>, Headers)), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 400}, Resp). + +http_controller_handler_post_invalid_json() -> + [{doc, "Check /api/v1/controller invalid POST API"}]. +http_controller_handler_post_invalid_json(_Config) -> + Body = <<"text">>, + Resp = gun_post(Body), + #{headers := Headers} = Resp, + ?match(<<"application/problem+json">>, proplists:get_value(<<"content-type">>, Headers)), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match(#{status := 400}, Resp). + +http_controller_handler_check_http2_support() -> + [{doc, "Check /api/v1/controller that the POST API is supported HTTP/2 only"}]. +http_controller_handler_check_http2_support(Config) -> + URL = get_test_url(), + ContentType = "application/json", + Body = prepare_json_body(<<"ip_pools">>, Config), + Resp = httpc:request(post, {URL, [], ContentType, Body}, [], []), + ct:pal("Request~p~nResponse ~p~n", [Body, Resp]), + ?match({ok, {{_, 505, _}, _, _}}, Resp). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +get_test_url() -> + Port = ranch:get_port(ergw_http_listener), + Path = "/api/v1/controller", + lists:flatten(io_lib:format("http://localhost:~w~s", [Port, Path])). + +prepare_json_body(Name, Config) -> + #{Name := Data} = proplists:get_value(test_post_data, Config), + jsx:encode(#{Name => Data}). + +gun_post(Body) -> + Pid = gun_http2(), + Headers = [{<<"content-type">>, <<"application/json">>}], + StreamRef = gun:post(Pid, <<"/api/v1/controller">>, Headers, Body), + Resp = gun_reponse(#{pid => Pid, stream_ref => StreamRef, acc => <<>>}), + ok = gun:close(Pid), + maps:without([pid, stream_ref], Resp). + +gun_http2() -> + Port = ranch:get_port(ergw_http_listener), + Opts = #{http2_opts => #{keepalive => infinity}, protocols => [http2]}, + {ok, Pid} = gun:open("localhost", Port, Opts), + {ok, http2} = gun:await_up(Pid), + Pid. + +gun_reponse(#{pid := Pid, stream_ref := StreamRef, acc := Acc} = Opts) -> + case gun:await(Pid, StreamRef) of + {response, fin, Status, Headers} -> + Opts#{status => Status, headers => Headers}; + {response, nofin, Status, Headers} -> + gun_reponse(Opts#{status => Status, headers => Headers}); + {data, nofin, Data} -> + gun_reponse(Opts#{acc => <>}); + {data, fin, Data} -> + Opts#{acc => <>}; + {error, timeout} = Response -> + Response; + {error, _Reason} = Response -> + Response + end. diff --git a/apps/ergw/test/http_controller_handler_SUITE_data/ggsn.json b/apps/ergw/test/http_controller_handler_SUITE_data/ggsn.json new file mode 100644 index 00000000..f5c95d98 --- /dev/null +++ b/apps/ergw/test/http_controller_handler_SUITE_data/ggsn.json @@ -0,0 +1,967 @@ +{ + "node": { + "accept_new": true, + "node_id": "GGSN", + "plmn_id": { + "mcc": "001", + "mnc": "01" + } + }, + "apns": [ + { + "inactivity_timeout": { + "timeout": 8, + "unit": "hour" + }, + "apn": "example.net", + "bearer_type": "IPv4v6", + "ip_pools": [ + "pool-A" + ], + "ipv6_ue_interface_id": "default", + "prefered_bearer_type": "IPv6", + "vrfs": [ + { + "apn": "sgi" + } + ] + } + ], + "charging": { + "profiles": [ + { + "name": "default", + "offline": { + "enable": true, + "triggers": { + "cgi-sai-change": "container", + "ecgi-change": "container", + "max-cond-change": "cdr", + "ms-time-zone-change": "cdr", + "qos-change": "container", + "rai-change": "container", + "rat-change": "cdr", + "sgsn-sgw-change": "cdr", + "sgsn-sgw-plmn-id-change": "cdr", + "tai-change": "container", + "tariff-switch-change": "container", + "user-location-info-change": "container" + } + }, + "online": {} + } + ], + "rules": [ + { + "name": "r-0001", + "Rating-Group": 3000, + "Flow-Information": [ + { + "Flow-Description": "permit out ip from any to assigned", + "Flow-Direction": 1 + }, + { + "Flow-Description": "permit out ip from any to assigned", + "Flow-Direction": 2 + } + ], + "Metering-Method": 1, + "Precedence": 100, + "Offline": 1 + } + ], + "rulebase": [ + { + "name": "m2m0001", + "rules": [ + "r-0001" + ] + } + ] + }, + "cluster": { + "enabled": false, + "initial_timeout": { + "timeout": 1, + "unit": "minute" + }, + "release_cursor_every": 0, + "seed_nodes": "{erlang,nodes,[known]}" + }, + "handlers": [ + { + "aaa": { + "AAA-Application-Id": "ergw_aaa_provider", + "Password": { + "default": "ergw" + }, + "Username": { + "default": [ + "IMSI", + "/", + "IMEI", + "/", + "MSISDN", + "/", + "ATOM", + "/", + [ + 84, + 69, + 88, + 84 + ], + "/", + 12345, + "@", + "APN" + ], + "from_protocol_opts": true + } + }, + "handler": "ggsn_gn", + "name": "gn-1", + "node_selection": [ + "default" + ], + "protocol": "gn", + "sockets": [ + "irx" + ] + } + ], + "http_api": { + "enabled": true, + "ip": { + "ipv4Addr": "127.0.0.1" + }, + "num_acceptors": 100, + "port": 0 + }, + "ip_pools": [ + { + "DNS-Server-IPv6-Address": [ + "2001:4860:4860::8888", + "2001:4860:4860::8844" + ], + "MS-Primary-DNS-Server": "8.8.8.8", + "MS-Primary-NBNS-Server": "127.0.0.1", + "MS-Secondary-DNS-Server": "8.8.4.4", + "MS-Secondary-NBNS-Server": "127.0.0.1", + "handler": "ergw_local_pool", + "name": "pool-A", + "ranges": [ + { + "end": { + "ipv4Addr": "10.187.255.254" + }, + "prefix_len": 32, + "start": { + "ipv4Addr": "10.180.0.1" + } + }, + { + "end": { + "ipv6Addr": "8001:0:7:ffff:ffff:ffff:ffff:ffff" + }, + "prefix_len": 64, + "start": { + "ipv6Addr": "8001:0:1::" + } + } + ] + } + ], + "metrics": { + "gtp_path_rtt_millisecond_intervals": [ + 10, + 30, + 50, + 75, + 100, + 1000, + 2000 + ] + }, + "node_selection": [ + { + "name": "default", + "type": "static", + "entries": [ + { + "name": "_default.apn.epc.mnc001.mcc001.3gppnetwork.org", + "order": 300, + "preference": 64536, + "protocols": [ + "x-gn", + "x-gp" + ], + "replacement": "topon.s5s8.pgw.epc.mnc001.mcc001.3gppnetwork.org", + "service": "x-3gpp-ggsn", + "type": "naptr" + }, + { + "name": "_default.apn.epc.mnc001.mcc001.3gppnetwork.org", + "order": 300, + "preference": 64536, + "protocols": [ + "x-sxb" + ], + "replacement": "topon.sx.prox01.epc.mnc001.mcc001.3gppnetwork.org", + "service": "x-3gpp-upf", + "type": "naptr" + }, + { + "ip4": [ + "127.0.200.1" + ], + "ip6": [], + "name": "topon.gn.ggsn.epc.mnc001.mcc001.3gppnetwork.org", + "type": "host" + }, + { + "ip4": [ + "127.0.200.1" + ], + "ip6": [], + "name": "topon.sx.prox01.epc.mnc001.mcc001.3gppnetwork.org", + "type": "host" + }, + { + "ip4": [ + "127.0.200.2" + ], + "ip6": [], + "name": "topon.sx.prox02.epc.mnc001.mcc001.3gppnetwork.org", + "type": "host" + } + ] + } + ], + "upf_nodes": { + "default": { + "heartbeat": { + "interval": { + "timeout": 5, + "unit": "second" + }, + "retry": 5, + "timeout": { + "timeout": 500, + "unit": "millisecond" + } + }, + "ue_ip_pools": [ + {"ip_pools": ["pool-A"], + "vrf": { + "apn": "sgi" + }, + "ip_versions": ["v4", "v6"] + } + ], + "node_selection": "default", + "request": { + "retry": 5, + "timeout": { + "timeout": 30, + "unit": "second" + } + }, + "vrfs": [ + { + "features": [ + "SGi-LAN" + ], + "name": { + "apn": "sgi" + } + }, + { + "features": [ + "Access" + ], + "name": { + "apn": "irx" + } + }, + { + "features": [ + "CP-Function" + ], + "name": { + "apn": "cp" + } + } + ] + } + }, + "path_management": { + "down": { + "echo": { + "timeout": 10, + "unit": "minute" + }, + "timeout": { + "timeout": 1, + "unit": "hour" + } + }, + "idle": { + "echo": { + "timeout": 10, + "unit": "minute" + }, + "timeout": { + "timeout": 30, + "unit": "minute" + } + }, + "busy": { + "echo": { + "timeout": 1, + "unit": "minute" + }, + "n3": 5, + "t3": 10000, + "events": { + "icmp_error": "warning" + } + } + }, + "sockets": [ + { + "burst_size": 10, + "ip": { + "ipv4Addr": "127.0.0.1" + }, + "name": "sx", + "reuseaddr": true, + "socket": "cp", + "type": "pfcp" + }, + { + "burst_size": 10, + "ip": { + "ipv4Addr": "127.0.0.1" + }, + "name": "irx", + "reuseaddr": true, + "send_port": 0, + "type": "gtp-c", + "vrf": { + "apn": "irx" + } + }, + { + "burst_size": 10, + "freebind": true, + "ip": { + "ipv4Addr": "127.0.0.1" + }, + "name": "cp", + "reuseaddr": true, + "send_port": 0, + "type": "gtp-u", + "vrf": { + "apn": "cp" + } + } + ], + "teid": { + "len": 0, + "prefix": 0 + }, + "sbi_client": { + "upf_selection": { + "endpoint": "https://example.com/nf-selection-api/v1", + "timeout": { + "timeout": 500, + "unit": "millisecond" + }, + "default": "fallback" + } + }, + "aaa": { + "apps": [ + { + "application": "default", + "procedures": [ + { + "api": "gy", + "procedure": "CCR-Update", + "steps": [] + }, + { + "api": "gy", + "procedure": "CCR-Terminate", + "steps": [] + }, + { + "api": "gy", + "procedure": "CCR-Initial", + "steps": [] + }, + { + "api": "gx", + "procedure": "CCR-Update", + "steps": [ + { + "answer": "Update-Gx", + "service": "Default" + } + ] + }, + { + "api": "gx", + "procedure": "CCR-Terminate", + "steps": [ + { + "answer": "Final-Gx", + "service": "Default" + } + ] + }, + { + "api": "gx", + "procedure": "CCR-Initial", + "steps": [ + { + "answer": "Initial-Gx", + "service": "Default" + } + ] + }, + { + "procedure": "stop", + "steps": [] + }, + { + "procedure": "start", + "steps": [] + }, + { + "procedure": "interim", + "steps": [] + }, + { + "procedure": "init", + "steps": [ + { + "service": "Default" + } + ] + }, + { + "procedure": "authorize", + "steps": [] + }, + { + "procedure": "authenticate", + "steps": [] + } + ] + } + ], + "handlers": [ + { + "defaults": { + "Charging-Rule-Base-Name": "m2m0001", + "NAS-Identifier": "NAS-Identifier", + "Node-Id": "PGW-001" + }, + "handler": "ergw_aaa_static" + } + ], + "services": [ + { + "answers": [ + { + "name": "Final-Gx", + "avps": { + "Result-Code": 2001 + } + }, + { + "name": "Final-OCS", + "avps": { + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Base-Name": [ + "m2m0001" + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-Fail-1", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Base-Name": [ + "m2m0001", + "unknown-rulebase" + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-Fail-2", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Name": [ + "r-0001", + "unknown-rule" + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-Redirect", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Definition": [ + { + "Charging-Rule-Name": "m2m", + "Flow-Information": [ + { + "Flow-Description": [ + "permit out ip from any to assigned" + ], + "Flow-Direction": [ + 1 + ] + }, + { + "Flow-Description": [ + "permit out ip from any to assigned" + ], + "Flow-Direction": [ + 2 + ] + } + ], + "Metering-Method": [ + 1 + ], + "Offline": [ + 1 + ], + "Precedence": [ + 100 + ], + "Rating-Group": [ + 3000 + ], + "Redirect-Information": [ + { + "Redirect-Address-Type": [ + 2 + ], + "Redirect-Server-Address": [ + "http://www.heise.de/" + ], + "Redirect-Support": [ + 1 + ] + } + ] + } + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-Split1", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Base-Name": [ + "m2m0001-split1" + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-Split2", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Base-Name": [ + "m2m0001-split2" + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-Gx-TDF-App", + "avps": { + "Charging-Rule-Install": [ + { + "Charging-Rule-Definition": [ + { + "Charging-Rule-Name": "m2m", + "Metering-Method": [ + 1 + ], + "Offline": [ + 1 + ], + "Precedence": [ + 100 + ], + "Rating-Group": [ + 3000 + ], + "TDF-Application-Identifier": [ + "Gold" + ] + } + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-OCS", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 3600 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-OCS-TTC", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ], + "Tariff-Time-Change": [ + "2019-08-26T14:14:00Z" + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Tariff-Time-Change": [ + "2019-08-26T14:14:00Z" + ], + "Time-Quota-Threshold": [ + 60 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Initial-OCS-VT", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 2 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Update-Gx", + "avps": { + "Result-Code": 2001 + } + }, + { + "name": "Update-OCS", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 3600 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Update-OCS-Fail", + "avps": { + "Result-Code": 3001 + } + }, + { + "name": "Update-OCS-GxGy", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 3600 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + }, + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 4000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 3600 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Update-OCS-TTC", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ], + "Tariff-Time-Change": [ + "2019-08-26T14:14:00Z" + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + }, + { + "name": "Update-OCS-VT", + "avps": { + "Multiple-Services-Credit-Control": [ + { + "Envelope-Reporting": [ + 0 + ], + "Granted-Service-Unit": [ + { + "CC-Time": [ + 3600 + ], + "CC-Total-Octets": [ + 102400 + ] + } + ], + "Rating-Group": [ + 3000 + ], + "Result-Code": [ + 2001 + ], + "Time-Quota-Threshold": [ + 60 + ], + "Validity-Time": [ + 2 + ], + "Volume-Quota-Threshold": [ + 10240 + ] + } + ], + "Result-Code": 2001 + } + } + ], + "defaults": { + "Charging-Rule-Base-Name": "m2m0001", + "NAS-Identifier": "NAS-Identifier", + "Node-Id": "PGW-001" + }, + "handler": "ergw_aaa_static", + "service": "Default" + } + ] + } +} diff --git a/apps/ergw/test/http_controller_handler_SUITE_data/post_test_data.json b/apps/ergw/test/http_controller_handler_SUITE_data/post_test_data.json new file mode 100644 index 00000000..90d9e2a6 --- /dev/null +++ b/apps/ergw/test/http_controller_handler_SUITE_data/post_test_data.json @@ -0,0 +1,113 @@ +{ + "apns": [ + { + "inactivity_timeout": { + "timeout": 8, + "unit": "hour" + }, + "apn": "example.net", + "bearer_type": "IPv4v6", + "ip_pools": [ + "pool-A" + ], + "ipv6_ue_interface_id": "default", + "prefered_bearer_type": "IPv6", + "vrfs": [ + { + "apn": "sgi" + } + ] + } + ], + "ip_pools": [ + { + "DNS-Server-IPv6-Address": [ + "2001:4860:4860::8888", + "2001:4860:4860::8844" + ], + "MS-Primary-DNS-Server": "8.8.8.8", + "MS-Primary-NBNS-Server": "127.0.0.1", + "MS-Secondary-DNS-Server": "8.8.4.4", + "MS-Secondary-NBNS-Server": "127.0.0.1", + "handler": "ergw_local_pool", + "name": "pool-A", + "ranges": [ + { + "end": { + "ipv4Addr": "10.187.255.254" + }, + "prefix_len": 32, + "start": { + "ipv4Addr": "10.180.0.1" + } + }, + { + "end": { + "ipv6Addr": "8001:0:7:ffff:ffff:ffff:ffff:ffff" + }, + "prefix_len": 64, + "start": { + "ipv6Addr": "8001:0:1::" + } + } + ] + } + ], + "upf_nodes": { + "default": { + "heartbeat": { + "interval": { + "timeout": 5, + "unit": "second" + }, + "retry": 5, + "timeout": { + "timeout": 500, + "unit": "millisecond" + } + }, + "ue_ip_pools": [ + {"ip_pools": ["pool-A"], + "vrf": { + "apn": "sgi" + }, + "ip_versions": ["v4", "v6"] + } + ], + "node_selection": "default", + "request": { + "retry": 5, + "timeout": { + "timeout": 30, + "unit": "second" + } + }, + "vrfs": [ + { + "features": [ + "SGi-LAN" + ], + "name": { + "apn": "sgi" + } + }, + { + "features": [ + "Access" + ], + "name": { + "apn": "irx" + } + }, + { + "features": [ + "CP-Function" + ], + "name": { + "apn": "cp" + } + } + ] + } + } +}