├── .gitignore
├── Makefile
├── start.sh
├── src
├── spawnpool.app.src
├── notifier_handler.erl
├── spawnpool_sup.erl
├── spawnpool_app.erl
├── redirect_handler.erl
└── nominator.erl
├── rebar.config
├── templates
├── error.dtl
└── zergling_config.dtl
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | deps
2 | ebin
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | .PHONY: default
3 |
4 | default:
5 | rebar compile
6 |
7 |
8 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | erl -pz ebin -pz deps/*/ebin -s spawnpool_app
4 |
5 |
--------------------------------------------------------------------------------
/src/spawnpool.app.src:
--------------------------------------------------------------------------------
1 | {application, spawnpool,
2 | [
3 | {description, ""},
4 | {vsn, "1"},
5 | {registered, []},
6 | {applications, [
7 | kernel,
8 | stdlib
9 | ]},
10 | {mod, { spawnpool_app, []}},
11 | {env, []}
12 | ]}.
13 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 |
2 | {deps,[
3 | {cowboy,".*",
4 | {git,"git://github.com/extend/cowboy.git","master"}},
5 | {erlydtl,".*",
6 | {git,"git://github.com/evanmiller/erlydtl.git","master"}},
7 | {egator,".*",
8 | {git,"git@bitbucket.org:mkharch/egator.git","master"}}
9 | ]}.
10 |
11 | %%EOF
12 |
--------------------------------------------------------------------------------
/templates/error.dtl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Over capacity
5 |
6 | The demo is limited to 16 concurrent instances and 2 libvirt connections. Due
7 | to these limitations we were unable to spawn a new instance to service your
8 | request. Please try again later.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/notifier_handler.erl:
--------------------------------------------------------------------------------
1 | -module(notifier_handler).
2 | -export([init/3]).
3 | -export([handle/2,terminate/3]).
4 |
5 | init({tcp,http}, Req, []) ->
6 | {ok,Req,[]}.
7 |
8 | handle(Req, St) ->
9 | {<<"POST">>,_} = cowboy_req:method(Req),
10 | {[DomName],_} = cowboy_req:path_info(Req),
11 | {{IpAddr,_Port},_} = cowboy_req:peer(Req),
12 | gen_server:cast(nominator, {ready,DomName,IpAddr}),
13 | {ok,Req,St}.
14 |
15 | terminate(_What, _Req, _St) ->
16 | ok.
17 |
18 | %%EOF
19 |
--------------------------------------------------------------------------------
/templates/zergling_config.dtl:
--------------------------------------------------------------------------------
1 |
2 | {{ inst.name }}
3 | {{ inst.memory|default:"32" }}
4 | {{ inst.memory|default:"32" }}
5 | 1
6 |
7 |
8 | linux
9 | /root/images/zergling.img
10 | -ipaddr {{ inst.ipaddr }} -netmask 255.255.255.0 -gateway 192.168.0.1 -notify 192.168.0.2:9009 -root /erlang -pz /zergling/ebin -pz /zergling/deps/cowboy/ebin -pz /zergling/deps/ranch/ebin -pz /zergling/deps/erlydtl/ebin -home /zergling -s zergling_app -ts_req_received {{ ts_req_received }} -shutdown_after 15
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/spawnpool_sup.erl:
--------------------------------------------------------------------------------
1 |
2 | -module(spawnpool_sup).
3 |
4 | -behaviour(supervisor).
5 |
6 | %% API
7 | -export([start_link/0]).
8 |
9 | %% Supervisor callbacks
10 | -export([init/1]).
11 |
12 | %% Helper macro for declaring children of supervisor
13 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
14 |
15 | %% ===================================================================
16 | %% API functions
17 | %% ===================================================================
18 |
19 | start_link() ->
20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
21 |
22 | %% ===================================================================
23 | %% Supervisor callbacks
24 | %% ===================================================================
25 |
26 | init([]) ->
27 | Nominator = ?CHILD(nominator, worker),
28 | {ok, { {one_for_one, 5, 10}, [Nominator]} }.
29 |
30 | %%EOF
31 |
--------------------------------------------------------------------------------
/src/spawnpool_app.erl:
--------------------------------------------------------------------------------
1 | -module(spawnpool_app).
2 |
3 | -behaviour(application).
4 |
5 | -export([start/0]).
6 |
7 | %% Application callbacks
8 | -export([start/2, stop/1]).
9 |
10 | start() ->
11 | application:start(crypto),
12 | application:start(ranch),
13 | application:start(cowlib),
14 | application:start(cowboy),
15 | application:start(spawnpool).
16 |
17 | %% ===================================================================
18 | %% Application callbacks
19 | %% ===================================================================
20 |
21 | start(_StartType, _StartArgs) ->
22 |
23 | Dispatch = cowboy_router:compile([
24 | {'_',[
25 | {'_',redirect_handler,[]}
26 | ]}
27 | ]),
28 |
29 | {ok,_} = cowboy:start_http(spawner, 100,
30 | [{port,8900},{ip,{127,0,0,1}}],
31 | [{env,[{dispatch,Dispatch}]}]),
32 |
33 | %% a listener to collect 'ready' messages from zerglings
34 |
35 | Notify = cowboy_router:compile([
36 | {'_',[
37 | {"/ready/[...]",notifier_handler,[]}
38 | ]}
39 | ]),
40 |
41 | {ok,_} = cowboy:start_http(notifier, 100,
42 | [{port,8909},{ip,{10,0,0,1}}],
43 | [{env,[{dispatch,Notify}]}]),
44 |
45 | spawnpool_sup:start_link().
46 |
47 | stop(_State) ->
48 | ok.
49 |
50 | %%EOF
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # The spawn pool for Zerg demo
3 |
4 | This application is an integral part of Zerg demo. It gets initial connection
5 | from nginx, spawns a new 'zergling' instance (see
6 | https://github.com/maximk/zergling), and instructs nginx to proxy the client
7 | connection to the newly spawned instance.
8 |
9 | # How to setup nginx
10 |
11 | nginx configuration file must have a block similar to this:
12 |
13 | server {
14 | listen 80;
15 | server_name zerg.yourdomain.com;
16 |
17 | # Suppress spawning a second instance just to return 404
18 | #
19 | location /favicon.ico {
20 | return 404;
21 | }
22 |
23 | # A catapult location to bypass X-Accel-Redirect limitation
24 | #
25 | location ~* ^/internal_redirect/(.*?)/(.*?)/(.*) {
26 | internal;
27 | set $redirect_host $1;
28 | set $redirect_port $2;
29 | set $redirect_uri $3;
30 | set $redirect_to http://$redirect_host:$redirect_port/$redirect_uri;
31 | proxy_pass $redirect_to;
32 | }
33 |
34 | location / {
35 | proxy_set_header X-Real-IP $remote_addr;
36 | proxy_set_header Host $http_host;
37 |
38 | # Put location of your 'spawningpool' application here
39 | #
40 | proxy_pass http://localhost:9001;
41 | }
42 | }
43 |
44 | # How to run the application
45 |
46 | The application recorgizes the following command-line flags:
47 |
48 | * _-gator_ the IP address (and port) of the Dom0 running gatord. Defaults to
49 | * "127.0.0.1:4287".
50 |
51 |
--------------------------------------------------------------------------------
/src/redirect_handler.erl:
--------------------------------------------------------------------------------
1 | -module(redirect_handler).
2 | -export([init/3]).
3 | -export([handle/2,terminate/3]).
4 |
5 | -define(ZERGLING_IMAGE, <<"/home/mk/zergling/vmling">>).
6 | -define(BRIDGE, <<"xenbr0">>).
7 | -define(BASIC_EXTRA,
8 | "-notify 10.0.0.1:8909 -shutdown_after 15000 "
9 | " -home /zergling "
10 | "-pz /zergling/ebin "
11 | "-pz /zergling/deps/cowboy/ebin "
12 | "-pz /zergling/deps/cowlib/ebin "
13 | "-pz /zergling/deps/ranch/ebin "
14 | "-pz /zergling/deps/erlydtl/ebin "
15 | "-s zergling_app").
16 |
17 | init({tcp,http}, Req, []) ->
18 | {ok,Req,[]}.
19 |
20 | handle(Req, St) ->
21 | TsReqReceived = timestamp(),
22 | {ok,DomName,{A0,B0,C0,D0}} = nominator:fresh(),
23 | {RealIp,_} = cowboy_req:header(<<"x-real-ip">>, Req, undefined),
24 | io:format("demo:~s:~s:", [real_host_name(RealIp),DomName]),
25 |
26 | Extra = io_lib:format("-ipaddr ~w.~w.~w.~w "
27 | "-netmask 255.0.0.0 "
28 | "-gateway 10.0.0.1 "
29 | "~s -ts_req_received ~w", [A0,B0,C0,D0,?BASIC_EXTRA,TsReqReceived]),
30 | DomOpts = [{extra,list_to_binary(Extra)},
31 | {memory,32},
32 | {bridge,?BRIDGE}],
33 | case egator:create(DomName, ?ZERGLING_IMAGE, DomOpts, []) of
34 | ok ->
35 | {ok,{A,B,C,D}} = nominator:wait_until_ready(DomName),
36 | io:format("~w.~w.~w.~w\n", [A,B,C,D]),
37 | {Path,_} = cowboy_req:path(Req),
38 | RedirectTo = io_lib:format("/internal_redirect/~w.~w.~w.~w/8000~s",
39 | [A,B,C,D,Path]),
40 | Headers = [{<<"X-Accel-Redirect">>,list_to_binary(RedirectTo)}],
41 | {ok,Reply} = cowboy_req:reply(200, Headers, Req),
42 | {ok,Reply,St};
43 |
44 | {error,Error} ->
45 | {ok,Body} = error_page(Error),
46 | {ok,Reply} = cowboy_req:reply(200, [], Body, Req),
47 | {ok,Reply,St}
48 | end.
49 |
50 | terminate(_What, _Req, _St) ->
51 | ok.
52 |
53 | %%------------------------------------------------------------------------------
54 |
55 | real_host_name(undefined) -> <<"not-set">>;
56 | real_host_name(IpAddr) when is_binary(IpAddr) ->
57 | case inet:gethostbyaddr(binary_to_list(IpAddr)) of
58 | {error,_} -> IpAddr;
59 | {ok,{hostent,undefined,_,_,_,_}} -> IpAddr;
60 | {ok,{hostent,HostName,_,_,_,_}} -> list_to_binary(HostName) end.
61 |
62 | error_page(Error) ->
63 | Vars = [{error,io_lib:format("~p~n", [Error])}],
64 | error_dtl:render(Vars).
65 |
66 | timestamp() ->
67 | {Mega,Secs,Micro} = now(),
68 | Mega *1000000.0 + Secs + Micro / 1000000.0.
69 |
70 | %%EOF
71 |
--------------------------------------------------------------------------------
/src/nominator.erl:
--------------------------------------------------------------------------------
1 | -module(nominator).
2 | -behaviour(gen_server).
3 | -define(SERVER, ?MODULE).
4 |
5 | %% ------------------------------------------------------------------
6 | %% API Function Exports
7 | %% ------------------------------------------------------------------
8 |
9 | -export([start_link/0]).
10 |
11 | -export([fresh/0]).
12 | -export([wait_until_ready/1]).
13 |
14 | -record(st, {taken =[],
15 | pending =[],
16 | ready = []}).
17 |
18 | %% ------------------------------------------------------------------
19 | %% gen_server Function Exports
20 | %% ------------------------------------------------------------------
21 |
22 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
23 | terminate/2, code_change/3]).
24 |
25 | %% ------------------------------------------------------------------
26 | %% API Function Definitions
27 | %% ------------------------------------------------------------------
28 |
29 | start_link() ->
30 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
31 |
32 | fresh() ->
33 | gen_server:call(?SERVER, fresh).
34 |
35 | wait_until_ready(DomName) ->
36 | gen_server:call(?SERVER, {wait_until_ready,DomName}).
37 |
38 | %% ------------------------------------------------------------------
39 | %% gen_server Function Definitions
40 | %% ------------------------------------------------------------------
41 |
42 | init(_Args) ->
43 | random:seed(now()),
44 | {ok,Domains} = egator:list([]),
45 | Taken = domain_addresses([D || {D,_} <- Domains]),
46 | {ok,#st{taken =Taken}}.
47 |
48 | handle_call(fresh, _From, #st{taken =Taken} =St) ->
49 | {ok,IpAddr,Taken1} = next_free(Taken),
50 | DomName = encode_name("zergling", IpAddr),
51 | {reply,{ok,DomName,IpAddr},St#st{taken =Taken1}};
52 |
53 | handle_call({wait_until_ready,DomName}, From, #st{pending =Pending,ready =Ready} =St) ->
54 | case lists:keytake(DomName, 1, Ready) of
55 | {value,IpAddr,Ready1} ->
56 | {reply,IpAddr,St#st{ready =Ready1}};
57 | false ->
58 | {noreply,St#st{pending =[{DomName,From}|Pending]}}
59 | end.
60 |
61 | handle_cast({ready,DomName,IpAddr}, #st{pending =Pending,ready =Ready} =St) ->
62 | case lists:keytake(DomName, 1, Pending) of
63 | {value,{_,From},Pending1} ->
64 | gen_server:reply(From, {ok,IpAddr}),
65 | {noreply,St#st{pending =Pending1}};
66 | false ->
67 | {noreply,St#st{ready =[{DomName,IpAddr}|Ready]}}
68 | end.
69 |
70 | handle_info(_Msg, State) ->
71 | {noreply, State}.
72 |
73 | terminate(_Reason, _State) ->
74 | ok.
75 |
76 | code_change(_OldVsn, State, _Extra) ->
77 | {ok, State}.
78 |
79 | %% ------------------------------------------------------------------
80 | %% Internal Function Definitions
81 | %% ------------------------------------------------------------------
82 |
83 | domain_addresses(Domains) -> domain_addresses(Domains, []).
84 |
85 | domain_addresses([], Acc) -> Acc;
86 | domain_addresses([Dom|Domains], Acc) ->
87 | case re:run(Dom, "[a-z]_([0-9]+)_([0-9]+)_([0-9]+)",
88 | [{capture,all_but_first,list}]) of
89 | {match,[B,C,D]} ->
90 | IpAddr = {10,list_to_integer(B),list_to_integer(C),list_to_integer(D)},
91 | domain_addresses(Domains, [IpAddr|Acc]);
92 | _ ->
93 | domain_addresses(Domains, Acc)
94 | end.
95 |
96 | next_free(Taken) ->
97 | B = random:uniform(256) -1,
98 | C = random:uniform(256) -1,
99 | D = random:uniform(256) -1,
100 | IpAddr = {10,B,C,D},
101 | case lists:member(IpAddr, Taken) of
102 | true -> next_free(Taken);
103 | false -> {ok,IpAddr,[IpAddr|Taken]} end.
104 |
105 | encode_name(Prefix, {10,B,C,D}) ->
106 | list_to_binary(io_lib:format("~s_~w_~w_~w", [Prefix,B,C,D])).
107 |
108 | %%EOF
109 |
--------------------------------------------------------------------------------