diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..a5040abd159bdd7db4b7be8b9954163e2ca7a1f1
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = CLMCservice
+omit = CLMCservice/tests.py
diff --git a/.gitignore b/.gitignore
index 21660e185426fc0c067bd4f1643ddbc053d287d7..9225ad6177e3a883536cf870c851dda742d4640c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,9 @@
 *_version.py*
 *reporc
 ubuntu-xenial-16.04-cloudimg-console.log
-**/.pytest_cache/
\ No newline at end of file
+.idea/
+*.egg
+*.pyc
+.pytest_cache
+.tox
+*$py.class
diff --git a/CLMCservice/__init__.py b/CLMCservice/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..80dfcc74f743b5e543ce9320d4e92541577b8f7b
--- /dev/null
+++ b/CLMCservice/__init__.py
@@ -0,0 +1,20 @@
+from pyramid.config import Configurator
+from pyramid.settings import asbool
+from CLMCservice.views import AggregatorConfig
+
+
+def main(global_config, **settings):
+    """ This function returns a Pyramid WSGI application."""
+
+    # a conversion is necessary so that the configuration value of the aggregator is stored as bool and not as string
+    aggregator_running = asbool(settings.get('aggregator_running', 'false'))
+    settings['aggregator_running'] = aggregator_running
+
+    config = Configurator(settings=settings)
+
+    config.add_route('aggregator', '/aggregator')
+    config.add_view(AggregatorConfig, attr='get', request_method='GET')
+    config.add_view(AggregatorConfig, attr='post', request_method='POST')
+
+    config.scan()
+    return config.make_wsgi_app()
diff --git a/CLMCservice/tests.py b/CLMCservice/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..c475fa7fa0e49591d13aefbf718ae536c6ec5980
--- /dev/null
+++ b/CLMCservice/tests.py
@@ -0,0 +1,75 @@
+import pytest
+from pyramid import testing
+from pyramid.httpexceptions import HTTPBadRequest
+
+
+class TestAggregatorConfig(object):
+    """
+    A pytest-implementation test for the aggregator configuration API calls
+    """
+
+    @pytest.fixture(autouse=True)
+    def app_config(self):
+        """
+        A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service
+        """
+
+        self.config = testing.setUp()
+        self.config.add_settings({'aggregator_running': False})
+
+        yield
+
+        testing.tearDown()
+
+    def test_GET(self):
+        """
+        Tests the GET method for the status of the aggregator.
+        """
+
+        from CLMCservice.views import AggregatorConfig  # nested import so that importing the class view is part of the test itself
+
+        assert not self.config.get_settings().get('aggregator_running'), "Initially aggregator is not running."
+
+        request = testing.DummyRequest()
+        response = AggregatorConfig(request).get()
+
+        assert type(response) == dict, "Response must be a dictionary representing a JSON object."
+        assert not response.get('aggregator_running'), "The response of the API call must return the aggregator status being set as False"
+        assert not self.config.get_settings().get('aggregator_running'), "A GET request must not modify the aggregator status."
+
+    @pytest.mark.parametrize("input_val, output_val", [
+        ("True", True),
+        ("true", True),
+        ("1", True),
+        ("False", False),
+        ("false", False),
+        ("0", False),
+        ("t", None),
+        ("f", None),
+    ])
+    def test_POST(self, input_val, output_val):
+        """
+        Tests the POST method for the status of the aggregator
+        :param input_val: the input form parameter
+        :param output_val: the expected output value, None for expecting an Exception
+        """
+
+        from CLMCservice.views import AggregatorConfig  # nested import so that importing the class view is part of the test itself
+
+        assert not self.config.get_settings().get('aggregator_running'), "Initially aggregator is not running."
+
+        request = testing.DummyRequest()
+
+        request.params['running'] = input_val
+        if output_val is not None:
+            response = AggregatorConfig(request).post()
+            assert response == {'aggregator_running': output_val}, "Response of POST request must include the new status of the aggregator"
+            assert self.config.get_settings().get('aggregator_running') == output_val, "Aggregator status must be updated to running."
+        else:
+            error_raised = False
+            try:
+                AggregatorConfig(request).post()
+            except HTTPBadRequest:
+                error_raised = True
+
+            assert error_raised, "Error must be raised in case of an invalid argument."
diff --git a/CLMCservice/utilities.py b/CLMCservice/utilities.py
new file mode 100644
index 0000000000000000000000000000000000000000..e17818ae15f7be180d85aebc79ec1574761a1840
--- /dev/null
+++ b/CLMCservice/utilities.py
@@ -0,0 +1,17 @@
+def str_to_bool(value):
+    """
+    A utility function to convert a string to boolean based on simple rules.
+    :param value: the value to convert
+    :return: True or False
+    :raises ValueError: if value cannot be converted to boolean
+    """
+
+    if type(value) is not str:
+        raise ValueError("This method only converts string to booolean.")
+
+    if value in ('False', 'false', '0'):
+        return False
+    elif value in ('True', 'true', '1'):
+        return True
+    else:
+        raise ValueError("Invalid argument for conversion")
diff --git a/CLMCservice/views.py b/CLMCservice/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b92523509077c7b2319c3d0d625a880ebd5cdbb
--- /dev/null
+++ b/CLMCservice/views.py
@@ -0,0 +1,46 @@
+from pyramid.view import view_defaults
+from pyramid.httpexceptions import HTTPBadRequest
+
+from CLMCservice.utilities import str_to_bool
+
+
+@view_defaults(route_name='aggregator', renderer='json')
+class AggregatorConfig(object):
+    """
+    A class-based view for accessing and mutating the status of the aggregator.
+    """
+
+    def __init__(self, request):
+        """
+        Initialises the instance of the view with the request argument.
+        :param request: client's call request
+        """
+
+        self.request = request
+
+    def get(self):
+        """
+        A GET API call for the status of the aggregator.
+        :return: A JSON response with the status of the aggregator.
+        """
+
+        aggregator_running = self.request.registry.settings.get('aggregator_running')
+        return {'aggregator_running': aggregator_running}
+
+    def post(self):
+        """
+        A POST API call for the status of the aggregator.
+        :return: A JSON response to the POST call (success or fail).
+        :raises HTTPBadRequest: if form argument cannot be converted to boolean
+        """
+
+        new_status = self.request.params.get('running')
+
+        try:
+            new_status = str_to_bool(new_status)
+        except ValueError:
+            raise HTTPBadRequest("Bad request parameter - expected a boolean, received {0}".format(self.request.params.get('running')))
+
+        self.request.registry.settings['aggregator_running'] = new_status
+        # TODO start/stop aggregator based on value of new status
+        return {'aggregator_running': new_status}
diff --git a/README.md b/README.md
index 14a7fd9e40655abcd724ddb05c1409fd79f1907c..56222b74a8e0afb4c9b66c0c83fd3fc29b7fac0b 100644
--- a/README.md
+++ b/README.md
@@ -104,4 +104,45 @@ Then the package is installed
 
 Then the tests are run
 
