2014-05-02 18:09:46 +00:00
|
|
|
-- Simulation of non-flood syncing of content, across a network of nodes.
|
|
|
|
|
|
|
|
module Main where
|
|
|
|
|
|
|
|
import System.Random
|
|
|
|
import Control.Monad.Random
|
|
|
|
import Control.Monad
|
|
|
|
import Control.Applicative
|
|
|
|
import Data.Ratio
|
|
|
|
import Data.Ord
|
|
|
|
import Data.List
|
2014-05-06 18:25:48 +00:00
|
|
|
import Data.Maybe
|
2014-05-02 18:09:46 +00:00
|
|
|
import qualified Data.Set as S
|
2014-05-03 12:49:39 +00:00
|
|
|
import qualified Data.Map.Strict as M
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
{-
|
|
|
|
- Tunable values
|
|
|
|
-}
|
|
|
|
|
|
|
|
totalFiles :: Int
|
2014-05-03 13:42:13 +00:00
|
|
|
totalFiles = 100
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
-- How likely is a given file to be wanted by any particular node?
|
|
|
|
probabilityFilesWanted :: Probability
|
|
|
|
probabilityFilesWanted = 0.10
|
|
|
|
|
|
|
|
-- How many different locations can each transfer node move between?
|
|
|
|
-- (Min, Max)
|
|
|
|
transferDestinationsRange :: (Int, Int)
|
2014-05-03 13:42:13 +00:00
|
|
|
transferDestinationsRange = (2, 3)
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
-- Controls how likely transfer nodes are to move around in a given step
|
|
|
|
-- of the simulation.
|
|
|
|
-- (They actually move slightly less because they may start to move and
|
|
|
|
-- pick the same location they are at.)
|
|
|
|
-- (Min, Max)
|
|
|
|
transferMoveFrequencyRange :: (Probability, Probability)
|
|
|
|
transferMoveFrequencyRange = (0.10, 1.00)
|
|
|
|
|
|
|
|
-- counts both immobile and transfer nodes as hops, so double Vince's
|
|
|
|
-- theoretical TTL of 3.
|
2014-05-06 18:25:48 +00:00
|
|
|
-- (30% loss on mocambos network w/o ttl of 4!)
|
2014-05-02 18:09:46 +00:00
|
|
|
maxTTL :: TTL
|
2014-05-06 18:25:48 +00:00
|
|
|
maxTTL = TTL (4 * 2)
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
numImmobileNodes :: Int
|
|
|
|
numImmobileNodes = 10
|
|
|
|
|
|
|
|
numTransferNodes :: Int
|
|
|
|
numTransferNodes = 20
|
|
|
|
|
|
|
|
numSteps :: Int
|
|
|
|
numSteps = 100
|
|
|
|
|
|
|
|
-- IO code
|
2014-05-06 18:25:48 +00:00
|
|
|
main :: IO ()
|
2014-05-03 14:55:01 +00:00
|
|
|
main = do
|
2014-05-06 18:25:48 +00:00
|
|
|
-- initialnetwork <- evalRandIO (seedFiles totalFiles =<< genNetwork)
|
|
|
|
initialnetwork <- evalRandIO (seedFiles totalFiles =<< mocambosNetwork)
|
|
|
|
networks <- evalRandIO (simulate numSteps initialnetwork)
|
|
|
|
let finalnetwork = last networks
|
|
|
|
putStrLn $ summarize initialnetwork finalnetwork
|
|
|
|
putStrLn "location history of file 1:"
|
|
|
|
print $ trace (traceHaveFile (File 1)) networks
|
|
|
|
putStrLn "request history of file 1:"
|
|
|
|
print $ trace (traceWantFile (File 1)) networks
|
2014-05-02 18:09:46 +00:00
|
|
|
-- Only pure code below :)
|
|
|
|
|
2014-05-03 12:49:39 +00:00
|
|
|
data Network = Network (M.Map NodeName ImmobileNode) [TransferNode]
|
2014-05-02 18:09:46 +00:00
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
data ImmobileNode = ImmobileNode NodeRepo
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
2014-05-03 12:49:39 +00:00
|
|
|
type NodeName = String
|
2014-05-02 18:09:46 +00:00
|
|
|
|
2014-05-03 13:42:13 +00:00
|
|
|
type Route = [NodeName]
|
|
|
|
|
2014-05-02 18:09:46 +00:00
|
|
|
data TransferNode = TransferNode
|
2014-05-03 12:49:39 +00:00
|
|
|
{ currentlocation :: NodeName
|
|
|
|
, possiblelocations :: [NodeName]
|
2014-05-02 18:09:46 +00:00
|
|
|
, movefrequency :: Probability
|
|
|
|
, transferrepo :: NodeRepo
|
|
|
|
}
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
data NodeRepo = NodeRepo
|
|
|
|
{ wantFiles :: [Request]
|
|
|
|
, haveFiles :: S.Set File
|
2014-05-09 19:41:05 +00:00
|
|
|
, satisfiedRequests :: S.Set Request
|
2014-05-02 18:09:46 +00:00
|
|
|
}
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
data File = File Int
|
|
|
|
deriving (Show, Eq, Ord)
|
|
|
|
|
|
|
|
randomFile :: (RandomGen g) => Rand g File
|
|
|
|
randomFile = File <$> getRandomR (0, totalFiles)
|
|
|
|
|
|
|
|
data Request = Request File TTL
|
2014-05-09 19:41:05 +00:00
|
|
|
deriving (Show, Ord)
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
-- compare ignoring TTL
|
|
|
|
instance Eq Request where
|
|
|
|
(Request f1 _) == (Request f2 _) = f1 == f2
|
|
|
|
|
|
|
|
requestedFile :: Request -> File
|
|
|
|
requestedFile (Request f _) = f
|
|
|
|
|
|
|
|
requestTTL :: Request -> TTL
|
|
|
|
requestTTL (Request _ ttl) = ttl
|
|
|
|
|
|
|
|
data TTL = TTL Int
|
|
|
|
deriving (Show, Eq, Ord)
|
|
|
|
|
|
|
|
incTTL :: TTL -> TTL
|
|
|
|
incTTL (TTL t) = TTL (t + 1)
|
|
|
|
|
|
|
|
decTTL :: TTL -> TTL
|
|
|
|
decTTL (TTL t) = TTL (t - 1)
|
|
|
|
|
|
|
|
staleTTL :: TTL -> Bool
|
|
|
|
staleTTL (TTL t) = t < 1
|
|
|
|
|
|
|
|
-- Origin of a request starts one higher than max, since the TTL
|
|
|
|
-- will decrement the first time the Request is transferred to another node.
|
|
|
|
originTTL :: TTL
|
|
|
|
originTTL = incTTL maxTTL
|
|
|
|
|
|
|
|
randomRequest :: (RandomGen g) => Rand g Request
|
|
|
|
randomRequest = Request
|
|
|
|
<$> randomFile
|
|
|
|
<*> pure originTTL
|
|
|
|
|
|
|
|
type Probability = Float
|
|
|
|
|
|
|
|
randomProbability :: (RandomGen g) => Rand g Probability
|
|
|
|
randomProbability = getRandomR (0, 1)
|
|
|
|
|
2014-05-06 18:25:48 +00:00
|
|
|
-- Returns the state of the network at each step of the simulation.
|
|
|
|
simulate :: (RandomGen g) => Int -> Network -> Rand g [Network]
|
|
|
|
simulate n net = go n [net]
|
|
|
|
where
|
|
|
|
go 0 nets = return (reverse nets)
|
|
|
|
go c (prev:nets) = do
|
|
|
|
new <- step prev
|
|
|
|
go (c - 1) (new:prev:nets)
|
2014-05-02 18:09:46 +00:00
|
|
|
|
|
|
|
-- Each step of the simulation, check if each TransferNode wants to move,
|
|
|
|
-- and if so:
|
|
|
|
-- 1. It and its current location exchange their Requests.
|
|
|
|
-- 2. And they exchange any requested files.
|
|
|
|
-- 3. Move it to a new random location.
|
|
|
|
--
|
|
|
|
-- Note: This implementation does not exchange requests between two
|
|
|
|
-- TransferNodes that both arrive at the same location at the same step,
|
|
|
|
-- and then move away in the next step.
|
|
|
|
step :: (RandomGen g) => Network -> Rand g Network
|
|
|
|
step (Network immobiles transfers) = go immobiles [] transfers
|
|
|
|
where
|
|
|
|
go is c [] = return (Network is c)
|
|
|
|
go is c (t:ts) = do
|
|
|
|
r <- randomProbability
|
|
|
|
if movefrequency t <= r
|
2014-05-03 12:49:39 +00:00
|
|
|
then case M.lookup (currentlocation t) is of
|
|
|
|
Nothing -> go is (c ++ [t]) ts
|
|
|
|
Just currentloc -> do
|
2014-05-09 19:41:05 +00:00
|
|
|
let (currentloc', t') = merge currentloc t
|
2014-05-03 12:49:39 +00:00
|
|
|
t'' <- move t'
|
|
|
|
go (M.insert (currentlocation t) currentloc' is) (c ++ [t'']) ts
|
2014-05-02 18:09:46 +00:00
|
|
|
else go is (c ++ [t]) ts
|
|
|
|
|
2014-05-09 19:41:05 +00:00
|
|
|
merge :: ImmobileNode -> TransferNode -> (ImmobileNode, TransferNode)
|
|
|
|
merge (ImmobileNode ir) t@(TransferNode { transferrepo = tr }) =
|
2014-05-02 18:09:46 +00:00
|
|
|
( ImmobileNode (go ir tr)
|
|
|
|
, t { transferrepo = go tr ir }
|
|
|
|
)
|
|
|
|
where
|
|
|
|
go r1 r2 = r1
|
2014-05-09 19:41:05 +00:00
|
|
|
{ wantFiles = wantFiles'
|
|
|
|
, haveFiles = haveFiles'
|
|
|
|
, satisfiedRequests = satisfiedRequests' `S.union` checkSatisfied wantFiles' haveFiles'
|
2014-05-02 18:09:46 +00:00
|
|
|
}
|
2014-05-09 19:41:05 +00:00
|
|
|
where
|
|
|
|
wantFiles' = foldr addRequest (wantFiles r1) (wantFiles r2)
|
|
|
|
haveFiles' = S.foldr (addFile wantFiles' satisfiedRequests') (haveFiles r1) (haveFiles r2)
|
|
|
|
satisfiedRequests' = satisfiedRequests r1 `S.union` satisfiedRequests r2
|
|
|
|
|
|
|
|
-- Adds a file to the set, when there's a request for it, and the request
|
|
|
|
-- has not already been satisfied.
|
|
|
|
addFile :: [Request] -> S.Set Request -> File -> S.Set File -> S.Set File
|
|
|
|
addFile rs srs f fs
|
|
|
|
| any (\sr -> f == requestedFile sr) (S.toList srs) = fs
|
2014-05-02 18:09:46 +00:00
|
|
|
| any (\r -> f == requestedFile r) rs = S.insert f fs
|
|
|
|
| otherwise = fs
|
|
|
|
|
2014-05-09 19:41:05 +00:00
|
|
|
-- Checks if any requests have been satisfied, and returns them,
|
|
|
|
-- to be added to satisfidRequests
|
|
|
|
checkSatisfied :: [Request] -> S.Set File -> S.Set Request
|
|
|
|
checkSatisfied want have = S.fromList (filter satisfied want)
|
|
|
|
where
|
|
|
|
satisfied r = requestTTL r == originTTL && S.member (requestedFile r) have
|
|
|
|
|
2014-05-02 18:09:46 +00:00
|
|
|
-- Decrements TTL, and avoids adding request with a stale TTL, or a
|
|
|
|
-- request for an already added file with the same or a lower TTL.
|
|
|
|
addRequest :: Request -> [Request] -> [Request]
|
|
|
|
addRequest (Request f ttl) rs
|
|
|
|
| staleTTL ttl' = rs
|
|
|
|
| any (\r -> requestTTL r >= ttl) similar = rs
|
|
|
|
| otherwise = r' : other
|
|
|
|
where
|
|
|
|
ttl' = decTTL ttl
|
|
|
|
r' = Request f ttl'
|
|
|
|
(other, similar) = partition (/= r') rs
|
|
|
|
|
|
|
|
move :: (RandomGen g) => TransferNode -> Rand g TransferNode
|
|
|
|
move t = do
|
|
|
|
newloc <- randomfrom (possiblelocations t)
|
|
|
|
return $ t { currentlocation = newloc }
|
|
|
|
|
|
|
|
genNetwork :: (RandomGen g) => Rand g Network
|
|
|
|
genNetwork = do
|
2014-05-06 18:25:48 +00:00
|
|
|
let immobiles = M.fromList (zip (map show [1..]) (replicate numImmobileNodes emptyImmobile))
|
2014-05-03 13:42:13 +00:00
|
|
|
transfers <- sequence (replicate numTransferNodes (mkTransfer $ M.keys immobiles))
|
2014-05-02 18:09:46 +00:00
|
|
|
return $ Network immobiles transfers
|
|
|
|
|
2014-05-06 18:25:48 +00:00
|
|
|
emptyImmobile :: ImmobileNode
|
2014-05-09 19:41:05 +00:00
|
|
|
emptyImmobile = ImmobileNode (NodeRepo [] S.empty S.empty)
|
2014-05-03 13:42:13 +00:00
|
|
|
|
|
|
|
mkTransfer :: (RandomGen g) => [NodeName] -> Rand g TransferNode
|
2014-05-02 18:09:46 +00:00
|
|
|
mkTransfer immobiles = do
|
|
|
|
-- Transfer nodes are given random routes. May be simplistic.
|
|
|
|
-- Also, some immobile nodes will not be serviced by any transfer nodes.
|
|
|
|
numpossiblelocs <- getRandomR transferDestinationsRange
|
2014-05-03 13:42:13 +00:00
|
|
|
possiblelocs <- sequence (replicate numpossiblelocs (randomfrom immobiles))
|
|
|
|
mkTransferBetween possiblelocs
|
|
|
|
|
|
|
|
mkTransferBetween :: (RandomGen g) => [NodeName] -> Rand g TransferNode
|
|
|
|
mkTransferBetween possiblelocs = do
|
2014-05-02 18:09:46 +00:00
|
|
|
currentloc <- randomfrom possiblelocs
|
|
|
|
movefreq <- getRandomR transferMoveFrequencyRange
|
|
|
|
-- transfer nodes start out with no files or requests in their repo
|
2014-05-09 19:41:05 +00:00
|
|
|
let repo = (NodeRepo [] S.empty S.empty)
|
2014-05-02 18:09:46 +00:00
|
|
|
return $ TransferNode currentloc possiblelocs movefreq repo
|
|
|
|
|
|
|
|
randomfrom :: (RandomGen g) => [a] -> Rand g a
|
|
|
|
randomfrom l = do
|
|
|
|
i <- getRandomR (1, length l)
|
|
|
|
return $ l !! (i - 1)
|
|
|
|
|
2014-05-06 18:25:48 +00:00
|
|
|
-- Seeds the network with the given number of files. Each file is added to
|
|
|
|
-- one of the immobile nodes of the network at random. And, one other node,
|
|
|
|
-- at random, is selected which wants to get the file.
|
|
|
|
seedFiles :: (RandomGen g) => Int -> Network -> Rand g Network
|
|
|
|
seedFiles 0 network = return network
|
|
|
|
seedFiles n network@(Network m t) = do
|
|
|
|
(origink, ImmobileNode originr) <- randnode
|
|
|
|
(destinationk, ImmobileNode destinationr) <- randnode
|
|
|
|
let file = File n
|
|
|
|
let origin = ImmobileNode $ originr
|
|
|
|
{ haveFiles = S.insert file (haveFiles originr) }
|
|
|
|
let destination = ImmobileNode $ destinationr
|
|
|
|
{ wantFiles = Request file originTTL : wantFiles destinationr }
|
|
|
|
let m' = M.insert origink origin $
|
|
|
|
M.insert destinationk destination m
|
|
|
|
seedFiles (n - 1) (Network m' t)
|
|
|
|
where
|
|
|
|
randnode = do
|
|
|
|
k <- randomfrom (M.keys m)
|
|
|
|
return (k, fromJust $ M.lookup k m)
|
|
|
|
|
2014-05-03 14:55:01 +00:00
|
|
|
summarize :: Network -> Network -> String
|
|
|
|
summarize _initial@(Network origis _) _final@(Network is _ts) = format
|
2014-05-02 18:09:46 +00:00
|
|
|
[ ("Total wanted files",
|
|
|
|
show (sum (overis (length . findoriginreqs . wantFiles . repo))))
|
|
|
|
, ("Wanted files that were not transferred to requesting node",
|
|
|
|
show (sum (overis (S.size . findunsatisfied . repo))))
|
2014-05-03 14:55:01 +00:00
|
|
|
, ("Nodes that failed to get files",
|
|
|
|
show (map withinitiallocs $ filter (not . S.null . snd)
|
|
|
|
(M.toList $ M.map (findunsatisfied . repo) is)))
|
2014-05-09 19:41:05 +00:00
|
|
|
, ("Total number of files on immobile nodes at end",
|
|
|
|
show (overis (S.size . haveFiles . repo)))
|
2014-05-03 14:55:01 +00:00
|
|
|
--, ("Immobile nodes at end", show is)
|
2014-05-02 18:09:46 +00:00
|
|
|
]
|
|
|
|
where
|
|
|
|
findoriginreqs = filter (\r -> requestTTL r == originTTL)
|
|
|
|
findunsatisfied r =
|
|
|
|
let wantedfs = S.fromList $ map requestedFile (findoriginreqs (wantFiles r))
|
|
|
|
in S.difference wantedfs (haveFiles r)
|
|
|
|
repo (ImmobileNode r) = r
|
2014-05-03 12:49:39 +00:00
|
|
|
overis f = map f $ M.elems is
|
2014-05-03 14:55:01 +00:00
|
|
|
format = unlines . map (\(d, s) -> d ++ ": " ++ s)
|
|
|
|
|
|
|
|
withinitiallocs (name, missingfiles) = (name, S.map addinitialloc missingfiles)
|
|
|
|
addinitialloc f = (f, M.lookup f initiallocs)
|
|
|
|
|
|
|
|
initiallocs = M.fromList $
|
|
|
|
concatMap (\(k, v) -> map (\f -> (f, k)) (S.toList $ haveFiles $ repo v)) $
|
|
|
|
M.toList origis
|
2014-05-03 13:42:13 +00:00
|
|
|
|
2014-05-06 18:25:48 +00:00
|
|
|
trace :: (Network -> S.Set NodeName) -> [Network] -> String
|
|
|
|
trace tracer networks = show $ go [] S.empty $ map tracer networks
|
|
|
|
where
|
|
|
|
go c old [] = reverse c
|
|
|
|
go c old (new:l) = go ((S.toList $ new `S.difference` old):c) new l
|
|
|
|
|
|
|
|
traceHaveFile :: File -> Network -> S.Set NodeName
|
|
|
|
traceHaveFile f (Network m _) = S.fromList $ M.keys $
|
|
|
|
M.filter (\(ImmobileNode r) -> f `S.member` haveFiles r) m
|
|
|
|
|
|
|
|
traceWantFile :: File -> Network -> S.Set NodeName
|
|
|
|
traceWantFile f (Network m _) = S.fromList $ M.keys $
|
|
|
|
M.filter (\(ImmobileNode r) -> any wantf (wantFiles r)) m
|
|
|
|
where
|
|
|
|
wantf (Request rf _ttl) = rf == f
|
|
|
|
|
|
|
|
mocambosNetwork :: (RandomGen g) => Rand g Network
|
|
|
|
mocambosNetwork = do
|
|
|
|
let major = map (immobilenamed . fst) communities
|
|
|
|
let minor = map immobilenamed (concatMap snd communities)
|
2014-05-03 13:42:13 +00:00
|
|
|
majortransfer <- mapM mkTransferBetween majorroutes
|
2014-05-06 18:25:48 +00:00
|
|
|
minortransfer <- mapM mkTransferBetween (concatMap minorroutes (concat (replicate 5 communities)))
|
2014-05-03 13:42:13 +00:00
|
|
|
return $ Network
|
|
|
|
(M.fromList (major++minor))
|
|
|
|
(majortransfer ++ minortransfer)
|
|
|
|
where
|
2014-05-06 18:25:48 +00:00
|
|
|
immobilenamed name = (name, emptyImmobile)
|
2014-05-03 13:42:13 +00:00
|
|
|
|
|
|
|
-- As a simplification, this only makes 2 hop routes, between minor
|
|
|
|
-- and major communities; no 3-legged routes.
|
|
|
|
minorroutes :: (NodeName, [NodeName]) -> [Route]
|
|
|
|
minorroutes (major, minors) = map (\n -> [major, n]) minors
|
|
|
|
|
|
|
|
communities :: [(NodeName, [NodeName])]
|
|
|
|
communities =
|
|
|
|
[ ("Tainá/SP",
|
|
|
|
[ "badtas"
|
|
|
|
, "vauedo ribera"
|
|
|
|
, "cofundo"
|
|
|
|
, "jao"
|
|
|
|
, "fazenda"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Odomode/RS",
|
|
|
|
[ "moradadapaz"
|
|
|
|
, "pelotas"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("MercadoSul/DF",
|
|
|
|
[ "mesquito"
|
|
|
|
, "kalungos"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Coco/PE",
|
|
|
|
[ "xambá"
|
|
|
|
, "alafin"
|
|
|
|
, "terreiaos"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Linharinho/ES",
|
|
|
|
[ "monte alegne"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Boneco/BA",
|
|
|
|
[ "barroso"
|
|
|
|
, "lagoa santa"
|
|
|
|
, "terravista"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Zumbidospalmanes/NA",
|
|
|
|
[ "allantana"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Casa Pneta/PA",
|
|
|
|
[ "marajó"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Purarue/PA",
|
|
|
|
[ "oriaminá"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
, ("Madiba/NET", [])
|
|
|
|
]
|
|
|
|
|
|
|
|
majorroutes :: [Route]
|
|
|
|
majorroutes =
|
|
|
|
-- person's routes
|
|
|
|
[ ["Tainá/SP", "Odomode/RS"]
|
|
|
|
, ["Tainá/SP", "MercadoSul/DF"]
|
|
|
|
, ["MercadoSul/DF", "Boneco/BA"]
|
|
|
|
, ["MercadoSul/DF", "Zumbidospalmanes/NA"]
|
|
|
|
, ["Zumbidospalmanes/NA", "Casa Pneta/PA"]
|
|
|
|
, ["Casa Pneta/PA", "Purarue/PA"]
|
|
|
|
, ["Casa Pneta/PA", "Linharinho/ES"]
|
|
|
|
, ["Boneco/BA", "Coco/PE"]
|
|
|
|
-- internet connections
|
|
|
|
, ["Tainá/SP", "MercadoSul/DF", "Coco/PE", "Purarue/PA", "Odomode/RS", "Madiba/NET"]
|
2014-05-06 18:25:48 +00:00
|
|
|
, ["Tainá/SP", "MercadoSul/DF", "Coco/PE", "Purarue/PA", "Odomode/RS", "Madiba/NET"]
|
|
|
|
, ["Tainá/SP", "MercadoSul/DF", "Coco/PE", "Purarue/PA", "Odomode/RS", "Madiba/NET"]
|
|
|
|
, ["Tainá/SP", "MercadoSul/DF", "Coco/PE", "Purarue/PA", "Odomode/RS", "Madiba/NET"]
|
|
|
|
, ["Tainá/SP", "MercadoSul/DF", "Coco/PE", "Purarue/PA", "Odomode/RS", "Madiba/NET"]
|
2014-05-03 13:42:13 +00:00
|
|
|
]
|