%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et: %% @doc EC2 Erlang node discovery. %% @end -module(nodefinder_ec2). -behaviour(gen_server). %% external interface -export([start_link/5, discover/1, validate_groups/1, validate_tags/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include_lib("erlcloud/include/erlcloud_aws.hrl"). -type group_input() :: string(). -type tag_input() :: {list(string()) | string(), list(string()) | string()} | list(string()) | string(). -type condition(Value) :: {'AND', list(Value)} | {'OR', list(Value)}. -type condition_meta(Value) :: condition(condition(condition(Value))). -type tag() :: condition_meta(tag_input()) | tag_input(). -type group() :: condition_meta(group_input()) | group_input(). -export_type([group/0, tag/0]). -type tag_output() :: list({string(), list(string())}). -type group_output() :: string(). -type tag_value() :: condition_meta(tag_output()). -type group_value() :: condition_meta(group_output()). -record(state, { ec2_config, ec2_instances, ec2_tagged_instances, groups :: list(group_value()), tags :: list(tag_value()), nodes :: list(node()), connect :: visible | hidden }). -define(NULL_EXPRESSION, [{'OR', []}]). %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ % finds nodes with security group id OR tags (union of both sets) -spec start_link(AccessKeyID :: string(), SecretAccessKey :: string(), EC2Host :: string(), Groups :: list(group()), Tags :: list(tag())) -> {ok, pid()} | ignore | {error, any()}. start_link(AccessKeyID, SecretAccessKey, EC2Host, Groups, Tags) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [AccessKeyID, SecretAccessKey, EC2Host, Groups, Tags], []). -spec discover(Timeout :: pos_integer()) -> ok | {error, ec2_connect_failed | ec2_connect_timeout | ec2_unavailable | discover_failed | timeout | noproc | any()}. discover(Timeout) -> try gen_server:call(?MODULE, discover, Timeout) catch exit:{Reason, _} -> {error, Reason} end. -spec validate_groups(Groups :: list(group())) -> ok | {error, any()}. validate_groups(Groups) when is_list(Groups) -> case preprocess(Groups, group) of {ok, _} -> ok; {error, _} = Error -> Error end; validate_groups(_) -> {error, invalid_type}. -spec validate_tags(Tags :: list(tag())) -> ok | {error, any()}. validate_tags(Tags) when is_list(Tags) -> case preprocess(Tags, tag) of {ok, _} -> ok; {error, _} = Error -> Error end; validate_tags(_) -> {error, invalid_type}. %%%------------------------------------------------------------------------ %%% Callback functions from gen_server %%%------------------------------------------------------------------------ init([AccessKeyID, SecretAccessKey, EC2Host, Groups, Tags]) -> Config = erlcloud_ec2:new(AccessKeyID, SecretAccessKey, EC2Host), NewConfig = Config#aws_config{http_client = httpc}, Connect = nodefinder_app:connect_type(), case preprocess(Tags, tag) of {ok, TagsExpressionTree} -> case preprocess(Groups, group) of {ok, GroupsExpressionTree} when TagsExpressionTree == ?NULL_EXPRESSION, GroupsExpressionTree == ?NULL_EXPRESSION -> {stop, {error, null_selection}}; {ok, GroupsExpressionTree} -> case do_discover(#state{ec2_config = NewConfig, groups = GroupsExpressionTree, tags = TagsExpressionTree, nodes = [], connect = Connect}) of {ok, #state{}} = Success -> Success; {error, _} = Error -> {stop, Error} end; {error, _} = Error -> {stop, Error} end; {error, _} = Error -> {stop, Error} end. handle_call(discover, _From, State) -> case do_discover(State) of {ok, NewState} -> {reply, ok, NewState}; {error, {socket_error, {failed_connect, _}}} -> {reply, {error, ec2_connect_failed}, State}; {error, {socket_error, socket_closed_remotely}} -> {reply, {error, ec2_connect_failed}, State}; {error, {socket_error, timeout}} -> {reply, {error, ec2_connect_timeout}, State}; {error, {http_error, 503, "Service Unavailable", _}} -> {reply, {error, ec2_unavailable}, State}; {error, {http_error, 500, "Internal Server Error", XMLBinary}} -> error_logger:error_msg("ec2 error: ~s~n", [XMLBinary]), {reply, {error, ec2_failed}, State}; {error, _} = Error -> {stop, Error, {error, discover_failed}, State} end; handle_call(Request, _From, State) -> {stop, lists:flatten(io_lib:format("Unknown call \"~p\"", [Request])), error, State}. handle_cast(Request, State) -> {stop, lists:flatten(io_lib:format("Unknown cast \"~p\"", [Request])), State}. handle_info(Request, State) -> {stop, lists:flatten(io_lib:format("Unknown info \"~p\"", [Request])), State}. terminate(_Reason, #state{}) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ preprocess_set_cleanup([], [], L2, _) -> lists:reverse(L2); preprocess_set_cleanup([], Merged, L2, Condition) -> [{Condition, Merged} | lists:reverse(L2)]; preprocess_set_cleanup([{Condition, L1} | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged ++ L1, L2, Condition); preprocess_set_cleanup([{'AND', []} | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged, L2, Condition); preprocess_set_cleanup([{'AND', _} = Entry | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged, [Entry | L2], Condition); preprocess_set_cleanup([{'OR', []} | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged, L2, Condition); preprocess_set_cleanup([{'OR', _} = Entry | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged, [Entry | L2], Condition); preprocess_set_cleanup([Entry | L0], Merged, L2, Condition) -> preprocess_set_cleanup(L0, Merged, [Entry | L2], Condition). preprocess_set_cleanup(L, Condition) -> preprocess_set_cleanup(L, [], [], Condition). preprocess_set_element_tags_value([]) -> ok; preprocess_set_element_tags_value([[I | _] | Values]) when is_integer(I) -> preprocess_set_element_tags_value(Values); preprocess_set_element_tags_value([Invalid | _]) -> {error, {tag_value, Invalid}}. preprocess_set_element_tags_key([], _, Lookup) -> {ok, Lookup}; preprocess_set_element_tags_key([[I | _] = Key | Keys], Values, Lookup) when is_integer(I) -> preprocess_set_element_tags_key(Keys, Values, orddict:store(Key, Values, Lookup)); preprocess_set_element_tags_key([Invalid | _], _, _) -> {error, {tag_key, Invalid}}. preprocess_set_element_tags(Keys, Values) when is_list(Keys), is_list(Values) -> case preprocess_set_element_tags_value(Values) of ok -> preprocess_set_element_tags_key(lists:usort(Keys), lists:usort(Values), orddict:new()); {error, _} = Error -> Error end. preprocess_set_element([I | _] = Group, group) when is_integer(I) -> {ok, Group}; preprocess_set_element([I | _] = Key, tag) when is_integer(I) -> preprocess_set_element_tags([Key], []); preprocess_set_element([[I | _] | _] = Keys, tag) when is_integer(I) -> preprocess_set_element_tags(Keys, []); preprocess_set_element({[I0 | _] = Key, [I1 | _] = Value}, tag) when is_integer(I0), is_integer(I1) -> preprocess_set_element_tags([Key], [Value]); preprocess_set_element({[[I0 | _] | _] = Keys, [I1 | _] = Value}, tag) when is_integer(I0), is_integer(I1) -> preprocess_set_element_tags(Keys, [Value]); preprocess_set_element({[I0 | _] = Key, [[I1 | _] | _] = Values}, tag) when is_integer(I0), is_integer(I1) -> preprocess_set_element_tags([Key], Values); preprocess_set_element({[[I0 | _] | _] = Keys, [[I1 | _] | _] = Values}, tag) when is_integer(I0), is_integer(I1) -> preprocess_set_element_tags(Keys, Values); preprocess_set_element(Entry, Type) -> {error, {Type, Entry}}. % set the hierarchy of the boolean expression (structure) preprocess_set([], L2, _Type) -> {ok, lists:reverse(L2)}; preprocess_set([{'AND', L1} | L0], L2, Type) -> case preprocess_set(preprocess_set_cleanup(L1, 'AND'), Type) of {ok, [{'AND', NewL1H} | NewL1T]} -> % merge ANDs at the same level preprocess_set(L0, [{'AND', NewL1H ++ NewL1T} | L2], Type); {ok, NewL1} -> preprocess_set(L0, [{'AND', NewL1} | L2], Type); {error, _} = Error -> Error end; preprocess_set([{'OR', L1} | L0], L2, Type) -> case preprocess_set(preprocess_set_cleanup(L1, 'OR'), Type) of {ok, [{'OR', NewL1H} | NewL1T]} -> % merge ORs at the same level preprocess_set(L0, [{'OR', NewL1H ++ NewL1T} | L2], Type); {ok, NewL1} -> preprocess_set(L0, [{'OR', NewL1} | L2], Type); {error, _} = Error -> Error end; preprocess_set([Entry | L0], L2, Type) -> case preprocess_set_element(Entry, Type) of {ok, NewEntry} -> preprocess_set(L0, [NewEntry | L2], Type); {error, _} = Error -> Error end. preprocess_set(L, Type) -> preprocess_set(L, [], Type). preprocess([{'OR', ORs}] = L, Type) when is_list(ORs) -> preprocess_set(L, Type); preprocess(L, Type) -> preprocess_set([{'OR', L}], Type). process_filter(Group, EC2Instances, group) -> lists:filter(fun(Reservation) -> {_, InstancesSet} = lists:keyfind(instances_set, 1, Reservation), lists:any(fun(Instance) -> {_, GroupSet} = lists:keyfind(group_set, 1, Instance), lists:any(fun(GroupSetElement) -> {_, GroupName} = lists:keyfind(group_name, 1, GroupSetElement), Group == GroupName end, GroupSet) end, InstancesSet) end, EC2Instances); process_filter(Tags, EC2Tags, tag) -> orddict:filter(fun(_, TagL) -> % do any of the tags for an instance match a single requirement lists:any(fun({Key, Value}) -> case orddict:find(Key, Tags) of {ok, []} -> true; {ok, Values} -> lists:member(Value, Values); error -> false end end, TagL) end, EC2Tags). process_and([], EC2Data, _Type) -> EC2Data; process_and([{'AND', L1} | L0], EC2Data, Type) -> process_and(L0, process_and(L1, EC2Data, Type), Type); process_and([{'OR', L1} | L0], EC2Data, Type) -> process_and(L0, process_or(L1, EC2Data, Type), Type); process_and([Data | L0], EC2Data, Type) -> process_and(L0, process_filter(Data, EC2Data, Type), Type). process_or([], EC2DataOut, _EC2DataIn, _Type) -> EC2DataOut; process_or([{'AND', L1} | L0], EC2DataOut, EC2DataIn, Type) -> NewEC2DataOut = lists:umerge(process_and(L1, EC2DataIn, Type), EC2DataOut), process_or(L0, NewEC2DataOut, EC2DataIn, Type); process_or([{'OR', L1} | L0], EC2DataOut, EC2DataIn, Type) -> NewEC2DataOut = lists:umerge(process_or(L1, EC2DataIn, Type), EC2DataOut), process_or(L0, NewEC2DataOut, EC2DataIn, Type); process_or([Data | L0], EC2DataOut, EC2DataIn, Type) -> NewEC2DataOut = lists:umerge(process_filter(Data, EC2DataIn, Type), EC2DataOut), process_or(L0, NewEC2DataOut, EC2DataIn, Type). process_or(L, EC2Data, Type) -> process_or(L, [], EC2Data, Type). process([{'OR', L}], EC2Instances, group) -> % output list of private dns names lists:foldl(fun(Reservation, Hosts) -> {_, InstancesSet} = lists:keyfind(instances_set, 1, Reservation), update_from_instances_set(InstancesSet, Hosts) end, [], process_or(L, lists:usort(EC2Instances), group)); process([{'OR', L}], EC2Instances, tag) -> % output list of instance ids EC2Tags = lists:foldl(fun(Reservation, D0) -> {_, InstancesSet} = lists:keyfind(instances_set, 1, Reservation), lists:foldl(fun(Instance, D1) -> {_, Id} = lists:keyfind(instance_id, 1, Instance), {_, TagsSet} = lists:keyfind(tag_set, 1, Instance), lists:foldl(fun(Tag, D2) -> {_, Key} = lists:keyfind(key, 1, Tag), {_, Value} = lists:keyfind(value, 1, Tag), InitialValue = [{Key, Value}], orddict:update(Id, fun(OldTagL) -> lists:umerge(OldTagL, InitialValue) end, InitialValue, D2) end, D1, TagsSet) end, D0, InstancesSet) end, orddict:new(), EC2Instances), [Id || {Id, _} <- orddict:to_list(process_or(L, EC2Tags, tag))]. node_names(Hosts, #state{} = State) -> Name = string:sub_word(erlang:atom_to_list(node()), 1, $@), Nodes = lists:foldl(fun(Host, L) -> lists:umerge(L, [erlang:list_to_atom(Name ++ [$@ | Host])]) end, [], Hosts), {ok, Nodes, State}. ec2_instances_get(#state{ec2_config = Config, ec2_instances = OldResult} = State) -> case erlcloud_ec2:describe_instances(Config) of {ok, OldResult} -> {ok, false, State}; {ok, NewResult} -> {ok, true, State#state{ec2_instances = NewResult}}; {error, _} = Error -> Error end. ec2_tagged_instances_get(#state{tags = ?NULL_EXPRESSION} = State) -> {ok, false, State}; ec2_tagged_instances_get(#state{ec2_instances = EC2Instances, ec2_tagged_instances = OldResult, tags = Tags} = State) -> case process(Tags, EC2Instances, tag) of OldResult -> {ok, false, State}; NewResult -> {ok, true, State#state{ec2_tagged_instances = NewResult}} end. update_from_instance(Instance, Hosts) -> {_, Host} = lists:keyfind(private_dns_name, 1, Instance), lists:umerge(Hosts, [Host]). update_from_instances_set([], Hosts) -> Hosts; update_from_instances_set([Instance | InstancesSet], Hosts) -> update_from_instances_set(InstancesSet, update_from_instance(Instance, Hosts)). update_from_instances_set([], Hosts, _) -> Hosts; update_from_instances_set([Instance | InstancesSet], Hosts, F) -> NextHosts = case F(Instance) of true -> update_from_instance(Instance, Hosts); false -> Hosts end, update_from_instances_set(InstancesSet, NextHosts, F). update_from_groups(#state{ec2_instances = EC2Instances, groups = Groups} = State) -> HostsFound = process(Groups, EC2Instances, group), node_names(HostsFound, State). update_from_tags(#state{ec2_instances = Instances, ec2_tagged_instances = TaggedInstances} = State) -> HostsFound = lists:foldl(fun(InstanceId, Hosts) -> Check = fun(Instance) -> case lists:keyfind(instance_id, 1, Instance) of {_, InstanceId} -> true; {_, _} -> false end end, lists:foldl(fun(Reservation, NextHosts) -> {_, InstancesSet} = lists:keyfind(instances_set, 1, Reservation), update_from_instances_set(InstancesSet, NextHosts, Check) end, Hosts, Instances) end, [], TaggedInstances), node_names(HostsFound, State). updates_gather(true, true, State) -> case update_from_groups(State) of {ok, Nodes0, NextState} -> case update_from_tags(NextState) of {ok, Nodes1, NewState} -> {ok, lists:umerge(Nodes0, Nodes1), NewState} end end; updates_gather(true, false, #state{nodes = OldNodes} = State) -> case update_from_groups(State) of {ok, Nodes, NewState} -> {ok, lists:umerge(Nodes, OldNodes), NewState} end; updates_gather(false, true, #state{nodes = OldNodes} = State) -> case update_from_tags(State) of {ok, Nodes, NewState} -> {ok, lists:umerge(Nodes, OldNodes), NewState} end; updates_gather(false, false, #state{nodes = OldNodes} = State) -> {ok, OldNodes, State}. update_nodes(Nodes, #state{connect = Connect} = State) -> ConnectNodes = lists:subtract(Nodes, nodes()), pforeach(fun(Node) -> % avoid the possibly long synchronous call here connect_node(Connect, Node) end, ConnectNodes), State#state{nodes = Nodes}. update(UpdateGroups, UpdateTags, State) -> case updates_gather(UpdateGroups, UpdateTags, State) of {ok, Nodes, NewState} -> {ok, update_nodes(Nodes, NewState)} end. do_discover(#state{} = State) -> case ec2_instances_get(State) of {ok, UpdatedGroups, NextState} -> {ok, UpdatedTags, NewState} = ec2_tagged_instances_get(NextState), update(UpdatedGroups, UpdatedTags, NewState); {error, _} = Error -> Error end. connect_node(visible, Node) -> net_kernel:connect_node(Node); connect_node(hidden, Node) -> net_kernel:hidden_connect_node(Node). pforeach(_, []) -> ok; pforeach(F, L) -> [erlang:spawn_link(fun() -> F(E) end) || E <- L], ok. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). logic_case1_tags() -> [[{instances_set, [[{instance_id, "A1"}, {tag_set, [[{key, "A"}, {value, "A"}]]}], [{instance_id, "A2"}, {tag_set, [[{key, "A"}, {value, "B"}]]}]]}], [{instances_set, [[{instance_id, "B1"}, {tag_set, [[{key, "B"}, {value, "A"}]]}]]}], [{instances_set, [[{instance_id, "C1"}, {tag_set, [[{key, "C"}, {value, "A"}]]}], [{instance_id, "C2"}, {tag_set, [[{key, "C"}, {value, "B"}]]}], [{instance_id, "C3"}, {tag_set, [[{key, "C"}, {value, "C"}]]}], [{instance_id, "C4"}, {tag_set, [[{key, "C"}, {value, "C"}]]}], [{instance_id, "C5"}, {tag_set, [[{key, "C"}, {value, "C"}]]}]]}]]. logic_case1_tags_preprocess(TagsInput) -> true = is_list(TagsInput), {ok, TagsOutput} = preprocess(TagsInput, tag), TagsOutput. logic_case1_tags_process(TagsInput) -> process(logic_case1_tags_preprocess(TagsInput), logic_case1_tags(), tag). logic_case2_groups() -> [[{instances_set, [[{group_set, [[{group_name, "A"}], [{group_name, "A/A"}]]}, {private_dns_name, "A1"}]]}], [{instances_set, [[{group_set, [[{group_name, "A"}], [{group_name, "A/B"}]]}, {private_dns_name, "A2"}]]}], [{instances_set, [[{group_set, [[{group_name, "B"}], [{group_name, "B/A"}]]}, {private_dns_name, "B1"}]]}], [{instances_set, [[{group_set, [[{group_name, "C"}], [{group_name, "C/A"}]]}, {private_dns_name, "C1"}]]}], [{instances_set, [[{group_set, [[{group_name, "C"}], [{group_name, "C/B"}]]}, {private_dns_name, "C2"}]]}], [{instances_set, [[{group_set, [[{group_name, "C"}], [{group_name, "C/C"}]]}, {private_dns_name, "C3"}]]}], [{instances_set, [[{group_set, [[{group_name, "C"}], [{group_name, "C/C"}]]}, {private_dns_name, "C4"}]]}], [{instances_set, [[{group_set, [[{group_name, "C"}], [{group_name, "C/C"}]]}, {private_dns_name, "C5"}]]}]]. logic_case2_groups_preprocess(GroupsInput) -> true = is_list(GroupsInput), {ok, GroupsOutput} = preprocess(GroupsInput, group), GroupsOutput. logic_case2_groups_process(GroupsInput) -> process(logic_case2_groups_preprocess(GroupsInput), logic_case2_groups(), group). logic1_case1_test() -> TagsInput1 = ["C"], TagsInput2 = [["C"]], TagsInput3 = [{'OR', ["C"]}], TagsInput4 = [{'OR', [["C"]]}], [{'OR',[[{"C",[]}]]}] = logic_case1_tags_preprocess(TagsInput1), [{'OR',[[{"C",[]}]]}] = logic_case1_tags_preprocess(TagsInput2), [{'OR',[[{"C",[]}]]}] = logic_case1_tags_preprocess(TagsInput3), [{'OR',[[{"C",[]}]]}] = logic_case1_tags_preprocess(TagsInput4), ["C1", "C2", "C3", "C4", "C5"] = logic_case1_tags_process(TagsInput1), ["C1", "C2", "C3", "C4", "C5"] = logic_case1_tags_process(TagsInput2), ["C1", "C2", "C3", "C4", "C5"] = logic_case1_tags_process(TagsInput3), ["C1", "C2", "C3", "C4", "C5"] = logic_case1_tags_process(TagsInput4), ok. logic2_case1_test() -> TagsInput1 = [{'AND', [{"C", "B"}, "C"]}], TagsInput2 = [{'AND', ["C", {"C", "B"}]}], TagsInput3 = [{'AND', ["B", {"C", "B"}]}], [{'OR',[{'AND',[[{"C",["B"]}],[{"C",[]}]]}]} ] = logic_case1_tags_preprocess(TagsInput1), [{'OR',[{'AND',[[{"C",[]}],[{"C",["B"]}]]}]} ] = logic_case1_tags_preprocess(TagsInput2), [{'OR',[{'AND',[[{"B",[]}],[{"C",["B"]}]]}]} ] = logic_case1_tags_preprocess(TagsInput3), ["C2"] = logic_case1_tags_process(TagsInput1), ["C2"] = logic_case1_tags_process(TagsInput2), [] = logic_case1_tags_process(TagsInput3), ok. logic3_case1_test() -> TagsInput1 = [{'OR', [{'AND', [{"C", "B"}, "C"]}, {'AND', ["A", {"A", "B"}]}]}], [{'OR',[{'AND',[[{"C",["B"]}],[{"C",[]}]]}, {'AND',[[{"A",[]}],[{"A",["B"]}]]}]} ] = logic_case1_tags_preprocess(TagsInput1), ["A2", "C2"] = logic_case1_tags_process(TagsInput1), ok. logic4_case1_test() -> TagsInput1 = [{'AND', [{"C", ["C"]}, "C"]}, {'AND', [{'OR', ["C", "C", "C", "C"]}, {"C", ["B"]}]}, {'AND', ["A", {["A", "B", "C"], ["A", "B"]}, {"A", "B"}]}], [{'OR',[{'AND',[[{"C",["C"]}],[{"C",[]}]]}, {'AND',[{'OR',[[{"C",[]}],[{"C",[]}],[{"C",[]}],[{"C",[]}]]}, [{"C",["B"]}]]}, {'AND',[[{"A",[]}], [{"A",["A","B"]}, {"B",["A","B"]}, {"C",["A","B"]}], [{"A",["B"]}]]}]} ] = logic_case1_tags_preprocess(TagsInput1), ["A2", "C2", "C3", "C4", "C5"] = logic_case1_tags_process(TagsInput1), ok. logic5_case1_test() -> TagsInput1 = [{'OR', [{"C", ["C"]}, "C"]}, {'OR', [{'OR', ["C", "C", "C", "C"]}, {"C", ["B"]}]}, {'OR', ["A", {["A", "B", "C"], ["A", "B"]}, {"A", "B"}]}], [{'OR',[[{"C",[]}], [{"C",[]}], [{"C",[]}], [{"C",[]}], [{"C",["C"]}], [{"C",[]}], [{"C",["B"]}], [{"A",[]}], [{"A",["A","B"]},{"B",["A","B"]},{"C",["A","B"]}], [{"A",["B"]}]]} ] = logic_case1_tags_preprocess(TagsInput1), ["A1","A2","B1","C1","C2","C3","C4","C5" ] = logic_case1_tags_process(TagsInput1), ok. logic1_case2_test() -> GroupsInput1 = ["C"], GroupsInput2 = [{'OR', ["C"]}], [{'OR',["C"]}] = logic_case2_groups_preprocess(GroupsInput1), [{'OR',["C"]}] = logic_case2_groups_preprocess(GroupsInput2), ["C1", "C2", "C3", "C4", "C5"] = logic_case2_groups_process(GroupsInput1), ["C1", "C2", "C3", "C4", "C5"] = logic_case2_groups_process(GroupsInput2), ok. logic2_case2_test() -> GroupsInput1 = [{'AND', ["C/B", "C"]}], GroupsInput2 = [{'AND', ["C", "C/B"]}], GroupsInput3 = [{'AND', ["B", "C/B"]}], [{'OR',[{'AND',["C/B","C"]}]} ] = logic_case2_groups_preprocess(GroupsInput1), [{'OR',[{'AND',["C","C/B"]}]} ] = logic_case2_groups_preprocess(GroupsInput2), [{'OR',[{'AND',["B","C/B"]}]} ] = logic_case2_groups_preprocess(GroupsInput3), ["C2"] = logic_case2_groups_process(GroupsInput1), ["C2"] = logic_case2_groups_process(GroupsInput2), [] = logic_case2_groups_process(GroupsInput3), ok. logic3_case2_test() -> GroupsInput1 = [{'OR', [{'AND', ["C/B", "C"]}, {'AND', ["A", "A/B"]}]}], [{'OR',[{'AND',["C/B","C"]}, {'AND',["A","A/B"]}]} ] = logic_case2_groups_preprocess(GroupsInput1), ["A2", "C2"] = logic_case2_groups_process(GroupsInput1), ok. logic4_case2_test() -> GroupsInput1 = [{'AND', ["C/C", "C"]}, {'AND', [{'OR', ["C", "C", "C", "C"]}, "C/B"]}, {'AND', ["A", {'OR', ["A/A", "A/B", "B/A", "B/B", "C/A", "C/B"]}, "A/B"]}], [{'OR',[{'AND',["C/C","C"]}, {'AND',[{'OR',["C","C","C","C"]}, "C/B"]}, {'AND',["A", {'OR', ["A/A", "A/B", "B/A", "B/B", "C/A", "C/B"]}, "A/B"]}]} ] = logic_case2_groups_preprocess(GroupsInput1), ["A2", "C2", "C3", "C4", "C5"] = logic_case2_groups_process(GroupsInput1), ok. merge1_test() -> TagsInput = [{'AND', [{"foobear", "1"}, "foobarish"]}], {ok, TagsExpressionTree} = preprocess(TagsInput, tag), Tags = TagsExpressionTree, EC2Instances = [[{instances_set, [[{instance_id, "instanceB"}, {tag_set, [[{key, "foobarish"}, {value, ""}]]}]]}], [{instances_set, [[{instance_id, "instanceA"}, {tag_set, [[{key, "foobear"}, {value, "1"}], [{key, "foobarish"}, {value, ""}]]}]]}]], TaggedInstances = process(Tags, EC2Instances, tag), ["instanceA"] = TaggedInstances, ok. -endif.