diff --git a/.gitattributes b/.gitattributes index 3e6943e574d902da76f56b877fd762e1a2de7d6c..bec2c414fdb06c08082df464c48c057083f8766f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ * text=auto *.txt text *.sh text eol=lf +reporc text eol=lf scripts/* text eol=lf # Denote all files that are truly binary and should not be modified. *.png binary diff --git a/.gitignore b/.gitignore index 21660e185426fc0c067bd4f1643ddbc053d287d7..6430516993f3cbd8987e44aaa002104e4d830ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,12 @@ *egg-info* *git-commit-ref* *_version.py* -*reporc ubuntu-xenial-16.04-cloudimg-console.log -**/.pytest_cache/ \ No newline at end of file +.idea/ +*.egg +*.pyc +.tox +*$py.class +**/.pytest_cache/ +build/ + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 801d2d779d2bba05a4d53a8659f1929254453232..58a0dd22566c1e165a85a9c53293c4ddd0dad0cd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,6 @@ ## Created By : Michael Boniface ## Created Date : 21-03-2018 ## Created for Project : FLAME - stages: - build - test @@ -29,10 +28,14 @@ build:tests: only: - schedules script: - - python setup.py sdist --dist-dir=build + - cd src/test + - python setup.py sdist --dist-dir=../../build + - cd ../../src/service + - python setup.py sdist --dist-dir=../../build artifacts: paths: - build/clmctest-SNAPSHOT.tar.gz + - build/clmcservice-SNAPSHOT.tar.gz expire_in: 1 day test:all: @@ -42,11 +45,15 @@ test:all: dependencies: - build:tests script: +# - echo "REPO_USER=${REPO_USER}" > reporc +# - echo "REPO_PASS=${REPO_PASS}" >> reporc - vagrant --fixture=scripts -- up - vagrant --fixture=scripts -- ssh test-runner -- -tt "pip3 install /vagrant/build/clmctest-SNAPSHOT.tar.gz" - vagrant --fixture=scripts -- ssh test-runner -- -tt "pytest -s --pyargs clmctest.scripts" - vagrant --fixture=monitoring -- up + - vagrant --fixture=monitoring -- ssh test-runner -- -tt "pip3 install /vagrant/build/clmcservice-SNAPSHOT.tar.gz" - vagrant --fixture=monitoring -- ssh test-runner -- -tt "pytest -s --pyargs clmctest.monitoring" + - vagrant --fixture=monitoring -- ssh test-runner -- -tt "pytest -s --pyargs clmcservice.tests" - vagrant --fixture=inputs -- up - vagrant --fixture=inputs -- ssh test-runner -- -tt "pytest -s --pyargs clmctest.inputs" when: on_success diff --git a/README.md b/README.md index 14a7fd9e40655abcd724ddb05c1409fd79f1907c..05f8934137f2f05f9519ff0c6b02eb0cda64c205 100644 --- a/README.md +++ b/README.md @@ -104,4 +104,4 @@ 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 +`vagrant --fixture=scripts -- ssh test-runner -- -tt "pytest -s --pyargs clmctest.scripts"` diff --git a/clmctest/services/loadtest-streaming/install.sh b/clmctest/services/loadtest-streaming/install.sh deleted file mode 100755 index 7d6ef6ddc357283c53da5f0475a517002c685f14..0000000000000000000000000000000000000000 --- a/clmctest/services/loadtest-streaming/install.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -#///////////////////////////////////////////////////////////////////////// -#// -#// (c) 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 : Michael Boniface -#// Created Date : 14/02/2018 -#// Created for Project : FLAME -#// -#///////////////////////////////////////////////////////////////////////// -set -euo pipefail - -echo "REPO_ROOT:"$REPO_ROOT -eval '$REPO_ROOT/test/services/vlc/install.sh' -eval '$REPO_ROOT/test/services/pytest/install.sh' \ No newline at end of file diff --git a/clmctest/services/loadtest-streaming/telegraf_loadtest_streaming.conf b/clmctest/services/loadtest-streaming/telegraf_loadtest_streaming.conf deleted file mode 100644 index 3e30465f39ca7035ec8217909c9bc5d29942fa4e..0000000000000000000000000000000000000000 --- a/clmctest/services/loadtest-streaming/telegraf_loadtest_streaming.conf +++ /dev/null @@ -1,112 +0,0 @@ -# Telegraf configuration - -# Telegraf is entirely plugin driven. All metrics are gathered from the -# declared inputs, and sent to the declared outputs. - -# Plugins must be declared in here to be active. -# To deactivate a plugin, comment out the name and any variables. - -# Use 'telegraf -config telegraf.conf -test' to see what metrics a config -# file would generate. - -# Global tags can be specified here in key="value" format. -[global_tags] - # location of the data centre - location="{{LOCATION}}" - # media service template id - sfc="{{SFC_ID}}" - # media service instance - sfc_i="{{SFC_ID_INSTANCE}}" - # service function type - sf="{{SF_ID}}" - # service function instance id - sf_i="{{SF_ID_INSTANCE}}" - # ipendpoint id aka surrogate instance - ipendpoint="{{IP_ENDPOINT_ID}}" - -# Configuration for telegraf agent -[agent] - ## Default data collection interval for all inputs - interval = "10s" - ## Rounds collection interval to 'interval' - ## ie, if interval="10s" then always collect on :00, :10, :20, etc. - round_interval = true - - ## Telegraf will cache metric_buffer_limit metrics for each output, and will - ## flush this buffer on a successful write. - metric_buffer_limit = 1000 - ## Flush the buffer whenever full, regardless of flush_interval. - flush_buffer_when_full = true - - ## Collection jitter is used to jitter the collection by a random amount. - ## Each plugin will sleep for a random time within jitter before collecting. - ## This can be used to avoid many plugins querying things like sysfs at the - ## same time, which can have a measurable effect on the system. - collection_jitter = "0s" - - ## Default flushing interval for all outputs. You shouldn't set this below - ## interval. Maximum flush_interval will be flush_interval + flush_jitter - flush_interval = "10s" - ## Jitter the flush interval by a random amount. This is primarily to avoid - ## large write spikes for users running a large number of telegraf instances. - ## ie, a jitter of 5s and interval 10s means flushes will happen every 10-15s - flush_jitter = "0s" - - ## Logging configuration: - ## Run telegraf in debug mode - debug = false - ## Run telegraf in quiet mode - quiet = false - ## Specify the log file name. The empty string means to log to stdout. - logfile = "/var/log/telegraf/telegraf.log" - - ## Override default hostname, if empty use os.Hostname() - hostname = "" - - -############################################################################### -# OUTPUTS # -############################################################################### - -# Configuration for influxdb server to send metrics to -[[outputs.influxdb]] - # The full HTTP or UDP endpoint URL for your InfluxDB instance. - # Multiple urls can be specified but it is assumed that they are part of the same - # cluster, this means that only ONE of the urls will be written to each interval. - # urls = ["udp://127.0.0.1:8089"] # UDP endpoint example - urls = ["{{INFLUXDB_URL}}"] # required - # The target database for metrics (telegraf will create it if not exists) - database = "{{DATABASE_NAME}}" # required - # Precision of writes, valid values are "ns", "us" (or "µs"), "ms", "s", "m", "h". - # note: using second precision greatly helps InfluxDB compression - precision = "s" - - ## Write timeout (for the InfluxDB client), formatted as a string. - ## If not provided, will default to 5s. 0s means no timeout (not recommended). - timeout = "5s" - # username = "telegraf" - # password = "metricsmetricsmetricsmetrics" - # Set the user agent for HTTP POSTs (can be useful for log differentiation) - # user_agent = "telegraf" - # Set UDP payload size, defaults to InfluxDB UDP Client default (512 bytes) - # udp_payload = 512 - - -############################################################################### -# INPUTS # -############################################################################### -# # Influx HTTP write listener -[[inputs.http_listener]] - ## Address and port to host HTTP listener on - service_address = ":8186" - - ## timeouts - read_timeout = "10s" - write_timeout = "10s" - - ## HTTPS - #tls_cert= "/etc/telegraf/cert.pem" - #tls_key = "/etc/telegraf/key.pem" - - ## MTLS - #tls_allowed_cacerts = ["/etc/telegraf/clientca.pem"] \ No newline at end of file diff --git a/clmctest/services/vlc/install.sh b/clmctest/services/vlc/install.sh deleted file mode 100755 index 02e3b8fe01696913d8e5b65967a152bfb485eee3..0000000000000000000000000000000000000000 --- a/clmctest/services/vlc/install.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -#///////////////////////////////////////////////////////////////////////// -#// -#// (c) 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 : Michael Boniface -#// Created Date : 12/02/2018 -#// Created for Project : FLAME -#// -#///////////////////////////////////////////////////////////////////////// - -add-apt-repository -y ppa:videolan/master-daily -apt-get update -apt-get -y install vlc \ No newline at end of file diff --git a/clmctest/streaming/__init__.py b/clmctest/streaming/__init__.py deleted file mode 100644 index 44f772595799f5fe338534918c95e23e08e80464..0000000000000000000000000000000000000000 --- a/clmctest/streaming/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/python3 \ No newline at end of file diff --git a/clmctest/streaming/conftest.py b/clmctest/streaming/conftest.py deleted file mode 100644 index 1eb9a2dbfb7998ed67998de242051a520f8ba7d4..0000000000000000000000000000000000000000 --- a/clmctest/streaming/conftest.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/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 : 25-02-2018 -## Created for Project : FLAME -""" - -import pytest -import yaml -import requests -import time -import pkg_resources - - -@pytest.fixture(scope="module") -def streaming_config(): - """ - Reads the service configuration deployed for the streaming simulation test. - - :param request: access the parameters of the fixture - :return: the python object representing the read YAML file - """ - rspec = pkg_resources.resource_filename('clmctest.streaming', 'rspec.yml') - print("rspec file: {0}".format(rspec)) - - with open(rspec, 'r') as stream: - data_loaded = yaml.load(stream) - return data_loaded - - -@pytest.fixture(scope="module", autouse=True, - params=[{'config': {'kapacitor_url': 'http://localhost:8888/chronograf/v1/sources/1/kapacitors', 'kapacitor_file': '/vagrant/test/streaming/kapacitor.json'}}]) -def kapacitor_config(request): - - kapacitor_configuration = request.param['config']['kapacitor_file'] - with open(kapacitor_configuration, "r") as rule_file: - data = "".join(line.strip() for line in rule_file.readlines()) - - kapacitor_url = request.param['config']['kapacitor_url'] - requests.post(url=kapacitor_url, data=data, headers={"Content-Type": "application/json"}) - time.sleep(1) diff --git a/clmctest/streaming/dashboard.json b/clmctest/streaming/dashboard.json deleted file mode 100644 index 52e7384e0c62b6cc33f7a09253fd951d5512d2c3..0000000000000000000000000000000000000000 --- a/clmctest/streaming/dashboard.json +++ /dev/null @@ -1 +0,0 @@ -{"id":1,"cells":[{"i":"396b0b14-1482-4b8a-a359-f144541170a4","x":6,"y":8,"w":6,"h":4,"name":"AdaptiveStreaming_SF_NetworkBytesSentPerSecond","queries":[{"query":"SELECT derivative(mean(\"bytes_sent\"), 1s) AS \"bytes_sent_per_second\" FROM \"CLMCMetrics\".\"autogen\".\"net\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx2' GROUP BY :interval:","queryConfig":{"database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT derivative(mean(\"bytes_sent\"), 1s) AS \"bytes_sent_per_second\" FROM \"CLMCMetrics\".\"autogen\".\"net\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx2' GROUP BY :interval:","range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/396b0b14-1482-4b8a-a359-f144541170a4"}},{"i":"480b4037-a816-4e1c-8c84-edb39b0c1f6d","x":0,"y":8,"w":6,"h":4,"name":"AdapativeStreaming_SF_NetworkBytesSentPerSecond","queries":[{"query":"SELECT derivative(mean(\"bytes_sent\"), 1s) AS \"bytes_sent_per_second\" FROM \"CLMCMetrics\".\"autogen\".\"net\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx1' GROUP BY :interval:","queryConfig":{"database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT derivative(mean(\"bytes_sent\"), 1s) AS \"bytes_sent_per_second\" FROM \"CLMCMetrics\".\"autogen\".\"net\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx1' GROUP BY :interval:","range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/480b4037-a816-4e1c-8c84-edb39b0c1f6d"}},{"i":"6ad170aa-c5f2-4930-a604-1e88579dffee","x":6,"y":4,"w":6,"h":4,"name":"AdaptiveStreaming_SF2_CPU","queries":[{"query":"SELECT 100-mean(\"usage_idle\") AS \"mean_usage_idle\" FROM \"CLMCMetrics\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx2' GROUP BY :interval:","queryConfig":{"database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT 100-mean(\"usage_idle\") AS \"mean_usage_idle\" FROM \"CLMCMetrics\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx2' GROUP BY :interval:","range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/6ad170aa-c5f2-4930-a604-1e88579dffee"}},{"i":"7e424259-32b8-40be-aa53-477aaf801f0e","x":0,"y":4,"w":6,"h":4,"name":"AdaptiveStreaming_SF1_CPU","queries":[{"query":"SELECT 100-mean(\"usage_idle\") AS \"mean_usage_idle\" FROM \"CLMCMetrics\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx1' GROUP BY :interval:","queryConfig":{"database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT 100-mean(\"usage_idle\") AS \"mean_usage_idle\" FROM \"CLMCMetrics\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx1' GROUP BY :interval:","range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/7e424259-32b8-40be-aa53-477aaf801f0e"}},{"i":"a095c820-8bac-45fe-974d-4030e1bb8770","x":6,"y":0,"w":6,"h":4,"name":"AdaptiveStreaming_SF2_ActiveConnections","queries":[{"query":"SELECT mean(\"active\") AS \"mean_active\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx2' GROUP BY :interval:","label":"nginx.active","queryConfig":{"database":"CLMCMetrics","measurement":"nginx","retentionPolicy":"autogen","fields":[{"field":"active","funcs":["mean"]}],"tags":{"ipendpoint":["adaptive_streaming_I1_nginx2"]},"groupBy":{"time":"auto","tags":[]},"areTagsAccepted":true,"rawText":null,"range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/a095c820-8bac-45fe-974d-4030e1bb8770"}},{"i":"63a7e85a-b411-46be-9478-8479405379a3","x":0,"y":0,"w":6,"h":4,"name":"AdaptiveStreaming_SF1_ActiveConnections","queries":[{"query":"SELECT mean(\"active\") AS \"mean_active\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE time \u003e :dashboardTime: AND \"ipendpoint\"='adaptive_streaming_I1_nginx1' GROUP BY :interval:","label":"nginx.active","queryConfig":{"database":"CLMCMetrics","measurement":"nginx","retentionPolicy":"autogen","fields":[{"field":"active","funcs":["mean"]}],"tags":{"ipendpoint":["adaptive_streaming_I1_nginx1"]},"groupBy":{"time":"auto","tags":[]},"areTagsAccepted":true,"rawText":null,"range":null}}],"type":"line","links":{"self":"/chronograf/v1/dashboards/1/cells/63a7e85a-b411-46be-9478-8479405379a3"}}],"templates":[],"name":"Adaptive Streaming Experiment Dashboard","links":{"self":"/chronograf/v1/dashboards/1","cells":"/chronograf/v1/dashboards/1/cells","templates":"/chronograf/v1/dashboards/1/templates"}} diff --git a/clmctest/streaming/influx.json b/clmctest/streaming/influx.json deleted file mode 100644 index 34bb14a56b2cad467d93d5b8451804a85ebbc707..0000000000000000000000000000000000000000 --- a/clmctest/streaming/influx.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "1", - "name": "Influx 1", - "url": "http://localhost:8086", - "default": true, - "telegraf": "CLMCMetrics" -} \ No newline at end of file diff --git a/clmctest/streaming/kapacitor.conf b/clmctest/streaming/kapacitor.conf deleted file mode 100644 index 7dfb7974600c5c2a8775d5c1d5a0f6caa8d94e87..0000000000000000000000000000000000000000 --- a/clmctest/streaming/kapacitor.conf +++ /dev/null @@ -1,720 +0,0 @@ -## © 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 : Simon Crowle -## Created Date : 15-02-2018 -## Created for Project : FLAME - -# The hostname of this node. -# Must be resolvable by any configured InfluxDB hosts. -hostname = "localhost" -# Directory for storing a small amount of metadata about the server. -data_dir = "/var/lib/kapacitor" - -# Do not apply configuration overrides during startup. -# Useful if the configuration overrides cause Kapacitor to fail startup. -# This option is intended as a safe guard and should not be needed in practice. -skip-config-overrides = false - -# Default retention-policy, if a write is made to Kapacitor and -# it does not have a retention policy associated with it, -# then the retention policy will be set to this value -default-retention-policy = "" - -[http] - # HTTP API Server for Kapacitor - # This server is always on, - # it serves both as a write endpoint - # and as the API endpoint for all other - # Kapacitor calls. - bind-address = ":9092" - log-enabled = true - write-tracing = false - pprof-enabled = false - https-enabled = false - https-certificate = "/etc/ssl/kapacitor.pem" - -[config-override] - # Enable/Disable the service for overridding configuration via the HTTP API. - enabled = true - -[logging] - # Destination for logs - # Can be a path to a file or 'STDOUT', 'STDERR'. - file = "/var/log/kapacitor/kapacitor.log" - # Logging level can be one of: - # DEBUG, INFO, ERROR - # HTTP logging can be disabled in the [http] config section. - level = "INFO" - -[load] - # Enable/Disable the service for loading tasks/templates/handlers - # from a directory - enabled = true - # Directory where task/template/handler files are set - dir = "/etc/kapacitor/load" - - -[replay] - # Where to store replay files, aka recordings. - dir = "/var/lib/kapacitor/replay" - -[task] - # Where to store the tasks database - # DEPRECATED: This option is not needed for new installations. - # It is only used to determine the location of the task.db file - # for migrating to the new `storage` service. - dir = "/var/lib/kapacitor/tasks" - # How often to snapshot running task state. - snapshot-interval = "60s" - -[storage] - # Where to store the Kapacitor boltdb database - boltdb = "/var/lib/kapacitor/kapacitor.db" - -[deadman] - # Configure a deadman's switch - # Globally configure deadman's switches on all tasks. - # NOTE: for this to be of use you must also globally configure at least one alerting method. - global = false - # Threshold, if globally configured the alert will be triggered if the throughput in points/interval is <= threshold. - threshold = 0.0 - # Interval, if globally configured the frequency at which to check the throughput. - interval = "10s" - # Id -- the alert Id, NODE_NAME will be replaced with the name of the node being monitored. - id = "node 'NODE_NAME' in task '{{ .TaskName }}'" - # The message of the alert. INTERVAL will be replaced by the interval. - message = "{{ .ID }} is {{ if eq .Level \"OK\" }}alive{{ else }}dead{{ end }}: {{ index .Fields \"collected\" | printf \"%0.3f\" }} points/INTERVAL." - - -# Multiple InfluxDB configurations can be defined. -# Exactly one must be marked as the default. -# Each one will be given a name and can be referenced in batch queries and InfluxDBOut nodes. -[[influxdb]] - # Connect to an InfluxDB cluster - # Kapacitor can subscribe, query and write to this cluster. - # Using InfluxDB is not required and can be disabled. - enabled = true - default = true - name = "localhost" - urls = ["http://localhost:8086"] - username = "" - password = "" - timeout = 0 - # Absolute path to pem encoded CA file. - # A CA can be provided without a key/cert pair - # ssl-ca = "/etc/kapacitor/ca.pem" - # Absolutes paths to pem encoded key and cert files. - # ssl-cert = "/etc/kapacitor/cert.pem" - # ssl-key = "/etc/kapacitor/key.pem" - - # Do not verify the TLS/SSL certificate. - # This is insecure. - insecure-skip-verify = false - - # Maximum time to try and connect to InfluxDB during startup - startup-timeout = "5m" - - # Turn off all subscriptions - disable-subscriptions = false - - # Subscription mode is either "cluster" or "server" - subscription-mode = "cluster" - - # Which protocol to use for subscriptions - # one of 'udp', 'http', or 'https'. - subscription-protocol = "http" - - # Subscriptions resync time interval - # Useful if you want to subscribe to new created databases - # without restart Kapacitord - subscriptions-sync-interval = "1m0s" - - # Override the global hostname option for this InfluxDB cluster. - # Useful if the InfluxDB cluster is in a separate network and - # needs special config to connect back to this Kapacitor instance. - # Defaults to `hostname` if empty. - kapacitor-hostname = "" - - # Override the global http port option for this InfluxDB cluster. - # Useful if the InfluxDB cluster is in a separate network and - # needs special config to connect back to this Kapacitor instance. - # Defaults to the port from `[http] bind-address` if 0. - http-port = 0 - - # Host part of a bind address for UDP listeners. - # For example if a UDP listener is using port 1234 - # and `udp-bind = "hostname_or_ip"`, - # then the UDP port will be bound to `hostname_or_ip:1234` - # The default empty value will bind to all addresses. - udp-bind = "" - # Subscriptions use the UDP network protocl. - # The following options of for the created UDP listeners for each subscription. - # Number of packets to buffer when reading packets off the socket. - udp-buffer = 1000 - # The size in bytes of the OS read buffer for the UDP socket. - # A value of 0 indicates use the OS default. - udp-read-buffer = 0 - - [influxdb.subscriptions] - # Set of databases and retention policies to subscribe to. - # If empty will subscribe to all, minus the list in - # influxdb.excluded-subscriptions - # - # Format - # db_name = <list of retention policies> - # - # Example: - # my_database = [ "default", "longterm" ] - [influxdb.excluded-subscriptions] - # Set of databases and retention policies to exclude from the subscriptions. - # If influxdb.subscriptions is empty it will subscribe to all - # except databases listed here. - # - # Format - # db_name = <list of retention policies> - # - # Example: - # my_database = [ "default", "longterm" ] - -[kubernetes] - # Enable/Disable the kubernetes service. - # Needed by the k8sAutoscale TICKscript node. - enabled = false - # There are several ways to connect to the kubernetes API servers: - # - # Via the proxy, start the proxy via the `kubectl proxy` command: - # api-servers = ["http://localhost:8001"] - # - # From within the cluster itself, in which case - # kubernetes secrets and DNS services are used - # to determine the needed configuration. - # in-cluster = true - # - # Direct connection, in which case you need to know - # the URL of the API servers, the authentication token and - # the path to the ca cert bundle. - # These value can be found using the `kubectl config view` command. - # api-servers = ["http://192.168.99.100:8443"] - # token = "..." - # ca-path = "/path/to/kubernetes/ca.crt" - # - # Kubernetes can also serve as a discoverer for scrape targets. - # In that case the type of resources to discoverer must be specified. - # Valid values are: "node", "pod", "service", and "endpoint". - # resource = "pod" - - - -[smtp] - # Configure an SMTP email server - # Will use TLS and authentication if possible - # Only necessary for sending emails from alerts. - enabled = false - host = "localhost" - port = 25 - username = "" - password = "" - # From address for outgoing mail - from = "" - # List of default To addresses. - # to = ["oncall@example.com"] - - # Skip TLS certificate verify when connecting to SMTP server - no-verify = false - # Close idle connections after timeout - idle-timeout = "30s" - - # If true the all alerts will be sent via Email - # without explicitly marking them in the TICKscript. - global = false - # Only applies if global is true. - # Sets all alerts in state-changes-only mode, - # meaning alerts will only be sent if the alert state changes. - state-changes-only = false - -[snmptrap] - # Configure an SNMP trap server - enabled = false - # The host:port address of the SNMP trap server - addr = "localhost:162" - # The community to use for traps - community = "kapacitor" - # Number of retries when sending traps - retries = 1 - - -[opsgenie] - # Configure OpsGenie with your API key and default routing key. - enabled = false - # Your OpsGenie API Key. - api-key = "" - # Default OpsGenie teams, can be overridden per alert. - # teams = ["team1", "team2"] - # Default OpsGenie recipients, can be overridden per alert. - # recipients = ["recipient1", "recipient2"] - # The OpsGenie API URL should not need to be changed. - url = "https://api.opsgenie.com/v1/json/alert" - # The OpsGenie Recovery URL, you can change this - # based on which behavior you want a recovery to - # trigger (Add Notes, Close Alert, etc.) - recovery_url = "https://api.opsgenie.com/v1/json/alert/note" - # If true then all alerts will be sent to OpsGenie - # without explicitly marking them in the TICKscript. - # The team and recipients can still be overridden. - global = false - -[victorops] - # Configure VictorOps with your API key and default routing key. - enabled = false - # Your VictorOps API Key. - api-key = "" - # Default VictorOps routing key, can be overridden per alert. - routing-key = "" - # The VictorOps API URL should not need to be changed. - url = "https://alert.victorops.com/integrations/generic/20131114/alert" - # If true the all alerts will be sent to VictorOps - # without explicitly marking them in the TICKscript. - # The routing key can still be overridden. - global = false - # Use JSON for the "data" field - # New installations will want to set this to true as it makes - # the data that triggered the alert available within VictorOps. - # The default is "false" for backwards compatibility reasons. - # json-data = false - -[pagerduty] - # Configure PagerDuty. - enabled = false - # Your PagerDuty Service Key. - service-key = "" - # The PagerDuty API URL should not need to be changed. - url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" - # If true the all alerts will be sent to PagerDuty - # without explicitly marking them in the TICKscript. - global = false - -[pushover] - # Configure Pushover. - enabled = false - # Your Pushover API token. - token = "" - # Your Pushover USER_TOKEN. - user-key = "" - # The URL for the Pushover API. - url = "https://api.pushover.net/1/messages.json" - -########################################## -# Configure Alert POST request Endpoints - -# As ENV variables: -# KAPACITOR_HTTPPOST_0_ENDPOINT = "example" -# KAPACITOR_HTTPPOST_0_URL = "http://example.com" -# KAPACITOR_HTTPPOST_0_HEADERS_Example = "header" - -# [[httppost]] -# endpoint = "example" -# url = "http://example.com" -# headers = { Example = "your-key" } -# basic-auth = { username = "my-user", password = "my-pass" } -# -# # Provide an alert template for constructing a custom HTTP body. -# # Alert templates are only used with post alert handlers as they consume alert data. -# # The template uses https://golang.org/pkg/text/template/ and has access to the following fields: -# # * .ID - The unique ID for this alert -# # * .Message - The message of the alert -# # * .Details - The details of the alert -# # * .Time - The time the alert event occurred -# # * .Duration - The duration of the alert event. -# # * .Level - The level of the alert, i.e INFO, WARN, or CRITICAL. -# # * .Data - The data that triggered the alert. -# # -# # Specify the template inline. -# alert-template = "{{.Message}}:{{range .Data.Series}}{{.Tags}},{{range .Values}}{{.}}{{end}}{{end}}" -# # Specify an absolute path to a template file. -# alert-template-file = "/path/to/template/file" -# -# # Provide a row template for constructing a custom HTTP body. -# # Row templates are only used with httpPost pipeline nodes as they consume a row at a time. -# # The template uses https://golang.org/pkg/text/template/ and has access to the following fields: -# # * .Name - The measurement name of the data stream -# # * .Tags - A map of tags on the data. -# # * .Values - A list of values, each entry is a map containing a "time" key for the time of the point -# # and keys for all other fields on the point. -# # -# # Specify the template inline. -# row-template = "{{.Name}} host={{index .Tags \"host\"}}{{range .Values}} {{index . "time"}} {{index . "value"}}{{end}}" -# # Specify an absolute path to a template file. -# row-template-file = "/path/to/template/file" - -[slack] - # Configure Slack. - enabled = true - # The Slack webhook URL, can be obtained by adding - # an Incoming Webhook integration. - # Visit https://slack.com/services/new/incoming-webhook - # to add new webhook for Kapacitor. - url = "https://hooks.slack.com/services/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" - # Default channel for messages - channel = "#clmc" - # If true all the alerts will be sent to Slack - # without explicitly marking them in the TICKscript. - global = false - # Only applies if global is true. - # Sets all alerts in state-changes-only mode, - # meaning alerts will only be sent if the alert state changes. - state-changes-only = false - -[telegram] - # Configure Telegram. - enabled = false - # The Telegram Bot URL should not need to be changed. - url = "https://api.telegram.org/bot" - # Telegram Bot Token, can be obtained From @BotFather. - token = "" - # Default recipient for messages, Contact @myidbot on Telegram to get an ID. - chat-id = "" - # Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your alert message. - #parse-mode = "Markdown" - # Disable link previews for links in this message - disable-web-page-preview = false - # Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound. - disable-notification = false - # If true the all alerts will be sent to Telegram - # without explicitly marking them in the TICKscript. - global = false - # Only applies if global is true. - # Sets all alerts in state-changes-only mode, - # meaning alerts will only be sent if the alert state changes. - state-changes-only = false - -[hipchat] - # Configure HipChat. - enabled = false - # The HipChat API URL. Replace subdomain with your - # HipChat subdomain. - # url = "https://subdomain.hipchat.com/v2/room" - # Visit https://www.hipchat.com/docs/apiv2 - # for information on obtaining your room id and - # authentication token. - # Default room for messages - room = "" - # Default authentication token - token = "" - # If true then all alerts will be sent to HipChat - # without explicitly marking them in the TICKscript. - global = false - # Only applies if global is true. - # Sets all alerts in state-changes-only mode, - # meaning alerts will only be sent if the alert state changes. - state-changes-only = false - -[alerta] - # Configure Alerta. - enabled = false - # The Alerta URL. - url = "" - # Default authentication token. - token = "" - # Default token prefix - # If you are on older versions of alerta you may need to change this to "Key" - token-prefix = "Bearer" - # Default environment. - environment = "" - # Default origin. - origin = "kapacitor" - -[sensu] - # Configure Sensu. - enabled = false - # The Sensu Client host:port address. - addr = "sensu-client:3030" - # Default JIT source. - source = "Kapacitor" - -[reporting] - # Send usage statistics - # every 12 hours to Enterprise. - enabled = true - url = "https://usage.influxdata.com" - -[stats] - # Emit internal statistics about Kapacitor. - # To consume these stats create a stream task - # that selects data from the configured database - # and retention policy. - # - # Example: - # stream|from().database('_kapacitor').retentionPolicy('autogen')... - # - enabled = true - stats-interval = "10s" - database = "_kapacitor" - retention-policy= "autogen" - -[udf] -# Configuration for UDFs (User Defined Functions) -[udf.functions] - # Example go UDF. - # First compile example: - # go build -o avg_udf ./udf/agent/examples/moving_avg.go - # - # Use in TICKscript like: - # stream.goavg() - # .field('value') - # .size(10) - # .as('m_average') - # - # uncomment to enable - #[udf.functions.goavg] - # prog = "./avg_udf" - # args = [] - # timeout = "10s" - - # Example python UDF. - # Use in TICKscript like: - # stream.pyavg() - # .field('value') - # .size(10) - # .as('m_average') - # - # uncomment to enable - #[udf.functions.pyavg] - # prog = "/usr/bin/python2" - # args = ["-u", "./udf/agent/examples/moving_avg.py"] - # timeout = "10s" - # [udf.functions.pyavg.env] - # PYTHONPATH = "./udf/agent/py" - - # Example UDF over a socket - #[udf.functions.myCustomUDF] - # socket = "/path/to/socket" - # timeout = "10s" - -[talk] - # Configure Talk. - enabled = false - # The Talk webhook URL. - url = "https://jianliao.com/v2/services/webhook/uuid" - # The default authorName. - author_name = "Kapacitor" - -# MQTT client configuration. -# Mutliple different clients may be configured by -# repeating [[mqtt]] sections. -[[mqtt]] - enabled = false - # Unique name for this broker configuration - name = "localhost" - # Whether this broker configuration is the default - default = true - # URL of the MQTT broker. - # Possible protocols include: - # tcp - Raw TCP network connection - # ssl - TLS protected TCP network connection - # ws - Websocket network connection - url = "tcp://localhost:1883" - - # TLS/SSL configuration - # A CA can be provided without a key/cert pair - # ssl-ca = "/etc/kapacitor/ca.pem" - # Absolutes paths to pem encoded key and cert files. - # ssl-cert = "/etc/kapacitor/cert.pem" - # ssl-key = "/etc/kapacitor/key.pem" - - # Unique ID for this MQTT client. - # If empty used the value of "name" - client-id = "" - - # Username - username = "" - # Password - password = "" - -[[swarm]] - # Enable/Disable the Docker Swarm service. - # Needed by the swarmAutoscale TICKscript node. - enabled = false - # Unique ID for this Swarm cluster - # NOTE: This is not the ID generated by Swarm rather a user defined - # ID for this cluster since Kapacitor can communicate with multiple clusters. - id = "" - # List of URLs for Docker Swarm servers. - servers = ["http://localhost:2376"] - # TLS/SSL Configuration for connecting to secured Docker daemons - ssl-ca = "" - ssl-cert = "" - ssl-key = "" - insecure-skip-verify = false - -################################## -# Input Methods, same as InfluxDB -# - -[collectd] - enabled = false - bind-address = ":25826" - database = "collectd" - retention-policy = "" - batch-size = 1000 - batch-pending = 5 - batch-timeout = "10s" - typesdb = "/usr/share/collectd/types.db" - -[opentsdb] - enabled = false - bind-address = ":4242" - database = "opentsdb" - retention-policy = "" - consistency-level = "one" - tls-enabled = false - certificate = "/etc/ssl/influxdb.pem" - batch-size = 1000 - batch-pending = 5 - batch-timeout = "1s" - -# Service Discovery and metric scraping - -[[scraper]] - enabled = false - name = "myscraper" - # Specify the id of a discoverer service specified below - discoverer-id = "" - # Specify the type of discoverer service being used. - discoverer-service = "" - db = "prometheus_raw" - rp = "autogen" - type = "prometheus" - scheme = "http" - metrics-path = "/metrics" - scrape-interval = "1m0s" - scrape-timeout = "10s" - username = "" - password = "" - bearer-token = "" - ssl-ca = "" - ssl-cert = "" - ssl-key = "" - ssl-server-name = "" - insecure-skip-verify = false - -# Supported discovery services - -[[azure]] - enabled = false - id = "myazure" - port = 80 - subscription-id = "" - tenant-id = "" - client-id = "" - client-secret = "" - refresh-interval = "5m0s" - -[[consul]] - enabled = false - id = "myconsul" - address = "127.0.0.1:8500" - token = "" - datacenter = "" - tag-separator = "," - scheme = "http" - username = "" - password = "" - ssl-ca = "" - ssl-cert = "" - ssl-key = "" - ssl-server-name = "" - insecure-skip-verify = false - -[[dns]] - enabled = false - id = "mydns" - refresh-interval = "30s" - ## Type can be SRV, A, or AAAA - type = "SRV" - ## Port is the port to scrape for records returned by A or AAAA types - port = 80 - -[[ec2]] - enabled = false - id = "myec2" - region = "us-east-1" - access-key = "" - secret-key = "" - profile = "" - refresh-interval = "1m0s" - port = 80 - -[[file-discovery]] - enabled = false - id = "myfile" - refresh-interval = "5m0s" - files = [] - -[[gce]] - enabled = false - id = "mygce" - project = "" - zone = "" - filter = "" - refresh-interval = "1m0s" - port = 80 - tag-separator = "," - -[[marathon]] - enabled = false - id = "mymarathon" - timeout = "30s" - refresh-interval = "30s" - bearer-token = "" - ssl-ca = "" - ssl-cert = "" - ssl-key = "" - ssl-server-name = "" - insecure-skip-verify = false - -[[nerve]] - enabled = false - id = "mynerve" - timeout = "10s" - -[[serverset]] - enabled = false - id = "myserverset" - timeout = "10s" - -[[static-discovery]] - enabled = false - id = "mystatic" - targets = ["localhost:9100"] - [static.labels] - region = "us-east-1" - -[[triton]] - enabled = false - id = "mytriton" - account = "" - dns-suffix = "" - endpoint = "" - port = 9163 - refresh-interval = "1m0s" - version = 1 - ssl-ca = "" - ssl-cert = "" - ssl-key = "" - ssl-server-name = "" - insecure-skip-verify = false diff --git a/clmctest/streaming/kapacitor.json b/clmctest/streaming/kapacitor.json deleted file mode 100644 index 60118860fc3351e456f1cccfe6145a169f50734f..0000000000000000000000000000000000000000 --- a/clmctest/streaming/kapacitor.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "1", - "name": "CLMCKapacitor", - "url": "http://localhost:9092", - "active": false -} \ No newline at end of file diff --git a/clmctest/streaming/manual.md b/clmctest/streaming/manual.md deleted file mode 100644 index 7db0fc7126408bbd70ad54775b03e90b67867b83..0000000000000000000000000000000000000000 --- a/clmctest/streaming/manual.md +++ /dev/null @@ -1,146 +0,0 @@ -<!-- -// © 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 : Michael Boniface -// Updated By : Simon Crowle -// Created Date : 18-12-2017 -// Update Date : 14-02-2018 -// Created for Project : FLAME ---> - -# CLMC Adaptive Streaming Test - -This test streams mpeg-dash video using the two nginx servers monitored by Telegraf configured with a default apache plugin and a net_response plugin. The data is stored in the `clmc-service` using database `CLMCMetrics` and measurements `nginx` and `net_response` - -The following command brings up the services - -`vagrant --fixture=streaming up` - -* clmc-service: configured with influx, kapacitor, chornograf -* nginx1@DC1, nginx2@DC2: configured with nginx and a test video located at http://192.168.50.11:80/test_video/stream.mpd on the internal vbox network and at http://localhost:8081/test_video/stream.mpd if accessing from the host machine - -### Run the test set-up - -`vagrant --fixture=streaming ssh clmc-service -- "sudo /vagrant/test/streaming/setupCLMC.sh /vagrant/test/streaming"` -`vagrant --fixture=streaming ssh nginx1 -- "sudo /vagrant/test/streaming/setupNGINX.sh"` - -### Run the automated test - -To run the load test using the following command (here, the last parameter '15' refers to the number of VLC player clients to be launched): - -`vagrant --fixture=streaming ssh loadtest-streaming -- "sudo /vagrant/test/streaming/run.sh /home/ubuntu/test/streaming http://192.168.50.11/test_video/stream.mpd 15` - -This test currently just generates the load and does not have any assertions. It breaks at 1000. - -And then point your browser to the Chronograf dashboard: - -`http://localhost:8888` - -### Run the automated PyTests - -SSH into the clmc-service VM: - -`vagrant --fixture=streaming ssh clmc-service` - -Run the automated tests written in pytest: - -`pytest -s /vagrant/test/streaming/` - -### Manual test - -## Manual set-up of Chronograf's CLMC data source - -If you __do not__ want to run the automatic set-up, basic entry to the Chronograf dashboard is as follows: - -1. Point your browser to: [http://localhost:8888](http://localhost:8888) -2. Enter your connection string: `http://localhost:8086` -3. Enter the Name: `Influx 1` -4. Enter the Telegraf database: `CLMCMetrics` - -## Manual test on Windows - -### View the video -Install VLC video client on the host machine, you must use a very recent version otherwise the MPD file cannot we read. At the time of writng the following nighly build was installed: - -https://nightlies.videolan.org/build/win32/vlc-3.0.0-rc1-20171201-0326/vlc-3.0.0-20171201-0326-rc1-win32.exe - -Start the VLC Player - -`Media->Open Network Stream` - -The test video is the FLAME project video and it can be viewed at the following location. - -`Enter the network URL: http://localhost:8081/test_video/stream.mpd for nginx1 server` - -The video should play. - -### Query the data - -Open Chronograph by entering the following URL into a browser on the host http://localhost:8888. Your CLMC data source, Kapacitor and demonstration dashboard should be ready for you to explore. - -Press the Data Explorer in the menu and select the nginx measurement and create a query such as - -`SELECT mean("requests") AS "mean_requests" FROM "CLMCMetrics"."autogen"."nginx" WHERE time > now() - 1h GROUP BY time(10s)` - -## KPI triggers - -In this demonstrator an example KPI rule has been set up in Kapacitor which fires when the average number of active connections per 5 seconds on the Nginx 1 or Nginx 2 server goes above certain thresholds ( a 'warning' at 10 connections/5 seconds ). The TICKscript specification for this rule is as follows: - -``` -dbrp "CLMCMetrics"."autogen" - -// Nginx 1 rule -// ------------- -var n1Data = batch - |query(''' SELECT mean("active") AS "mean_active" FROM "CLMCMetrics"."autogen"."nginx" WHERE "ipendpoint"='adaptive_streaming_I1_nginx1' ''') - .period(5s) - .every(5s) - -varn n1Alert = n1Data - |alert() - .id('{{ .Name }}/adaptive_streaming_I1_nginx1') - .message('{{ .ID }} is {{ .Level }} Mean active connections: {{ index .Fields "mean_active" }}') - .warn(lambda: "mean_active" > 10) - .slack() - .log( '/tmp/RPSLoad.log' ) - -// Nginx 2 rule -// ------------- -var n2Data = batch - |query(''' SELECT mean("active") AS "mean_active" FROM "CLMCMetrics"."autogen"."nginx" WHERE "ipendpoint"='adaptive_streaming_I1_nginx2' ''') - .period(5s) - .every(5s) - -var n2Alert = n2Data - |alert() - .id('{{ .Name }}/adaptive_streaming_I1_nginx2') - .message('{{ .ID }} is {{ .Level }} Mean active connections: {{ index .Fields "mean_active" }}') - .warn(lambda: "mean_active" > 10) - .slack() - .log( '/tmp/RPSLoad.log' ) -``` - -Alerts are sent to both an internal logging within the CLMC service file system and also to a FLAME demo Slack service: - -https://flamedemo-itinnov.slack.com - -Alerts can be found under the '#clmc' channel. - -### Kapacitor rules in Chronograf's GUI - -Additional rules can be added to this demonstrator either via the Chronograf GUI (see [here](https://docs.influxdata.com/chronograf/v1.4/introduction/getting-started/#4-connect-chronograf-to-kapacitor) for more information) or by using the Kapacitor HTTP API and TICKscript (for an introduction, [look here](https://docs.influxdata.com/kapacitor/v1.4/tick/)). diff --git a/clmctest/streaming/report.sh b/clmctest/streaming/report.sh deleted file mode 100644 index 0179a1e652f0c61b34788360e0c57129db0ca323..0000000000000000000000000000000000000000 --- a/clmctest/streaming/report.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -## © 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 : Stephen Phillips -## Created Date : 19-02-2018 -## Created for Project : FLAME - -# This script reads stdin and expects the output of cvlc. -# It is used by the run.sh script and receives the output of the cvlc client. -# It counts the number of times the frame "dropping" error is seen and every 10 times it sends a message to telegraf reporting "another 10" errors. - -if [ "$#" -ne 1 ]; then - echo "Error: illegal number of arguments: "$# - echo "Usage: report.sh <client number>" - exit -fi - -COUNTER=$1 -TELEGRAF=http://localhost:8186 - -ERR_COUNT=0 -while read line; do - if [[ $line = *"dropping"* ]]; then - ERR_COUNT=$(($ERR_COUNT + 1)) - fi - TEN=$((ERR_COUNT % 10)) - if [ $TEN -eq 0 ]; then - curl -i -XPOST "${TELEGRAF}/write?precision=s" --data-binary "vlc,client=${COUNTER} drop_error=10 $(date +%s)" >& /dev/null - fi -done \ No newline at end of file diff --git a/clmctest/streaming/rules.json b/clmctest/streaming/rules.json deleted file mode 100644 index faad48b28216716cc63622b59106fad9d7130cbb..0000000000000000000000000000000000000000 --- a/clmctest/streaming/rules.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id" : "Request_Rate_Alert_NGINXServers", - "type" : "batch", - "dbrps" : [{"db": "CLMCMetrics", "rp" : "autogen"}], - - "script" : "\/\/ NGINX 1 Rule\r\n\/\/ -------------\r\nvar n1Data = batch\r\n |query(''' SELECT mean(\"active\") AS \"mean_active\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE \"ipendpoint\"='adaptive_streaming_I1_nginx1' ''')\r\n .period(5s)\r\n .every(5s)\r\n\r\nvar n1Alert = n1Data\r\n |alert()\r\n .id('{{ .Name }}\/adaptive_streaming_I1_nginx1')\r\n .message('{{ .ID }} is {{ .Level }} Mean active connections: {{ index .Fields \"mean_active\" }}')\r\n .warn(lambda: \"mean_active\" > 10)\r\n .slack()\r\n .log( '\/tmp\/RPSLoad.log' )\r\n\r\n\/\/ NGINX 2 Rule\r\n\/\/ -------------\r\nvar n2Data = batch\r\n |query(''' SELECT mean(\"active\") AS \"mean_active\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE \"ipendpoint\"='adaptive_streaming_I1_nginx2' ''')\r\n .period(5s)\r\n .every(5s)\r\n\r\nvar n2Alert = n2Data\r\n |alert()\r\n .id('{{ .Name }}\/adaptive_streaming_I1_nginx2')\r\n .message('{{ .ID }} is {{ .Level }} Mean active connections: {{ index .Fields \"mean_active\" }}')\r\n .warn(lambda: \"mean_active\" > 10)\r\n .slack()\r\n .log( '\/tmp\/RPSLoad.log' )", - - "status" : "enabled" -} \ No newline at end of file diff --git a/clmctest/streaming/run.sh b/clmctest/streaming/run.sh deleted file mode 100644 index 81c7d5f6aba81658bf416e8decd9d62d7b96f6a1..0000000000000000000000000000000000000000 --- a/clmctest/streaming/run.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -#///////////////////////////////////////////////////////////////////////// -#// -#// (c) 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 : Michael Boniface -#// Created Date : 15/02/2017 -#// Created for Project : FLAME -#// -#///////////////////////////////////////////////////////////////////////// - -if [ "$#" -ne 3 ]; then - echo "Error: illegal number of arguments: "$# - echo "Usage: run.sh TEST_RUN_DIR STREAM_URI MAX_CLIENTS" - exit -fi - -# create test directories -TEST_FOLDER=$(date +%Y%m%d%H%M%S) -TEST_RUN_DIR=$1 -TEST_DIR=$TEST_RUN_DIR"/streaming/"$TEST_FOLDER -echo "Test directory: "$TEST_DIR -mkdir -p "$TEST_DIR" - -# run testplan -cd $TEST_DIR - -#jmeter -n -LDEBUG -t /vagrant/test/streaming/testplan.jmx -l results.jtx -j jmeter.log - -# quick bash equivalent in case Jmeter fails -STREAM_URI=$2 -COUNTER=0 -MAX_CLIENTS=$3 -while [ $COUNTER -lt $MAX_CLIENTS ]; do - # run cvlc headless, redirect stderr into stdout, pipe that into the report.sh script - cvlc -Vdummy --no-audio $STREAM_URI 2>&1 | /vagrant/test/streaming/report.sh ${COUNTER} & - sleep 1 - let COUNTER=COUNTER+1 -done - - - diff --git a/clmctest/streaming/setupCLMC.sh b/clmctest/streaming/setupCLMC.sh deleted file mode 100644 index e7e2fc91b89cef38d690781cac3f1c92ba39a901..0000000000000000000000000000000000000000 --- a/clmctest/streaming/setupCLMC.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -#///////////////////////////////////////////////////////////////////////// -#// -#// (c) 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 : Simon Crowle -#// Created Date : 14/02/2018 -#// Created for Project : FLAME -#// -#///////////////////////////////////////////////////////////////////////// - -TEST_DIR=$1 - -# copy Kapacitor conf to /etc/kapacitor and restart - -systemctl stop kapacitor -echo $TEST_DIR"/kapacitor.conf" -cp $TEST_DIR/kapacitor.conf /etc/kapacitor/kapacitor.conf -systemctl start kapacitor - -# Set up Influx data source -curl -i -X POST -H "Content-Type: application/json" http://localhost:8888/chronograf/v1/sources -d @$TEST_DIR/influx.json - -# Set up dashboard -curl -i -X POST -H "Content-Type: application/json" http://localhost:8888/chronograf/v1/dashboards -d @$TEST_DIR/dashboard.json diff --git a/clmctest/streaming/setupNGINX.sh b/clmctest/streaming/setupNGINX.sh deleted file mode 100644 index 3833350c7e2a157538c5014d12195627c5aaf538..0000000000000000000000000000000000000000 --- a/clmctest/streaming/setupNGINX.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -#///////////////////////////////////////////////////////////////////////// -#// -#// (c) 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 : Simon Crowle -#// Created Date : 14/02/2018 -#// Created for Project : FLAME -#// -#///////////////////////////////////////////////////////////////////////// - -# NGINX -DEST_DIR="/usr/share/nginx/html" - -TEST_VIDEO="20180212104221flame-project-full.mp4" -TEST_VIDEO_ARCHIVE=$TEST_VIDEO".gz" -DEST_FILE=$DEST_DIR"/"$TEST_VIDEO_ARCHIVE - -echo "ftp://ftp.it-innovation.soton.ac.uk/testdata/video/"$TEST_VIDEO_ARCHIVE - -# Copy files for MPEG-DASH testing -curl "ftp://ftp.it-innovation.soton.ac.uk/testdata/video/"$TEST_VIDEO_ARCHIVE --user flame-rw:DR8ngj3ogSjd8gl -o $DEST_FILE -tar -xvf $DEST_FILE -C $DEST_DIR - -rm -rf $DEST_FILE -mv $DEST_DIR"/"$TEST_VIDEO $DEST_DIR"/"test_video diff --git a/clmctest/streaming/stop.sh b/clmctest/streaming/stop.sh deleted file mode 100644 index 55c8a28389b00dc5dc6e4360ad4b9536106b5d8b..0000000000000000000000000000000000000000 --- a/clmctest/streaming/stop.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -""" -// © 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-02-2018 -// Created for Project : FLAME -""" - -for pid in $(ps -ef | grep "/usr/bin/vlc" | awk '{print $2}'); do kill -9 $pid; done -# TODO: 'killall vlc' should work: need to test though \ No newline at end of file diff --git a/clmctest/streaming/test_rspec.py b/clmctest/streaming/test_rspec.py deleted file mode 100644 index b90f5010f8f6b0d18d3389f7e591c98f957c934d..0000000000000000000000000000000000000000 --- a/clmctest/streaming/test_rspec.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/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 : 24-02-2018 -// Created for Project : FLAME -""" - -from subprocess import run -from platform import system -import pytest - - -@pytest.mark.parametrize("service_name", [ - 'clmc-service', - 'nginx1', - 'nginx2', - 'loadtest-streaming' -]) -def test_service_names(streaming_config, service_name): - """ - Tests the service names in the configuration. - - :param streaming_config: the configuration fixture collected from conftest.py - :param service_name the service name to test - """ - - assert any(s['name'] == service_name for s in streaming_config['hosts']), "{0} not in list of hosts".format(service_name) - print("\nSuccessfully passed configuration test for service name {0}\n".format(service_name)) - - -def test_ping(streaming_config): - """ - Pings each service to test for liveliness - - :param streaming_config: the configuration fixture collected from conftest.py - """ - - print("\n") # blank line printed for formatting purposes - - ping_count = 1 - system_dependent_param = "-n" if system().lower() == "windows" else "-c" - - for service in streaming_config['hosts']: - command = ["ping", system_dependent_param, str(ping_count), service['ip_address']] - assert run(command).returncode == 0, "Service ping test failed for {0} with ip address {1}".format(service['name'], service['ip_address']) - print("\nSuccessfully passed ping test for service: {0}\n".format(service['name'])) diff --git a/clmctest/streaming/test_rule1.json b/clmctest/streaming/test_rule1.json deleted file mode 100644 index 17d4cdec27f49226968e54cb0f5cb3e794eddd33..0000000000000000000000000000000000000000 --- a/clmctest/streaming/test_rule1.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id" : "TestRule1", - "type" : "batch", - "dbrps" : [{"db": "CLMCMetrics", "rp" : "autogen"}], - - "script" : "var ruleData = batch\r\n |query(''' SELECT mean(\"handled\") AS \"mean_handled\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE \"ipendpoint\"='adaptive_streaming_I1_nginx1' ''')\r\n .period(5s)\r\n .every(5s)\r\n\r\nvar ruleAlert = ruleData\r\n |alert()\r\n .id('{{ .Name }}\/adaptive_streaming_I1_nginx1')\r\n .message('{{ .ID }} is {{ .Level }} Mean handled connections: {{ index .Fields \"mean_handled\" }}')\r\n .warn(lambda: \"mean_handled\" > 10)\r\n .log( '\/tmp\/TestRule1.log' )", - - "status" : "enabled" -} \ No newline at end of file diff --git a/clmctest/streaming/test_rule2.json b/clmctest/streaming/test_rule2.json deleted file mode 100644 index c9adb8401df6662ba14882f3316c84b27e9aa50c..0000000000000000000000000000000000000000 --- a/clmctest/streaming/test_rule2.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id" : "TestRule2", - "type" : "batch", - "dbrps" : [{"db": "CLMCMetrics", "rp" : "autogen"}], - - "script" : "var ruleData = batch\r\n |query(''' SELECT mean(\"waiting\") AS \"mean_waiting\" FROM \"CLMCMetrics\".\"autogen\".\"nginx\" WHERE \"ipendpoint\"='adaptive_streaming_I1_nginx1' ''')\r\n .period(5s)\r\n .every(5s)\r\n\r\nvar ruleAlert = ruleData\r\n |alert()\r\n .id('{{ .Name }}\/adaptive_streaming_I1_nginx1')\r\n .message('{{ .ID }} is {{ .Level }} Mean waiting connections: {{ index .Fields \"mean_waiting\" }}')\r\n .warn(lambda: \"mean_waiting\" > 10)\r\n .log( '\/tmp\/TestRule2.log' )", - - "status" : "enabled" -} \ No newline at end of file diff --git a/clmctest/streaming/test_streaming.py b/clmctest/streaming/test_streaming.py deleted file mode 100644 index 9097b817770e94351695199ef31e87fdde4c2a4e..0000000000000000000000000000000000000000 --- a/clmctest/streaming/test_streaming.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/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 : 19-03-2018 -// Created for Project : FLAME -""" - -from threading import Thread -from time import sleep -from queue import Queue -from xml.etree import ElementTree -from urllib.parse import urljoin -from os.path import isfile, dirname, join -from os import remove, system -import pytest -import requests -import json - - -class TestStreamingAlerts(object): - """ - A testing class used to group all the tests related to the streaming scenario. - """ - - kapacitor_url = "http://localhost:9092/kapacitor/v1/tasks" - - @pytest.mark.parametrize("rule, log", [ - ("rules.json", "/tmp/RPSLoad.log"), - ("test_rule1.json", "/tmp/TestRule1.log"), - ("test_rule2.json", "/tmp/TestRule2.log"), - ]) - def test_alerts(self, rule, log, streaming_url, streaming_manifest): - """ - This test case generates some streaming requests to the server to ensure an alert is triggered and then tests the log file for this alert. Different logs can be tested by - appending to the list of parameters in the pytest decorator. - - Format for pytest parameters under test: - ([filename], [log]) - where [filename] is the name of the json file for the rule under test (must be in the same folder as this test is) - [log] is the absolute path of the log file that must be created due to an alert - - :param rule: the name of the rule json file - :param log: the path of the log file that is under test - :param streaming_url: the fixture providing the streaming url for this test case - :param streaming_manifest: the fixture providing the root of the XML streaming manifest - """ - - kapacitor_setter = self.kapacitor_setting(rule, log) - next(kapacitor_setter) # Setup the test rule - - print("Testing alert creation for rule: {0}".format(rule)) - - segments = streaming_manifest.findall(".//{urn:mpeg:DASH:schema:MPD:2011}SegmentURL") - - threads_num = 30 - threads_queue = Queue(maxsize=threads_num) # a synchronized queue is used to track if all the threads has finished execution - threads = [StreamingThread(streaming_url, segments, threads_queue) for _ in range(threads_num)] - for t in threads: - t.start() - - alert_created = False - counter = 0 - time_delay = 2.5 - while True: - # loop while threads are execution and do a check every 2.5 seconds to check if either alert log has been created or threads have finished execution - sleep(time_delay) - if isfile(log): - for t in threads: # kill all running threads in case log file is created beforehand - t.stop() - alert_created = True - - if threads_queue.full(): - break - - counter += time_delay # the counter tracks the time taken; for the rules under test usually a 30 seconds time frame is enough to trigger the alert - if counter >= 12*time_delay: - for t in threads: # kill all running threads in case of test failure - t.stop() - break - - assert alert_created, "Alerts test failed: no log file is created indicating a triggered alert for rule {0}.".format(rule) - - print("Successfully passed alert creation test for rule: {0}.".format(rule)) - - next(kapacitor_setter) # Teardown the test rule - - def kapacitor_setting(self, rule, log): - """ - A generator function used to provide setUp/tearDown actions for a particular kapacitor rule. - On setUp rule is initialized, on tearDown rule is deleted. Interleaving is achieved using the generator pattern. - - :param rule: the name of the json file for the rule under test - :param log: the absolute path of the log file that's being tested - """ - - # check if the log file is already created due to a previous test - try: - if isfile(log): - remove(log) # delete log file if existing from previous tests - except PermissionError: - system("sudo rm {0}".format(log)) # handles the case for running on linux where permission will be required to delete the old log file - - # Initialization of the kapacitor rule - Test setUp (UnitTest style) - with open(join(dirname(__file__), rule), "r") as rule_file: - data = "".join(line.strip() for line in rule_file.readlines()) - - rule_data = json.loads(data) - requests.delete(url=urljoin(self.kapacitor_url + "/", rule_data.get("id"))) # delete in case of a task with the same ID already set in the kapacitor - r = requests.post(url=self.kapacitor_url, data=data, headers={"Content-Type": "application/json"}) - assert r.status_code == 200, "Couldn't create alert rule {0}".format(rule) - print("\nSuccessfully created test rule {0}".format(rule)) - - yield - - # Deleting the kapacitor rule used for testing - Test tearDown (UnitTest style) - requests.delete(url=urljoin(self.kapacitor_url + "/", rule_data.get("id"))) - - # check if the log file is created and clean it up - try: - if isfile(log): - remove(log) # delete log file if existing from previous tests - except PermissionError: - system("sudo rm {0}".format(log)) # handles the case for running on linux where permission will be required to delete the old log file - yield - - @staticmethod - @pytest.fixture(scope="class", params=[{"server": "http://192.168.50.11", "video": "/test_video/stream.mpd"}]) - def streaming_url(request): - """ - A fixture with class scope - used only in the scope of the testing class. - - :param request: the parameters for this fixture - server url and video relative url - :return: the combined URL for the video used for streaming - """ - - return urljoin(request.param["server"], request.param["video"]) - - @staticmethod - @pytest.fixture(scope="class") - def streaming_manifest(streaming_url): - """ - A fixture to download the manifest file for the streamed video and parse the downloaded XML content - - :param streaming_url: the fixture which provides the streaming url - :return: an XML root node object - """ - - manifest_xml = requests.get(streaming_url).text - root = ElementTree.fromstring(manifest_xml) - return root - - -class StreamingThread(Thread): - - def __init__(self, url, segments, queue): - """ - Subclassing the Thread class to create a custom streaming thread. - - :param url: the streaming url - :param segments: the list of SegmentURL XML nodes - :param queue: an auxiliary parameter used to indicate when this thread has finished execution - """ - - super(StreamingThread, self).__init__() - self.running = False - self.url = url - self.segments = segments - self.queue = queue - self._test_finished = False # a flag to indicate whether the thread should stop running - - def stop(self): - """ - Kill this thread and suspend its execution. - """ - - self._test_finished = True - - def run(self): - """ - A function, which simulates an actual streaming by downloading different audio/video segments from the server using a request session, - which leaves the connection open until executing. - """ - - size = len(self.segments) - size = size if size % 2 == 0 else size - 1 - - s = requests.session() - - for i in range(0, int(size / 2), 1): - segment_audio = self.segments[0] - segment_video = self.segments[int(size / 2) + i] - segment_audio_url = segment_audio.attrib.get('media') - segment_video_url = segment_video.attrib.get('media') - - s.get(urljoin(self.url, segment_audio_url)) - s.get(urljoin(self.url, segment_video_url)) - - # check if thread is killed in case the test has already succeeded - if self._test_finished: - break - - # a small time out to mimic the behaviour of a real streaming - sleep(2.5) - - self.queue.put(True) diff --git a/docs/Measuring-E2E-MS-Performance.md b/docs/Measuring-E2E-MS-Performance.md new file mode 100644 index 0000000000000000000000000000000000000000..278444bf1fd34dbb043b181e938cdf6bb0d8553c --- /dev/null +++ b/docs/Measuring-E2E-MS-Performance.md @@ -0,0 +1,298 @@ +<!-- +// © 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, Simon Crowle +// Created Date : 02-05-2018 +// Created for Project : FLAME +--> + +# **Flame CLMC - Measuring E2E Media Service Performance** + +#### **Authors** + +|Authors|Organisation| +|---|---| +|[Simon Crowle](mailto:sgc@it-innovation.soton.ac.uk)|[University of Southampton, IT Innovation Centre](http://www.it-innovation.soton.ac.uk)| +|[Nikolay Stanchev](mailto:ns17@it-innovation.soton.ac.uk)|[University of Southampton, IT Innovation Centre](http://www.it-innovation.soton.ac.uk)| + +### Definitions + +Readers of this document are assumed to have at least read the [CLMC information model](clmc-information-model.md). Here we explore the requirements which inform the definition of metrics that determine *'end-to-end'* media service performance. Before continuing, some terms are defined: + +| term | definition | +| --- | --- | +| *client* | an end-user of a FLAME media service - typically somebody accessing the service via an mobile computing device connected to an _EP router_ | +| *network node* | is a _service function router_ or other hardware that receives and sends network traffic along network connections attached to it | +| *service function router* | a _service function router_ (SFR) is a VM that allows _clients_ or _endpoints_ to communicate with one another using fully qualified domain names (FQDN), rather than IP addresses | +| *service function instance* | a _service function instance_ (SFI) is a process that in part or wholly realizes the functionality of a media service | +| *endpoint* | an endpoint (EP) is a virtual machine (VM) that implements an SFI and is connected to the FLAME network by a _service function router_ | +| *E2E path* | the directed, acyclic traversal of FLAME network nodes, beginning with a source _EP_ and moving to a target _EP_ via network nodes in the FLAME network | +| *round trip time* | the total time taken for a service request to i) traverse an _E2E path_, ii) be processed by the media service, iii) be returned as a response via an _E2E path_ + +### **Assumptions** + +Here, we list the assumptions we make for measuring and understanding E2E performance of an EP that implements a SFIs: + +* Network measurement - the assumption is that we have a measurement for the network path delays between service function routers, called **network_delays**, providing the following information: + +| path_ID (tag) | source_SFR (tag) | target_SFR (tag) | delay | time | +| --- | --- | --- | --- | --- | +| path identifier | source SFR | target SFR | e2e delay for the given path (ms) | timestamp of measurement | + +Here, the **path_ID** tag value is the identifier of the path between two service function routers in the network topology obtained from FLIPS. The **source_SFR** tag value is the source service router for the identified path, while the **target_SFR** tag value is the target service router. The delay field value is the network end-to-end delay in milliseconds that a packet would experience when traversing the path between the two SFRs identified in the tag values. + +An example row would be: + +| path_ID (tag) | source_SFR (tag) | target_SFR (tag) | delay | time | +| --- | --- | --- | --- | --- | +| SFR-A---S1---S2---S3---SFR-B | SFR-A | SFR-B | 10 | 1525334761282000 | + +The semantics of the row is that a packet traversing the path from SFR-A through S1, S2, S3 (switches) to SFR-B will experience an averaged delay of 10ms. + +* Request/Response path - the assumption is that 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 service response time, containing at least the following information: + +| sf_instance (tag) | sfr (tag) | endpoint (tag) | response_time | time | +| --- | --- | --- | --- | --- | +| media SF instance ID (FQDN) | SFR that connects the SFI endpoint to the FLAME network | SFI EP identifier | response time for the media service (ms) | timestamp of measurement | + +Note that all FLAME service function EPs are expected to contain this and other decision context related data in their global tags, see the [CLMC monitoring documentation](monitoring.md) for further information. Above, the **sf_instance**, **sfr** and **endpoint** tag values identify a unique response time measurement. The response time field value is the time elapsed (measured in milliseconds) for a specific SFI/EP implementation only, and it does not take into account any of the network measurements. An example row would be: + +| sf_instance (tag) | sfr (tag) | endpoint (tag) | response_time | time | +| --- | --- | --- | --- | --- | +| media-service.ict-flame.eu | SFR-B | server1 | 27 | 1525334761282000 | + +The semantics of the row is that the response time for a SFI with an identity of _media-service.ict-flame.eu_ that is implemented by endpoint _server1_ and connected to the FLAME network through service function router *SFR-B* will have an averaged response time of 27 ms. + +## E2E Model + +In the sections that follow we set out some basic properties of a potential media service and then explore these in more detail with a concrete example. Following on from this analysis we provide a test-based approach to the specification of E2E media service performance measures. + +### E2E SFC + +Let us begin by identifying some simple, generic interactions within a media service function chain (SFC): + +``` +// simple chain +Client --> data storage SFI/EP1 + +// sequential chain +Client --> data processor SFI/EP1 --> data storage SFI/EP1 + +// complex chain +Client --> data processor SFI_A/EP1 --> data processor SFI_B/EP1 + |-> data storage SFI/EP1 <-| +``` + +The first example above imagines a client simply requesting some data be stored in (or retrieved from) a database managed by the SFI responsible for persistence. In the second case, the client requests some processing of some data held in the data store, the results of which are also stored. Finally, the third case outlines a more complex scenario in which the client requests some processing of data which in turn generates further requests for additional data processing in other SFIs which also may depend on storage I/O functionality. Here additional data processing by related SFIs could include job scheduling or task decomposition and distribution to worker nodes. An advanced media service, such as a modern computer game, is a useful example of such a service in which graphics rendering; game state modelling; artificial intelligence and network communications are handled in parallel using varying problem decomposition methods. + +### E2E simple chain + +Next we will define a very simple network into which we will place a data processing EP and a data storage EP - we assert the clients could connect to any of _service function routers_ that link these SFI implementations together. + + + +Our simple network consists of three _service function routers_ (SFRs) that connect clients with SFI data and storage functionality; a demand from client 1 for the storage function could be routed in one network hop from router 'A' to router 'C' or in two from routers 'A' -> 'B' -> 'C'. A demand for storage function from _client 2_ would include zero network hops. + +> __Side note: FLAME network scope__ +> +> Readers are reminded that low-level network traffic metrics gathered by the FLAME platform are restricted to observations of network performance between SFRs. The first and last steps (typically between a client and SFI) are not captured at the time of writing - these links are denoted by a dotted line (`--->`) in our diagrams. +> + +### E2E simple chain metrics + +A principal metric we use to understand E2E performance is the average end-to-end _delay_: the _mean_ time taken between a request or response being transmitted and received _within the FLAME network_. Scoping the E2E delay to within the FLAME network is an important qualification since it is only within this network that all necessary measurements can reliably be taken. + +An out-going simple E2E request chain looks like this: + + + +the delay associated with the processing of the service request is isolated to within the storage SFI: + + + +whilst for the response E2E delay, we see this: + + + +Above we denote the time required for an service function router to handle (or pass on) an in-coming message as _handle request_ or _handle response_. When a message is _handled_ by a service function router there are a number of processes that incur (small amounts of) delay: + +* _Processing delay_: error checking and an optimized route through the network for the HTTP packet must be determined +* _Queuing delay_: the time a HTTP packet waits in a queue whilst other packets ahead of it are transmitted +* _Transmission delay_: the time taken for the packet bits to be copied out into the transmission medium of the network + +The _round trip time_ is the sum of the request, service processing and response delays. + +> __Side note:__ +> To understand _delay_ more robustly, we may also consider the rate at which requests or responses arrive (_arrival rate_) at each node in the network since message management (queuing, for example) will have an effect at scale. Similarly, the _payload size_ of the messages being handled could also be observed since the quantity of data traversing the SFC will also impact delay in similar, large scale scenarios. +> + +### E2E extended chain + +Up until this point we have considered an elementary SFC in which there is only one class of SFI. In a more realistic scenario, we would expect a media service function to be composed of multiple SFIs that are distributed and connected to multiple nodes in the FLAME network. Below we have extended the simple chain to include a greater level of complexity with respect to service function chains, whilst holding the network topology constant (adding network SFRs simply introduces additional hops to the problem space at this stage). In addition to indicating extra clients and SFIs, weights have been added to the network arcs to indicate relative network latency between SFRs. __In this case study we also make the assumption that chained service function calls, such as those between one SFI and another, are synchronized and blocking__. + + + +Imagine a media service that both stores and processes high volumes of complex media streams. Consider as well a distributed population of clients making demands on this service. Successfully handling high demand for this service could mean deploying several EPs that implement its SFIs (storage and processing) across multiple VMs that interoperate and share the demand load. Since clients and EPs are distributed, service function requests (made by both) will likely give rise to propagating waves of activity, load (and delay) from multiple nodes across the FLAME platform. For simplicity, let us assume our multimedia service implements a request by processing some media data from the client and then storing it (returning some result to client). Here is client 1's request as it passes through the FLAME network and its SFIs: + + + +In the figure above the green arcs indicate service request travel whilst the blue denotes the response path. The shortest route directs the request to SFR 'B' and the consequent storage request travels on to SFR 'C'. __Responses return along the path used by the request__. Indicative service response times are provided by numeric values in the active SFI/EP boxes. Let's see the same request from client 2, who has just joined the network: + + + +For this client, the _locality of reference_ for processing and then storing her data is high: both of the associated SFIs are located on VMs attached to the same SFR. We could expect client 2's response time to be low for this reason. + +_Now for the sake of example only, let us assume that the hardware running SFI Processor A can only effectively handle one request at any time and that any more than this will result in a substantial degradation in processing performance_. + +Client 3 joins the network: + + + +In calculating a service function route that optimizes for the complete _round trip_ delay, we need to take into account the likely delays that are incurred from both network related latencies and also _all_ SFI response times. The orange route illustrated above shows how the gains made by selecting a fast route through the network are offset by penalities in using an EP processor for the SFI that is overloaded; conversely a slower route that selects a SFI with computational resources to spare resolves to an over-all faster round-trip response time. + +## E2E Measurement + +Our aim is to aggregate network measurement points with media service measurement points to 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 using a CLMC E2E monitoring process that aggregates data from network and media service measurements within a given sample period, e.g. every 10 seconds. This process then posts the aggregated data back to Influx in a new measurement. + +### **Goal** + +The ultimate goal is to populate a new measurement, called **e2e_delays**, which will be provided with the following information: + +| path_ID (tag) | source_SFR (tag) | target_SFR (tag) | sf_instance (tag) | endpoint (tag) | delay_forward | delay_reverse | delay_service | time | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | + +* *path_ID* - tag ID used to identify the network path (bidirectional path identifier) +* *source_SFR* - tag used to identify the source service function router (the start of the network path) +* *target_SFR* - tag used to identify the target service function router (the end of the network path) +* *sf_instance* - tag used to identify the SFI +* *endpoint* - tag used to identify the EP implementing the SFI +* *delay_forward* - network delay for the path in forward direction +* *delay_reverse* - network delay for path in reverse direction +* *delay_service* - media service component 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. + +### Monitoring network delays + +Here, we describe the process of obtaining network delays between two service function routers in the network topology. CLMC retrieves network path delays between any two SFRs, see below (**SFR** denotes a service function router, **S** denotes a switch): + + + +SFR monitoring provides us with FIDs at each service function router, which are bidirectional path IDs. From those, we derive the desired SFR-SFR network latencies. For instance, if we take the network graph example and analyse service function router **SFR3**. We would get 2 FIDs for this router - one for the path to reach SFR2 and one for the path to reach SFR1. + +We assume that the FID for reaching *SFR1* from *SFR3* tells us the path goes through nodes *S3* and *S6*. + + + +Hence, we accumulate the individual link delays to derive the full SFR-SFR delay for both forward and reverse direction. + +delay_forward = SFR3-S3 + S3-S6 + S6-SFR1 = 12 + 3 + 3 = 18 +delay_reverse = SFR1-S6 + S6-S3 + S3-SFR3 = 1 + 5 + 10 = 16 + +Now, we assume that the FID for reaching *SFR2* from *SFR3* tells us the path goes through nodes *S4* and *S2*. + + + +Hence, we accumulate the individual link delays to derive the full SFR-SFR delay for both forward and reverse direction. + +delay_forward = SFR3-S4 + S4-S2 + S2-SFR2 = 12 + 4 + 5 = 21 +delay_reverse = SFR2-S2 + S2-S4 + S4-SFR3 = 8 + 2 + 11 = 21 + +Overall, from this analysis, the following data will be reported to Influx in the **network_delays** measurement: + +| path_ID (tag) | source_SFR (tag) | target_SFR (tag) | delay | time | +| --- | --- | --- | --- | --- | +| SFR3-SFR1 | SFR3 | SFR1 | 18 | 1525334761282000 | +| SFR3-SFR1 | SFR1 | SFR3 | 16 | 1525334761282000 | +| SFR3-SFR2 | SFR3 | SFR2 | 21 | 1525334761282000 | +| SFR3-SFR2 | SFR2 | SFR3 | 21 | 1525334761282000 | + +### Monitoring SFI/EP response times + +Readers of the [CLMC information model](clmc-information-model.md) will already be aware of the approach to identifying and reporting SFI performance metrics in the FLAME project. The global measurement tags that help in a decision context are used in this case to provide the mapping between network measurements and a specific service response time. Specifically, we use the SFR tag encapsulated in the media service global tags to cross-reference against target SFR tags (described above). + +In its simplest case, a media SFI's response time could be defined as a single value that derives from the (average) time spent processing requests in local memory and/or on disk. Indeed, a number of the FLAME foundation media service metrics sent to the CLMC could be described as such. In more advanced cases (such as for clients 1 and 3 in our example above) the full service function chain is implemented across more than one endpoint. Here we have at least two options: + +1. Let the first SFI in a SFC be representative of the entire service function delay (making opaque the sub-calls to other SFIs required to fullfil the client's request) + +2. Construct a more complex view of the service function response time as an aggregate of internal SFI processing delays and their related, dependent network delays + +#### TO BE DISCUSSED FURTHER + +> __NOTE: ABOVE TO BE VALIDATED__ +> __***************************__ +> + +### E2E Aggregation process + +The aggregation process provides similar functionality to that of an INFLUX continuous query. During each sample period the process collects and averages network and service delay data for the last 10 seconds (for example). The executed queries are: + +* Network delays query - to obtain the network delay values and group them by their **path_ID**, **source_SFR** and **target_SFR** identifiers: + +``` +SELECT mean(delay) as "net_delay" FROM "CLMCMetrics"."autogen"."network_delays" WHERE time >= now() - 10s and time < now() GROUP BY path_ID, source_SFR, target_SFR +``` + +* Media service response time query - to obtain the response time values of the media service instances and group them by **endpoint**, **sf_instance** and **sfr** identifiers: +``` +SELECT mean(response_time) as "response_time" FROM "CLMCMetrics"."autogen"."service_delays" WHERE time >= now() - 10s and time < now() GROUP BY endpoint, sf_instance, sfr +``` + +The results of the queries are then matched against each other on the **target** and **sfr** tag values (for *network_delays* and *service_delays* respectively): +on every match of the **sfr** tag of the **service_delays** measurement with the **target** service function router of the **network_delays** measurement, the rows are combined +to obtain an **e2e_delay** measurement row, which is posted back to influx. + +Example: + +Let's assume we have these results from the two queries: + +* Result from first query + +``` +name: network_delays +tags: path_ID=SFR-A---SFR-B, source_SFR=SFR-A, target_SFR=SFR-B +time net_delay +---- --------- +1524833145975682287 9.2 + +name: network_delays +tags: path_ID=SFR-A---SFR-B, source_SFR=SFR-B, target_SFR=SFR-A +time net_delay +---- --------- +1524833145975682287 10.3 +``` + +* Result from second query + +``` +name: service_delays +tags: endpoint=server1, sfr=SFR-B, sf_instance=ms-A.ict-flame.eu +time response_time +---- ------------- +1524833145975682287 11 +``` + +The E2E aggregation process will merge those rows, because there is a match on network delay target SFR and service delay SFR - namely **SFR-B**. + +| path_ID (tag) | source_SFR (tag) | target_SFR (tag) | endpoint (tag) | sf_instance (tag) | delay_forward | delay_reverse | delay_service | time | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| SFR-A---SFR-B | SFR-A | SFR-B | server1 | ms-A.ict-flame.eu | 9.2 | 10.3 | 11 | 1524833145975682287 | + +The resulting row would then be posted back to influx in the **e2e_delays** measurement. \ No newline at end of file diff --git a/docs/aggregation.md b/docs/aggregation.md new file mode 100644 index 0000000000000000000000000000000000000000..bedb0e28c57adb9487d443bcf14c4e9b7c2bfcda --- /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 "CLMCMetrics"."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 "CLMCMetrics"."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/clmc-information-model.md b/docs/clmc-information-model.md index 46ffb27e3646420913dedec692739705d0b843fc..c338f8eaf5336100c0378dca96abc575bbe8f112 100644 --- a/docs/clmc-information-model.md +++ b/docs/clmc-information-model.md @@ -392,7 +392,7 @@ A simple example and some measurement rows for an endpoint configuration states Each sample period is 1 second. -First sample period reports the VM being in state __unpalced__ for 0.7s, then changing state to __placing__ for 0.3 seconds. __placing__ is not +First sample period reports the VM being in state __unplaced__ for 0.7s, then changing state to __placing__ for 0.3 seconds. __placing__ is not reported since it is not a **completed state**. The mean state time value for __unplaced__ is the same as the sum value because the VM has only been once in this state. Then the VM is reported to be in current state __placing__ for the whole sample period (1s) for 9 consecutive times. Only the 'current_state' tag value and the 'current_state_time' diff --git a/docs/clmc-service.md b/docs/clmc-service.md new file mode 100644 index 0000000000000000000000000000000000000000..c8ff0204e6fa2d13a59d6a6a9f30a8c4019bd2ff --- /dev/null +++ b/docs/clmc-service.md @@ -0,0 +1,234 @@ +<!-- +// © 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 : 02-05-2018 +// Created for Project : FLAME +--> + +# **Flame CLMC Service Documentation** + +#### **Authors** + +|Authors|Organisation| +|---|---| +|[Nikolay Stanchev](mailto:ns17@it-innovation.soton.ac.uk)|[University of Southampton, IT Innovation Centre](http://www.it-innovation.soton.ac.uk)| + +#### Description + +This document describes the CLMC service and its API endpoints. The CLMC service is implemented in the *Python* framework called **Pyramid**. +It offers different API endpoints to configure and control the aggregator, which is an essential part in the process of measuring the end-to-end performance. +All source code, tests and configuration files of the service can be found in the **src/clmc-webservice** folder. + +#### API Endpoints + +* **GET** ***/aggregator/config*** + + This API method retrieves information about the configuration of the aggregator. + + * Response: + + Returns a JSON-formatted response with the configuration data of the aggregator - *aggregator_report_period*, *aggregator_database_name*, + *aggregator_database_url*. + + * Response Body Example: + + ```json + { + "aggregator_report_period": 5, + "aggregator_database_name": "CLMCMetrics", + "aggregator_database_url": "http://172.40.231.51:8086" + } + ``` + +* **PUT** ***/aggregator/config*** + + This API method updates the configuration of the aggregator. + + * Request: + + Expects a JSON-formatted request body with the new configuration of the aggregator. The body should contain only + three key fields - *aggregator_report_period* (positive integer, seconds), *aggregator_database_name* and *aggregator_database_url* (a valid URL). + + * Request Body Example: + + ```json + { + "aggregator_report_period": 25, + "aggregator_database_name": "CLMCMetrics", + "aggregator_database_url": "http://172.50.231.61:8086" + } + ``` + + * Response: + + The body of the request is first validated before updating the configuration. If validation is successful, returns + a JSON-formatted response with the new configuration data. Otherwise, an **HTTP Bad Request** response is returned. + + * Response Body Example: + + ```json + { + "aggregator_report_period": 25, + "aggregator_database_name": "CLMCMetrics", + "aggregator_database_url": "http://172.50.231.61:8086" + } + ``` + + * Notes: + + If the configuration is updated, while the aggregator is running, it is not automatically restarted. An explicit API call + must be made with a *restart* request to apply the updated configuration. In the case of such PUT request as the one described + above, the response will contain more information indicating that the configuration of the aggregator is in a malformed state. + + * Response Body Example: + + ```json + { + "aggregator_report_period": 125, + "aggregator_database_name": "CLMCMetrics", + "aggregator_database_url": "http://172.50.231.61:8086/", + "malformed": true, + "comment": "Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used." + } + ``` + +* **GET** ***/aggregator/control*** + + This API method retrieves information about the status of the aggregator - whether it is running or not. + + * Response: + + Returns a JSON-formatted response with the status data of the aggregator - *aggregator_running* field. If the aggregator + is running in a malformed state, the response will also indicate this with two additional fields - *malformed* and *comment*. + + * Response Body Example: + + ```json + { + "aggregator_running": true + } + ``` + + * Response Body Example - for malformed configuration: + + ```json + { + "aggregator_running": true, + "malformed": true, + "comment": "Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used." + } + ``` + +* **PUT** ***/aggregator/control*** + + This API method updates the status of the aggregator - a user can start, stop or restart it. + + * Request: + + Expects a JSON-formatted request body with the new status of the aggregator. The body should contain only one key + field - *action* (the action to undertake, which can be **start**, **restart** or **stop**) + + * Request Body Example: + + ```json + { + "action": "start" + } + ``` + + * Response: + + The body of the request is first validated before taking any actions. If the action is not one of the listed above, + then the validation will fail. If validation is successful, returns a JSON-formatted response with the new status of + the aggregator. Otherwise, an **HTTP Bad Request** response is returned. + + * Response Body Example: + + ```json + { + "aggregator_running": true + } + ``` + + * Notes: + + * If a **start** action is requested, while the aggregator is running, then the request will be ignored. To restart the + aggregator, a user should use a **restart** action. + + * If a **stop** action is requested, while the aggregator is not running, then the request will be ignored. + + * A request with a **restart** action, while the aggregator is not running, has the same functionality as a request + with a **start** action. + + * The functionality of a request with a **restart** action is the same as the functionlity of a **stop** action + followed by a **start** action. + +#### Installing and running the CLMC service (development mode) + +Before installing the CLMC service and its dependencies, it is recommended to use a python virtual environment. To easily +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 installed relative to this environment only. + +The easiest way to install and use the CLMC service locally is to use **pip**. Navigate to the clmc-webservice folder: +``` +cd src/clmc-webservice +``` + +Test the CLMC service using **tox** along with the ***tox.ini*** configuration file. If tox is not installed run: + +``` +pip install tox +``` + +After it is installed, simply use the **tox** command: + +``` +tox +``` + +Then install the service in development mode. + +``` +pip install -e . +``` + +Finally, start the service on localhost by using pyramid's **pserve**: + +``` +pserve development.ini --reload +``` + +You should now be able to make requests to the CLMC service on http://localhost:9080/aggregator/config and http://localhost:9080/aggregator/control. diff --git a/docs/figures/e2eFigures.graphml b/docs/figures/e2eFigures.graphml index 7e150e6312fdfa0d3b14f03d04423e8faceb54ec..635028fc2fe5224a0a2bc8d26d96491726951fe7 100644 --- a/docs/figures/e2eFigures.graphml +++ b/docs/figures/e2eFigures.graphml @@ -15,43 +15,62 @@ <graph edgedefault="directed" id="G"> <data key="d0"/> <node id="n0"> - <data key="d5"/> + <data key="d6"> + <y:SVGNode> + <y:Geometry height="22.891233402652546" width="20.06220742959614" x="820.3669601387119" y="986.4070328236099"/> + <y:Fill color="#CCCCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.031103714798064" y="26.89123340265246"> + <y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="0.5" offsetX="0.0" offsetY="4.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:SVGNodeProperties usingVisualBounds="true"/> + <y:SVGModel svgBoundsPolicy="0"> + <y:SVGContent refid="1"/> + </y:SVGModel> + </y:SVGNode> + </data> + </node> + <node id="n1"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="152.4000000000001" width="350.79999999999984" x="222.89999999999998" y="986.8"/> + <y:Geometry height="152.4000000000001" width="142.72602478550982" x="222.89999999999998" y="986.8"/> <y:Fill hasColor="false" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="dotted" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.01953125" x="141.39023437499998" y="156.4000000000001">FLAME network</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.01953125" x="37.35324676775491" y="156.4000000000001">FLAME network</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n1"> - <data key="d5"/> + <node id="n2"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="152.4000000000001" width="350.79999999999984" x="222.89999999999998" y="663.4"/> + <y:Geometry height="152.4000000000001" width="142.72602478550982" x="222.89999999999998" y="663.4"/> <y:Fill hasColor="false" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="dotted" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.01953125" x="141.39023437499992" y="156.4000000000001">FLAME network</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.01953125" x="37.35324676775491" y="156.4000000000001">FLAME network</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n2" yfiles.foldertype="group"> + <node id="n3" yfiles.foldertype="group"> <data key="d4"/> <data key="d6"> <y:ProxyAutoBoundsNode> <y:Realizers active="0"> <y:GroupNode> - <y:Geometry height="511.0354454457677" width="480.9759773254389" x="1009.4572265625002" y="101.66553111673232"/> + <y:Geometry height="511.0354454457677" width="454.02041091918886" x="659.5838274174812" y="92.70182647705082"/> <y:Fill color="#F5F5F5" transparent="false"/> <y:BorderStyle color="#000000" type="dashed" width="1.0"/> - <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="480.9759773254389" x="0.0" y="0.0">E2E complex chain</y:NodeLabel> + <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="454.02041091918886" x="0.0" y="0.0">E2E extended chain</y:NodeLabel> <y:Shape type="roundrectangle"/> <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/> <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/> - <y:BorderInsets bottom="26" bottomF="25.725000000000023" left="0" leftF="5.6843418860808015E-14" right="0" rightF="0.0" top="0" topF="0.0"/> + <y:BorderInsets bottom="26" bottomF="25.725000000000023" left="0" leftF="0.0" right="25" rightF="25.323066704152325" top="0" topF="0.0"/> </y:GroupNode> <y:GroupNode> <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/> @@ -66,11 +85,11 @@ </y:Realizers> </y:ProxyAutoBoundsNode> </data> - <graph edgedefault="directed" id="n2:"> - <node id="n2::n0"> + <graph edgedefault="directed" id="n3:"> + <node id="n3::n0"> <data key="d6"> <y:SVGNode> - <y:Geometry height="35.32593311666594" width="30.960157775878884" x="1229.7199211120605" y="139.04199596048232"/> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="839.2703500920416" y="130.07829132080082"/> <y:Fill color="#CCCCFF" transparent="false"/> <y:BorderStyle color="#000000" type="line" width="1.0"/> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="39.325933116665965">Client 1<y:LabelModel> @@ -87,43 +106,43 @@ </y:SVGNode> </data> </node> - <node id="n2::n1"> + <node id="n3::n1"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="42.5" width="42.5" x="1223.9499999999998" y="241.2500000000001"/> + <y:Geometry height="42.5" width="42.5" x="833.5004289799808" y="232.2862953603186"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-81.8427734375" y="13.12451171875">service router 'A'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="13.12451171875">SFR 'A'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n2::n2"> + <node id="n3::n2"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="42.5" width="42.5" x="1106.3000000000002" y="445.225"/> + <y:Geometry height="42.5" width="42.5" x="715.8504289799812" y="436.2612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-81.8427734375" y="13.12451171875">service router 'B'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="13.12451171875">SFR 'B'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n2::n3"> + <node id="n3::n3"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="42.5" width="42.5" x="1343.1999999999998" y="445.225"/> + <y:Geometry height="42.5" width="42.5" x="952.7504289799808" y="436.2612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="e" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.39453125" x="46.5" y="13.12451171875">service router 'C'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="e" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="46.5" y="13.12451171875">SFR 'C'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n2::n4"> + <node id="n3::n4"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="30.0" width="30.0" x="1217.475" y="375.2500000000001"/> + <y:Geometry height="30.0" width="30.0" x="827.0254289799809" y="366.2862953603186"/> <y:Fill hasColor="false" transparent="false"/> <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.677734375" x="-6.3388671875" y="12.849414062499932">FLAME SDN<y:LabelModel> @@ -137,10 +156,10 @@ </y:ShapeNode> </data> </node> - <node id="n2::n5"> + <node id="n3::n5"> <data key="d6"> <y:SVGNode> - <y:Geometry height="35.32593311666594" width="30.960157775878884" x="1392.5" y="348.48399192096474"/> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="1002.050428979981" y="339.52028728128323"/> <y:Fill color="#CCCCFF" transparent="false"/> <y:BorderStyle color="#000000" type="line" width="1.0"/> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="39.325933116665965">Client 2<y:LabelModel> @@ -157,10 +176,10 @@ </y:SVGNode> </data> </node> - <node id="n2::n6"> + <node id="n3::n6"> <data key="d6"> <y:SVGNode> - <y:Geometry height="35.32593311666594" width="30.960157775878884" x="1440.9999999999995" y="348.48399192096474"/> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="896.8406101048615" y="153.4505065377179"/> <y:Fill color="#CCCCFF" transparent="false"/> <y:BorderStyle color="#000000" type="line" width="1.0"/> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="39.325933116665965">Client 3<y:LabelModel> @@ -177,51 +196,51 @@ </y:SVGNode> </data> </node> - <node id="n2::n7"> + <node id="n3::n7"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="30.0" width="30.0" x="1112.5500000000002" y="521.725"/> + <y:Geometry height="30.0" width="30.0" x="722.1004289799812" y="512.7612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="80.611328125" x="-25.3056640625" y="34.0">Processor 'A' MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-32.29296875" y="34.0">Processor SFI / EP1</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n2::n8"> + <node id="n3::n8"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="30.0" width="30.0" x="1313.1999999999998" y="521.725"/> + <y:Geometry height="30.0" width="30.0" x="910.4186844613916" y="512.7612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="80.611328125" x="-25.3056640625" y="34.0">Processor 'B' MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-32.29296875" y="34.0">Processor SFI / EP2</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n2::n9"> + <node id="n3::n9"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="30.0" width="30.0" x="1392.9800788879395" y="521.725"/> + <y:Geometry height="30.0" width="30.0" x="1015.9833200700177" y="512.7612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.3544921875" x="-13.67724609375" y="34.0">Storage MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.595703125" x="-27.2978515625" y="34.0">Storage SFI / EP1</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> </graph> </node> - <node id="n3" yfiles.foldertype="group"> + <node id="n4" yfiles.foldertype="group"> <data key="d4"/> <data key="d6"> <y:ProxyAutoBoundsNode> <y:Realizers active="0"> <y:GroupNode> - <y:Geometry height="511.0354454457677" width="499.31093826293886" x="36.58222656250018" y="92.70182647705082"/> + <y:Geometry height="511.0354454457677" width="491.84365310668886" x="81.85224609375018" y="92.70182647705082"/> <y:Fill color="#F5F5F5" transparent="false"/> <y:BorderStyle color="#000000" type="dashed" width="1.0"/> - <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="499.31093826293886" x="0.0" y="0.0">E2E simple chain</y:NodeLabel> + <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="491.84365310668886" x="0.0" y="0.0">E2E simple chain</y:NodeLabel> <y:Shape type="roundrectangle"/> <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/> <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/> @@ -240,8 +259,8 @@ </y:Realizers> </y:ProxyAutoBoundsNode> </data> - <graph edgedefault="directed" id="n3:"> - <node id="n3::n0"> + <graph edgedefault="directed" id="n4:"> + <node id="n4::n0"> <data key="d6"> <y:SVGNode> <y:Geometry height="35.32593311666594" width="30.960157775878884" x="256.84492111206055" y="130.07829132080082"/> @@ -261,40 +280,40 @@ </y:SVGNode> </data> </node> - <node id="n3::n1"> + <node id="n4::n1"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="251.07499999999982" y="232.2862953603186"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-81.8427734375" y="13.12451171875">service router 'A'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="115.6455078125" x="-119.6455078125" y="13.12451171875">service function router 'A'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n3::n2"> + <node id="n4::n2"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="133.42500000000018" y="436.2612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-81.8427734375" y="13.12451171875">service router 'B'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="115.6455078125" x="-36.57275390625" y="46.5">service function router 'B'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n3::n3"> + <node id="n4::n3"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="370.3249999999998" y="436.2612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="e" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.39453125" x="46.5" y="13.12451171875">service router 'C'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="e" textColor="#000000" verticalTextPosition="bottom" visible="true" width="116.197265625" x="46.5" y="13.12451171875">service function router 'C'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n3::n4"> + <node id="n4::n4"> <data key="d6"> <y:ShapeNode> <y:Geometry height="30.0" width="30.0" x="244.5999999999999" y="366.2862953603186"/> @@ -311,7 +330,7 @@ </y:ShapeNode> </data> </node> - <node id="n3::n5"> + <node id="n4::n5"> <data key="d6"> <y:SVGNode> <y:Geometry height="35.32593311666594" width="30.960157775878884" x="419.625" y="339.52028728128323"/> @@ -331,20 +350,20 @@ </y:SVGNode> </data> </node> - <node id="n3::n6"> + <node id="n4::n6"> <data key="d6"> <y:ShapeNode> <y:Geometry height="30.0" width="30.0" x="420.1050788879395" y="512.7612953603185"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.3544921875" x="-13.67724609375" y="34.0">Storage MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.595703125" x="-27.2978515625" y="34.0">Storage SFI / EP1</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> </graph> </node> - <node id="n4"> + <node id="n5"> <data key="d6"> <y:SVGNode> <y:Geometry height="35.32593311666594" width="30.960157775878884" x="67.64492111206054" y="726.3883582041351"/> @@ -364,40 +383,40 @@ </y:SVGNode> </data> </node> - <node id="n5"> + <node id="n6"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="201.47499999999985" y="722.8013247624681"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-17.67138671875" y="46.5">service router 'A'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="2.61669921875" y="46.5">SFR 'A'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n6"> + <node id="n7"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="346.84492111206026" y="722.8013247624681"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.39453125" x="-17.947265625" y="46.5">service router 'C'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="2.3408203125" y="46.5">SFR 'C'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n7"> + <node id="n8"> <data key="d6"> <y:ShapeNode> <y:Geometry height="30.0" width="30.0" x="509.81484222412064" y="729.0513247624681"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.3544921875" x="-13.67724609375" y="34.0">Storage MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.359375" x="-13.6796875" y="34.0">Storage SFI</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n8"> + <node id="n9"> <data key="d6"> <y:SVGNode> <y:Geometry height="35.32593311666594" width="30.960157775878884" x="67.64492111206062" y="1052.188358204135"/> @@ -417,191 +436,577 @@ </y:SVGNode> </data> </node> - <node id="n9"> + <node id="n10"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="201.4749999999999" y="1048.6013247624683"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.8427734375" x="-17.67138671875" y="46.5">service router 'A'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="2.61669921875" y="46.5">SFR 'A'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n10"> + <node id="n11"> <data key="d6"> <y:ShapeNode> <y:Geometry height="42.5" width="42.5" x="346.8449211120603" y="1048.6013247624683"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="78.39453125" x="-17.947265625" y="46.5">service router 'C'</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="2.3408203125" y="46.5">SFR 'C'</y:NodeLabel> <y:Shape type="ellipse"/> </y:ShapeNode> </data> </node> - <node id="n11"> + <node id="n12"> <data key="d6"> <y:ShapeNode> <y:Geometry height="30.0" width="30.0" x="509.81484222412064" y="1054.8513247624683"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.3544921875" x="-13.67724609375" y="34.0">Storage MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.359375" x="-13.6796875" y="34.0">Storage SFI</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <node id="n12"> + <node id="n13"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="30.0" width="30.0" x="373.42968444824123" y="892.3026495249364"/> + <y:Geometry height="30.0" width="30.0" x="279.2630123927549" y="892.3026495249364"/> <y:Fill color="#FFFFFF" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.3544921875" x="-13.67724609375" y="34.0">Storage MC</y:NodeLabel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="57.359375" x="-13.6796875" y="34.0">Storage SFI</y:NodeLabel> <y:Shape type="rectangle"/> </y:ShapeNode> </data> </node> - <edge id="n2::e0" source="n2::n1" target="n2::n3"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> - </data> - </edge> - <edge id="n2::e1" source="n2::n3" target="n2::n2"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + <node id="n14"> + <data key="d6"> + <y:SVGNode> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="835.4490886803685" y="654.3896829666031"/> + <y:Fill color="#CCCCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="39.325933116665965">Client 1<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="0.5" offsetX="0.0" offsetY="4.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:SVGNodeProperties usingVisualBounds="true"/> + <y:SVGModel svgBoundsPolicy="0"> + <y:SVGContent refid="1"/> + </y:SVGModel> + </y:SVGNode> </data> - </edge> - <edge id="n2::e2" source="n2::n1" target="n2::n2"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n15"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="782.6626280068207" y="826.5539742874045"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="2.37451171875">SFR 'B'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e3" source="n2::n0" target="n2::n1"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="none" target="short"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n16"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="785.6626280068207" y="886.404048954476"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-39.79296875" y="19.0">Processor SFI / EP1</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e4" source="n2::n5" target="n2::n3"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="none" target="short"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n17"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="899.2912191224618" y="826.5539742874045"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="n" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="-8.4091796875" y="-20.2509765625">SFR 'C'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e5" source="n2::n6" target="n2::n3"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="none" target="short"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n18"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="996.9659866539561" y="886.404048954476"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.595703125" x="-34.7978515625" y="19.0">Storage SFI / EP1</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e6" source="n2::n2" target="n2::n7"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n19"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="840.429167568308" y="729.0513247624681"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="2.37451171875">SFR 'A'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e7" source="n2::n3" target="n2::n8"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n20"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="902.2912191224618" y="886.404048954476"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-39.79296875" y="19.0">Processor SFI / EP2</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> </data> - </edge> - <edge id="n2::e8" source="n2::n3" target="n2::n9"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n21"> + <data key="d6"> + <y:SVGNode> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="1162.0434929124954" y="685.8129035619129"/> + <y:Fill color="#CCCCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="n" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="-20.2509765625">Client 2</y:NodeLabel> + <y:SVGNodeProperties usingVisualBounds="true"/> + <y:SVGModel svgBoundsPolicy="0"> + <y:SVGContent refid="1"/> + </y:SVGModel> + </y:SVGNode> </data> - </edge> - <edge id="n3::e0" source="n3::n1" target="n3::n3"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n22"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="1128.131571876991" y="779.8773298641443"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="-41.818359375" y="2.37451171875">SFR 'C'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> </data> - </edge> - <edge id="n3::e1" source="n3::n3" target="n3::n2"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n23"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1228.676272678266" y="839.7274045312158"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.595703125" x="-34.7978515625" y="19.0">Storage SFI / EP1</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> </data> - </edge> - <edge id="n3::e2" source="n3::n1" target="n3::n2"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="standard" target="standard"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n24"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1131.131571876991" y="839.7274045312158"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-39.79296875" y="19.0">Processor SFI / EP2</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> </data> - </edge> - <edge id="n3::e3" source="n3::n0" target="n3::n1"> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="none" target="short"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> + </node> + <node id="n25"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="787.6427068947602" y="1152.3539742874045"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="2.37451171875">SFR 'B'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> + </data> + </node> + <node id="n26"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="790.6427068947602" y="1212.204048954476"/> + <y:Fill color="#FFCC00" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-39.79296875" y="19.0">Processor SFI / EP1</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n27"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="904.2712980104013" y="1152.3539742874045"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="n" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.818359375" x="-8.4091796875" y="-20.2509765625">SFR 'C'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> + </data> + </node> + <node id="n28"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1004.8159988116762" y="1212.204048954476"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.595703125" x="-34.7978515625" y="19.0">Storage SFI / EP1</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n29"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="21.0" width="21.0" x="845.4092464562475" y="1054.8513247624683"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="w" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.2666015625" x="-41.2666015625" y="2.37451171875">SFR 'A'</y:NodeLabel> + <y:Shape type="ellipse"/> + </y:ShapeNode> + </data> + </node> + <node id="n30"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="907.2712980104013" y="1212.204048954476"/> + <y:Fill color="#FFFFFF" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="s" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.5859375" x="-39.79296875" y="19.0">Processor SFI / EP2</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n31"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="790.6427068947602" y="1212.204048954476"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.34765625" x="-1.173828125" y="0.599609375">150<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n32"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="907.2712980104013" y="1212.204048954476"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="17.34765625" x="-1.173828125" y="0.599609375">100<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n33"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1004.8159988116762" y="1212.204048954476"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">10<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n34"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="800.4092464562475" y="1098.503899620333"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="8.44921875" x="3.275390625" y="0.599609375">5<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n35"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="896.4092464562475" y="1098.503899620333"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">10<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n36"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="848.4092464562475" y="1162.65659876567"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="8.44921875" x="3.275390625" y="0.599609375">5<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n37"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="785.6626280068207" y="886.404048954476"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">50<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n38"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="996.9659866539561" y="886.404048954476"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">10<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n39"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1131.131571876991" y="839.7274045312158"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">50<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n40"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="15.0" width="15.0" x="1228.676272678266" y="839.7274045312158"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle hasColor="false" raised="false" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="12.8984375" x="1.05078125" y="0.599609375">10<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n41"> + <data key="d6"> + <y:SVGNode> + <y:Geometry height="22.891233402652546" width="20.06220742959614" x="955.2712980104013" y="1123.7414631686688"/> + <y:Fill color="#CCCCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="8.031103714798064" y="26.891233402652688"> + <y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="0.5" offsetX="0.0" offsetY="4.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:SVGNodeProperties usingVisualBounds="true"/> + <y:SVGModel svgBoundsPolicy="0"> + <y:SVGContent refid="1"/> + </y:SVGModel> + </y:SVGNode> + </data> + </node> + <node id="n42"> + <data key="d6"> + <y:SVGNode> + <y:Geometry height="35.32593311666594" width="30.960157775878884" x="840.429167568308" y="980.1896829666033"/> + <y:Fill color="#CCCCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="10" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="16.2509765625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.90625" x="-3.4730461120605014" y="39.32593311666608">Client 3<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="-0.5" nodeRatioX="0.0" nodeRatioY="0.5" offsetX="0.0" offsetY="4.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:SVGNodeProperties usingVisualBounds="true"/> + <y:SVGModel svgBoundsPolicy="0"> + <y:SVGContent refid="1"/> + </y:SVGModel> + </y:SVGNode> + </data> + </node> + <edge id="n3::e0" source="n3::n1" target="n3::n3"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="center_on_edge" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="14.0107421875" x="55.292258431999926" y="76.12963032125606">10<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="center" side="on_edge" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e1" source="n3::n3" target="n3::n2"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="9.00537109375" x="-101.70268381298774" y="-17.025875049837737">5<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e2" source="n3::n1" target="n3::n2"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="9.00537109375" x="-63.54654294166278" y="76.06703876852168">5<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e3" source="n3::n0" target="n3::n1"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="short"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e4" source="n3::n5" target="n3::n3"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="short"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e5" source="n3::n6" target="n3::n1"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="short"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e6" source="n3::n2" target="n3::n7"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e7" source="n3::n3" target="n3::n8"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n3::e8" source="n3::n3" target="n3::n9"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> </data> </edge> - <edge id="n3::e4" source="n3::n5" target="n3::n3"> + <edge id="n4::e0" source="n4::n1" target="n4::n3"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/> - <y:Arrows source="none" target="short"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n4::e1" source="n4::n3" target="n4::n2"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> <y:BendStyle smoothed="false"/> </y:PolyLineEdge> </data> </edge> - <edge id="n3::e5" source="n3::n3" target="n3::n6"> + <edge id="n4::e2" source="n4::n1" target="n4::n2"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> @@ -611,7 +1016,37 @@ </y:PolyLineEdge> </data> </edge> - <edge id="e0" source="n4" target="n5"> + <edge id="n4::e3" source="n4::n0" target="n4::n1"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="short"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n4::e4" source="n4::n5" target="n4::n3"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="short"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="n4::e5" source="n4::n3" target="n4::n6"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e0" source="n5" target="n6"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> @@ -623,7 +1058,7 @@ </y:PolyLineEdge> </data> </edge> - <edge id="e1" source="n5" target="n5"> + <edge id="e1" source="n6" target="n6"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> @@ -631,8 +1066,7 @@ </y:Path> <y:LineStyle color="#000000" type="line" width="1.0"/> <y:Arrows source="none" target="standard"/> - <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="26.0517578125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.54052734375" x="-27.658209228515773" y="-49.023382268781916">handle request -route specification<y:LabelModel> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="63.5458984375" x="-21.160894775390773" y="-41.954412542219416">handle request<y:LabelModel> <y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/> </y:LabelModel> <y:ModelParameter> @@ -644,7 +1078,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e2" source="n5" target="n6"> + <edge id="e2" source="n6" target="n7"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> @@ -656,11 +1090,11 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e3" source="n6" target="n7"> + <edge id="e3" source="n7" target="n8"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> <y:Arrows source="none" target="standard"/> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="two_pos" modelPosition="head" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="61.02783203125" x="29.72105598449673" y="-17.025884710188166">request transit<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> </y:EdgeLabel> @@ -668,7 +1102,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e4" source="n6" target="n6"> + <edge id="e4" source="n7" target="n7"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> @@ -688,7 +1122,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e5" source="n8" target="n9"> + <edge id="e5" source="n9" target="n10"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> @@ -700,7 +1134,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e6" source="n9" target="n9"> + <edge id="e6" source="n10" target="n10"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> @@ -720,7 +1154,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e7" source="n9" target="n10"> + <edge id="e7" source="n10" target="n11"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> @@ -732,11 +1166,11 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e8" source="n10" target="n11"> + <edge id="e8" source="n11" target="n12"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> <y:Arrows source="standard" target="none"/> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="two_pos" modelPosition="head" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.03271484375" x="26.21861457824673" y="-17.025872503156734">response transit<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> </y:EdgeLabel> @@ -744,7 +1178,7 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e9" source="n10" target="n10"> + <edge id="e9" source="n11" target="n11"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> @@ -752,8 +1186,7 @@ route specification<y:LabelModel> </y:Path> <y:LineStyle color="#000000" type="line" width="1.0"/> <y:Arrows source="standard" target="none"/> - <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="26.0517578125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.54052734375" x="-27.65819168090843" y="-49.023309026594234">handle response -route specification<y:LabelModel> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="70.55078125" x="-24.66331863403343" y="-41.954339300031734">handle response<y:LabelModel> <y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/> </y:LabelModel> <y:ModelParameter> @@ -765,15 +1198,15 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> - <edge id="e10" source="n12" target="n12"> + <edge id="e10" source="n13" target="n13"> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> - <y:Point x="388.42968444824123" y="867.3026495249364"/> + <y:Point x="294.2630123927549" y="867.3026495249364"/> </y:Path> <y:LineStyle color="#000000" type="line" width="1.0"/> <y:Arrows source="standard" target="none"/> - <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.03271484375" x="-19.017062377930642" y="-34.36452576803231">process request<y:LabelModel> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="15.02587890625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="68.03271484375" x="-19.017077939276362" y="-34.36452576803231">process request<y:LabelModel> <y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/> </y:LabelModel> <y:ModelParameter> @@ -785,6 +1218,293 @@ route specification<y:LabelModel> </y:PolyLineEdge> </data> </edge> + <edge id="e11" source="n14" target="n19"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="817.7093602092111" y="710.1235840570342"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="two_pos" modelPosition="head" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="-13.466990994052935" y="19.181343801702837"> + <y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e12" source="n19" target="n15"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="808.7337834694587" y="780.034585010323"/> + </y:Path> + <y:LineStyle color="#339966" type="line" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="two_pos" modelPosition="tail" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="-37.17839147178756" y="49.91116197308247"> + <y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e13" source="n15" target="n16"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="766.4727539175045" y="864.6529545773675"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="9" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="two_pos" modelPosition="head" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="-21.455190774668722" y="24.36413409014051"> + <y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e14" source="n16" target="n15"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="820.4060236982481" y="867.0125351302751"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e15" source="n15" target="n17"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="849.7230685028238" y="845.83317414283"/> + </y:Path> + <y:LineStyle color="#339966" type="line" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e16" source="n17" target="n18"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e17" source="n18" target="n17"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="-2.175385463925977" tx="0.0" ty="0.0"> + <y:Point x="975.5878932316491" y="837.0539742874045"/> + </y:Path> + <y:LineStyle color="#3366FF" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e18" source="n17" target="n15"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="848.644403107209" y="825.877864323955"/> + </y:Path> + <y:LineStyle color="#3366FF" type="line" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e19" source="n19" target="n14"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="883.4405327544924" y="712.6517060780066"/> + </y:Path> + <y:LineStyle color="#3366FF" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e20" source="n15" target="n19"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="834.6217529642156" y="794.0572351533164"/> + </y:Path> + <y:LineStyle color="#3366FF" type="line" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e21" source="n17" target="n20"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#C0C0C0" type="dashed" width="1.0"/> + <y:Arrows source="none" target="none"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e22" source="n22" target="n23"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e23" source="n23" target="n22"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="-2.175385463925977" tx="0.0" ty="0.0"> + <y:Point x="1236.176272678266" y="790.3773298641443"/> + </y:Path> + <y:LineStyle color="#3366FF" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e24" source="n21" target="n22"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="-14.980663742331217" sy="15.468648468886613" tx="0.0" ty="0.0"> + <y:Point x="1138.631571876991" y="718.9445185891325"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e25" source="n22" target="n21"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="14.818145939894293" ty="17.656223449753725"> + <y:Point x="1192.3417177403294" y="754.0011439466159"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e26" source="n22" target="n24"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="1112.654633510916" y="817.3727359389893"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e27" source="n24" target="n22"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="1165.239571547141" y="819.3952335557674"/> + </y:Path> + <y:LineStyle color="#339966" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e28" source="n29" target="n33"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="786.16382775501" y="1161.9550581506196"/> + <y:Point x="795.7136771353722" y="1211.8264938036223"/> + <y:Point x="808.4136504432845" y="1159.8991877979026"/> + <y:Point x="904.4426914347049" y="1159.8991877979026"/> + <y:Point x="915.0867943899003" y="1174.6881906577692"/> + </y:Path> + <y:LineStyle color="#FF9900" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="side_slider" preferredPlacement="anywhere" ratio="0.0" textColor="#FF9900" verticalTextPosition="bottom" visible="true" width="128.48828125" x="-149.19656670838287" y="12.111822809343266">Faster route; slower round-trip time<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e29" source="n29" target="n33"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="904.4426914347049" y="1159.8991877979026"/> + <y:Point x="914.0257000143044" y="1212.109788369876"/> + <y:Point x="924.6366437702625" y="1161.8342192564348"/> + </y:Path> + <y:LineStyle color="#800080" type="line" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="8" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="13.80078125" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#800080" verticalTextPosition="bottom" visible="true" width="127.16015625" x="17.375070192673547" y="18.986005865495827">Slower route; faster round-trip time<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/> + </y:EdgeLabel> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e30" source="n42" target="n29"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="829.1049907674028" y="1036.6644652049567"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e31" source="n29" target="n42"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="887.1335894327984" y="1039.9803851286938"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e32" source="n41" target="n27"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#999999" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e33" source="n0" target="n29"> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="829.1049907674028" y="1036.6644652049567"/> + </y:Path> + <y:LineStyle color="#999999" type="dashed" width="1.0"/> + <y:Arrows source="standard" target="standard"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> </graph> <data key="d7"> <y:Resources> diff --git a/docs/image/e2e-extended-chain-network.png b/docs/image/e2e-extended-chain-network.png new file mode 100644 index 0000000000000000000000000000000000000000..88eb8c94032db1e735c39093574f4ac644237a9e Binary files /dev/null and b/docs/image/e2e-extended-chain-network.png differ diff --git a/docs/image/e2e-extended-client1-path.png b/docs/image/e2e-extended-client1-path.png new file mode 100644 index 0000000000000000000000000000000000000000..cb158c1478fe7764e9c84b1a7b9f83111d70e95c Binary files /dev/null and b/docs/image/e2e-extended-client1-path.png differ diff --git a/docs/image/e2e-extended-client2-path.png b/docs/image/e2e-extended-client2-path.png new file mode 100644 index 0000000000000000000000000000000000000000..1d084bba503d5cdeb77987c9b8c15aa8a4908225 Binary files /dev/null and b/docs/image/e2e-extended-client2-path.png differ diff --git a/docs/image/e2e-extended-client3-path.png b/docs/image/e2e-extended-client3-path.png new file mode 100644 index 0000000000000000000000000000000000000000..819d6f61356a4804d21db955ac886c0d62307b10 Binary files /dev/null and b/docs/image/e2e-extended-client3-path.png differ diff --git a/docs/image/e2e-simple-chain-mc-processing.png b/docs/image/e2e-simple-chain-mc-processing.png index 16c80c3f6b942e6ae554206b5f41d4c6b8b24729..453da28646023ea465d229bdbfd7bdc689cd275c 100644 Binary files a/docs/image/e2e-simple-chain-mc-processing.png and b/docs/image/e2e-simple-chain-mc-processing.png differ diff --git a/docs/image/e2e-simple-chain-network.png b/docs/image/e2e-simple-chain-network.png index 8c054d0719314f2a5bcde4b17813f3d8043aeb70..21ef62e588f1b781345fa496feb9596974c0f79e 100644 Binary files a/docs/image/e2e-simple-chain-network.png and b/docs/image/e2e-simple-chain-network.png differ diff --git a/docs/image/e2e-simple-chain-request-steps.png b/docs/image/e2e-simple-chain-request-steps.png index 9c82d58aa0e5faeb3f3ff9be38d12d75d0fe6d0a..540bcce090785984f37eab146435cfc9c14607fb 100644 Binary files a/docs/image/e2e-simple-chain-request-steps.png and b/docs/image/e2e-simple-chain-request-steps.png differ diff --git a/docs/image/e2e-simple-chain-response-steps.png b/docs/image/e2e-simple-chain-response-steps.png index 7401e2692bf4916979846f390039b7827c6aaecf..948032bf664a7be2e94d35f96fde0d2ac33d3c53 100644 Binary files a/docs/image/e2e-simple-chain-response-steps.png and b/docs/image/e2e-simple-chain-response-steps.png differ diff --git a/docs/image/network-SFR3-SFR1.png b/docs/image/network-SFR3-SFR1.png new file mode 100644 index 0000000000000000000000000000000000000000..80d83afa18c14e8b70f48f33eddb71ca81eb81d7 Binary files /dev/null and b/docs/image/network-SFR3-SFR1.png differ diff --git a/docs/image/network-SFR3-SFR2.png b/docs/image/network-SFR3-SFR2.png new file mode 100644 index 0000000000000000000000000000000000000000..43e5638c382a58509add4bce4a107b68c03af079 Binary files /dev/null and b/docs/image/network-SFR3-SFR2.png differ diff --git a/docs/image/network_graph.png b/docs/image/network_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..6792fcd92222dbe6f5c7ed3f4b49a3e887775cc3 Binary files /dev/null and b/docs/image/network_graph.png differ 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/docs/total-service-request-delay.md b/docs/total-service-request-delay.md index 774c3616b10d852140934c67dc84fce568ca78e4..a27f47665f3f77341193feff8f74f51e89f5bb3b 100644 --- a/docs/total-service-request-delay.md +++ b/docs/total-service-request-delay.md @@ -16,6 +16,7 @@ If we ignore the OSI L6 protocol (e.g. HTTP, FTP, Tsunami) then we are modelling ``` network_delay = latency + (time difference from start of the data to the end of the data) + = latency + data_delay ``` ### Latency @@ -24,10 +25,10 @@ The latency (or propagation delay) of the network path is the time taken for a p latency = distance / speed -For optical fibre (or even an eletric wire), the speed naively would be the speed of light. In fact, the speed is slower than this (in optical fibre this is because of the internal refraction that occurs, which is different for different wavelengths). According to http://www.m2optics.com/blog/bid/70587/Calculating-Optical-Fiber-Latency the delay (1/speed) is approximately 5 microseconds / km +For optical fibre (or even an eletric wire), the speed naively would be the speed of light. In fact, the speed is slower than this (in optical fibre this is because of the internal refraction that occurs, which is different for different wavelengths). According to [m2.optics.com](http://www.m2optics.com/blog/bid/70587/Calculating-Optical-Fiber-Latency) the delay (1/speed) is approximately 5 microseconds / km ``` -if +if distance is in m delay is in s/m latency is in s @@ -48,35 +49,179 @@ if data_size is in Bytes bandwidth is in Mb/s data_delay is in s -then +then data_delay = data_size * 8 / bandwidth * 1E6 ``` -The data_size naively is the size of the data you want to send over the network (call this the "file_size"). However, the data is split into packets and each packet has a header on it so the amount of data going over the network is actually more than the amount sent. +The data_size naively is the size of the data you want to send over the network (call this the "file_size"). However, the data is split into packets and each packet has a header on it so the amount of data going over the network is actually more than the amount sent. The header includes contributions from (at least) the L6 protocol (e.g. HTTP), L4 (e.g. TCP) and L3 (e.g. IP) layers. ``` -let +let packet_size = packet_header_size + packet_payload_size then data_size = (packet_size / packet_payload_size) * file_size or - data_size = (packet_size / packet_size - packet_header_size) * file_size + data_size = [packet_size / (packet_size - packet_header_size)] * file_size + = file_size * packet_size / (packet_size - packet_header_size) ``` -### Total delay +### Measuring and Predicting + +Bringing the above parts together we have: ``` -delay = latency + data_delay - = (distance * 5 / 1E9) + {[(packet_size / packet_size - packet_header_size) * file_size] * 8 / bandwidth * 1E6} +network_delay = latency + data_delay + = (distance * 5 / 1E9) + {[file_size * packet_size / (packet_size - packet_header_size)] * 8 / (bandwidth * 1E6)} + = (distance * 5 / 1E9) + (8 / 1E6) * (file_size / bandwidth) * [packet_size / (packet_size - packet_header_size)] ``` -### Effect of Protocol +i.e. `file_size / bandwidth` with an adjustment to increase the size of the data transmitted because of the packet header and some unit factors. + +We want to be able to measure the `network_delay` and also want to be able to predict what the delay is likely to be for a given deployment. + +Parameter | Known / measured +----------|-------- +latency | measured by network probes +distance | sometimes known +packet_size | known (a property of the network) +packet_header_size | known (at least for L3 and L4) +file_size | measured at the service function +bandwidth | known (a property of the network), can also be measured + +Measuring the actual `latency` can be done in software. For a given `file_size`, the `network_delay` could then be predicted. + +*We are ignoring network congestion and the effect of the protocol (see below).* + +### Effect of protocol -The choice of protocol has a large effect in networks with a high bandwidth-delay product. +The analysis above ignores the network protocol. However, the choice of protocol has a large effect in networks with a high bandwidth-delay product. In data communications, bandwidth-delay product is the product of a data link's capacity (in bits per second) and its round-trip delay time (in seconds). The result, an amount of data measured in bits (or bytes), is equivalent to the maximum amount of data on the network circuit at any given time, i.e., data that has been transmitted but not yet acknowledged. TCP for instance expects acknowledgement of every packet sent and if the sender has not received an acknowledgement within a specified time period then the packet will be retransmitted. Furthermore, TCP uses a flow-control method whereby the receiver specifies how much data it is willing to buffer and the sending host must pause sending and wait for acknowledgement once that amount of data is sent. +### Effect of congestion + +The analysis above considers the best case where the whole bandwidth of the link is available for the data transfer. + ## Service Delay +A particular service function may have several operations (API calls) on it. A model of service function performance needs to consider the resource the service function is deployed upon (and its variability and reliability), the availability of the resource (i.e. whether the service function have the resource to itself), the workload (a statistical distribution of API calls and request sizes) and the speed at which the resource can compute the basic computations invoked by the requests. + +We must simplify sufficiently to make the problem tractable but not too much so that the result is of no practical use. + +To simplify we can: + +* assume that the resource is invariable, 100% available and 100% reliable; +* assume that the distribution of API calls is constant and that the workload can be represented sufficiently by the average request size. + +To be concrete, if a service function has two API calls: `transcode_video(video_data)` and `get_status()` then we would like to model the average response time over "normal" usage which might be 10% of calls to `transcode_video` and 90% of calls to `get_status` and a variety of `video_data` sizes with a defined average size. + +### Measuring + +As an example, the `minio` service reports the average response time over all API calls already so, for that service at least, measuring the `service_delay` is easy. We expect to also be able to measure the average `file_size` which will do as a measure of workload. + +### Predicting + +As noted above, a simple model must consider: + +* the resource the service function is deployed upon (e.g. CPU, memory, disk); +* the workload (an average request size) +* the speed at which the resource can compute the basic computations invoked by the requests (dependent on the service function). + +We can therefore write that: + +``` +service_delay = f(resource, workload, service function characteristics) +``` + +For our simplified workload we could assume that this can be written as: + +``` +service_delay = workload * f(resource, service function characteristics) +``` + +The resource could be described in terms of the number of CPUs, amount of RAM and amount of disk. Even if the resource was a physical machine more detail would be required such as the CPU clock speed, CPU cache sizes, RAM speed, disk speed, etc. In a virtualised environment it is even more complicated as elements of the physical CPU may or may not be exposed to the virtual CPU (which may in fact be emulated). + +Benchmarks are often used to help measure the performance of a resource so that one resource may be compared to another without going into all the detail of the precise artchitecture. Application benchmarks (those executing realistic workloads such as matrix operations or fast fourier transforms) can be more useful than general benchmark scores (such as SPECint or SPECfp). For more information on this, see [Snow White Clouds and the Seven Dwarfs](https://eprints.soton.ac.uk/273157/1/23157.pdf). + +The best benchmark for a service function is the service function itself combined with a representative workload. That is, to predict the performance of a service function on a given resource, it is best to just run it and find out. In the absence of that, the next best would be to execute Dwarf benchmarks on each resource type and correlate them with the service functions, but that is beyond what we can do now. + +We might execute a single benchmark such as the [Livermoor Loops](http://www.netlib.org/benchmark/livermorec) benchmark which stresses a variety of CPU operations. Livermoor Loops provides a single benchmark figure in Megaflops/sec. + +Our service_delay equation would then just reduce to: + +``` +service_delay = workload * f(benchmark, service function characteristics) + = workload * service_function_scaling_factor / benchmark +``` + +The `service_function_scaling_factor` essentially scales the `workload` number into a number of Megaflops. So for a `workload` in bytes the `service_function_scaling_factor` would be representing Megaflops/byte. + +If we don't have a benchmark then the best we can do is approximate the benchmark by the number of CPUs: + +``` +service_delay = workload * f(benchmark, service function characteristics) + = workload * service_function_scaling_factor / cpus +``` + +Is this a simplification too far? It ignores the size of RAM for instance which cannot normally be included as a linear factor (i.e. twice as much RAM does not always give twice the performance). Not having sufficient RAM results in disk swapping or complete failure. Once you have enough for a workload, adding more makes no difference. + +## Conclusion + +The total delay is: + +``` +total_delay = forward_network_delay + service_delay + reverse_network_delay +``` + +To *measure* the `total_delay` we need: + +``` +total_delay = forward_latency + forward_data_delay + service_delay + reverse_latency + reverse_data_delay + = forward_latency + + {(8 / 1E6) * (request_size / bandwidth) * [packet_size / (packet_size - packet_header_size)]} + + service_delay + + reverse_latency + + {(8 / 1E6) * (response_size / bandwidth) * [packet_size / (packet_size - packet_header_size)]} +``` + +With: + +* forward_latency / s (measured by network probe) +* reverse_latency / s (measured by network probe) +* request_size / Bytes (measured at service) +* response_size / Bytes (measured at service) +* bandwidth / Mb/s (b = bit) (assumed constant and known or measured) +* packet_size / Bytes (constant and known) +* packet_header_size / Bytes (constant and known) + +This calculation assumes: + +* there is no network congestion, i.e. the whole bandwidth is available +* that the protocol (such as TCP) has no effect (see discussion of flow control above) +* there is no data loss on the network +* that the service delay is proportional to the `request_size`, i.e. that the service is processing the data in the request +* that the service does not start processing until the complete request is received +* that the amount of memory and disk on the compute resource is irrelevant +* that the service delay is inversely proprtional to the number of CPUs (and all CPUs are equal) +* that the compute resource is invariable, 100% available and 100% reliable +* that the distribution of API calls is constant and that the workload can be represented sufficiently by the average request size + +To *predict* the `total_delay` we need: + +``` +total_delay = forward_latency + forward_data_delay + service_delay + reverse_latency + reverse_data_delay + = forward_latency + + {(8 / 1E6) * (request_size / bandwidth) * [packet_size / (packet_size - packet_header_size)]} + + request_size * service_function_scaling_factor / cpus + + reverse_latency + + {(8 / 1E6) * (response_size / bandwidth) * [packet_size / (packet_size - packet_header_size)]} +``` + +With: + +* service_function_scaling_factor / Mflops/Byte (known, somehow) +* cpus (unitless) (known) + +As discussed above, you could also predict the latency if you know the length of a link but this seems a bit too theoretical. diff --git a/docs/understanding-E2E-performance.md b/docs/understanding-E2E-performance.md deleted file mode 100644 index 342752a0fe4ad26247e6986763d9fa7c70eadba5..0000000000000000000000000000000000000000 --- a/docs/understanding-E2E-performance.md +++ /dev/null @@ -1,75 +0,0 @@ -# Understanding end-to-end media service performance in the FLAME platform - -© University of Southampton IT Innovation Centre, 2018 - -This document describe the FLAME model of end-to-end (E2E) media service performance as it is observed and measured using the CLMC on the FLAME platform. - -#### **Authors** - -|Authors|Organisation| -|-|-| -|[Simon Crowle](mailto:sgc@it-innovation.soton.ac.uk)|[University of Southampton, IT Innovation Centre](http://www.it-innovation.soton.ac.uk)| - -## Introduction - -Readers of this document are assumed to have at least read the [CLMC information model](clmc-information-model.md). Here we explore the requirements which inform the definition of metrics that determine *'end-to-end'* media service performance. Before continuing, some terms are defined: - -| term | definition | -| --- | --- | -| *client* | an end-user of a FLAME media service - typically somebody accessing the service via an mobile computing device connected to an _service router_ | -| *endpoint* | an endpoint (EP) is a virtual machine (VM) connected to the FLAME network | -| *service router* | an EP that allows other EPs to communicate with one another using fully qualified domain names (FQDN), rather than IP addresses | -| *network node* | an _EP_, _service router_ or other hardware that receives and sends network traffic along network connections attached to it | -| *media component* | a media component (MC) is a process that in part or wholly realizes the functionality of a media service | -| *E2E path* | the directed, acyclic traversal of FLAME network nodes, beginning with a source _EP_ and moving to a target _EP_ via network nodes in the FLAME network | -| *E2E response time* | the total time taken for a service request to i) traverse an _E2E path_, ii) be processed at the _MC_, iii) be returned as a response via an _E2E path_ - -In the sections that follow we set out some basic properties of a potential media service and then explore these in more detail with a concrete example. Following on from this analysis we provide a test-based approach to the specification of E2E media service performance measures. - -## E2E SFC chains - -Let us begin by identifying some simple, generic interactions within a media service function chain (SFC): - -``` -// simple chain -Client --> data storage MC - -// sequential chain -Client --> data processor MC --> data storage MC - -// complex chain -Client --> data processor MC_A --> data processor MC_B - |-> data storage MC <-| -``` - -The first example above imagines a client requesting data be stored in (or retrieved from) a database managed by the MC responsible for persistence. In the second case, the client requests some processing of some data held in the data store, the results of which are also stored. Finally, the third case outlines a more complex scenario in which the client requests some processing of data which in turn generates further requests for additional data processing in other MCs which also may depend on storage I/O functionality. Here additional data processing by related MCs could include job scheduling or task decomposition and distribution to worker nodes. An advanced media service, such as a game server, is a useful example of such a service in which graphics rendering; game state modelling; artificial intelligence and network communications are handled in parallel using varying problem decomposition methods. - -## E2E simple chain - -Next we will define a simple network into which we will place a data processing EP and a data storage EP - we assert the clients could connect to any of _service routers_ that link these MC together. - - - -Our simple network consists of three _service routers_ that connect clients with MC data and storage functionality; each demand from client 1 for the storage function could be routed in one network hop from router 'A' to router 'C' or in two from routers 'A' -> 'B' -> 'C'. A demand for storage function from _client 2_ would include zero network hops. - -### E2E simple chain metrics - -A principal metric we use to understand _E2E response time_: the average time taken between a request or response being transmitted and received _within the FLAME network_. Scoping the E2E response time to within the FLAME network is an important qualification since it is only within this network that all necessary measurements can reliably be taken. - -An out-going simple E2E request chain looks like this: - - - -the delay associated with the processing of the service request is isolated to within the storage MC: - - - -whilst for the response E2E delay, we see this: - - - -Above we denote the time required for an service router to handle (or pass on) an in-coming message as _handle request_ or _handle response_. When a message is first encountered by a service router, an optimized path through the FLAME network must also be determined; this is labelled above as _route specification_. The _e2e response time_ is the sum of the request, service processing and response delays. - -> __Side note:__ -> To understand _delay_ more robustly, we may also consider the rate at which requests or responses arrive (_arrival rate_) at each node in the network since message management (queuing, for example) will have an effect at scale. Similarly, the _payload size_ of the messages being handled could also be observed since the quantity of data traversing the SFC will also impact delay in similar, large scale scenarios. -> \ No newline at end of file diff --git a/reporc b/reporc new file mode 100644 index 0000000000000000000000000000000000000000..55ddc3e14ff38e011c8f9566e068528be60ac8a8 --- /dev/null +++ b/reporc @@ -0,0 +1,2 @@ +REPO_USER=itinnov.flame.integration +REPO_PASS=1tc3ntr3 \ No newline at end of file diff --git a/scripts/clmc-agent/configure.sh b/scripts/clmc-agent/configure.sh index 06504dd8e0eb109363c5422c8f9a668ae7ea1e36..35226d643103e9fbcdf2c5799c371f33ded9791d 100755 --- a/scripts/clmc-agent/configure.sh +++ b/scripts/clmc-agent/configure.sh @@ -57,11 +57,13 @@ TELEGRAF_OUTPUT_CONF_FILE=$TELEGRAF_INCLUDE_CONF_DIR"/telegraf_output.conf" sed -i 's/$LOCATION/'$LOCATION'/g' $TELEGRAF_CONF_FILE sed -i 's/$SFC_ID/'$SFC_ID'/g' $TELEGRAF_CONF_FILE sed -i 's/$SFC_ID_INSTANCE/'$SFC_ID_INSTANCE'/g' $TELEGRAF_CONF_FILE -sed -i 's/$SF_ID/'$SF_ID'/g' /etc/telegraf/telegraf.conf +sed -i 's/$SF_ID/'$SF_ID'/g' $TELEGRAF_CONF_FILE sed -i 's/$SF_ID_INSTANCE}}/'$SF_ID_INSTANCE'/g' $TELEGRAF_CONF_FILE sed -i 's/$IP_ENDPOINT_ID/'$IP_ENDPOINT_ID'/g' $TELEGRAF_CONF_FILE sed -i 's/$SR_ID/'$SR_ID'/g' $TELEGRAF_CONF_FILE +echo "Telegraf Output Configuration File: ${TELEGRAF_OUTPUT_CONF_FILE}" + # Replace parameters in output configuration file sed -i 's|$INFLUXDB_URL|'$INFLUXDB_URL'|g' $TELEGRAF_OUTPUT_CONF_FILE sed -i 's/$DATABASE_NAME/'$DATABASE_NAME'/g' $TELEGRAF_OUTPUT_CONF_FILE \ No newline at end of file diff --git a/scripts/clmc-service/install.sh b/scripts/clmc-service/install.sh index f39a6498db720a3f72e0088221206703988976d8..c39d26f32fd52eb777daa483ea20fcd18906ed80 100755 --- a/scripts/clmc-service/install.sh +++ b/scripts/clmc-service/install.sh @@ -24,8 +24,22 @@ #// #///////////////////////////////////////////////////////////////////////// -# Force fail on command fail -set -euo pipefail +# Force fail on command fail (off for now as virtualenvwrapper install fails) +# set -euo pipefail + + +echo "Configuring CLMC service" + +# Get command line parameters +if [ "$#" -ne 3 ]; then + echo "Error: illegal number of arguments: "$# + echo "Usage: install.sh INFLUX_URL DATABASE_NAME REPORT_PERIOD" + exit 1 +fi + +INFLUX_URL=$1 +DATABASE_NAME=$2 +REPORT_PERIOD=$3 # Define tickstack software versions INFLUX_VERSION=1.5.2 @@ -37,9 +51,16 @@ KAPACITOR_CHECKSUM=eea9b215f241906570eafe3857e1d4c5 CHRONOGRAF_VERSION=1.4.4.2 CHRONOGRAF_CHECKSUM=eea6915aa6db8f134fcd3b095e863b773bfb3a16a26e346dd65904a07df97963 -# install python for the simulator +# install virtualenvwrapper to manage python environments - and check apt-get update -apt-get -y install python wget +echo "----> Installing Python3 and Pip3" +apt-get install -y python3 python3-pip wget +update-alternatives --install /usr/bin/python python /usr/bin/python3 10 + +echo "----> Installing virtualenv and wrapper" +apt-get install -y python3-virtualenv virtualenvwrapper +pip3 install virtualenv +pip3 install virtualenvwrapper # install influx wget https://dl.influxdata.com/influxdb/releases/influxdb_${INFLUX_VERSION}_amd64.deb 2> /dev/null @@ -71,3 +92,86 @@ dpkg -i chronograf_${CHRONOGRAF_VERSION}_amd64.deb systemctl start influxdb systemctl start kapacitor systemctl start chronograf + +## CLMC-SERVICE +## ---------------------------------------------------------------------------------- + + + +echo "----> Configuring virtualenvwrapper" +export WORKON_HOME=$HOME/.virtualenvs +source /usr/local/bin/virtualenvwrapper.sh + +# check the mkvirtualenv with a return value of 1 if version comes back correctly +mkvirtualenv --version +if [ $? -ne 1 ] ; then + echo "Failed: installing virtualenvwrapper" + exit 1 +fi + +# create CLMC virtual environment - and check +echo "----> Making CLMC Python environment" +mkvirtualenv CLMC +if [ $? -ne 0 ] ; then + echo "Failed: creating CLMC python environment" + exit 1 +fi + +# switch the CLMC environment - and check +echo "----> Switching to use CLMC python environment" +workon CLMC +if [ $? -ne 0 ] ; then + echo "Failed: switching to CLMC python environment" + exit 1 +fi + +# install tox - and check +echo "----> Installing TOX" +pip3 install tox +tox --version +if [ $? -ne 0 ] ; then + echo "Failed: installing tox" + exit 1 +fi + +# navigate to the clmc-webservice - and check +echo "----> Moving to CLMC webservice" +cd ${REPO_ROOT}/src/service +if [ $? -ne 0 ] ; then + echo "Failed: could not find clmc-webservice" + exit 1 +fi + +# install the service +echo "----> Installing CLMC web service" +pip3 install . +if [ $? -ne 0 ] ; then + echo "Failed: installing clmc-webservice" + exit 1 +fi + +# start the service +echo "----> Starting CLMC web service" +nohup pserve production.ini > /dev/null 2>&1 & +if [ $? -ne 0 ] ; then + echo "Failed: starting clmc-webservice" + exit 1 +else + echo "CLMC service started." +fi + +# wait for the clmc service to start +while ! nc -z localhost 9080 +do + echo "Waiting for clmc service port 9080 to be ready on localhost..." + sleep 5 +done + +# configure the CLMC service +JSON="{\"aggregator_report_period\": ${REPORT_PERIOD}, \"aggregator_database_name\": \"${DATABASE_NAME}\", \"aggregator_database_url\": \"${INFLUX_URL}\"}" +curl -H 'Content-Type: application/json' -X PUT -d "${JSON}" http://localhost:9080/aggregator/config + +# start the aggregator +JSON="{\"action\": \"start\"}" +curl -H 'Content-Type: application/json' -X PUT -d "${JSON}" http://localhost:9080/aggregator/control + diff --git a/scripts/test/conts-create.sh b/scripts/test/conts-create.sh new file mode 100644 index 0000000000000000000000000000000000000000..ca3aac39375941c356f69e14b19798aab342b3e6 --- /dev/null +++ b/scripts/test/conts-create.sh @@ -0,0 +1,91 @@ +#!/bin/bash +set -eu -o pipefail + +source conts-env.sh + +# iterate through each service required for integration testing +service_names=$(jq -r '.[].name' ${rspec_file}) +for service_name in $service_names; do + if ! lxc-info -n ${service_name}; then + # create a container with a static ip address + echo "Creating container: ${service_name}" + SERVICE=$(jq --arg NAME ${service_name} '.[] | select(.name==$NAME)' ${rspec_file}) + echo $SERVICE + ip=$(echo $SERVICE | jq -r '.ip_address') + echo "dhcp-host=${service_name},${ip}" >> /etc/lxc/dnsmasq.conf + service lxc-net restart + + lxc-create -t download -n ${service_name} -- --dist ubuntu --release xenial --arch amd64 + + # copy flame clmc files into the root container + echo "Copying files to rootfs" + container_dir="/var/lib/lxc/"${service_name}"/rootfs" + container_vagrant_dir=${container_dir}"${repo_root}" + mkdir -p ${container_vagrant_dir} + cp -f ${repo_root}/reporc "${container_vagrant_dir}" + cp -rf ${repo_root}/scripts ${container_vagrant_dir} + cp -rf ${repo_root}/src ${container_vagrant_dir} + + # start the container + echo "Starting: ${service_name}" + lxc-start -n ${service_name} + echo "Waiting for container to start: ${service_name}" + STARTED="0" + while [ "$STARTED" == "0" ]; do STARTED=$(lxc-info -n ${service_name} -i | wc -l); done; + + # provision software into each container + echo "Provisioning: ${service_name}" + if [ ${service_name} == "clmc-service" ] + then + influxdb_url=$(echo $SERVICE | jq -r '.influxdb_url') + database_name=$(echo $SERVICE | jq -r '.database_name') + report_period=$(echo $SERVICE | jq -r '.report_period') + cmd="${repo_root}/scripts/clmc-service/install.sh ${influxdb_url} ${database_name} ${report_period}" + lxc-attach -n ${service_name} -v REPO_ROOT=${repo_root} -- ${cmd} + elif [ ${service_name} == "test-runner" ] + then + cmd=${repo_root}/src/test/clmctest/services/pytest/install.sh + lxc-attach -n ${service_name} -- ${cmd} + else + # get container parameters + location=$(echo $SERVICE | jq -r '.location') + sf_id=$(echo $SERVICE | jq -r '.sf_id') + sf_id_instance=$(echo $SERVICE | jq -r '.sf_id_instance') + sfc_id=$(echo $SERVICE | jq -r '.sfc_id') + sfc_id_instance=$(echo $SERVICE | jq -r '.sfc_id_instance') + sr_id=$(echo $SERVICE | jq -r '.sr_id') + ipendpoint_id=$(echo $SERVICE | jq -r '.ipendpoint_id') + influxdb_url=$(echo $SERVICE | jq -r '.influxdb_url') + database_name=$(echo $SERVICE | jq -r '.database_name') + + # install service function specific software + cmd=${repo_root}/src/test/clmctest/services/${sf_id}/install.sh + lxc-attach -n ${service_name} -v REPO_ROOT=${repo_root} -- ${cmd} + + # install telegraf + cmd=${repo_root}/scripts/clmc-agent/install.sh + lxc-attach -n ${service_name} -v REPO_ROOT=${repo_root} -- ${cmd} + + # copy telegraf configuration templates + cp -f ${repo_root}/scripts/clmc-agent/telegraf.conf ${container_dir}/etc/telegraf/ + cp -f ${repo_root}/scripts/clmc-agent/telegraf_output.conf ${container_dir}/etc/telegraf/telegraf.d/ + cp ${repo_root}/src/test/clmctest/services/${sf_id}/telegraf_${sf_id}.conf ${container_dir}/etc/telegraf/telegraf.d/ + + # replace telegraf template with container parameters + # @todo do we really need both scripts to do this? + cmd=${repo_root}/scripts/clmc-agent/configure_template.sh + lxc-attach -n ${service_name} -- ${cmd} + cmd="${repo_root}/scripts/clmc-agent/configure.sh ${location} ${sfc_id} ${sfc_id_instance} ${sf_id} ${sf_id_instance} ${ipendpoint_id} ${sr_id} ${influxdb_url} ${database_name}" + lxc-attach -n ${service_name} -- ${cmd} + + # start telegraf + lxc-attach -n ${service_name} -- service telegraf restart + fi + fi +done + +#./conts-stop.sh + +#service lxc-net restart + +#./conts-start.sh \ No newline at end of file diff --git a/scripts/test/conts-destroy.sh b/scripts/test/conts-destroy.sh new file mode 100644 index 0000000000000000000000000000000000000000..9a707149bd224f58cc441108647439afef269f29 --- /dev/null +++ b/scripts/test/conts-destroy.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -eu -o pipefail + +source conts-env.sh + +service_names=$(jq -r '.[].name' ${rspec_file}) +for service_name in $service_names; do + if lxc-info -n ${service_name}; then + echo "Stopping container: ${service_name}" + lxc-stop -n ${service_name} + echo "Destroying container: ${service_name}" + lxc-destroy -n ${service_name} + ip=$(jq -r --arg NAME ${service_name} '.[] | select(.name==$NAME) | .ip_address' ${rspec_file}) + #TODO: this line doesn't work for some reason and it's also dangerous because an IP could match more than 1 line in the file + #sed -i '/${ip}/d' /etc/lxc/dnsmasq.conf + fi +done diff --git a/scripts/test/conts-env.sh b/scripts/test/conts-env.sh new file mode 100644 index 0000000000000000000000000000000000000000..742d54779cb3a2434cb70b07ef1dff8ceb461f92 --- /dev/null +++ b/scripts/test/conts-env.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +repo_root="/vagrant" +rspec_file=$repo_root"/src/test/clmctest/rspec.json" \ No newline at end of file diff --git a/scripts/test/conts-start.sh b/scripts/test/conts-start.sh new file mode 100644 index 0000000000000000000000000000000000000000..f15a3a1dc6f457bbd81ea0074b3c9a88a97dacf7 --- /dev/null +++ b/scripts/test/conts-start.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -eu -o pipefail + +source conts-env.sh + +service_names=$(jq -r '.[].name' ${rspec_file}) +for service_name in $service_names; do + if lxc-info -n ${service_name}; then + echo "Starting container: ${service_name}" + lxc-start -n ${service_name} + fi +done diff --git a/scripts/test/conts-stop.sh b/scripts/test/conts-stop.sh new file mode 100644 index 0000000000000000000000000000000000000000..33d89360d8b7254dc4b4f5280b07474d0b78abd4 --- /dev/null +++ b/scripts/test/conts-stop.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -eu -o pipefail + +source conts-env.sh + +service_names=$(jq -r '.[].name' ${rspec_file}) +for service_name in $service_names; do + if lxc-info -n ${service_name}; then + echo "Stopping container: ${service_name}" + lxc-stop -n ${service_name} + fi +done \ No newline at end of file diff --git a/scripts/test/deleteallvms.py b/scripts/test/deleteallvms.py new file mode 100644 index 0000000000000000000000000000000000000000..27bbeee836b3cf2c960b626fbc4a5399ca967cff --- /dev/null +++ b/scripts/test/deleteallvms.py @@ -0,0 +1,46 @@ +#!/bin/python +## © 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 : Michael Boniface +## Created Date : 28-03-2018 +## Created for Project : FLAME +import subprocess +import sys + +proc = subprocess.Popen(['vboxmanage', 'list', 'vms'], stdout=subprocess.PIPE, shell=True) +out, err = proc.communicate() +if proc.returncode != 0: + print("Could list virtualbox vms") + sys.exit() + +vms = out.strip().decode('ascii') +lines = vms.split("\n") +for vm in lines: + vm_id = vm.split()[0].replace('"', '') + print("Removing VM: {0}".format(vm_id)) + + proc = subprocess.Popen(['vboxmanage', 'controlvm', vm_id, 'poweroff'], stdout=subprocess.PIPE, shell=True) + out, err = proc.communicate() + if proc.returncode != 0: + print("Error power off VM: {0}, {1}, {2}".format(vm_id, out, err)) + + proc = subprocess.Popen(['vboxmanage', 'unregistervm', vm_id, '--delete'], stdout=subprocess.PIPE, shell=True) + out, err = proc.communicate() + if proc.returncode != 0: + print("Error unregister VM: {0}, {1}, {2}".format(vm_id, out, err)) + diff --git a/scripts/test/deleteallvms.sh b/scripts/test/deleteallvms.sh new file mode 100644 index 0000000000000000000000000000000000000000..be8b957507e754a604bf3e7fad1a80e439d90cf2 --- /dev/null +++ b/scripts/test/deleteallvms.sh @@ -0,0 +1,32 @@ +#!/bin/bash +## © 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 : Michael Boniface +## Created Date : 28-03-2018 +## Created for Project : FLAME + +vboxmanage list vms | awk ' {print $1} ' | awk -F',' '{gsub(/"/, "", $1); print $1}' > runningvms + +while read vm_id; do + echo $vm_id + vboxmanage controlvm $vm_id poweroff + vboxmanage unregistervm $vm_id --delete +done <runningvms + +rm runningvms + diff --git a/src/clmcagent/__init__.py b/src/clmcagent/__init__.py deleted file mode 100644 index 44f772595799f5fe338534918c95e23e08e80464..0000000000000000000000000000000000000000 --- a/src/clmcagent/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/python3 \ No newline at end of file diff --git a/src/clmcagent/config_collector.py b/src/clmcagent/config_collector.py deleted file mode 100644 index 6ebed75071e2d9f804d1f9e4c3b5136c0926a3b8..0000000000000000000000000000000000000000 --- a/src/clmcagent/config_collector.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/python3 -import threading -import time -import random -import logging -from influxdb import InfluxDBClient - -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger() - -class ConfigCollector(threading.Thread): - STATE_NAME = 0 - STATE_TIME = 1 - - def __init__(self, sample_func, write_func, resource_name, sample_rate=2, agg_period=10): - threading.Thread.__init__(self) - self._start_event = threading.Event() - self.sample_func = sample_func - self.write_func = write_func - self.resource_name = resource_name - self.sample_rate = sample_rate - self.agg_period = agg_period - self.agg_states = {} - self.current_measurement = {} - return - - def run(self): - # if thread running then return - if(self._start_event.is_set()): - return - self._start_event.set() - - # set start period to current time - start_period = time.time() - logger.debug("start time = {0}".format(start_period)) - # set end period to the aggregation period - end_period = start_period + self.agg_period - logger.debug("end time = {0}".format(end_period)) - # initialise the time in the current state - current_state_time = 0 - samples = [] - while(self._start_event.is_set()): - # get sample using sampler function - (sample_state, sample_time) = self.sample_func() - # add sample to list of samples - samples.append((sample_state, sample_time)) - logger.debug("Sample state {0}".format(sample_state)) - logger.debug("Sample count: {0}".format(len(samples))) - # if last sample was at the end of the aggregation period then process - if sample_time >= end_period: - # aggregate samples into single measurement - self.current_measurement = self.create_measurement(samples, current_state_time, sample_time) - # write output - write_thread = WriteThread(self.write_func, self.current_measurement) - write_thread.start() - # set time in current state - current_state_time = self.current_measurement[0]['fields']['current_state_time'] - # remove all processed samples - samples.clear() - # add last sample as 1st sample of the next period - samples.append((sample_state, sample_time)) - # set new end period - end_period = sample_time + self.agg_period - logger.debug("Number of samples after agg: {0}".format(len(samples))) - logger.debug("Next end time {0}".format(end_period)) - - # calc how long it took to process samples - processing_time = time.time() - sample_time - logger.debug("Processing time {0}".format(processing_time)) - # calc the remaining time to wait until next sample - sleep_time = self.sample_rate - processing_time - logger.debug("Sleep time {0}".format(sleep_time)) - # if processing took longer than the sample rate we have a problemm - # and we will need to put processing into a worker thread - if(sleep_time < 0): - logger.warn("Aggregation processing took longer that sample rate") - sleep_time = 0 - logger.debug("Sleeping for sample {0}".format(sleep_time)) - # wait for the next sample - time.sleep(sleep_time) - logger.debug("Finished collection thread") - return - - def stop(self): - logger.debug("Stopping thread") - self._start_event.clear() - - def create_measurement(self, samples, initial_state_time, current_time): - logger.debug("Samples: {0}".format(str(samples))) - - # aggregate samples into states - states = self.aggregate_samples(samples) - logger.debug("States: {0}".format(str(states))) - - # aggregate the states into a measurement - fields = self.aggregate_states(states, initial_state_time) - measurement_time = int(current_time*1000000000) - measurement = [{"measurement": "service_config_state", - "tags": { - "resource_name": self.resource_name - }, - "time": measurement_time - }] - measurement[0]['fields'] = fields['fields'] - logger.debug("Report: {0}".format(str(measurement))) - - return measurement - - def aggregate_samples(self, samples): - states = [] - - sample_count = len(samples) - logger.debug("Sample count {0}".format(sample_count)) - # error if no samples to aggregate - if sample_count == 0: - raise ValueError('No samples in the samples list') - - # no aggregation needed if only one sample - if sample_count == 1: - return samples[0] - - # aggregate samples - last_index = sample_count-1 - for index, sample in enumerate(samples): - # for the 1st sample we set the current state and state_start_time - if index == 0: - current_state = sample[self.STATE_NAME] - state_start_time = sample[self.STATE_TIME] - logger.debug("Start time : {0}".format(state_start_time)) - else: - # add state duration for previous state after transition - if current_state != sample[self.STATE_NAME]: - # calc time in current state - state_time = sample[self.STATE_TIME] - state_start_time - states.append([current_state,state_time]) - # set the state to the next state - current_state = sample[self.STATE_NAME] - # set the start time of the next state - state_start_time = state_start_time + state_time - # deal with the final sample - if index == last_index: - # calc state duration if last sample is the same as previous state - if current_state == sample[self.STATE_NAME]: - state_time = sample[self.STATE_TIME] - state_start_time - states.append([current_state,state_time]) - # add transition in final sample with zero duration - elif current_state != sample[self.STATE_NAME]: - states.append([current_state,0]) - return states - - def aggregate_states(self, states, initial_state_time): - # set initial state to the 1st sample - initial_state = states[0][self.STATE_NAME] - logger.debug("Initial state : {0}".format(initial_state)) - logger.debug("Initial state time : {0}".format(initial_state_time)) - # set the current state as the last state sampled - current_state = states[-1][self.STATE_NAME] - # if no change in state take the initial state time and add current state time - if initial_state == current_state and len(states) == 1: - current_state_time = initial_state_time + states[-1][self.STATE_TIME] - state_sum_key = current_state + "_sum" - state_count_key = current_state + "_count" - # initialise the number of transitions if it's the 1st time - if state_sum_key not in self.agg_states: - self.agg_states[state_count_key] = 1 - self.agg_states[state_sum_key] = current_state_time - else: - # current state time is the last state time - current_state_time = states[-1][self.STATE_TIME] - # calc the total duration and number of transitions in each state. - for state in states: - # if first occurance of state add with initial duration and a single transition - state_sum_key = state[self.STATE_NAME] + "_sum" - state_count_key = state[self.STATE_NAME] + "_count" - if state_sum_key not in self.agg_states: - logger.debug("Adding state: {0}".format(state[self.STATE_NAME])) - self.agg_states[state_sum_key] = state[self.STATE_TIME] - self.agg_states[state_count_key] = 1 - else: - logger.debug("Aggregating state: {0}".format(state[self.STATE_NAME])) - # increment number of times in the state - self.agg_states[state_count_key] += 1 - logger.debug("increment number of times in the state") - # add state time to aggregate total - self.agg_states[state_sum_key] += state[self.STATE_TIME] - logger.debug("Duration: {0}".format(self.agg_states[state_sum_key])) - - # Create report - measurement = {} - measurement['fields'] = self.agg_states - measurement['fields']['current_state'] = current_state - measurement['fields']['current_state_time'] = current_state_time - return measurement - -class WriteThread(threading.Thread): - def __init__(self, write_func, measurement): - threading.Thread.__init__(self) - self._start_event = threading.Event() - self.write_func = write_func - self.measurement = measurement - return - - def run(self): - # if thread running then return - if(self._start_event.is_set()): - return - self._start_event.set() - self.write_func(self.measurement) - - def stop(self): - self._start_event.clear() - -class InfluxWriter(): - def __init__(self, hostname, port, database): - self.db_client = InfluxDBClient(host=hostname, port=port, database=database, timeout=10) - return - - def write(self, measurement): - # if thread running then return - try: - points = [] - points.append(measurement) - self.db_client.write_points(points) - except Exception as e: - print(e) diff --git a/src/clmcagent/stop_systemctl_monitor.sh b/src/clmcagent/stop_systemctl_monitor.sh deleted file mode 100644 index 5cd1d4df8515b992af631f14f4fe04d0e703e166..0000000000000000000000000000000000000000 --- a/src/clmcagent/stop_systemctl_monitor.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -pid=`ps aux | egrep "[s]ystemctl_monitor.py" | awk '{ print $2 }'` && kill $pid \ No newline at end of file diff --git a/src/clmcagent/systemctl_monitor.py b/src/clmcagent/systemctl_monitor.py deleted file mode 100644 index db2cefcdce4d4ceee828bac26646aaf757b2d525..0000000000000000000000000000000000000000 --- a/src/clmcagent/systemctl_monitor.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/python3 -import argparse -import subprocess -import logging -import time -import urllib.parse -from config_collector import ConfigCollector, InfluxWriter -from influxdb import InfluxDBClient - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger() - -class SystemctlMonitor: - ACTIVE_STATE_KEY='ActiveState' - SUBSTATE_KEY='SubState' - LOAD_STATE_KEY='LoadState' - - def __init__(self, service_name, sample_rate, agg_period, hostname, port, database): - self.service_name = service_name - self.writer = InfluxWriter(hostname, port, database) - self.collection_thread = ConfigCollector(self.get_systemctl_sample, self.writer.write_func, self.service_name, sample_rate, agg_period) - - def start(self): - self.collection_thread.start() - - def stop(self): - self.collection_thread.stop() - - def get_current_measurement(self): - return self.collection_thread.current_measurement - - def get_systemctl_sample(self): - return (self.get_systemctl_status(self.service_name), time.time()) - - def get_systemctl_status(self, service_name): - load_state = 'unknown' - active_state = 'unknown' - sub_state = 'unknown' - - cmd = "systemctl show {0}".format(service_name) - proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, shell=True) - out, err = proc.communicate() - if out: out = out.decode('ascii') - if err: err = err.decode('ascii') - logger.debug("Return code = {0}".format(proc.returncode)) - if proc.returncode != 0: - logger.error("Could not get status for service {0}, {1}".format(service_name, err)) - raise Exception("Could not get status for service {0}, {1}".format(service_name, err)) - - for line in iter(out.splitlines()): - parts = line.split('=') - if parts[0] == SystemctlMonitor.LOAD_STATE_KEY: - load_state = parts[1] - elif parts[0] == SystemctlMonitor.ACTIVE_STATE_KEY: - active_state = parts[1] - elif parts[0] == SystemctlMonitor.SUBSTATE_KEY: - sub_state = parts[1] - return load_state + "." + active_state + "." + sub_state - - -def main(): - - parser = argparse.ArgumentParser(description='systemctrl state monitor') - parser.add_argument('-service', help='service name', required=True) - parser.add_argument('-rate', help='sample rate', required=True) - parser.add_argument('-agg', help='aggregation period', required=True) - parser.add_argument('-host', help='telegraf hostname', required=True) - parser.add_argument('-port', help='telegraf port', required=True) - parser.add_argument('-db', help='database name', required=True) - parser.add_argument('-debug', '--debug', action='store_true') - args = parser.parse_args() - print("Starting SystemctlMonitor : {0}, {1}, {2}, {3}, {4}, {5}".format(args.service, args.rate, args.agg, args.host, args.port, args.db)) - - if args.debug == True: - print("Setting logging level to to DEBUG") - logger.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - - mon = SystemctlMonitor(args.service, int(args.rate), int(args.agg), args.host, int(args.port), args.db) - mon.start() - -if __name__== "__main__": - main() - - diff --git a/src/service/.coveragerc b/src/service/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..a3edc11a103c310fff7b5b2dc3ef5937f1dcfa0d --- /dev/null +++ b/src/service/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = clmcservice +omit = clmcservice/tests.py diff --git a/src/service/MANIFEST.in b/src/service/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..eaf16db0e28d54b95ae8f085c1d4c3cfa1b8278d --- /dev/null +++ b/src/service/MANIFEST.in @@ -0,0 +1,2 @@ +include MANIFEST.in +recursive-include clmcservice \ No newline at end of file diff --git a/src/service/clmcservice/__init__.py b/src/service/clmcservice/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bd560622e940ec78c2e6b7f5daaa340c325170e1 --- /dev/null +++ b/src/service/clmcservice/__init__.py @@ -0,0 +1,58 @@ +""" +// © 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 : 15-05-2018 +// Created for Project : FLAME +""" + +from pyramid.config import Configurator +from pyramid.settings import asbool + +from clmcservice.utilities import RUNNING_FLAG, MALFORMED_FLAG + + +def main(global_config, **settings): + """ + This function returns a Pyramid WSGI application. + """ + + # a conversion is necessary so that the configuration values of the aggregator are stored with the right type instead of strings + aggregator_running = asbool(settings.get(RUNNING_FLAG, False)) + settings[RUNNING_FLAG] = asbool(aggregator_running) + + aggregator_report_period = int(settings.get('aggregator_report_period', 5)) + settings['aggregator_report_period'] = aggregator_report_period + + settings[MALFORMED_FLAG] = False + + config = Configurator(settings=settings) + + config.add_route('aggregator_config', '/aggregator/config') + config.add_view('clmcservice.views.AggregatorConfig', attr='get', request_method='GET') + config.add_view('clmcservice.views.AggregatorConfig', attr='put', request_method='PUT') + + config.add_route('aggregator_controller', '/aggregator/control') + config.add_view('clmcservice.views.AggregatorController', attr='get', request_method='GET') + config.add_view('clmcservice.views.AggregatorController', attr='put', request_method='PUT') + + config.add_route('round_trip_time_query', '/query/round-trip-time') + config.add_view('clmcservice.views.RoundTripTimeQuery', attr='get', request_method='GET') + + config.scan() + return config.make_wsgi_app() diff --git a/src/service/clmcservice/aggregator.py b/src/service/clmcservice/aggregator.py new file mode 100644 index 0000000000000000000000000000000000000000..ab855d991610fccccd43b9cb9da74ebcca5ce586 --- /dev/null +++ b/src/service/clmcservice/aggregator.py @@ -0,0 +1,225 @@ +#!/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 threading import Event +from influxdb import InfluxDBClient +from time import time, sleep +from urllib.parse import urlparse +from clmcservice.utilities import generate_e2e_delay_report +import getopt +import sys + + +class Aggregator(object): + """ + A class used to perform the aggregation feature of the CLMC - aggregating network and media service measurements. Implemented as a separate process. + """ + + REPORT_PERIOD = 5 # default report period is 5s, that is every 5 seconds the mean delay values for the last 5 seconds are aggregated + DATABASE = 'CLMCMetrics' # default database the aggregator uses + DATABASE_URL = 'http://203.0.113.100:8086' # default database URL the aggregator uses + RETRY_PERIOD = 5 # number of seconds to wait before retrying connection/posting data to Influx + + def __init__(self, database_name=DATABASE, database_url=DATABASE_URL, report_period=REPORT_PERIOD): + """ + Constructs an Aggregator instance. + + :param database_name: database name to use + :param database_url: database url to use + :param report_period: the report period in seconds + """ + + # initialise a database client using the database url and the database name + print("Creating InfluxDB Connection") + url_object = urlparse(database_url) + while True: + try: + self.db_client = InfluxDBClient(host=url_object.hostname, port=url_object.port, database=database_name, timeout=10) + break + except: + sleep(self.RETRY_PERIOD) + + self.db_url = database_url + self.db_name = database_name + self.report_period = report_period + + # a cache-like dictionaries to store the last reported values, which can be used to fill in missing values + self.network_cache = {} + self.service_cache = {} + + # a stop flag event object used to handle the stopping of the process + self._stop_flag = Event() + + def stop(self): + """ + Stop the aggregator from running. + """ + + 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. + """ + + print("Running aggregator") + + current_time = int(time()) + while not self._stop_flag.is_set(): + + boundary_time = current_time - self.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 = {} + + while True: + + try: + print("Query for network delays") + result = self.db_client.query( + 'SELECT mean(latency) as "net_latency", mean(bandwidth) as "net_bandwidth" FROM "{0}"."autogen"."network_delays" WHERE time >= {1} and time < {2} GROUP BY path, source, target'.format(self.db_name, + boundary_time_nano, current_time_nano)) + break + except Exception as e: + print("Exception getting network delay") + print(e) + sleep(self.RETRY_PERIOD) + + for item in result.items(): + metadata, result_points = item + # measurement = metadata[0] + tags = metadata[1] + + result = next(result_points) + network_delays[(tags['path'], tags['source'], tags['target'])] = result['net_latency'], result['net_bandwidth'] + self.network_cache[(tags['path'], tags['source'], tags['target'])] = result['net_latency'], result['net_bandwidth'] + + # query the service delays and group them by endpoint, service function instance and sfr + service_delays = {} + + while True: + try: + print("Query for service delays") + result = self.db_client.query( + 'SELECT mean(response_time) as "response_time", mean(request_size) as "request_size", mean(response_size) as "response_size" FROM "{0}"."autogen"."service_delays" WHERE time >= {1} and time < {2} GROUP BY endpoint, sf_instance, sfr'.format(self.db_name, + boundary_time_nano, current_time_nano)) + break + except Exception as e: + print("Exception getting service delay") + print(e) + sleep(self.RETRY_PERIOD) + + for item in result.items(): + metadata, result_points = item + # measurement = metadata[0] + tags = metadata[1] + result = next(result_points) + service_delays[tags['sfr']] = (result['response_time'], result['request_size'], result['response_size'], tags['endpoint'], tags['sf_instance']) + self.service_cache[tags['sfr']] = (result['response_time'], result['request_size'], result['response_size'], 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 and target not in self.service_cache: + # 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, "avg_request_size": None, "avg_response_size": None, "avg_bandwidth": None, "time": boundary_time} + + e2e_arguments['path_ID'] = path_id + e2e_arguments['source_SFR'] = source + e2e_arguments['target_SFR'] = target + e2e_arguments['delay_forward'] = network_delays[path][0] + e2e_arguments['avg_bandwidth'] = network_delays[path][1] + + # reverse the path ID to get the network delay for the reversed path + reversed_path = (path_id, target, source) + if reversed_path in network_delays or reversed_path in self.network_cache: + # get the reverse delay, use the latest value if reported or the cache value + e2e_arguments['delay_reverse'] = network_delays.get(reversed_path, self.network_cache.get(reversed_path))[0] + else: + e2e_arguments['delay_reverse'] = None + + # get the response time of the media component connected to the target SFR + service_delay = service_delays.get(target, self.service_cache.get(target)) + response_time, request_size, response_size, endpoint, sf_instance = service_delay + # put these points in the e2e arguments dictionary + e2e_arguments['delay_service'] = response_time + e2e_arguments['avg_request_size'] = request_size + e2e_arguments['avg_response_size'] = response_size + 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.values(): + + while True: + try: + self.db_client.write_points( + 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["avg_request_size"], e2e_arguments['avg_response_size'], e2e_arguments['avg_bandwidth'], + e2e_arguments['time'])) + break + except: + sleep(self.RETRY_PERIOD) + + 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__': + + # Parse command line options + try: + opts, args = getopt.getopt(sys.argv[1:], "p:d:u:", ['period=', 'database=', 'url=']) + + arg_period = Aggregator.REPORT_PERIOD + arg_database_name = Aggregator.DATABASE + arg_database_url = Aggregator.DATABASE_URL + + # Apply parameters if given + for opt, arg in opts: + if opt in ('-p', '--period'): + arg_period = int(arg) + elif opt in ('-d', '--database'): + arg_database_name = arg + elif opt in ('-u', '--url'): + arg_database_url = arg + + Aggregator(database_name=arg_database_name, database_url=arg_database_url, report_period=arg_period).run() + + # print the error messages in case of a parse error + except getopt.GetoptError as err: + print(err) + print('Parse error; run the script using the following format: python aggregator.py -p <seconds> -d <database name> -u <database url>') diff --git a/src/service/clmcservice/tests.py b/src/service/clmcservice/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..581608e0758f2b5c009ee1528b6486e6e41e8c63 --- /dev/null +++ b/src/service/clmcservice/tests.py @@ -0,0 +1,419 @@ +""" +// © 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 : 15-05-2018 +// Created for Project : FLAME +""" + +import pytest +from pyramid import testing +from pyramid.httpexceptions import HTTPBadRequest +from time import sleep +from clmcservice.utilities import CONFIG_ATTRIBUTES, PROCESS_ATTRIBUTE, RUNNING_FLAG, MALFORMED_FLAG, URL_REGEX +import os +import signal + + +class TestAggregatorAPI(object): + """ + A pytest-implementation test for the aggregator 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, 'malformed': False, 'aggregator_report_period': 5, + 'aggregator_database_name': 'CLMCMetrics', 'aggregator_database_url': "http://172.40.231.51:8086"}) + + yield + + testing.tearDown() + + def test_GET_config(self): + """ + Tests the GET method for the configuration of the aggregator. + """ + + from clmcservice.views import AggregatorConfig # nested import so that importing the class view is part of the test itself + + assert self.config.get_settings().get('aggregator_report_period') == 5, "Initial report period is 5 seconds." + assert self.config.get_settings().get('aggregator_database_name') == 'CLMCMetrics', "Initial database name the aggregator uses is CLMCMetrics." + assert self.config.get_settings().get('aggregator_database_url') == "http://172.40.231.51:8086", "Initial aggregator url is http://172.40.231.51:8086" + + request = testing.DummyRequest() + response = AggregatorConfig(request).get() + + assert response == {'aggregator_report_period': 5, + 'aggregator_database_name': 'CLMCMetrics', + 'aggregator_database_url': "http://172.40.231.51:8086"}, "Response must be a dictionary representing a JSON object with the correct configuration data of the aggregator." + + assert self.config.get_settings().get('aggregator_report_period') == 5, "A GET request must not modify the aggregator configuration data." + assert self.config.get_settings().get('aggregator_database_name') == 'CLMCMetrics', "A GET request must not modify the aggregator configuration data." + assert self.config.get_settings().get('aggregator_database_url') == "http://172.40.231.51:8086", "A GET request must not modify the aggregator configuration data." + + @pytest.mark.parametrize("input_body, output_value", [ + ('{"aggregator_report_period": 10, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://171.40.231.51:8086"}', + {'aggregator_report_period': 10, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://171.40.231.51:8086"}), + ('{"aggregator_report_period": 15, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://172.50.231.51:8086"}', + {'aggregator_report_period': 15, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://172.50.231.51:8086"}), + ('{"aggregator_report_period": 20, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://172.60.231.51:8086"}', + {'aggregator_report_period': 20, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://172.60.231.51:8086"}), + ('{"aggregator_report_period": 25, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://172.60.231.51:8086"}', + {'aggregator_report_period': 25, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://172.60.231.51:8086"}), + ('{"aggregator_report_period": 200, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "https://172.50.231.51:8086"}', + {'aggregator_report_period': 200, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "https://172.50.231.51:8086"}), + ('{"aggregator_report_period": 150, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "https://localhost:8086"}', + {'aggregator_report_period': 150, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "https://localhost:8086"}), + ("{aggregator_report_period: 2hb5, aggregator_database_name: CLMCMetrics, aggregator_database_url: http://172.60.231.51:8086}", None), + ("{aggregator_report_period: 250-, aggregator_database_name: CLMCMetrics, aggregator_database_url: http://172.60.231.52:8086}", None), + ("{aggregator_report_period: 25, aggregator_database_name: CLMCMetrics, aggregator_database_url: ftp://172.60.231.51:8086}", None), + ("{aggregator_report_period: 25, aggregator_database_name: CLMCMetrics, aggregator_database_url: http://172.60.231.51:8086/query param}", None), + ("{aggregator_report_period: 250, aggregator_database_name: CLMCMetrics, aggregator_database_url: http://172.60.231.52:808686}", None), + ("{}", None), + ("{aggregator_running: true}", None), + ]) + def test_PUT_config(self, input_body, output_value): + """ + Tests the PUT method for the configuration of the aggregator + :param input_body: the input body parameter + :param output_value: 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(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get('aggregator_report_period') == 5, "Initial report period is 5 seconds." + assert self.config.get_settings().get('aggregator_database_name') == 'CLMCMetrics', "Initial database name the aggregator uses is CLMCMetrics." + assert self.config.get_settings().get('aggregator_database_url') == "http://172.40.231.51:8086", "Initial aggregator url is http://172.40.231.51:8086" + + request = testing.DummyRequest() + request.body = input_body.encode(request.charset) + + if output_value is not None: + response = AggregatorConfig(request).put() + assert response == output_value, "Response of PUT request must include the new configuration of the aggregator" + + for attribute in CONFIG_ATTRIBUTES: + assert self.config.get_settings().get(attribute) == output_value.get(attribute), "Aggregator settings configuration is not updated." + + assert not self.config.get_settings().get(RUNNING_FLAG), "Aggregator running status should not be updated after a configuration update." + else: + error_raised = False + try: + AggregatorConfig(request).put() + except HTTPBadRequest: + error_raised = True + + assert error_raised, "Error must be raised in case of an invalid argument." + + def test_start(self): + """ + Tests starting the aggregator through an API call. + """ + + from clmcservice.views import AggregatorController # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + + request = testing.DummyRequest() + input_body = '{"action": "start"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: True}, "The aggregator should have been started." + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been started." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is not None, "Aggregator process should have been initialized." + + # kill the started process after the test is over + pid = request.registry.settings[PROCESS_ATTRIBUTE].pid + os.kill(pid, signal.SIGTERM) + + def test_stop(self): + """ + Tests stopping the aggregator through an API call. + """ + + from clmcservice.views import AggregatorController # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + + # send a start request to trigger the aggregator + request = testing.DummyRequest() + input_body = '{"action": "start"}' + request.body = input_body.encode(request.charset) + AggregatorController(request).put() + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is not None, "Aggregator process should have been initialized." + + # test stopping the aggregator process when it is running + request = testing.DummyRequest() + input_body = '{"action": "stop"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: False}, "The aggregator should have been stopped." + assert not self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been stopped." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Aggregator process should have been terminated." + + sleep(2) # put a 2 seconds timeout so that the aggregator process can terminate + + # test stopping the aggregator process when it is not running + request = testing.DummyRequest() + input_body = '{"action": "stop"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: False}, "The aggregator should have been stopped." + assert not self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been stopped." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Aggregator process should have been terminated." + + def test_restart(self): + """ + Tests restarting the aggregator through an API call. + """ + + from clmcservice.views import AggregatorController # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + + # test restarting the aggregator process when it is stopped + request = testing.DummyRequest() + input_body = '{"action": "restart"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: True}, "The aggregator should have been restarted." + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been restarted." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE), "The aggregator process should have been reinitialised." + + # test restarting the aggregator process when it is running + request = testing.DummyRequest() + input_body = '{"action": "restart"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: True}, "The aggregator should have been restarted." + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been restarted." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE), "The aggregator process should have been reinitialised." + + # kill the started process after the test is over + pid = request.registry.settings[PROCESS_ATTRIBUTE].pid + os.kill(pid, signal.SIGTERM) + + @pytest.mark.parametrize("input_body", [ + '{"action": "malformed"}', + '{"action": true}', + '{"action": false}', + '{"action": 1}', + '{invalid-json}', + '{"action": "start", "unneeded_argument": false}', + '{}' + ]) + def test_malformed_actions(self, input_body): + """ + Tests sending a malformed type of action to the aggregator through an API call. + """ + + from clmcservice.views import AggregatorController # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + + # test restarting the aggregator process when it is running + request = testing.DummyRequest() + input_body = input_body + request.body = input_body.encode(request.charset) + + error_raised = False + try: + AggregatorController(request).put() + except HTTPBadRequest: + error_raised = True + + assert error_raised + + def test_GET_status(self): + """ + Tests the GET method for the status of the aggregator. + """ + + from clmcservice.views import AggregatorController # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + + request = testing.DummyRequest() + response = AggregatorController(request).get() + + assert response == {'aggregator_running': False}, "Response must be a dictionary representing a JSON object with the correct status data of the aggregator." + + assert not self.config.get_settings().get(RUNNING_FLAG), "A GET request must not modify the aggregator status flag." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "A GET request must not start the aggregator process." + + # test status with malformed configuration + self.config.get_settings()[MALFORMED_FLAG] = True + self.config.get_settings()[RUNNING_FLAG] = True + request = testing.DummyRequest() + response = AggregatorController(request).get() + + assert response == {'aggregator_running': True, + 'malformed': True, + 'comment': 'Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used.'}, \ + "Response must be a dictionary representing a JSON object with the correct status data of the aggregator." + + assert self.config.get_settings().get(RUNNING_FLAG), "A GET request must not modify the aggregator status flag." + assert self.config.get_settings().get(MALFORMED_FLAG), "A GET request must not modify the aggregator malformed flag." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "A GET request must not start the aggregator process." + + def test_malformed_flag_behaviour(self): + """ + Tests the behaviour of the malformed configuration flag of the aggregator when doing a sequence of API calls. + """ + + from clmcservice.views import AggregatorController, AggregatorConfig # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get(RUNNING_FLAG), "Initially aggregator is not running." + assert not self.config.get_settings().get(MALFORMED_FLAG), "Initially aggregator is not in a malformed state" + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "Initially no aggregator process is running." + assert self.config.get_settings().get('aggregator_report_period') == 5, "Initial report period is 5 seconds." + assert self.config.get_settings().get('aggregator_database_name') == 'CLMCMetrics', "Initial database name the aggregator uses is CLMCMetrics." + assert self.config.get_settings().get('aggregator_database_url') == "http://172.40.231.51:8086", "Initial aggregator url is http://172.40.231.51:8086" + + # start the aggregator with the default configuration + request = testing.DummyRequest() + input_body = '{"action": "start"}' + request.body = input_body.encode(request.charset) + + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: True}, "The aggregator should have been started." + + # update the configuration of the aggregator while it is running + config_body = '{"aggregator_report_period": 15, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://172.50.231.51:8086"}' + output_body = {'aggregator_report_period': 15, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://172.50.231.51:8086", 'malformed': True, + 'comment': 'Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used.'} + request = testing.DummyRequest() + request.body = config_body.encode(request.charset) + response = AggregatorConfig(request).put() + assert response == output_body, "Response of PUT request must include the new configuration of the aggregator" + + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator shouldn't be stopped when the configuration is updated." + assert self.config.get_settings().get(MALFORMED_FLAG), "The malformed flag should be set when the configuration is updated while the process is running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is not None, "The aggregator shouldn't be stopped when the configuration is updated." + + # check that the malformed flag has been updated through a GET call + request = testing.DummyRequest() + response = AggregatorController(request).get() + assert response == {'aggregator_running': True, + 'malformed': True, + 'comment': 'Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used.'}, \ + "Response must be a dictionary representing a JSON object with the correct status data of the aggregator." + + # restart the aggregator with the new configuration + request = testing.DummyRequest() + input_body = '{"action": "restart"}' + request.body = input_body.encode(request.charset) + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: True}, "The aggregator should have been restarted." + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been restarted." + assert not self.config.get_settings().get(MALFORMED_FLAG), "The malformed flag should have been reset to False." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is not None, "The aggregator should have been restarted." + + # update the configuration again while the aggregator is running + config_body = '{"aggregator_report_period": 30, "aggregator_database_name": "CLMCMetrics", "aggregator_database_url": "http://172.50.231.51:8086"}' + output_body = {'aggregator_report_period': 30, 'aggregator_database_name': "CLMCMetrics", 'aggregator_database_url': "http://172.50.231.51:8086", 'malformed': True, + 'comment': 'Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used.'} + request = testing.DummyRequest() + request.body = config_body.encode(request.charset) + response = AggregatorConfig(request).put() + assert response == output_body, "Response of PUT request must include the new configuration of the aggregator" + + assert self.config.get_settings().get(RUNNING_FLAG), "The aggregator shouldn't be stopped when the configuration is updated." + assert self.config.get_settings().get(MALFORMED_FLAG), "The malformed flag should be set when the configuration is updated while the process is running." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is not None, "The aggregator shouldn't be stopped when the configuration is updated." + + # stop the aggregator - this should also reset the malformed status flag + # restart the aggregator with the new configuration + request = testing.DummyRequest() + input_body = '{"action": "stop"}' + request.body = input_body.encode(request.charset) + response = AggregatorController(request).put() + assert response == {RUNNING_FLAG: False}, "The aggregator should have been stopped." + assert not self.config.get_settings().get(RUNNING_FLAG), "The aggregator should have been stopped." + assert not self.config.get_settings().get(MALFORMED_FLAG), "The malformed flag should have been reset to False." + assert self.config.get_settings().get(PROCESS_ATTRIBUTE) is None, "The aggregator should have been stopped." + + +class TestRegexURL(object): + """ + A pytest-implementation test for the regular expression the service uses to validate the database URL + """ + + @pytest.mark.parametrize("valid_url", [ + "http://localhost:8080/", + "https://localhost:80/url/path", + "https://192.168.20.20/?query=param", + "http://custom.domain.com", + "http://domain.net:8888/", + "https://10.160.150.4:21", + "http://localhost:12345", + "http://domain.com:21/path", + "http://domain.com:32?path", + "http://domain.com:43#path" + ]) + def test_valid_urls(self, valid_url): + """ + Tests that the regular expression can detect valid URLs. + + :param valid_url: a string representing a valid URL + """ + + matched_object = URL_REGEX.match(valid_url) + + assert matched_object is not None, "The regular expression fails in validating a correct URL." + + assert matched_object.group() is not None, "The matched object should return the full-match string" + + @pytest.mark.parametrize("invalid_url", [ + "ftp://localhost:80/url/path", + "tcp://192.168.20.20/?query=param", + "http:/localhost:80/", + "https//localhost:8080/", + "https://domain:1234/url/path", + "http://domain.com:808080/", + "http://localhost:8-080/", + "http://localhost:port80/", + "http://domain.com:8080url/path", + "http://domain.com:8080/?url path", + ]) + def test_invalid_urls(self, invalid_url): + """ + Tests that the regular expression can detect invalid URLs. + + :param invalid_url: a string representing an invalid URL + """ + + matched_object = URL_REGEX.match(invalid_url) + + assert matched_object is None, "The regular expression fails in detecting an invalid URL." diff --git a/src/service/clmcservice/utilities.py b/src/service/clmcservice/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..44ccffed7ce4120b22c31b55a7b81cde33344f9b --- /dev/null +++ b/src/service/clmcservice/utilities.py @@ -0,0 +1,162 @@ +""" +// © 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 : 15-05-2018 +// Created for Project : FLAME +""" + +from json import loads +from re import compile, IGNORECASE + +CONFIG_ATTRIBUTES = ('aggregator_report_period', 'aggregator_database_name', 'aggregator_database_url') # all of the configuration attributes - to be used as dictionary keys + +RUNNING_FLAG = 'aggregator_running' # Attribute for storing the flag, which shows whether the aggregator is running or not - to be used as a dictionary key + +PROCESS_ATTRIBUTE = 'aggregator_process' # Attribute for storing the process object of the aggregator - to be used as a dictionary key + +# a 'malformed' running state of the aggregator is when the configuration is updated, but the aggregator is not restarted so it is running with an old version of the conf. +MALFORMED_FLAG = 'malformed' # Attribute for storing the flag, which shows whether the aggregator is running in an malformed state or not - to be used as a dictionary key + +# used to indicate a malformed configuration message +COMMENT_ATTRIBUTE = 'comment' +COMMENT_VALUE = 'Aggregator is running in a malformed state - it uses an old version of the configuration. Please, restart it so that the updated configuration is used.' + +# the attributes of the JSON response body that are expected when querying round trip time +ROUND_TRIP_ATTRIBUTES = ('media_service', 'start_timestamp', 'end_timestamp') + + +URL_REGEX = compile( + r'^https?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain, e.g. example.domain.com + r'localhost|' # or localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # or IP address (IPv4 format) + r'(?::\d{2,5})?' # optional port number + r'(?:[/?#][^\s]*)?$', # URL path or query parameters + IGNORECASE) + + +def validate_config_content(configuration): + """ + A utility function to validate a configuration string representing a JSON dictionary. + + :param configuration: the configuration string to validate + :return the validated configuration dictionary object with the values converted to their required type + :raise AssertionError: if the argument is not a valid configuration + """ + + global CONFIG_ATTRIBUTES + + try: + configuration = loads(configuration) + except: + raise AssertionError("Configuration must be a JSON object.") + + assert len(configuration) == len(CONFIG_ATTRIBUTES), "Configuration mustn't contain more attributes than the required ones." + + for attribute in CONFIG_ATTRIBUTES: + assert attribute in configuration, "Required attribute not found in the request content." + + assert type(configuration.get('aggregator_report_period')) == int, "Report period must be an integer, received {0} instead.".format(configuration.get('aggregator_report_period')) + + assert configuration.get('aggregator_report_period') > 0, "Report period must be a positive integer, received {0} instead.".format(configuration.get('aggregator_report_period')) + + assert URL_REGEX.match(configuration.get('aggregator_database_url')) is not None, "The aggregator must have a valid database URL in its configuration, received {0} instead.".format(configuration.get('aggregator_database_url')) + + return configuration + + +def validate_action_content(content): + """ + A utility function to validate a content string representing a JSON dictionary. + + :param content: the content string to validate + :return: the validated content dictionary + :raise AssertionError: if the argument is not a valid json content + """ + + try: + content = loads(content) + except: + raise AssertionError("Content must be a JSON object.") + + assert len(content) == 1, "Content mustn't contain more attributes than the required one." + + assert content['action'] in ('start', 'stop', 'restart') + + return content + + +def validate_round_trip_query_params(params): + """ + A utility function to validate a dictionary of parameters. + + :param params: the params dict to validate + :return: the validated parameters dictionary + :raise AssertionError: if the argument is not a valid json content + """ + + global ROUND_TRIP_ATTRIBUTES + + assert len(params) == len(ROUND_TRIP_ATTRIBUTES), "Content mustn't contain more attributes than the required ones." + + for attribute in ROUND_TRIP_ATTRIBUTES: + assert attribute in params, "Required attribute not found in the request content." + + return params + + +def generate_e2e_delay_report(path_id, source_sfr, target_sfr, endpoint, sf_instance, delay_forward, delay_reverse, delay_service, avg_request_size, avg_response_size, avg_bandwidth, 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 avg_request_size: averaged request size + :param avg_response_size: averaged response size + :param avg_bandwidth: averaged bandwidth + :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), + "avg_request_size": float(avg_request_size), + "avg_response_size": float(avg_response_size), + "avg_bandwidth": float(avg_bandwidth) + }, + "time": int(1000000000*time) + }] + + return result diff --git a/src/service/clmcservice/views.py b/src/service/clmcservice/views.py new file mode 100644 index 0000000000000000000000000000000000000000..049be9daacb187ea3ecfb837f9c86a75a3660804 --- /dev/null +++ b/src/service/clmcservice/views.py @@ -0,0 +1,279 @@ +""" +// © 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 : 15-05-2018 +// Created for Project : FLAME +""" + +from pyramid.view import view_defaults +from pyramid.httpexceptions import HTTPBadRequest, HTTPInternalServerError +from influxdb import InfluxDBClient +from urllib.parse import urlparse +from subprocess import Popen, DEVNULL +from clmcservice.utilities import validate_config_content, validate_action_content, validate_round_trip_query_params, \ + CONFIG_ATTRIBUTES, ROUND_TRIP_ATTRIBUTES, RUNNING_FLAG, PROCESS_ATTRIBUTE, MALFORMED_FLAG, COMMENT_ATTRIBUTE, COMMENT_VALUE +import os.path + + +@view_defaults(route_name='aggregator_config', renderer='json') +class AggregatorConfig(object): + """ + A class-based view for accessing and mutating the configuration 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 configuration of the aggregator. + + :return: A JSON response with the configuration of the aggregator. + """ + + aggregator_data = self.request.registry.settings + config = {key: aggregator_data.get(key) for key in CONFIG_ATTRIBUTES} + + return config + + def put(self): + """ + A PUT API call for the status of the aggregator. + + :return: A JSON response to the PUT call - essentially with the new configured data and comment of the state of the aggregator + :raises HTTPBadRequest: if request body is not a valid JSON for the configurator + """ + + old_config = {attribute: self.request.registry.settings.get(attribute) for attribute in CONFIG_ATTRIBUTES} + new_config = self.request.body.decode(self.request.charset) + + try: + new_config = validate_config_content(new_config) + + for attribute in CONFIG_ATTRIBUTES: + self.request.registry.settings[attribute] = new_config.get(attribute) + + # if configuration is not already malformed, check whether the configuration is updated + if not self.request.registry.settings[MALFORMED_FLAG]: + malformed = old_config != new_config and self.request.registry.settings[RUNNING_FLAG] + self.request.registry.settings[MALFORMED_FLAG] = malformed + if malformed: + new_config[MALFORMED_FLAG] = True + new_config[COMMENT_ATTRIBUTE] = COMMENT_VALUE + + return new_config + + except AssertionError: + raise HTTPBadRequest("Bad request content - configuration format is incorrect.") + + +@view_defaults(route_name='aggregator_controller', renderer='json') +class AggregatorController(object): + + """ + A class-based view for controlling 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 - running or not. + + :return: A JSON response with the status of the aggregator. + """ + + aggregator_data = self.request.registry.settings + config = {RUNNING_FLAG: aggregator_data.get(RUNNING_FLAG)} + + if aggregator_data[MALFORMED_FLAG] and aggregator_data[RUNNING_FLAG]: + config[MALFORMED_FLAG] = True + config[COMMENT_ATTRIBUTE] = COMMENT_VALUE + + return config + + def put(self): + """ + A PUT API call for the status of the aggregator. + + :return: A JSON response to the PUT call - essentially saying whether the aggregator is running or not + :raises HTTPBadRequest: if request body is not a valid JSON for the controller + """ + + content = self.request.body.decode(self.request.charset) + + try: + content = validate_action_content(content) + + config = {attribute: self.request.registry.settings.get(attribute) for attribute in CONFIG_ATTRIBUTES} + + action = content['action'] + + if action == 'start': + aggregator_started = self.request.registry.settings[RUNNING_FLAG] + if not aggregator_started: + process = self.start_aggregator(config) + self.request.registry.settings[RUNNING_FLAG] = True + self.request.registry.settings[PROCESS_ATTRIBUTE] = process + elif action == 'stop': + self.stop_aggregator(self.request.registry.settings.get(PROCESS_ATTRIBUTE)) + self.request.registry.settings[RUNNING_FLAG] = False + self.request.registry.settings[PROCESS_ATTRIBUTE] = None + self.request.registry.settings[MALFORMED_FLAG] = False + elif action == 'restart': + self.stop_aggregator(self.request.registry.settings.get(PROCESS_ATTRIBUTE)) + process = self.start_aggregator(config) + self.request.registry.settings[RUNNING_FLAG] = True + self.request.registry.settings[PROCESS_ATTRIBUTE] = process + self.request.registry.settings[MALFORMED_FLAG] = False + + return {RUNNING_FLAG: self.request.registry.settings.get(RUNNING_FLAG)} + + except AssertionError: + raise HTTPBadRequest('Bad request content - must be in JSON format: {"action": value}, where value is "start", "stop" or "restart".') + + @staticmethod + def start_aggregator(config): + """ + An auxiliary method to start the aggregator. + + :param config: the configuration containing the arguments for the aggregator + :return: the process object of the started aggregator script + """ + + dir_path = os.path.dirname(os.path.realpath(__file__)) + command = ['python', 'aggregator.py', '--period', str(config.get('aggregator_report_period')), '--database', + config.get('aggregator_database_name'), '--url', config.get('aggregator_database_url')] + process = Popen(command, cwd=dir_path, stdout=DEVNULL, stderr=DEVNULL, stdin=DEVNULL) + print("\nStarted aggregator process with PID: {0}\n".format(process.pid)) + + return process + + @staticmethod + def stop_aggregator(process): + """ + An auxiliary method to stop the aggregator. + + :param process: the process to terminate + """ + + # check if the process is started before trying to terminate it - process.poll() only returns something if the process has terminated, hence we check for a None value + if process is not None and process.poll() is None: + process.terminate() + print("\nStopped aggregator process with PID: {0}\n".format(process.pid)) + + +@view_defaults(route_name='round_trip_time_query', renderer='json') +class RoundTripTimeQuery(object): + + """ + A class-based view for querying the round trip time in a given range. + """ + + 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 averaged round trip time of a specific media service over a given time range. + + :return: A JSON response with the round trip time and its contributing parts. + """ + + params = {} + for attribute in ROUND_TRIP_ATTRIBUTES: + if attribute in self.request.params: + params[attribute] = self.request.params.get(attribute) + + try: + params = validate_round_trip_query_params(params) + config_data = {config_attribute: self.request.registry.settings.get(config_attribute) for config_attribute in CONFIG_ATTRIBUTES} + + media_service = params.get(ROUND_TRIP_ATTRIBUTES[0]) + start_timestamp = params.get(ROUND_TRIP_ATTRIBUTES[1]) + end_timestamp = params.get(ROUND_TRIP_ATTRIBUTES[2]) + influx_db_name = config_data.get(CONFIG_ATTRIBUTES[1]) + influx_db_url = config_data.get(CONFIG_ATTRIBUTES[2]) + + url_object = urlparse(influx_db_url) + try: + db_client = InfluxDBClient(host=url_object.hostname, port=url_object.port, database=influx_db_name, timeout=10) + query = 'SELECT mean(*) FROM "{0}"."autogen"."e2e_delays" WHERE time >= {1} and time < {2} and sf_instance = \'{3}\''.format( + influx_db_name, start_timestamp, end_timestamp, media_service) + print(query) + result = db_client.query(query) + + actual_result = next(result.get_points(), None) + if actual_result is None: + return {"result": None} + else: + forward_latency = actual_result.get("mean_delay_forward") + reverse_latency = actual_result.get("mean_delay_reverse") + service_delay = actual_result.get("mean_delay_service") + request_size = actual_result.get("mean_avg_request_size") + response_size = actual_result.get("mean_avg_response_size") + bandwidth = actual_result.get("mean_avg_bandwidth") + + rtt = self.calculate_round_trip_time(forward_latency, reverse_latency, service_delay, request_size, response_size, bandwidth) + return {"result": rtt} + except: + raise HTTPInternalServerError("Cannot instantiate connection with database {0} on url {1}.".format(influx_db_name, influx_db_url)) + + except AssertionError: + raise HTTPBadRequest('Bad request content - must be in JSON format: {"media_service": value, "start_timestamp": value, "end_timestamp": value}.') + + @staticmethod + def calculate_round_trip_time(forward_latency, reverse_latency, service_delay, request_size, response_size, bandwidth, packet_size=1500, packet_header_size=50): + """ + Calculates the round trip time given the list of arguments. + + :param forward_latency: network latency in forward direction (s) + :param reverse_latency: network latency in reverse direction (s) + :param service_delay: media service delay (s) + :param request_size: request size (bytes) + :param response_size: response size (bytes) + :param bandwidth: network bandwidth (Mb/s) + :param packet_size: size of packet (bytes) + :param packet_header_size: size of the header of the packet (bytes) + :return: the calculated round trip time + """ + + forward_data_delay = (8/10**6) * (request_size / bandwidth) * (packet_size / (packet_size - packet_header_size)) + reverse_data_delay = (8/10**6) * (response_size / bandwidth) * (packet_size / (packet_size - packet_header_size)) + + return forward_latency + forward_data_delay + service_delay + reverse_latency + reverse_data_delay diff --git a/src/service/development.ini b/src/service/development.ini new file mode 100644 index 0000000000000000000000000000000000000000..269aea605df38ab006cac8a850e5556770cfef90 --- /dev/null +++ b/src/service/development.ini @@ -0,0 +1,62 @@ +### +# 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 +aggregator_report_period = 5 +aggregator_database_name = CLMCMetrics +aggregator_database_url = http://172.40.231.51:8086 + +# 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:9080 + +### +# 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/src/service/production.ini b/src/service/production.ini new file mode 100644 index 0000000000000000000000000000000000000000..62035f0a37ef3ef978a72ed0cb04e68e69de81bf --- /dev/null +++ b/src/service/production.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 = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +## Aggregator configuration +aggregator_running = false +aggregator_report_period = 5 +aggregator_database_name = CLMCMetrics +aggregator_database_url = http://172.40.231.51:8086 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:9080 + +### +# 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/src/service/pytest.ini b/src/service/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..2fb94a6c8131efcc6dcc9357a5b56f68af607de3 --- /dev/null +++ b/src/service/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = clmcservice +python_files = *.py diff --git a/src/service/setup.py b/src/service/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..0b195c5433419b000bf0ff63abf59f9d01e00123 --- /dev/null +++ b/src/service/setup.py @@ -0,0 +1,84 @@ +""" +// (c) 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 : 15-05-2018 +// Created for Project : FLAME +""" + + +import os +import os.path +from setuptools import setup, find_packages + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +def get_version(fname): + if os.path.isfile(fname): + git_revision = read(fname) + else: + git_revision = "SNAPSHOT" + + return git_revision + + +requires = [ + 'plaster_pastedeploy', + 'pyramid', + 'pyramid_debugtoolbar', + 'waitress', + 'influxdb', + 'pytest', +] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest-cov', +] + +setup( + name = "clmcservice", + version = get_version("_version.py"), + author = "Michael Boniface", + author_email = "mjb@it-innovation.soton.ac.uk", + description = "FLAME CLMC Service Module", + long_description="FLAME CLMC Service", + license = "https://gitlab.it-innovation.soton.ac.uk/FLAME/flame-clmc/blob/integration/LICENSE", + keywords = "FLAME CLMC service", + url = 'https://gitlab.it-innovation.soton.ac.uk/FLAME/flame-clmc', + packages=find_packages(), + include_package_data=True, + install_requires=requires, + extras_require={ + 'testing': tests_require, + }, + package_data={'': ['_version.py']}, + classifiers=[ + "Development Status :: Alpha", + "Topic :: FLAME CLMC Service", + "License :: ", + ], + entry_points={ + 'paste.app_factory': [ + 'main = clmcservice:main', + ], + }, +) \ No newline at end of file diff --git a/src/service/tox.ini b/src/service/tox.ini new file mode 100644 index 0000000000000000000000000000000000000000..49efb9f07566378a3a7d1af73bc6e73fbd819dad --- /dev/null +++ b/src/service/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py35 +[testenv] +deps=pytest +commands=pytest \ No newline at end of file diff --git a/MANIFEST.in b/src/test/MANIFEST.in similarity index 100% rename from MANIFEST.in rename to src/test/MANIFEST.in diff --git a/clmctest/__init__.py b/src/test/clmctest/__init__.py similarity index 100% rename from clmctest/__init__.py rename to src/test/clmctest/__init__.py diff --git a/src/test/clmctest/e2e_response_time/rspec.yml b/src/test/clmctest/e2e_response_time/rspec.yml new file mode 100644 index 0000000000000000000000000000000000000000..4fe3767ddb075e033c3b10acd81a885d73cdc18a --- /dev/null +++ b/src/test/clmctest/e2e_response_time/rspec.yml @@ -0,0 +1,56 @@ +## (c) 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 : 02-02-2018 +## Created for Project : FLAME + +hosts: + - name: clmc-service + cpus: 1 + memory: 2048 + disk: "10GB" + forward_ports: + - guest: 8086 + host: 8086 + - guest: 8888 + host: 8888 + - guest: 9092 + host: 9092 + ip_address: "172.40.231.51" + - name: minio + service_name: "minio" + cpus: 1 + memory: 2048 + disk: "10GB" + forward_ports: + - guest: 9000 + host: 9000 + ip_address: "172.40.231.155" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "adaptive_streaming" + sf_id_instance: "adaptive_streaming_I1" + ipendpoint_id: "adaptive_streaming_I1_minio" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: test-runner + cpus: 1 + memory: 2048 + disk: "10GB" + ip_address: "172.40.231.200" diff --git a/clmctest/inputs/__init__.py b/src/test/clmctest/inputs/__init__.py similarity index 100% rename from clmctest/inputs/__init__.py rename to src/test/clmctest/inputs/__init__.py diff --git a/clmctest/inputs/conftest.py b/src/test/clmctest/inputs/conftest.py similarity index 100% rename from clmctest/inputs/conftest.py rename to src/test/clmctest/inputs/conftest.py diff --git a/src/test/clmctest/inputs/rspec.yml b/src/test/clmctest/inputs/rspec.yml new file mode 100644 index 0000000000000000000000000000000000000000..5084b29ebdb308e7c116f9db5827f0c15ff289d0 --- /dev/null +++ b/src/test/clmctest/inputs/rspec.yml @@ -0,0 +1,147 @@ +## (c) 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 : 02-02-2018 +## Created for Project : FLAME + +hosts: + - name: clmc-service + cpus: 1 + memory: 2048 + disk: "10GB" + forward_ports: + - guest: 8086 + host: 8086 + - guest: 8888 + host: 8888 + - guest: 9092 + host: 9092 + ip_address: "172.40.231.51" + - name: apache + cpus: 1 + memory: 2048 + disk: "10GB" + service_name: "apache" + forward_ports: + - guest: 80 + host: 8881 + ip_address: "172.40.231.150" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "adaptive_streaming" + sf_id_instance: "adaptive_streaming_I1" + ipendpoint_id: "adaptive_streaming_I1_apache1" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: nginx + cpus: 1 + memory: 2048 + disk: "10GB" + service_name: "nginx" + forward_ports: + - guest: 80 + host: 8882 + ip_address: "172.40.231.151" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "adaptive_streaming" + sf_id_instance: "adaptive_streaming_nginx_I1" + ipendpoint_id: "adaptive_streaming_nginx_I1_apache1" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: mongo + cpus: 1 + memory: 2048 + disk: "10GB" + service_name: "mongo" + forward_ports: + - guest: 80 + host: 8883 + ip_address: "172.40.231.152" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "metadata_database" + sf_id_instance: "metadata_database_I1" + ipendpoint_id: "metadata_database_I1_apache1" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: ffmpeg + cpus: 1 + memory: 2048 + disk: "10GB" + service_name: "ffmpeg" + forward_ports: + - guest: 80 + host: 8884 + ip_address: "172.40.231.153" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "metadata_database" + sf_id_instance: "metadata_database_I1" + ipendpoint_id: "metadata_database_I1_apache1" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: host + cpus: 1 + memory: 2048 + disk: "10GB" + service_name: "host" + forward_ports: + - guest: 80 + host: 8885 + ip_address: "172.40.231.154" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "adaptive_streaming" + sf_id_instance: "adaptive_streaming_I1" + ipendpoint_id: "adaptive_streaming_I1_apache1" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: test-runner + cpus: 2 + memory: 4096 + disk: "10GB" + ip_address: "172.40.231.200" + - name: minio + service_name: "minio" + cpus: 1 + memory: 2048 + disk: "10GB" + forward_ports: + - guest: 9000 + host: 9000 + ip_address: "172.40.231.155" + location: "DC1" + sfc_id: "MS_Template_1" + sfc_id_instance: "MS_I1" + sf_id: "adaptive_streaming" + sf_id_instance: "adaptive_streaming_I1" + ipendpoint_id: "adaptive_streaming_I1_minio" + sr_id: "service_router" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" \ No newline at end of file diff --git a/clmctest/inputs/test_rspec.py b/src/test/clmctest/inputs/test_rspec.py similarity index 100% rename from clmctest/inputs/test_rspec.py rename to src/test/clmctest/inputs/test_rspec.py diff --git a/clmctest/inputs/test_telegraf_agents.py b/src/test/clmctest/inputs/test_telegraf_agents.py similarity index 100% rename from clmctest/inputs/test_telegraf_agents.py rename to src/test/clmctest/inputs/test_telegraf_agents.py diff --git a/src/test/clmctest/monitoring/E2ESim.py b/src/test/clmctest/monitoring/E2ESim.py new file mode 100644 index 0000000000000000000000000000000000000000..4a3fa2d6d4d9768fed96188d571bc34c22ac0aad --- /dev/null +++ b/src/test/clmctest/monitoring/E2ESim.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 +""" +## Copyright 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 = 'CLMCMetrics' # default database name + DATABASE_URL = 'http://172.40.231.51: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': 'SR2', + 'source': 'SR1', + 'path_id': 'SR1---SR2', + 'latency': 5, + 'bandwidth': 100*1024*1024 + }, + { + 'target': 'SR1', + 'source': 'SR2', + 'path_id': 'SR1---SR2', + 'latency': 5, + 'bandwidth': 100*1024*1024 + }, + { + 'target': 'SR3', + 'source': 'SR1', + 'path_id': 'SR1---SR3', + 'latency': 5, + 'bandwidth': 100*1024*1024 + }, + { + 'target': 'SR1', + 'source': 'SR3', + 'path_id': 'SR1---SR3', + 'latency': 5, + 'bandwidth': 100*1024*1024 + } + ] + + service_function_instances = [ + { + 'endpoint': 'ms1.flame.org', + 'sf_instance': 'sr2.ms1.flame.org', # TODO: what did we decide the sf_instance would look like? + 'sfr': 'SR2', + 'service_delay': 40, + 'cpus': 1 + }, + { + 'endpoint': 'ms1.flame.org', + 'sf_instance': 'sr3.ms1.flame.org', # TODO: what did we decide the sf_instance would look like? + 'sfr': 'SR3', + 'service_delay': 10, + 'cpus': 4 + } + ] + + av_request_size = 10 * 1024 * 1024 # average request size measured by service function / Bytes + av_response_size = 1 * 1024 # average request size measured by service function / Bytes + + # 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 + + sample_period_net = 1 # sample period for reporting network delays (measured in seconds) + sample_period_media = 5 # sample period for reporting media service delays (measured in seconds) + + for i in range(0, self.SIMULATION_LENGTH): + # report one of the network delays every sample_period_net seconds + if i % sample_period_net == 0: + path = random.choice(paths) + self.db_client.write_points( + lp.generate_network_delay_report(path['path_id'], path['source'], path['target'], path['latency'], path['bandwidth'], sim_time)) + + # increase/decrease the delay in every sample report (min delay is 1) + path['latency'] = max(1, path['latency'] + random.randint(-3, 3)) + + # report one of the service_function_instance response times every sample_period_media seconds + if i % sample_period_media == 0: + service = random.choice(service_function_instances) + self.db_client.write_points(lp.generate_service_delay_report( + service['endpoint'], service['sf_instance'], service['sfr'], service['service_delay'], av_request_size, av_response_size, sim_time)) + + # 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/src/test/clmctest/monitoring/E2ETestAggregatorThread.py b/src/test/clmctest/monitoring/E2ETestAggregatorThread.py new file mode 100644 index 0000000000000000000000000000000000000000..a6c00976b7650d991ddb10bc8e8583dfd36bd32f --- /dev/null +++ b/src/test/clmctest/monitoring/E2ETestAggregatorThread.py @@ -0,0 +1,68 @@ +#!/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 threading import Thread +from clmcservice.aggregator import Aggregator + + +class TestAggregator(Thread): + + 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 = 'CLMCMetrics' # 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, report_period=REPORT_PERIOD): + """ + Constructs an Aggregator instance. + + :param database: database name to use + :param database_url: database url to use + """ + + super(TestAggregator, self).__init__() # call the constructor of the thread + + self.aggregator = Aggregator(database_name=database, database_url=database_url, report_period=report_period) + + def stop(self): + """ + A method used to stop the thread. + """ + + self.aggregator.stop() + + def set_event_lock(self, event): + """ + Auxiliary method to set a thread-safe event lock object to the aggregator (used for testing). + + :param event: the event lock object + """ + + setattr(self, 'event', event) + + def run(self): + if hasattr(self, 'event'): + self.event.set() + + self.aggregator.run() diff --git a/clmctest/monitoring/LineProtocolGenerator.py b/src/test/clmctest/monitoring/LineProtocolGenerator.py similarity index 74% rename from clmctest/monitoring/LineProtocolGenerator.py rename to src/test/clmctest/monitoring/LineProtocolGenerator.py index 76ffd32d28acf86ab201a71a3d56dbc1cbaf8828..b9bf9a6249234131506e9fddca2fa77af8450ede 100644 --- a/clmctest/monitoring/LineProtocolGenerator.py +++ b/src/test/clmctest/monitoring/LineProtocolGenerator.py @@ -29,6 +29,107 @@ 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, avg_request_size, avg_response_size, avg_bandwidth, 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 avg_request_size: averaged request size + :param avg_response_size: averaged response size + :param avg_bandwidth: averaged bandwidth + :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), + "avg_request_size": float(avg_request_size), + "avg_response_size": float(avg_response_size), + "avg_bandwidth": float(avg_bandwidth) + }, + "time": _getNSTime(time) + }] + + return result + + +def generate_network_delay_report(path_id, source_sfr, target_sfr, latency, bandwidth, 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 latency: the e2e network delay for traversing the path between the two service routers + :param bandwidth: the bandwidth of the path (minimum of bandwidths of the links it is composed of) + :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": { + "latency": latency, + "bandwidth": bandwidth + }, + "time": _getNSTime(time) + }] + + return result + + +def generate_service_delay_report(endpoint, sf_instance, sfr, response_time, request_size, response_size, time): + """ + Generates a service measurement about the media service response time. + + :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 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 request_size: the size of the request received by the service in Bytes + :param response_size: the size of the response received by the service in Bytes + :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, + "request_size": request_size, + "response_size": response_size + }, + "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 +226,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/StreamingSim.py b/src/test/clmctest/monitoring/StreamingSim.py similarity index 100% rename from clmctest/monitoring/StreamingSim.py rename to src/test/clmctest/monitoring/StreamingSim.py diff --git a/clmctest/monitoring/__init__.py b/src/test/clmctest/monitoring/__init__.py similarity index 100% rename from clmctest/monitoring/__init__.py rename to src/test/clmctest/monitoring/__init__.py diff --git a/clmctest/monitoring/conftest.py b/src/test/clmctest/monitoring/conftest.py similarity index 69% rename from clmctest/monitoring/conftest.py rename to src/test/clmctest/monitoring/conftest.py index d00695ef5f77d5730112649d997d37ff6525e88f..def5e5332e7aba686b2b425708fb4354b486fc0b 100644 --- a/clmctest/monitoring/conftest.py +++ b/src/test/clmctest/monitoring/conftest.py @@ -27,6 +27,8 @@ import json import pkg_resources from influxdb import InfluxDBClient from clmctest.monitoring.StreamingSim import Sim +from clmctest.monitoring.E2ESim import Simulator +from clmctest.monitoring.E2ETestAggregatorThread import TestAggregator @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[0]['ip_address'] + ":8086" @@ -74,3 +82,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 TestAggregator(database_url=influx_url) diff --git a/clmctest/streaming/rspec.yml b/src/test/clmctest/monitoring/rspec.yml similarity index 54% rename from clmctest/streaming/rspec.yml rename to src/test/clmctest/monitoring/rspec.yml index 5a0c594c066c59a3066a138ef4c968d989f02613..fa5297ab2aca0d58b3c9ecd9b5373c5a3ee3f49e 100644 --- a/clmctest/streaming/rspec.yml +++ b/src/test/clmctest/monitoring/rspec.yml @@ -31,58 +31,50 @@ hosts: host: 8888 - guest: 9092 host: 9092 - ip_address: "192.168.50.10" - - name: nginx1 + - guest: 9080 + host: 9080 + ip_address: "172.40.231.51" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + report_period: 25 + - name: ipendpoint1 cpus: 1 memory: 2048 disk: "10GB" - service_name: "nginx" + service_name: "ipendpoint" forward_ports: - guest: 80 host: 8081 - ip_address: "192.168.50.11" - location: "DC1" - sfc_id: "MS_Template_1" - sfc_id_instance: "MS_I1" - sf_id: "adaptive_streaming" - sf_id_instance: "adaptive_streaming_I1" - ipendpoint_id: "adaptive_streaming_I1_nginx1" + ip_address: "172.40.231.170" + location: "nova" + sfc_id: "media_service_A" + sfc_id_instance: "StackID" + sf_id: "test-sf-clmc-agent-build" + sf_id_instance: "ms-A.ict-flame.eu" + ipendpoint_id: "endpoint1.ms-A.ict-flame.eu" sr_id: "service_router" - influxdb_url: "http://192.168.50.10:8086" + influxdb_url: "http://172.40.231.51:8086" database_name: "CLMCMetrics" - - name: nginx2 + - name: ipendpoint2 cpus: 1 memory: 2048 disk: "10GB" - service_name: "nginx" + service_name: "ipendpoint" forward_ports: - guest: 80 host: 8082 - ip_address: "192.168.50.12" - location: "DC2" - sfc_id: "MS_Template_1" - sfc_id_instance: "MS_I1" - sf_id: "adaptive_streaming" - sf_id_instance: "adaptive_streaming_I1" - ipendpoint_id: "adaptive_streaming_I1_nginx2" - sr_id: "service_router" - influxdb_url: "http://192.168.50.10:8086" - database_name: "CLMCMetrics" - - name: loadtest-streaming - cpus: 2 - memory: 4096 - disk: "10GB" - service_name: "loadtest-streaming" - forward_ports: - - guest: 80 - host: 8083 - ip_address: "192.168.50.13" - location: "DC1" - sfc_id: "MS_Template_1" - sfc_id_instance: "MS_I1" - sf_id: "adaptive_streaming_client" - sf_id_instance: "adaptive_streaming_I1" - ipendpoint_id: "adaptive_streaming_I1_client1" + ip_address: "172.40.231.171" + location: "nova" + sfc_id: "media_service_A" + sfc_id_instance: "StackID" + sf_id: "test-sf-clmc-agent-build" + sf_id_instance: "ms-A.ict-flame.eu" + ipendpoint_id: "endpoint2.ms-A.ict-flame.eu" sr_id: "service_router" - influxdb_url: "http://192.168.50.10:8086" - database_name: "CLMCMetrics" + influxdb_url: "http://172.40.231.51:8086" + database_name: "CLMCMetrics" + - name: test-runner + cpus: 1 + memory: 2048 + disk: "10GB" + ip_address: "172.40.231.200" \ No newline at end of file diff --git a/src/test/clmctest/monitoring/test_e2eresults.py b/src/test/clmctest/monitoring/test_e2eresults.py new file mode 100644 index 0000000000000000000000000000000000000000..c7506067af9b6bac42d1fba5a81f7e3b1d0a232e --- /dev/null +++ b/src/test/clmctest/monitoring/test_e2eresults.py @@ -0,0 +1,108 @@ +#!/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 +import threading + + +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...") + event = threading.Event() + e2e_aggregator.set_event_lock(event) + e2e_aggregator.start() + + event.wait() # wait until the aggregator thread has set the event lock (it has reached its run method and is ready to start) + + print("Running simulation, please wait...") + e2e_simulator.run() + + print("Waiting for INFLUX to finish receiving simulation data...") + time.sleep(e2e_simulator.SIMULATION_LENGTH+10) # 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 "CLMCMetrics"."autogen"."network_delays"', + {"time": "1970-01-01T00:00:00Z", "count_latency": 120, "count_bandwidth": 120}), + ('SELECT count(*) FROM "CLMCMetrics"."autogen"."service_delays"', + {"time": "1970-01-01T00:00:00Z", "count_response_time": 24, "count_request_size": 24, "count_response_size": 24}), + ('SELECT count(*) FROM "CLMCMetrics"."autogen"."e2e_delays"', + {"time": "1970-01-01T00:00:00Z", "count_delay_forward": 38, "count_delay_reverse": 38, "count_delay_service": 38, + "count_avg_request_size": 38, "count_avg_response_size": 38, "count_avg_bandwidth": 38}), + + ('SELECT mean(*) FROM "CLMCMetrics"."autogen"."e2e_delays"', + {"time": "1970-01-01T00:00:00Z", "mean_delay_forward": 8.010964912280702, "mean_delay_reverse": 12.881578947368423, "mean_delay_service": 23.42105263157895, + 'mean_avg_request_size': 10485760, 'mean_avg_response_size': 1024, 'mean_avg_bandwidth': 104857600}), + ]) + 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()) + + for key in expected_result: + print("expected_result == actual_result {0}, {1}".format(expected_result.get(key), actual_result.get(key))) + + if type(expected_result.get(key)) == float: + assert expected_result.get(key) == pytest.approx(actual_result.get(key), 0.3) # approximate only when comparing float values + else: + assert expected_result.get(key) == actual_result.get(key), "E2E Simulation test failure" + + assert expected_result == actual_result, "E2E Simulation test failure" + + diff --git a/clmctest/monitoring/test_rspec.py b/src/test/clmctest/monitoring/test_rspec.py similarity index 100% rename from clmctest/monitoring/test_rspec.py rename to src/test/clmctest/monitoring/test_rspec.py diff --git a/clmctest/monitoring/test_simresults.py b/src/test/clmctest/monitoring/test_simresults.py similarity index 86% rename from clmctest/monitoring/test_simresults.py rename to src/test/clmctest/monitoring/test_simresults.py index ce64c922d85ce7f48db29ce4ca75d520d4db8a31..9d8670e7957e417554916f697e4e20af8fcf04be 100644 --- a/clmctest/monitoring/test_simresults.py +++ b/src/test/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/src/test/clmctest/rspec.json b/src/test/clmctest/rspec.json new file mode 100644 index 0000000000000000000000000000000000000000..376194cc5f292ad1c919f6728451bf6e0145d5ea --- /dev/null +++ b/src/test/clmctest/rspec.json @@ -0,0 +1,105 @@ +[{ + "name": "clmc-service", + "ip_address": "172.40.231.51", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics", + "report_period": "25" +}, +{ + "name": "apache", + "ip_address": "172.40.231.150", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "apache", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "nginx", + "ip_address": "172.40.231.151", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "nginx", + "sf_id_instance": "adaptive_streaming_nginx_I1", + "ipendpoint_id": "adaptive_streaming_nginx_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "mongo", + "service_name": "mongo", + "ip_address": "172.40.231.152", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "mongo", + "sf_id_instance": "metadata_database_I1", + "ipendpoint_id": "metadata_database_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "host", + "service_name": "host", + "ip_address": "172.40.231.154", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "host", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "minio", + "ip_address": "172.40.231.155", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "minio", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_minio", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "ipendpoint1", + "ip_address": "172.40.231.170", + "location": "nova", + "sfc_id": "media_service_A", + "sfc_id_instance": "StackID", + "sf_id": "ipendpoint", + "sf_id_instance": "ms-A.ict-flame.eu", + "ipendpoint_id": "endpoint1.ms-A.ict-flame.eu", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "ipendpoint2", + "ip_address": "172.40.231.171", + "location": "nova", + "sfc_id": "media_service_A", + "sfc_id_instance": "StackID", + "sf_id": "ipendpoint", + "sf_id_instance": "ms-A.ict-flame.eu", + "ipendpoint_id": "endpoint2.ms-A.ict-flame.eu", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "test-runner", + "ip_address": "172.40.231.200" +} +] \ No newline at end of file diff --git a/src/test/clmctest/rspec.json.ffmpeg b/src/test/clmctest/rspec.json.ffmpeg new file mode 100644 index 0000000000000000000000000000000000000000..2a5bfab67d0e284c87b8aa32a718d7fa45039811 --- /dev/null +++ b/src/test/clmctest/rspec.json.ffmpeg @@ -0,0 +1,119 @@ +[{ + "name": "clmc-service", + "ip_address": "172.40.231.51", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics", + "report_period": "25" +}, +{ + "name": "apache", + "ip_address": "172.40.231.150", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "apache", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "nginx", + "ip_address": "172.40.231.151", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "nginx", + "sf_id_instance": "adaptive_streaming_nginx_I1", + "ipendpoint_id": "adaptive_streaming_nginx_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "mongo", + "service_name": "mongo", + "ip_address": "172.40.231.152", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "mongo", + "sf_id_instance": "metadata_database_I1", + "ipendpoint_id": "metadata_database_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "ffmpeg", + "service_name": "ffmpeg", + "ip_address": "172.40.231.153", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "ffmpeg", + "sf_id_instance": "metadata_database_I1", + "ipendpoint_id": "metadata_database_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "host", + "service_name": "host", + "ip_address": "172.40.231.154", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "host", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_apache1", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "minio", + "ip_address": "172.40.231.155", + "location": "DC1", + "sfc_id": "MS_Template_1", + "sfc_id_instance": "MS_I1", + "sf_id": "minio", + "sf_id_instance": "adaptive_streaming_I1", + "ipendpoint_id": "adaptive_streaming_I1_minio", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "ipendpoint1", + "ip_address": "172.40.231.170", + "location": "nova", + "sfc_id": "media_service_A", + "sfc_id_instance": "StackID", + "sf_id": "ipendpoint", + "sf_id_instance": "ms-A.ict-flame.eu", + "ipendpoint_id": "endpoint1.ms-A.ict-flame.eu", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "ipendpoint2", + "ip_address": "172.40.231.171", + "location": "nova", + "sfc_id": "media_service_A", + "sfc_id_instance": "StackID", + "sf_id": "ipendpoint", + "sf_id_instance": "ms-A.ict-flame.eu", + "ipendpoint_id": "endpoint2.ms-A.ict-flame.eu", + "sr_id": "service_router", + "influxdb_url": "http://172.40.231.51:8086", + "database_name": "CLMCMetrics" +}, +{ + "name": "test-runner", + "ip_address": "172.40.231.200" +} +] \ No newline at end of file diff --git a/clmctest/scripts/__init__.py b/src/test/clmctest/scripts/__init__.py similarity index 100% rename from clmctest/scripts/__init__.py rename to src/test/clmctest/scripts/__init__.py diff --git a/src/test/clmctest/scripts/rspec.yml b/src/test/clmctest/scripts/rspec.yml new file mode 100644 index 0000000000000000000000000000000000000000..9ac204e30956200806964d23fcb162a6f620b38a --- /dev/null +++ b/src/test/clmctest/scripts/rspec.yml @@ -0,0 +1,28 @@ +## (c) 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 : 20-03-2018 +## Created for Project : FLAME + +hosts: + - name: test-runner + cpus: 1 + memory: 2048 + disk: "10GB" + ip_address: "172.40.231.200" + \ No newline at end of file diff --git a/clmctest/scripts/test_config_telegraf.py b/src/test/clmctest/scripts/test_config_telegraf.py similarity index 100% rename from clmctest/scripts/test_config_telegraf.py rename to src/test/clmctest/scripts/test_config_telegraf.py diff --git a/clmctest/services/apache/install.sh b/src/test/clmctest/services/apache/install.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/apache/install.sh rename to src/test/clmctest/services/apache/install.sh diff --git a/clmctest/services/apache/telegraf_apache.conf b/src/test/clmctest/services/apache/telegraf_apache.conf similarity index 100% rename from clmctest/services/apache/telegraf_apache.conf rename to src/test/clmctest/services/apache/telegraf_apache.conf diff --git a/clmctest/services/ffmpeg/install.sh b/src/test/clmctest/services/ffmpeg/install.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/ffmpeg/install.sh rename to src/test/clmctest/services/ffmpeg/install.sh diff --git a/clmctest/services/ffmpeg/telegraf_ffmpeg.conf b/src/test/clmctest/services/ffmpeg/telegraf_ffmpeg.conf similarity index 100% rename from clmctest/services/ffmpeg/telegraf_ffmpeg.conf rename to src/test/clmctest/services/ffmpeg/telegraf_ffmpeg.conf diff --git a/clmctest/services/ffmpeg/transcode.sh b/src/test/clmctest/services/ffmpeg/transcode.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/ffmpeg/transcode.sh rename to src/test/clmctest/services/ffmpeg/transcode.sh diff --git a/clmctest/services/host/install.sh b/src/test/clmctest/services/host/install.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/host/install.sh rename to src/test/clmctest/services/host/install.sh diff --git a/clmctest/services/host/telegraf_host.conf b/src/test/clmctest/services/host/telegraf_host.conf similarity index 100% rename from clmctest/services/host/telegraf_host.conf rename to src/test/clmctest/services/host/telegraf_host.conf diff --git a/clmctest/services/ipendpoint/install.sh b/src/test/clmctest/services/ipendpoint/install.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/ipendpoint/install.sh rename to src/test/clmctest/services/ipendpoint/install.sh diff --git a/clmctest/services/ipendpoint/telegraf_ipendpoint.conf b/src/test/clmctest/services/ipendpoint/telegraf_ipendpoint.conf similarity index 100% rename from clmctest/services/ipendpoint/telegraf_ipendpoint.conf rename to src/test/clmctest/services/ipendpoint/telegraf_ipendpoint.conf diff --git a/clmctest/services/minio/install.sh b/src/test/clmctest/services/minio/install.sh similarity index 97% rename from clmctest/services/minio/install.sh rename to src/test/clmctest/services/minio/install.sh index 9779b25b063720b66643f3c6bee4359233b52fb9..bfbf7be6b27d1ea8f7c43ba394b52aa3924d15e1 100644 --- a/clmctest/services/minio/install.sh +++ b/src/test/clmctest/services/minio/install.sh @@ -56,7 +56,7 @@ cd minio go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" # check minio configuration available -MINIO_CONF_SOURCE=$REPO_ROOT"/clmctest/services/minio/minio.conf" +MINIO_CONF_SOURCE=$REPO_ROOT"/src/test/clmctest/services/minio/minio.conf" if [ ! -f "$MINIO_CONF_SOURCE" ]; then echo "Error: MINIO conf file does not exist on in the repo. "$MINIO_CONF_SOURCE exit 1 diff --git a/clmctest/services/minio/minio.conf b/src/test/clmctest/services/minio/minio.conf similarity index 100% rename from clmctest/services/minio/minio.conf rename to src/test/clmctest/services/minio/minio.conf diff --git a/clmctest/services/minio/telegraf_minio.conf b/src/test/clmctest/services/minio/telegraf_minio.conf similarity index 100% rename from clmctest/services/minio/telegraf_minio.conf rename to src/test/clmctest/services/minio/telegraf_minio.conf diff --git a/clmctest/services/mongo/install.sh b/src/test/clmctest/services/mongo/install.sh old mode 100755 new mode 100644 similarity index 100% rename from clmctest/services/mongo/install.sh rename to src/test/clmctest/services/mongo/install.sh diff --git a/clmctest/services/mongo/telegraf_mongo.conf b/src/test/clmctest/services/mongo/telegraf_mongo.conf similarity index 100% rename from clmctest/services/mongo/telegraf_mongo.conf rename to src/test/clmctest/services/mongo/telegraf_mongo.conf diff --git a/clmctest/services/nginx/install.sh b/src/test/clmctest/services/nginx/install.sh old mode 100755 new mode 100644 similarity index 96% rename from clmctest/services/nginx/install.sh rename to src/test/clmctest/services/nginx/install.sh index 06f74d2a3ab75895b77b50bf65ce966b106a5e4a..7acbbad86340888702a67d9f79456f5ad8308940 --- a/clmctest/services/nginx/install.sh +++ b/src/test/clmctest/services/nginx/install.sh @@ -31,7 +31,7 @@ apt-get install nginx -y # Need to set up basic stats as this not configured by default # http://nginx.org/en/docs/http/ngx_http_stub_status_module.html -NGINX_CONF_SOURCE=$REPO_ROOT"/clmctest/services/nginx/nginx.conf" +NGINX_CONF_SOURCE=$REPO_ROOT"/src/test/clmctest/services/nginx/nginx.conf" NGINX_CONF_TARGET="/etc/nginx/nginx.conf" # Check the target telegraf directory exists diff --git a/clmctest/services/nginx/nginx.conf b/src/test/clmctest/services/nginx/nginx.conf similarity index 100% rename from clmctest/services/nginx/nginx.conf rename to src/test/clmctest/services/nginx/nginx.conf diff --git a/clmctest/services/nginx/telegraf_nginx.conf b/src/test/clmctest/services/nginx/telegraf_nginx.conf similarity index 100% rename from clmctest/services/nginx/telegraf_nginx.conf rename to src/test/clmctest/services/nginx/telegraf_nginx.conf diff --git a/clmctest/services/pytest/install.sh b/src/test/clmctest/services/pytest/install.sh old mode 100755 new mode 100644 similarity index 95% rename from clmctest/services/pytest/install.sh rename to src/test/clmctest/services/pytest/install.sh index ce998ad1deef418d05a721cce5cc20f3aa3bd098..a545894cd663fe5e162bdf0e93492faa1f70f876 --- a/clmctest/services/pytest/install.sh +++ b/src/test/clmctest/services/pytest/install.sh @@ -24,7 +24,8 @@ #// #///////////////////////////////////////////////////////////////////////// apt-get update -apt-get -y install python3 python3-pip python-influxdb +apt-get -y install python3 python3-pip python-influxdb update-alternatives --install /usr/bin/python python /usr/bin/python3 10 + pip3 install pytest pyyaml pip3 install --upgrade influxdb diff --git a/setup.py b/src/test/setup.py similarity index 89% rename from setup.py rename to src/test/setup.py index 72cae3a9d31413d969a931a41509dca9c4272ea6..78c16282f64d6cbfd25cc32ed19cbe13849ed05e 100644 --- a/setup.py +++ b/src/test/setup.py @@ -21,19 +21,16 @@ // 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() + def get_version(fname): if os.path.isfile(fname): git_revision = read(fname) @@ -42,9 +39,10 @@ def get_version(fname): return git_revision + setup( - name = "clmc", - version = get_version("clmctest/_version.py"), + name = "clmctest", + version = get_version("_version.py"), author = "Michael Boniface", author_email = "mjb@it-innovation.soton.ac.uk", description = "FLAME CLMC Test Module", @@ -60,4 +58,4 @@ setup( "Topic :: FLAME Tests", "License :: ", ], -) \ No newline at end of file +)