auser-poolparty 0.2.53 → 0.2.54
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +15 -1
- data/README.txt +20 -4
- data/bin/cloud-ensure-provisioning +13 -7
- data/bin/cloud-handle-load +1 -0
- data/bin/cloud-start +1 -1
- data/bin/messenger-get-current-nodes +13 -0
- data/bin/server-show-stats +1 -1
- data/bin/server-start-client +15 -8
- data/bin/server-start-master +11 -7
- data/bin/server-start-node +12 -10
- data/bin/server-stop-client +3 -0
- data/bin/server-stop-master +3 -0
- data/bin/server-stop-node +3 -0
- data/lib/erlang/messenger/include/defines.hrl +1 -0
- data/lib/erlang/messenger/pm_client_rel-0.1.script +1 -1
- data/lib/erlang/messenger/pm_master_rel-0.1.script +1 -1
- data/lib/erlang/messenger/pm_node_rel-0.1.script +1 -1
- data/lib/erlang/messenger/src/client_server.erl +22 -8
- data/lib/erlang/messenger/src/pm_master.erl +7 -9
- data/lib/erlang/messenger/src/pm_node.erl +0 -1
- data/lib/erlang/messenger/src/pm_strings.erl +11 -0
- data/lib/erlang/messenger/src/utils.erl +12 -4
- data/lib/erlang/messenger/useful_snippets +1 -1
- data/lib/poolparty.rb +10 -2
- data/lib/poolparty/base_packages/poolparty.rb +23 -16
- data/lib/poolparty/base_packages/runit.rb +21 -0
- data/lib/poolparty/helpers/binary.rb +0 -1
- data/lib/poolparty/helpers/provisioner_base.rb +4 -0
- data/lib/poolparty/helpers/provisioners/master.rb +15 -8
- data/lib/poolparty/modules/file_writer.rb +10 -2
- data/lib/poolparty/net/messenger.rb +9 -3
- data/lib/poolparty/net/remoter.rb +12 -8
- data/lib/poolparty/plugins/runit.rb +96 -0
- data/lib/poolparty/pool/cloud.rb +1 -1
- data/lib/poolparty/pool/resource.rb +2 -1
- data/lib/poolparty/pool/resources/custom_service.rb +30 -0
- data/lib/poolparty/pool/resources/package.rb +5 -2
- data/lib/poolparty/templates/messenger/client/log-run.erb +2 -0
- data/lib/poolparty/templates/messenger/client/run.erb +4 -0
- data/lib/poolparty/templates/messenger/master/log-run.erb +2 -0
- data/lib/poolparty/templates/messenger/master/run.erb +4 -0
- data/lib/poolparty/templates/messenger/node/log-run.erb +2 -0
- data/lib/poolparty/templates/messenger/node/run.erb +4 -0
- data/lib/poolparty/version.rb +1 -1
- data/poolparty.gemspec +58 -504
- data/spec/poolparty/net/remote_spec.rb +1 -1
- metadata +21 -7
data/Manifest.txt
CHANGED
@@ -21,6 +21,7 @@ bin/cloud-ssh
|
|
21
21
|
bin/cloud-start
|
22
22
|
bin/cloud-stats
|
23
23
|
bin/cloud-terminate
|
24
|
+
bin/messenger-get-current-nodes
|
24
25
|
bin/pool
|
25
26
|
bin/pool-console
|
26
27
|
bin/pool-describe
|
@@ -37,6 +38,9 @@ bin/server-show-stats
|
|
37
38
|
bin/server-start-client
|
38
39
|
bin/server-start-master
|
39
40
|
bin/server-start-node
|
41
|
+
bin/server-stop-client
|
42
|
+
bin/server-stop-master
|
43
|
+
bin/server-stop-node
|
40
44
|
config/hoe.rb
|
41
45
|
config/requirements.rb
|
42
46
|
examples/basic.rb
|
@@ -74,8 +78,8 @@ lib/erlang/messenger/ebin/pm_node.beam
|
|
74
78
|
lib/erlang/messenger/ebin/pm_node_rel-0.1.rel
|
75
79
|
lib/erlang/messenger/ebin/pm_node_supervisor.beam
|
76
80
|
lib/erlang/messenger/ebin/pm_packager.beam
|
81
|
+
lib/erlang/messenger/ebin/pm_strings.beam
|
77
82
|
lib/erlang/messenger/ebin/utils.beam
|
78
|
-
lib/erlang/messenger/erl_crash.dump
|
79
83
|
lib/erlang/messenger/include/defines.hrl
|
80
84
|
lib/erlang/messenger/lib/eunit/AUTHORS
|
81
85
|
lib/erlang/messenger/lib/eunit/CHANGELOG
|
@@ -154,6 +158,7 @@ lib/erlang/messenger/src/pm_master_supervisor.erl
|
|
154
158
|
lib/erlang/messenger/src/pm_node.erl
|
155
159
|
lib/erlang/messenger/src/pm_node_supervisor.erl
|
156
160
|
lib/erlang/messenger/src/pm_packager.erl
|
161
|
+
lib/erlang/messenger/src/pm_strings.erl
|
157
162
|
lib/erlang/messenger/src/utils.erl
|
158
163
|
lib/erlang/messenger/useful_snippets
|
159
164
|
lib/poolparty.rb
|
@@ -162,6 +167,7 @@ lib/poolparty/base_packages/haproxy.rb
|
|
162
167
|
lib/poolparty/base_packages/heartbeat.rb
|
163
168
|
lib/poolparty/base_packages/poolparty.rb
|
164
169
|
lib/poolparty/base_packages/ruby.rb
|
170
|
+
lib/poolparty/base_packages/runit.rb
|
165
171
|
lib/poolparty/config/allowed_commands.yml
|
166
172
|
lib/poolparty/config/postlaunchmessage.txt
|
167
173
|
lib/poolparty/core/array.rb
|
@@ -220,6 +226,7 @@ lib/poolparty/net/remoter.rb
|
|
220
226
|
lib/poolparty/net/remoter_base.rb
|
221
227
|
lib/poolparty/plugins/git.rb
|
222
228
|
lib/poolparty/plugins/line.rb
|
229
|
+
lib/poolparty/plugins/runit.rb
|
223
230
|
lib/poolparty/plugins/svn.rb
|
224
231
|
lib/poolparty/pool/base.rb
|
225
232
|
lib/poolparty/pool/cloud.rb
|
@@ -232,6 +239,7 @@ lib/poolparty/pool/resource.rb
|
|
232
239
|
lib/poolparty/pool/resources/class_package.rb
|
233
240
|
lib/poolparty/pool/resources/conditional.rb
|
234
241
|
lib/poolparty/pool/resources/cron.rb
|
242
|
+
lib/poolparty/pool/resources/custom_service.rb
|
235
243
|
lib/poolparty/pool/resources/directory.rb
|
236
244
|
lib/poolparty/pool/resources/exec.rb
|
237
245
|
lib/poolparty/pool/resources/file.rb
|
@@ -251,6 +259,12 @@ lib/poolparty/templates/gem
|
|
251
259
|
lib/poolparty/templates/ha.cf
|
252
260
|
lib/poolparty/templates/haproxy.conf
|
253
261
|
lib/poolparty/templates/haresources
|
262
|
+
lib/poolparty/templates/messenger/client/log-run.erb
|
263
|
+
lib/poolparty/templates/messenger/client/run.erb
|
264
|
+
lib/poolparty/templates/messenger/master/log-run.erb
|
265
|
+
lib/poolparty/templates/messenger/master/run.erb
|
266
|
+
lib/poolparty/templates/messenger/node/log-run.erb
|
267
|
+
lib/poolparty/templates/messenger/node/run.erb
|
254
268
|
lib/poolparty/templates/namespaceauth.conf
|
255
269
|
lib/poolparty/templates/poolparty.monitor
|
256
270
|
lib/poolparty/templates/puppet.conf
|
data/README.txt
CHANGED
@@ -4,18 +4,30 @@ http://poolpartyrb.com
|
|
4
4
|
|
5
5
|
== DESCRIPTION:
|
6
6
|
|
7
|
-
|
7
|
+
PoolParty makes it easy and simple to configure any cloud of computers. In clear language, describe your cloud
|
8
|
+
with language such as:
|
9
|
+
|
10
|
+
pool :cloud do
|
11
|
+
cloud :app do
|
12
|
+
apache do
|
13
|
+
has_virtualhost(:name => "/var/www/sites/poolpartyrb.com")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
8
17
|
|
9
18
|
== FEATURES/PROBLEMS:
|
10
19
|
|
11
|
-
|
20
|
+
* Written in Ruby and Erlang
|
21
|
+
* Written from the ground up to be extensible with plugins
|
22
|
+
* Easy git-style commands to communicate with your clouds
|
23
|
+
* Much much more
|
12
24
|
|
13
25
|
== SYNOPSIS:
|
14
26
|
|
15
|
-
|
27
|
+
PoolParty is written with the intention of being as application-agnostic as possible. It installs only the basic
|
16
28
|
required software to glue the cloud together on the instances as listed below.
|
17
29
|
|
18
|
-
|
30
|
+
PoolParty is easily configuration. In fact, it makes little assumptions about your development environment and allows
|
19
31
|
several options on how to begin configuring the cloud.
|
20
32
|
|
21
33
|
== REQUIREMENTS:
|
@@ -26,6 +38,10 @@ COMING SOON
|
|
26
38
|
|
27
39
|
sudo gem install auser-poolparty
|
28
40
|
|
41
|
+
== TODO:
|
42
|
+
* Replace services with Runit
|
43
|
+
* Refactor provisioning to use erlang
|
44
|
+
|
29
45
|
== LICENSE:
|
30
46
|
|
31
47
|
(The MIT License)
|
@@ -8,17 +8,23 @@ o = PoolParty::Optioner.new(ARGV) do |opts, optioner|
|
|
8
8
|
opts.on('-n name', '--name name', 'Host name') { |h| optioner.hostname h }
|
9
9
|
opts.on('-l', '--no-shell', 'No shell') {optioner.noshell true}
|
10
10
|
end
|
11
|
-
@hostname = o.hostname ? o.hostname : `hostname`.chomp
|
12
|
-
@hostname = "node0" if @hostname == "master" # Quick fix to make sure we have a node running on the master as well
|
13
11
|
|
14
12
|
o.loaded_clouds.each do |cloud|
|
15
13
|
|
16
|
-
with_cloud(cloud
|
14
|
+
with_cloud(cloud) do
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
@nonprovisioned_nodes = list_of_running_instances.map {|a| a.name } - cloud.get_current_nodes
|
17
|
+
# @tp = ThreadPool.new(10)
|
18
|
+
@nonprovisioned_nodes.each do |node|
|
19
|
+
vputs "Provisioning #{node}"
|
20
|
+
next if node == "master"
|
21
|
+
# @tp.process do
|
22
|
+
PoolParty::Provisioner.process_clean_reconfigure_for!(node, cloud)
|
23
|
+
`cloud-provision -i #{node.gsub(/node/, '')}`
|
24
|
+
PoolParty::Provisioner.process_clean_reconfigure_for!(node, cloud)
|
25
|
+
# end
|
26
|
+
end
|
27
|
+
# @tp.join
|
22
28
|
end
|
23
29
|
|
24
30
|
end
|
data/bin/cloud-handle-load
CHANGED
@@ -11,6 +11,7 @@ o.loaded_clouds.each do |cloud|
|
|
11
11
|
|
12
12
|
with_cloud(cloud) do
|
13
13
|
vputs header("Load handling cloud #{name}")
|
14
|
+
vputs "should_expand_cloud: #{should_contract_cloud?}"
|
14
15
|
if should_expand_cloud?
|
15
16
|
vputs "Expanding cloud based on load"
|
16
17
|
logger.debug "Expanding cloud based on load"
|
data/bin/cloud-start
CHANGED
@@ -22,7 +22,7 @@ o.loaded_clouds.each do |cloud|
|
|
22
22
|
provisioning_complete
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
25
|
+
puts open(::File.join(File.dirname(__FILE__), "..", "lib", "poolparty", "config", "postlaunchmessage.txt")).read ^ {:master_ip => master.ip.chomp}
|
26
26
|
clear_base_directory unless testing
|
27
27
|
end
|
28
28
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
3
|
+
require "poolparty"
|
4
|
+
require "poolpartycl"
|
5
|
+
|
6
|
+
o = PoolParty::Optioner.new(ARGV) do |opts, optioner|
|
7
|
+
end
|
8
|
+
|
9
|
+
o.loaded_clouds.each do |cloud|
|
10
|
+
# @nodes = cloud.messenger_send!("get_current_nodes")
|
11
|
+
# @nodes = @nodes.split(" ").map {|a| a.split(/@/)[-1] }
|
12
|
+
puts cloud.get_current_nodes.join(" ")
|
13
|
+
end
|
data/bin/server-show-stats
CHANGED
data/bin/server-start-client
CHANGED
@@ -10,13 +10,20 @@ end
|
|
10
10
|
o.loaded_clouds.each do |cloud|
|
11
11
|
|
12
12
|
with_cloud(cloud) do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
|
14
|
+
# ruby /var/lib/gems/1.8/bin/server-start-client
|
15
|
+
already_running = %x[ps aux | grep beam | grep -v grep | grep client]
|
16
|
+
if already_running.chomp.empty?
|
17
|
+
|
18
|
+
boot_file = "#{Messenger.append_dir}/pm_client_rel-0.1"
|
19
|
+
|
20
|
+
Kernel.system "cd #{Messenger.append_dir} && rake build_boot_scripts" unless ::File.file?("#{boot_file}.boot") || testing
|
21
|
+
command = Messenger.erl_command("client", "-boot #{boot_file} #{noshell ? "" : "-detached -heart -noshell"}", 7049, 7050)
|
22
|
+
vputs "Running #{command}"
|
23
|
+
|
24
|
+
Kernel.system "export HOME=/root && #{command}" unless testing
|
25
|
+
end
|
20
26
|
end
|
21
27
|
|
22
|
-
end
|
28
|
+
end
|
29
|
+
|
data/bin/server-start-master
CHANGED
@@ -10,13 +10,17 @@ end
|
|
10
10
|
o.loaded_clouds.each do |cloud|
|
11
11
|
|
12
12
|
with_cloud(cloud) do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
13
|
+
|
14
|
+
already_running = %x[ps aux | grep beam | grep -v grep | grep master]
|
15
|
+
if already_running.chomp.empty?
|
16
|
+
|
17
|
+
boot_file = "#{Messenger.append_dir}/pm_master_rel-0.1"
|
18
|
+
Kernel.system ". /etc/profile && server-build-messenger" unless ::File.file?("#{boot_file}.boot") || testing
|
19
|
+
command = Messenger.erl_command("master", "-boot #{boot_file} #{noshell ? "" : "-noshell -detached -heart"}")
|
20
|
+
vputs "Running #{command}"
|
21
|
+
|
22
|
+
Kernel.system "export HOME=/root && #{command}" unless testing
|
23
|
+
end
|
20
24
|
end
|
21
25
|
|
22
26
|
end
|
data/bin/server-start-node
CHANGED
@@ -15,16 +15,18 @@ available_monitors = PoolParty::Monitors.available_monitors
|
|
15
15
|
|
16
16
|
o.loaded_clouds.each do |cloud|
|
17
17
|
|
18
|
-
with_cloud(cloud) do
|
19
|
-
# TODO: Change this to be app specfic
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
18
|
+
with_cloud(cloud, {:hostname => @hostname}) do
|
19
|
+
# TODO: Change this to be app specfic
|
20
|
+
already_running = %x[ps aux | grep beam | grep -v grep | grep node]
|
21
|
+
if already_running.chomp.empty?
|
22
|
+
boot_file = "#{Messenger.append_dir}/pm_node_rel-0.1"
|
23
|
+
|
24
|
+
Kernel.system ". /etc/profile && server-build-messenger" unless ::File.file?("#{boot_file}.boot") || testing
|
25
|
+
command = Messenger.erl_command(hostname, "-boot #{boot_file} #{noshell ? "" : "-noshell -detached -heart"} -- #{available_monitors.join(" ")}")
|
26
|
+
vputs "Running #{command}"
|
27
|
+
|
28
|
+
Kernel.system "export HOME=/root && #{command}" unless testing
|
29
|
+
end
|
28
30
|
end
|
29
31
|
|
30
32
|
end
|
@@ -5,34 +5,48 @@
|
|
5
5
|
|
6
6
|
-define (RECONNECT_TIMEOUT, 10000).
|
7
7
|
|
8
|
-
start() ->
|
9
|
-
|
10
|
-
connect_to_master(),
|
11
|
-
global:sync(),
|
8
|
+
start() ->
|
9
|
+
utils:start_timer(client_timer, ?UPDATE_TIME, fun() -> client_server:connect_to_master() end),
|
12
10
|
pm_client:start(?MODULE, 7050, {?MODULE, loop}).
|
13
11
|
|
14
12
|
master_server() -> global:whereis_name(pm_master).
|
15
13
|
|
16
14
|
loop(Socket) ->
|
17
15
|
case gen_tcp:recv(Socket, 0) of
|
18
|
-
{ok, Data} ->
|
16
|
+
{ok, Data} ->
|
19
17
|
?TRACE("received", [master_server(), erlang:binary_to_list(Data)]),
|
20
18
|
% Args = [Item || K <- string:tokens(erlang:binary_to_list(Data), " "), Item <- erlang:list_to_atom(K)],
|
21
19
|
[Meth|Args] = string:tokens(erlang:binary_to_list(Data), " "),
|
22
20
|
?TRACE("received", [Meth, Args]),
|
23
21
|
Output = gen_server:call(master_server(), {erlang:list_to_atom(Meth), Args}),
|
24
22
|
?TRACE("received from gen_server", [Output]),
|
25
|
-
|
26
|
-
io:format("~p~n", [Output]),
|
23
|
+
send_back_appropriate_response(Socket, Output),
|
27
24
|
?TRACE("posted", [Output]),
|
28
25
|
loop(Socket);
|
29
26
|
{error, closed} ->
|
30
27
|
ok
|
31
28
|
end.
|
32
29
|
|
30
|
+
% send_back_appropriate_response(Socket, Output) when is_float(Output) -> gen_tcp:send(Socket, erlang:float_to_list(Output));
|
31
|
+
% Figure out how to do this the best... damnit
|
32
|
+
send_back_appropriate_response(Socket, Output) ->
|
33
|
+
[H|_T] = Output,
|
34
|
+
case erlang:is_atom(H) of
|
35
|
+
true ->
|
36
|
+
?TRACE("NewOut", [Output]),
|
37
|
+
NewOut = pm_strings:string_join( [erlang:atom_to_list(K) || K <- Output], " ");
|
38
|
+
_ ->
|
39
|
+
ListOfFloats = pm_strings:string_join( [erlang:float_to_list(V) || V <- Output], " "),
|
40
|
+
NewOut = [ListOfFloats]
|
41
|
+
end,
|
42
|
+
?TRACE("NewOut", [NewOut]),
|
43
|
+
gen_tcp:send(Socket, NewOut).
|
44
|
+
|
45
|
+
|
33
46
|
connect_to_master() ->
|
34
47
|
case net_adm:ping(?MASTER_LOCATION) of
|
35
|
-
pong ->
|
48
|
+
pong ->
|
49
|
+
global:sync(),
|
36
50
|
ok;
|
37
51
|
_ ->
|
38
52
|
receive
|
@@ -44,8 +44,7 @@
|
|
44
44
|
get_load(Type) ->
|
45
45
|
% {Loads, _} = pm_cluster:send_call(get_load_for_type, [Type]),
|
46
46
|
% {Loads, _} = gen_server:call(?SERVER, {get_load, [Type]}),
|
47
|
-
|
48
|
-
utils:average_of_list(Loads).
|
47
|
+
gen_server:call(?SERVER, {get_current_load, Type}).
|
49
48
|
|
50
49
|
% Send reconfigure tasks to every node
|
51
50
|
reconfigure_cloud() -> gen_server:cast(?SERVER, {force_reconfig}).
|
@@ -59,7 +58,7 @@ shutdown_cloud() ->
|
|
59
58
|
pm_cluster:send_call(stop, []),
|
60
59
|
{ok}.
|
61
60
|
|
62
|
-
get_current_nodes() -> gen_server:call(?SERVER, {
|
61
|
+
get_current_nodes() -> gen_server:call(?SERVER, {get_current_nodes, []}).
|
63
62
|
|
64
63
|
stop() -> gen_server:cast(?MODULE, stop).
|
65
64
|
%%--------------------------------------------------------------------
|
@@ -101,13 +100,12 @@ handle_call({get_load, Args}, _From, State) ->
|
|
101
100
|
Nodes = pm_cluster:get_live_nodes(),
|
102
101
|
List = rpc:multicall(Nodes, pm_node, get_load_for_type, [Args]),
|
103
102
|
Loads = utils:convert_responses_to_int_list(List),
|
104
|
-
{reply, Loads, State};
|
105
|
-
handle_call({get_current_load,
|
106
|
-
LoadForType = get_load_for_type(Type, State),
|
103
|
+
{reply, Loads, State};
|
104
|
+
handle_call({get_current_load, Types}, _From, State) ->
|
105
|
+
LoadForType = [utils:average_of_list(get_load_for_type(Type, State)) || Type <- Types],
|
107
106
|
?TRACE("LoadForType: ",[LoadForType]),
|
108
|
-
|
109
|
-
|
110
|
-
handle_call({get_live_nodes}, _From, State) ->
|
107
|
+
{reply, LoadForType, State};
|
108
|
+
handle_call({get_current_nodes, _Args}, _From, State) ->
|
111
109
|
{reply, get_live_nodes(State), State}.
|
112
110
|
|
113
111
|
% handle_call(_Request, _From, State) ->
|
@@ -0,0 +1,11 @@
|
|
1
|
+
-module (pm_strings).
|
2
|
+
-include_lib("../include/defines.hrl").
|
3
|
+
-compile(export_all).
|
4
|
+
|
5
|
+
string_join(Items, Sep) ->
|
6
|
+
lists:flatten(lists:reverse(string_join1(Items, Sep, []))).
|
7
|
+
|
8
|
+
string_join1([Head | []], _Sep, Acc) ->
|
9
|
+
[Head | Acc];
|
10
|
+
string_join1([Head | Tail], Sep, Acc) ->
|
11
|
+
string_join1(Tail, Sep, [Sep, Head | Acc]).
|
@@ -1,4 +1,5 @@
|
|
1
1
|
-module (utils).
|
2
|
+
-include_lib("../include/defines.hrl").
|
2
3
|
-compile(export_all).
|
3
4
|
|
4
5
|
-ifdef(EUNIT).
|
@@ -12,18 +13,25 @@ convert_responses_to_int_list(L) ->
|
|
12
13
|
average_for_list(Sum, L).
|
13
14
|
|
14
15
|
% Start a timer to fire off Fun after Time number of milliseconds
|
16
|
+
start_timer(Name, Time, Fun) ->
|
17
|
+
case whereis(Name) of
|
18
|
+
undefined -> register(Name, spawn(fun() -> tick_timer(Time, Fun) end));
|
19
|
+
_ -> ok
|
20
|
+
end.
|
15
21
|
start_timer(Time, Fun) ->
|
16
|
-
|
22
|
+
start_timer(?MODULE, Time, Fun).
|
23
|
+
% register(?MODULE, spawn(fun() -> tick_timer(Time, Fun) end)).
|
17
24
|
|
25
|
+
stop_timer(Name) -> erlang:whereis(Name) ! stop.
|
18
26
|
stop_timer() -> ?MODULE ! stop.
|
19
27
|
|
28
|
+
|
20
29
|
tick_timer(Time, Fun) ->
|
21
30
|
receive
|
22
|
-
stop ->
|
23
|
-
void
|
31
|
+
stop -> void
|
24
32
|
after Time ->
|
25
33
|
Fun(),
|
26
|
-
tick_timer(Time, Fun)
|
34
|
+
tick_timer(Time, Fun)
|
27
35
|
end.
|
28
36
|
|
29
37
|
average_of_list(L) ->
|