-`vagrant --fixture=scripts -- ssh test-runner -- -tt "pytest -s --pyargs clmctest.scripts"`
\ No newline at end of file
+`sudo apt-get install python3-pip` 
+  
+`pip3 install pytest`
+
+
+#### CLMC Service
+
+The CLMC service is implemented using the Pyramid framework. (currently under development)
+
+Before installing the CLMC service and its dependencies, it is recommended to use a virtual environment. To manage virtual
+environments, **virtualenvwrapper** can be used.
+
+```
+pip install virtualenvwrapper
+```
+
+To create a virtual environment use the **mkvirtualenv** command:
+
+```
+mkvirtualenv CLMC
+```
+
+When created, you should already be set to use the new virtual environment, but to make sure of this use the **workon** command:
+
+```
+workon CLMC
+```
+
+Now, any installed libraries will be specificly installed in this environment only. To install and use the CLMC service 
+locally, the easiest thing to do is to use **pip** (make sure you are in the root folder of the project - ***flame-clmc***):
+
+```
+pip install -e .
+```
+
+Finally, start the service on localhost by using pyramid's **pserve**:
+
+```
+pserve development.ini --reload
+```
+
+You should now be able to see the 'Hello world' message when visiting **http://localhost:8080** in your browser.
diff --git a/clmctest/monitoring/E2EAggregator.py b/clmctest/monitoring/E2EAggregator.py
new file mode 100644
index 0000000000000000000000000000000000000000..8bef3d921ef0345fc3db6eb7ef6933b6c7101f34
--- /dev/null
+++ b/clmctest/monitoring/E2EAggregator.py
@@ -0,0 +1,147 @@
+#!/usr/bin/python3
+"""
+## © University of Southampton IT Innovation Centre, 2018
+##
+## Copyright in this software belongs to University of Southampton
+## IT Innovation Centre of Gamma House, Enterprise Road,
+## Chilworth Science Park, Southampton, SO16 7NS, UK.
+##
+## This software may not be used, sold, licensed, transferred, copied
+## or reproduced in whole or in part in any manner or form or in or
+## on any media by any person other than in accordance with the terms
+## of the Licence Agreement supplied with the software, or otherwise
+## without the prior written consent of the copyright owners.
+##
+## This software is distributed WITHOUT ANY WARRANTY, without even the
+## implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+## PURPOSE, except where stated in the Licence Agreement supplied with
+## the software.
+##
+##      Created By :            Nikolay Stanchev
+##      Created Date :          25-04-2018
+##      Created for Project :   FLAME
+"""
+
+from influxdb import InfluxDBClient
+from time import time, sleep
+from urllib.parse import urlparse
+from threading import Thread, Event
+import clmctest.monitoring.LineProtocolGenerator as lp
+
+
+class Aggregator(Thread):
+    """
+    A class used to perform the aggregation feature of the CLMC - aggregation network and media service measurements. Currently, implemented as a thread,
+    so that the implementation can be tested using pytest.
+    """
+
+    REPORT_PERIOD = 5  # currently, report period is 5s, that is every 5 seconds the mean delay values for the last 5 seconds are aggregated
+    DATABASE = 'E2EMetrics'  # default database the aggregator uses
+    DATABASE_URL = 'http://203.0.113.100:8086'  # default database URL the aggregator uses
+    
+    def __init__(self, database=DATABASE, database_url=DATABASE_URL):
+        """
+        Constructs an Aggregator instance.
+
+        :param database: database name to use
+        :param database_url: database url to use
+        """
+
+        super(Aggregator, self).__init__()  # call the constructor of the thread
+
+        # initialise a database client using the database url and the database name
+        url_object = urlparse(database_url)
+        self.db_client = InfluxDBClient(host=url_object.hostname, port=url_object.port, database=database, timeout=10)
+
+        self.db_url = database_url
+        self.db_name = database
+
+        # a stop flag event object used to handle the killing of the thread
+        self._stop_flag = Event()
+
+    def stop(self):
+        """
+        A method used to stop the thread.
+        """
+
+        self._stop_flag.set()
+
+    def run(self):
+        """
+        Performs the functionality of the aggregator - query data from both measurements merge that data and post it back in influx every 5 seconds.
+        """
+
+        current_time = int(time())
+        while True:
+            if self._stop_flag.is_set():
+                break
+
+            boundary_time = current_time - Aggregator.REPORT_PERIOD
+
+            boundary_time_nano = boundary_time * 1000000000
+            current_time_nano = current_time * 1000000000
+
+            # query the network delays and group them by path ID
+            network_delays = {}
+            result = self.db_client.query(
+                'SELECT mean(delay) as "net_delay" FROM "E2EMetrics"."autogen"."network_delays" WHERE time >= {0} and time < {1} GROUP BY path, source, target'.format(
+                    boundary_time_nano, current_time_nano))
+            for item in result.items():
+                metadata, result_points = item
+                # measurement = metadata[0]
+                tags = metadata[1]
+
+                network_delays[(tags['path'], tags['source'], tags['target'])] = next(result_points)['net_delay']
+
+            # query the service delays and group them by endpoint, service function instance and sfr
+            service_delays = {}
+            result = self.db_client.query('SELECT mean(response_time) as "response_time" FROM "E2EMetrics"."autogen"."service_delays" WHERE time >= {0} and time < {1} GROUP BY endpoint, sf_instance, sfr'.format(boundary_time_nano, current_time_nano))
+            for item in result.items():
+                metadata, result_points = item
+                # measurement = metadata[0]
+                tags = metadata[1]
+                service_delays[tags['sfr']] = (next(result_points)['response_time'], tags['endpoint'], tags['sf_instance'])
+
+            # for each network path check if there is a media service delay report for the target sfr - if so, generate an e2e_delay measurement
+            for path in network_delays:
+                # check if target sfr is reported in service delays, in other words - if there is a media service instance being connected to target sfr
+                path_id, source, target = path
+                if target not in service_delays:
+                    # if not continue with the other network path reports
+                    continue
+
+                e2e_arguments = {"path_ID": None, "source_SFR": None, "target_SFR": None, "endpoint": None, "sf_instance": None, "delay_forward": None, "delay_reverse": None,
+                                 "delay_service": None, "time": boundary_time}
+
+                e2e_arguments['path_ID'] = path_id
+                e2e_arguments['delay_forward'] = network_delays[path]
+
+                # reverse the path ID to get the network delay for the reversed path
+                reversed_path = (path_id, target, source)
+                assert reversed_path in network_delays  # reversed path must always be reported with the forward one - if there is network path A-B, there is also network path B-A
+                e2e_arguments['delay_reverse'] = network_delays[reversed_path]
+
+                # get the response time of the media component connected to the target SFR
+                service_delay = service_delays[target]
+                response_time, endpoint, sf_instance = service_delay
+                # put these points in the e2e arguments dictionary
+                e2e_arguments['delay_service'] = response_time
+                e2e_arguments['endpoint'] = endpoint
+                e2e_arguments['sf_instance'] = sf_instance
+
+                # if all the arguments of the e2e delay measurements were reported, then generate and post to Influx an E2E measurement row
+                if None not in e2e_arguments.items():
+                    self.db_client.write_points(
+                        lp.generate_e2e_delay_report(e2e_arguments['path_ID'], e2e_arguments['source_SFR'], e2e_arguments['target_SFR'], e2e_arguments['endpoint'],
+                                                     e2e_arguments['sf_instance'], e2e_arguments['delay_forward'], e2e_arguments['delay_reverse'], e2e_arguments['delay_service'],
+                                                     e2e_arguments['time']))
+
+            old_timestamp = current_time
+            # wait until {REPORT_PERIOD} seconds have passed
+            while current_time != old_timestamp + self.REPORT_PERIOD:
+                sleep(1)
+                current_time = int(time())
+
+
+if __name__ == '__main__':
+    Aggregator().start()
diff --git a/clmctest/monitoring/E2ESim.py b/clmctest/monitoring/E2ESim.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8974f880916ff5b568b98a859e3161b39f17d03
--- /dev/null
+++ b/clmctest/monitoring/E2ESim.py
@@ -0,0 +1,129 @@
+#!/usr/bin/python3
+"""
+## © University of Southampton IT Innovation Centre, 2018
+##
+## Copyright in this software belongs to University of Southampton
+## IT Innovation Centre of Gamma House, Enterprise Road,
+## Chilworth Science Park, Southampton, SO16 7NS, UK.
+##
+## This software may not be used, sold, licensed, transferred, copied
+## or reproduced in whole or in part in any manner or form or in or
+## on any media by any person other than in accordance with the terms
+## of the Licence Agreement supplied with the software, or otherwise
+## without the prior written consent of the copyright owners.
+##
+## This software is distributed WITHOUT ANY WARRANTY, without even the
+## implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+## PURPOSE, except where stated in the Licence Agreement supplied with
+## the software.
+##
+##      Created By :            Michael Boniface
+##      Created Date :          15-04-2018
+##      Updated By :            Nikolay Stanchev
+##      Updated Date :          16-04-2018
+##      Created for Project :   FLAME
+"""
+
+
+from influxdb import InfluxDBClient
+import clmctest.monitoring.LineProtocolGenerator as lp
+import urllib.parse
+import time
+import random
+
+
+class Simulator(object):
+    """
+    Simulator used to generate E2E measurements.
+    """
+
+    DATABASE = 'E2EMetrics'  # default database name
+    DATABASE_URL = 'http://203.0.113.100:8086'  # default database url
+
+    TICK = 1  # a simulation tick represents 1s
+    SIMULATION_LENGTH = 120  # simulation time in seconds
+
+    def __init__(self, database_url=DATABASE_URL, database=DATABASE):
+        """
+        Initialises the simulator by creating a db client object and resetting the database.
+
+        :param database_url: db url
+        :param database: db name
+        """
+
+        url_object = urllib.parse.urlparse(database_url)
+        self.db_client = InfluxDBClient(host=url_object.hostname, port=url_object.port, database=database, timeout=10)
+
+        self.db_url = database_url
+        self.db_name = database
+
+        self._reset_db()
+
+    def _reset_db(self):
+        """
+        Reset the database using the already initialised db client object.
+        """
+
+        self.db_client.drop_database(self.db_name)
+        self.db_client.create_database(self.db_name)
+
+    def run(self):
+        """
+        Runs the simulation.
+        """
+
+        # all network delays start from 1ms, the dictionary stores the information to report
+        paths = [
+            {'target': 'SR3',
+             'source': 'SR1',
+             'path_id': 'SR1---SR3',
+             'network_delay': 1},
+            {'target': 'SR1',
+             'source': 'SR3',
+             'path_id': 'SR1---SR3',
+             'network_delay': 1}
+        ]
+
+        # current time in seconds (to test the aggregation we write influx data points related to future time), so we start from the current time
+        start_time = int(time.time())
+
+        sim_time = start_time
+
+        mean_delay_seconds_media = 10  # initial mean media service delay
+        sample_period_net = 2  # sample period for reporting network delays (measured in seconds) - net measurements reported every 2s
+        sample_period_media = 5  # sample period for reporting media service delays (measured in seconds) - service measurements reported every 5 seconds
+
+        for i in range(0, self.SIMULATION_LENGTH):
+            # measure net delay every 2 seconds for path SR1-SR3 (generates on tick 0, 2, 4, 6, 8, 10.. etc.)
+            if i % sample_period_net == 0:
+                path = paths[0]
+                self.db_client.write_points(lp.generate_network_delay_report(path['path_id'], path['source'], path['target'], path['network_delay'], sim_time))
+
+                # increase/decrease the delay in every sample report (min delay is 1)
+                path['network_delay'] = max(1, path['network_delay'] + random.randint(-3, 3))
+
+            # measure net delay every 2 seconds for path SR2-SR3 (generates on tick 1, 3, 5, 7, 9, 11.. etc.)
+            if (i+1) % sample_period_net == 0:
+                path = paths[1]
+                self.db_client.write_points(lp.generate_network_delay_report(path['path_id'], path['source'], path['target'], path['network_delay'], sim_time))
+
+                # increase/decrease the delay in every sample report (min delay is 1)
+                path['network_delay'] = max(1, path['network_delay'] + random.randint(-3, 3))
+
+            # measure service response time every 5 seconds
+            if i % sample_period_media == 0:
+                self.db_client.write_points(lp.generate_service_delay_report(mean_delay_seconds_media, "endpoint-1",
+                                                                             "ms-A.ict-flame.eu", "SR3",  sim_time))
+
+                # increase/decrease the delay in every sample report (min delay is 10)
+                mean_delay_seconds_media = max(10, mean_delay_seconds_media + random.choice([random.randint(10, 20), random.randint(-20, -10)]))
+
+            # increase the time by one simulation tick
+            sim_time += self.TICK
+
+        end_time = sim_time
+        print("Simulation finished. Start time: {0}, End time: {1}".format(start_time, end_time))
+
+
+if __name__ == "__main__":
+    Simulator().run()
diff --git a/clmctest/monitoring/LineProtocolGenerator.py b/clmctest/monitoring/LineProtocolGenerator.py
index 76ffd32d28acf86ab201a71a3d56dbc1cbaf8828..432f27d41769bcf68d776b91f719d9f4bcb8d122 100644
--- a/clmctest/monitoring/LineProtocolGenerator.py
+++ b/clmctest/monitoring/LineProtocolGenerator.py
@@ -29,6 +29,95 @@ import uuid
 from random import randint
 
 
+def generate_e2e_delay_report(path_id, source_sfr, target_sfr, endpoint, sf_instance, delay_forward, delay_reverse, delay_service, time):
+    """
+    Generates a combined averaged measurement about the e2e delay and its contributing parts
+
+    :param path_ID: The path identifier, which is a bidirectional path ID for the request and the response path
+    :param source_SFR: source service router
+    :param target_SFR: target service router
+    :param endpoint: endpoint of the media component
+    :param sf_instance: service function instance (media component)
+    :param delay_forward: Path delay (Forward direction)
+    :param delay_reverse: Path delay (Reverse direction)
+    :param delay_service: the media service component response time
+    :param time: measurement timestamp
+    :return: a list of dict-formatted reports to post on influx
+    """
+
+    result = [{"measurement": "e2e_delays",
+               "tags": {
+                   "path_ID": path_id,
+                   "source_SFR": source_sfr,
+                   "target_SFR": target_sfr,
+                   "endpoint": endpoint,
+                   "sf_instance": sf_instance
+               },
+               "fields": {
+                   "delay_forward": float(delay_forward),
+                   "delay_reverse": float(delay_reverse),
+                   "delay_service": float(delay_service)
+               },
+               "time": _getNSTime(time)
+               }]
+
+    return result
+
+
+def generate_network_delay_report(path_id, source_sfr, target_sfr, e2e_delay, time):
+    """
+    Generates a platform measurement about the network delay between two specific service routers.
+
+    :param path_id: the identifier of the path between the two service routers
+    :param source_sfr: the source service router
+    :param target_sfr: the target service router
+    :param e2e_delay: the e2e network delay for traversing the path between the two service routers
+    :param time: the measurement timestamp
+    :return: a list of dict-formatted reports to post on influx
+    """
+
+    result = [{"measurement": "network_delays",
+               "tags": {
+                   "path": path_id,
+                   "source": source_sfr,
+                   "target": target_sfr
+               },
+               "fields": {
+                   "delay": e2e_delay
+               },
+               "time": _getNSTime(time)
+               }]
+
+    return result
+
+
+def generate_service_delay_report(response_time, endpoint, sf_instance, sfr, time):
+    """
+    Generates a service measurement about the media service response time.
+
+    :param response_time: the media service response time (This is not the response time for the whole round-trip, but only for the processing part of the media service component)
+    :param endpoint: endpoint of the media component
+    :param sf_instance: service function instance
+    :param sfr: the service function router that connects the endpoint of the SF instance to the FLAME network
+    :param time: the measurement timestamp
+    :return: a list of dict-formatted reports to post on influx
+    """
+
+    result = [{"measurement": "service_delays",
+               "tags": {
+                   "endpoint": endpoint,
+                   "sf_instance": sf_instance,
+                   "sfr": sfr
+               },
+               "fields": {
+                   "response_time": response_time,
+               },
+               "time": _getNSTime(time)
+               }]
+
+    return result
+
+
 # Reports TX and RX, scaling on requested quality
 def generate_network_report(recieved_bytes, sent_bytes, time):
     result = [{"measurement": "net_port_io",
@@ -125,6 +214,7 @@ def generate_endpoint_config(time, cpu, mem, storage, current_state, current_sta
 
     return result
 
+
 def generate_mc_service_config( time, mcMeasurement, current_state, current_state_time, config_state_values ):
     """
     generates a measurement line for a media component configuration state
diff --git a/clmctest/monitoring/conftest.py b/clmctest/monitoring/conftest.py
index 69389c98a172e75e4600a8baf909a204ecfc9447..72791eeaaedc266ed8ea1593b05674db679e962a 100644
--- a/clmctest/monitoring/conftest.py
+++ b/clmctest/monitoring/conftest.py
@@ -27,6 +27,8 @@ import yaml
 import pkg_resources
 from influxdb import InfluxDBClient
 from clmctest.monitoring.StreamingSim import Sim
+from clmctest.monitoring.E2ESim import Simulator
+from clmctest.monitoring.E2EAggregator import Aggregator
 
 
 @pytest.fixture(scope="module")
@@ -59,6 +61,12 @@ def influx_db(streaming_sim_config, request):
 
 @pytest.fixture(scope="module")
 def simulator(streaming_sim_config):
+    """
+    A fixture to obtain a simulator instance with the configuration parameters.
+
+    :param streaming_sim_config: the configuration object
+    :return: an instance of the simulator
+    """
 
     influx_url     = "http://" + streaming_sim_config['hosts'][0]['ip_address'] + ":8086"
     influx_db_name = streaming_sim_config['hosts'][1]['database_name']
@@ -70,3 +78,31 @@ def simulator(streaming_sim_config):
     simulator.reset()
 
     return simulator
+
+
+@pytest.fixture(scope="module")
+def e2e_simulator(streaming_sim_config):
+    """
+    A fixture to obtain a simulator instance with the configuration parameters.
+
+    :param streaming_sim_config: the configuration object
+    :return: an instance of the E2E simulator
+    """
+
+    influx_url = "http://" + streaming_sim_config['hosts'][0]['ip_address'] + ":8086"
+
+    return Simulator(database_url=influx_url)
+
+
+@pytest.fixture(scope="module")
+def e2e_aggregator(streaming_sim_config):
+    """
+    A fixture to obtain an instance of the Aggregator class with the configuration parameters.
+
+    :param streaming_sim_config: the configuration object
+    :return: an instance of the Aggregator class
+    """
+
+    influx_url = "http://" + streaming_sim_config['hosts'][0]['ip_address'] + ":8086"
+
+    return Aggregator(database_url=influx_url)
diff --git a/clmctest/monitoring/test_e2eresults.py b/clmctest/monitoring/test_e2eresults.py
new file mode 100644
index 0000000000000000000000000000000000000000..5486bcb618f645dbdbc69887adf6000c2c423e08
--- /dev/null
+++ b/clmctest/monitoring/test_e2eresults.py
@@ -0,0 +1,94 @@
+#!/usr/bin/python3
+"""
+## © University of Southampton IT Innovation Centre, 2018
+##
+## Copyright in this software belongs to University of Southampton
+## IT Innovation Centre of Gamma House, Enterprise Road,
+## Chilworth Science Park, Southampton, SO16 7NS, UK.
+##
+## This software may not be used, sold, licensed, transferred, copied
+## or reproduced in whole or in part in any manner or form or in or
+## on any media by any person other than in accordance with the terms
+## of the Licence Agreement supplied with the software, or otherwise
+## without the prior written consent of the copyright owners.
+##
+## This software is distributed WITHOUT ANY WARRANTY, without even the
+## implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+## PURPOSE, except where stated in the Licence Agreement supplied with
+## the software.
+##
+##      Created By :            Nikolay Stanchev
+##      Created Date :          17-04-2018
+##      Created for Project :   FLAME
+"""
+
+import pytest
+import random
+import time
+
+
+class TestE2ESimulation(object):
+    """
+    A testing class used to group all the tests related to the E2E simulation data
+    """
+
+    @pytest.fixture(scope='class', autouse=True)
+    def run_simulator(self, e2e_simulator, e2e_aggregator):
+        """
+        A fixture, which runs the simulation before running the tests.
+
+        :param e2e_simulator: the simulator for the end-to-end data
+        :param e2e_aggregator: the aggregator which merges the network and service measurements
+        """
+
+        random.seed(0)  # Seed random function so we can reliably test for average queries
+
+        print("Starting aggregator...")
+        e2e_aggregator.start()
+
+        print("Running simulation, please wait...")
+        e2e_simulator.run()
+
+        print("Waiting for INFLUX to finish receiving simulation data...")
+        time.sleep(e2e_simulator.SIMULATION_LENGTH)  # wait for data to finish arriving at the INFLUX database
+        print("... simulation data fixture finished")
+
+        print("... stopping aggregator")
+        e2e_aggregator.stop()
+
+
+    @pytest.mark.parametrize("query, expected_result", [
+        ('SELECT count(*) FROM "E2EMetrics"."autogen"."network_delays"',
+         {"time": "1970-01-01T00:00:00Z", "count_delay": 120}),
+        ('SELECT count(*) FROM "E2EMetrics"."autogen"."service_delays"',
+         {"time": "1970-01-01T00:00:00Z", "count_response_time": 24}),
+        ('SELECT count(*) FROM "E2EMetrics"."autogen"."e2e_delays"',
+         {"time": "1970-01-01T00:00:00Z", "count_delay_forward": 24, "count_delay_reverse": 24, "count_delay_service": 24}),
+
+        ('SELECT mean(*) FROM "E2EMetrics"."autogen"."e2e_delays"',
+         {"time": "1970-01-01T00:00:00Z", "mean_delay_forward": 13.159722222222223, "mean_delay_reverse": 3.256944444444444, "mean_delay_service": 32.791666666666664}),
+        ])
+    def test_simulation(self, influx_db, query, expected_result):
+        """
+        This is the entry point of the test. This method will be found and executed when the module is ran using pytest
+
+        :param query: the query to execute (value obtained from the pytest parameter decorator)
+        :param expected_result: the result expected from executing the query (value obtained from the pytest parameter decorator)
+        :param influx_db the import db client fixture - imported from contest.py
+        """
+
+        # pytest automatically goes through all queries under test, declared in the parameters decorator
+        print("\n")  # prints a blank line for formatting purposes
+
+        # the raise_errors=False argument is given so that we could actually test that the DB didn't return any errors instead of raising an exception
+        query_result = influx_db.query(query, raise_errors=False)
+
+        # test the error attribute of the result is None, that is no error is returned from executing the DB query
+        assert query_result.error is None, "An error was encountered while executing query {0}.".format(query)
+
+        # get the dictionary of result points; the next() function just gets the first element of the query results generator (we only expect one item in the generator)
+        actual_result = next(query_result.get_points())
+
+        assert expected_result == actual_result, "E2E Simulation test failure"
+
+        print("Successfully passed test for the following query: {0}".format(query))
diff --git a/clmctest/monitoring/test_simresults.py b/clmctest/monitoring/test_simresults.py
index ce64c922d85ce7f48db29ce4ca75d520d4db8a31..9d8670e7957e417554916f697e4e20af8fcf04be 100644
--- a/clmctest/monitoring/test_simresults.py
+++ b/clmctest/monitoring/test_simresults.py
@@ -65,20 +65,32 @@ class TestSimulation(object):
         ('SELECT count(*) FROM "CLMCMetrics"."autogen"."mpegdash_mc_config" WHERE ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "count_current_state_time": 3607, "count_running_mst": 3607, "count_running_sum": 3607, "count_starting_mst": 3607, "count_starting_sum": 3607, "count_stopped_mst": 3607, "count_stopped_sum": 3607, "count_stopping_mst": 3607, "count_stopping_sum": 3607}),
 
+        ('SELECT mean(unplaced_mst) as "unplaced_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE unplaced_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "unplaced_mst": 0.7}),
         ('SELECT mean(placing_mst) as "placing_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE placing_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "placing_mst": 9.4}),
+        ('SELECT mean(placed_mst) as "placed_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE placed_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "placed_mst": 1.7000000000000002}),
         ('SELECT mean(booting_mst) as "booting_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE booting_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "booting_mst": 9.6}),
+        ('SELECT mean(booted_mst) as "booted_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE booted_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "booted_mst": 2.1}),
         ('SELECT mean(connecting_mst) as "connecting_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE connecting_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "connecting_mst":  10.2}),
         ('SELECT mean(connected_mst) as "connected_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE connected_mst <> 0 and ipendpoint=\'endpoint1.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "connected_mst": 3605.0}),
+        ('SELECT mean(unplaced_mst) as "unplaced_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE unplaced_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "unplaced_mst": 0.7}),
         ('SELECT mean(placing_mst) as "placing_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE placing_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "placing_mst": 9.4}),
+        ('SELECT mean(placed_mst) as "placed_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE placed_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "placed_mst": 1.7000000000000002}),
         ('SELECT mean(booting_mst) as "booting_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE booting_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "booting_mst": 9.6}),
+        ('SELECT mean(booted_mst) as "booted_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE booted_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
+         {"time": "1970-01-01T00:00:00Z", "booted_mst": 2.1}),
         ('SELECT mean(connecting_mst) as "connecting_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE connecting_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
-         {"time": "1970-01-01T00:00:00Z", "connecting_mst": 10.2}),
+         {"time": "1970-01-01T00:00:00Z", "connecting_mst":  10.2}),
         ('SELECT mean(connected_mst) as "connected_mst" FROM "CLMCMetrics"."autogen"."endpoint_config" WHERE connected_mst <> 0 and ipendpoint=\'endpoint2.ms-A.ict-flame.eu\'',
          {"time": "1970-01-01T00:00:00Z", "connected_mst": 3605.0}),
 
@@ -99,7 +111,6 @@ class TestSimulation(object):
         ('SELECT mean(stopping_mst) as "stopping_mst" FROM "CLMCMetrics"."autogen"."mpegdash_mc_config" WHERE stopping_mst <> 0',
          {"time": "1970-01-01T00:00:00Z", "stopping_mst": 1.1}),
     ])
-    
     def test_simulation(self, influx_db, query, expected_result):
         """
         This is the entry point of the test. This method will be found and executed when the module is ran using pytest
diff --git a/development.ini b/development.ini
new file mode 100644
index 0000000000000000000000000000000000000000..0d37eed41f6e4abf6db45b22dc6d186af0d77355
--- /dev/null
+++ b/development.ini
@@ -0,0 +1,59 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:CLMCservice
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes = pyramid_debugtoolbar
+aggregator_running = false
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:8080
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, CLMCservice
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_CLMCservice]
+level = DEBUG
+handlers =
+qualname = CLMCservice
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/docs/aggregation.md b/docs/aggregation.md
new file mode 100644
index 0000000000000000000000000000000000000000..cc0e6269df929392b48999688bf9ccb467e4835f
--- /dev/null
+++ b/docs/aggregation.md
@@ -0,0 +1,149 @@
+<!--
+// © University of Southampton IT Innovation Centre, 2017
+//
+// Copyright in this software belongs to University of Southampton
+// IT Innovation Centre of Gamma House, Enterprise Road, 
+// Chilworth Science Park, Southampton, SO16 7NS, UK.
+//
+// This software may not be used, sold, licensed, transferred, copied
+// or reproduced in whole or in part in any manner or form or in or
+// on any media by any person other than in accordance with the terms
+// of the Licence Agreement supplied with the software, or otherwise
+// without the prior written consent of the copyright owners.
+//
+// This software is distributed WITHOUT ANY WARRANTY, without even the
+// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+// PURPOSE, except where stated in the Licence Agreement supplied with
+// the software.
+//
+//      Created By :            Nikolay Stanchev
+//      Created Date :          27-04-2018
+//      Created for Project :   FLAME
+-->
+
+## **Flame CLMC - Network and Media Service measurements aggregation**
+
+### **Idea** 
+
+The idea is to aggregate platform measurement points with media service measurement points and obtain a third measurement from which we can easily
+understand both end-to-end and round-trip performance of a media service. This is achieved by having a python script running on the background and aggregating
+the data from both measurements on a given sample period, e.g. every 10 seconds. The script then posts the aggregated data back to Influx in a new measurement. 
+
+
+### **Assumptions**
+
+* Network measurement - assumption is that we have a measurement for the network link delays, called **network_delays**, providing the following information:  
+
+| path (tag) | delay | time |
+| --- | --- | --- |
+| path identifier | e2e delay for the given path | time of measurement |
+
+Here, the **path** tag value is the identifier of the path between two nodes in the network topology obtained from FLIPS. The assumption is that those identifiers
+will be structured in such a way that we can obtain the source and target endpoint IDs from the path identifier itself. For example:  
+ **endpoint1.ms-A.ict-flame.eu---endpoint2.ms-A.ict-flame.eu**  
+We can easily split the string on **'---'** and, thus, find the source endpoint is **endpoint1.ms-A.ict-flame.eu**, while the target endpoint is 
+**endpoint2.ms-A.ict-flame.eu**.  
+The delay field value is the network end-to-end delay in milliseconds for the path identified in the tag value.
+
+* A response will traverse the same network path as the request, but in reverse direction.
+
+* Media service measurement - assumption is that we have a measurement for media services' response time, called **service_delays**, providing the following information:
+
+| FQDN (tag) | sf_instance (tag) | endpoint (tag) | response_time | time |
+| --- | --- | --- | --- | --- |
+| media service FQDN | ID of the service function instance | endpoint identifier | response time for the media service (s) | time of measurement |
+
+Here, the **FQDN**, **sf_instance** and **endpoint** tag values identify a unique response time measurement. The response time field value is the 
+response time (measured in seconds) for the media service only, and it does not take into account any of the network measurements.
+
+
+### **Goal**
+
+The ultimate goal is to populate a new measurement, called **e2e_delays**, which will be provided with the following information:
+
+| pathID_F (tag) | pathID_R (tag) | FQDN (tag) | sf_instance (tag) | D_path_F | D_path_R | D_service | time |
+| --- | --- | --- | --- | --- | --- | --- | --- | 
+
+* *pathID_F* - tag used to identify the path in forward direction, e.g. **endpoint1.ms-A.ict-flame.eu---endpoint2.ms-A.ict-flame.eu**
+* *pathID_R* - tag used to identify the path in reverse direction, e.g. **endpoint2.ms-A.ict-flame.eu---endpoint1.ms-A.ict-flame.eu**
+* *FQDN* - tag used to identify the media service
+* *sf_instance* - tag used to identify the media service
+* *D_path_F* - network delay for path in forward direction
+* *D_path_R* - network delay for path in reverse direction
+* *D_service* - media service response time
+
+Then we can easily query on this measurement to obtain different performance indicators, such as end-to-end overall delays, 
+round-trip response time or any of the contributing parts in those performance indicators. 
+
+
+### **Aggregation script**
+
+What the aggregation script does is very similat to the functionality of a continuous query. Given a sample report period, e.g. 10s,
+the script executes at every 10-second-period querying the averaged data for the last 10 seconds. The executed queries are:  
+
+* Network delays query - to obtain the network delay values and group them by their **path** identifier:
+```
+SELECT mean(delay) as "Dnet" FROM "E2EMetrics"."autogen".network_delays WHERE time >= now() - 10s and time < now() GROUP BY path
+``` 
+
+* Media service response time query - to obtain the response time values of the media service instances and group them by **FQDN**, **sf_instance** and **endpoint** identifiers: 
+```
+SELECT mean(response_time) as "Dresponse" FROM "E2EMetrics"."autogen".service_delays WHERE time >= now() - 10s and time < now() GROUP BY FQDN, sf_instance, endpoint
+```
+
+The results of the queries are then matched against each other on endpoint ID: on every match of the **endpoint** tag of the **service_delays** measurement with
+the target endpoint ID of the **network_delays** measurement, the rows are combined to obtain an **e2e_delay** measurement row, which is posted back to influx.
+
+Example:
+
+* Result from first query:
+
+```
+name: network_delays
+tags: path=endpoint1.ms-A.ict-flame.eu---endpoint2.ms-A.ict-flame.eu
+time                Dnet
+----                ----
+1524833145975682287 9.2
+
+name: network_delays
+tags: path=endpoint2.ms-A.ict-flame.eu---endpoint1.ms-A.ict-flame.eu
+time                Dnet
+----                ----
+1524833145975682287 10.3
+```
+  
+* Result from second query
+
+```
+name: service_delays
+tags: FQDN=ms-A.ict-flame.eu, endpoint=endpoint2.ms-A.ict-flame.eu, sf_instance=test-sf-clmc-agent-build_INSTANCE
+time                Dresponse
+----                ---------
+1524833145975682287 11
+```
+
+
+The script will parse the path identifier **endpoint1.ms-A.ict-flame.eu---endpoint2.ms-A.ict-flame.eu** and find the target endpoint being
+**endpoint2.ms-A.ict-flame.eu**. Then the script checks if there is service delay measurement row matching this endpoint. Since there is one,
+those values will be merged, so the result will be a row like this:
+
+| pathID_F (tag) | pathID_R (tag) | FQDN (tag) | sf_instance (tag) | D_path_F | D_path_R | D_service | time |
+| --- | --- | --- | --- | --- | --- | --- | --- | 
+| endpoint1.ms-A.ict-flame.eu---endpoint2.ms-A.ict-flame.eu | endpoint2.ms-A.ict-flame.eu---endpoint1.ms-A.ict-flame.eu | ms-A.ict-flame.eu | test-sf-clmc-agent-build_INSTANCE | 9.2 | 10.3 | 11 | 1524833145975682287 | 
+  
+Here, another assumption is made that we can reverse the path identifier of a network delay row and that the reverse path delay would also 
+be reported in the **network_delays** measurement. 
+
+The resulting row would then be posted back to influx in the **e2e_delays** measurement.
+
+
+### **Reasons why we cannot simply use a continuous query to do the job of the script**
+
+* Influx is very limited in merging measurements functionality. When doing a **select into** from multiple measurements, e.g.   
+*SELECT * INTO measurement0 FROM measurement1, measurement2*  
+influx will try to merge the data on matching time stamps and tag values (if there are any tags). If the two measurements
+differ in tags, then we get rows with missing data.
+* When doing a continuous query, we cannot perform any kind of manipulations on the data, which disables us on choosing which
+rows to merge together.
+* Continuous queries were not meant to be used for merging measurements. The main use case the developers provide is for
+downsampling the data in one measurement.
\ No newline at end of file
diff --git a/docs/monitoring.md b/docs/monitoring.md
index ee5cf15af1fcb2377375919bb1b460f6581d817c..f1104ea6601edc279ba7a8d6d3c83cbe6ae1003a 100644
--- a/docs/monitoring.md
+++ b/docs/monitoring.md
@@ -382,19 +382,22 @@ From the whole sample period (1s), the VM has been 0.9s in state 'placed'. Hence
 the VM has been reported to be in state __placing__. Since it has exited state __placing__, the total time spent in this state (9.3s + 0.1s = 9.4s) is reported.
 This includes the state time from previous reports. The mean state time value for __placing__ is the same as the sum value because the VM has only been once in this state.
 
-| global tags | current_state (tag) | current_state_time | unplaced_sum | unplaced_mst | placing_sum | placing_mst | placed_sum | placed_mst | booting_sum | booting_mst | booted_sum | booted_mst | connecting_sum | connecting_mst | connected_sum | connected_mst | time |
-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
-| ... | placing | 0.3 | 0.7 | 0.7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 1.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 2.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 3.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 4.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 5.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 6.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 7.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 8.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placing | 9.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
-| ... | placed | 0.9 | 0 | 0 | 9.4 | 9.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| global tags | current_state (tag) | current_state_time | unplaced_sum | unplaced_mst | placing_sum | placing_mst | placed_sum | placed_mst | ... | time |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| ... | placing | 0.3 | 0.7 | 0.7 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 1.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 2.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 3.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 4.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 5.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 6.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 7.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 8.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placing | 9.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... |
+| ... | placed | 0.9 | 0 | 0 | 9.4 | 9.4 | 0 | 0 | 0 | ... |
+
+In the table above, the state fields __booting_sum__, __booting_mst__, __booted_sum__, __booted_mst__, __connecting_sum__, __connecting_mst__, __connected_sum__ and __connected_mst__ 
+were truncated, since these are always reported to be 0 and are not the states being monitored in the measurements row.
 
 
 ##### Media component configuration state model
@@ -418,6 +421,39 @@ An example (based on the figure above) of some measurement rows for a media comp
 
 ### Example endpoint state configuration queries
 
+The following queries illustrate how to calculate _mean time between failures_ (MTBF) and _mean down time_ (MDT) for a specific endpoint.
+
+_Q. What is the Mean Time Between Failures (MTBF) of endpoint 'adaptive_streaming_I1_apache1'?_
+
+```
+select mean(connected_mst) as "apache1_MTBF(s)" from "endpoint_config" where connected_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+```
+
+```
+name: endpoint_config
+time apache1_MTBF(s)
+---- ----------------
+0    3605
+```
+
+_Q. What is the Mean Down Time (MDT) of endpoint 'adaptive_streaming_I1_apache1'?_
+
+```
+select mean(unplaced_mst) as "unplaced_mdt" into "endpoint_config_mdt" from "endpoint_config" where unplaced_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select mean(placing_mst) as "placing_mdt" into "endpoint_config_mdt" from "endpoint_config" where placing_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select mean(placed_mst) as "placed_mdt" into "endpoint_config_mdt" from "endpoint_config" where placed_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select mean(booting_mst) as "booting_mdt" into "endpoint_config_mdt" from "endpoint_config" where booting_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select mean(booted_mst) as "booted_mdt" into "endpoint_config_mdt" from "endpoint_config" where booted_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select mean(connecting_mst) as "connecting_mdt" into "endpoint_config_mdt" from "endpoint_config" where connecting_mst <> 0 and ipendpoint = 'adaptive_streaming_I1_apache1'
+select (unplaced_mdt + placing_mdt + placed_mdt + booting_mdt + booted_mdt + connecting_mdt) as "MDT(s)" from "endpoint_config_mdt"
+```
+
+```
+name: endpoint_config_mdt
+time MDT(s)
+---- ------
+0    33.7
+```
 
 ### Example media component state configuration queries
 
diff --git a/production.ini b/production.ini
new file mode 100644
index 0000000000000000000000000000000000000000..d127f3a092030526e856386b5498c6a23bfa5ade
--- /dev/null
+++ b/production.ini
@@ -0,0 +1,54 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:CLMCservice
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+aggregator_running = false
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = *:8080
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, CLMCservice
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_CLMCservice]
+level = WARN
+handlers =
+qualname = CLMCservice
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..d30f667ec5a99c0f58df21f2c653592982fa731c
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = CLMCservice
+python_files = *.py
diff --git a/setup.py b/setup.py
index 72cae3a9d31413d969a931a41509dca9c4272ea6..93a649e3c05aa1067bc48493036d8ccaffb4c0c4 100644
--- a/setup.py
+++ b/setup.py
@@ -21,18 +21,21 @@
 //      Created for Project :   FLAME
 """
 
-import os
-import os.path
-import subprocess
-from glob import glob
-from os.path import basename
-from os.path import dirname
-from os.path import join
-from os.path import splitext
 from setuptools import setup, find_packages
 
-def read(fname):
-    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+requires = [
+    'plaster_pastedeploy',
+    'pyramid',
+    'pyramid_debugtoolbar',
+    'waitress',
+    'influxdb',
+]
+
+tests_require = [
+    'WebTest >= 1.3.1',  # py3 compat
+    'pytest',
+    'pytest-cov',
+]
 
 def get_version(fname):
     if os.path.isfile(fname):
@@ -43,21 +46,29 @@ def get_version(fname):
     return git_revision
 
 setup(
-    name = "clmc",
-    version = get_version("clmctest/_version.py"),
+    name = "CLMCservice",
+    version = "SNAPSHOT",
     author = "Michael Boniface",
     author_email = "mjb@it-innovation.soton.ac.uk",
-    description = "FLAME CLMC Test Module",
-    license = "https://gitlab.it-innovation.soton.ac.uk/FLAME/flame-clmc/blob/integration/LICENSE",
-    keywords = "FLAME CLMC",
-    url='https://gitlab.it-innovation.soton.ac.uk/FLAME/flame-clmc',
+    description = "FLAME CLMC Testing Module",
+    long_description="long description",
+    license = "license",
+    keywords = "FLAME CLMC service test",
     packages=find_packages(exclude=["services"]),
     include_package_data=True,
-    package_data={'': ['_version.py', '*.yml', '*.sh', '*.json', '*.conf']},        
-    long_description="FLAME CLMC",
+    install_requires=requires,
+    extras_require={
+        'testing': tests_require,
+    },
+    package_data={'': ['git-commit-ref', '*.yml', '*.sh', '*.json', '*.conf']},
     classifiers=[
         "Development Status :: Alpha",
         "Topic :: FLAME Tests",
         "License :: ",
     ],
+    entry_points={
+        'paste.app_factory': [
+            'main = CLMCservice:main',
+        ],
+    },
 )
\ No newline at end of file