Redesigned the telemetry backend app.

- Added migration code to modify 'records' table as follows:
   - Rename 'os_name' ==> 'system_name'
   - Rename 'machine' ==> 'host_type'
   - Rename 'payload_format_version' ==> 'payload_version'
   - Rename 'record_format_version' ==> 'record_version'
   - Rename 'tsp' ==> 'timestamp_client'
   - Rename 'tsp_server' ==> 'timestamp_server'
   - Modify 'payload' type from binary to text
   - Remove 'buildstamp'
   - Remove 'dupe_of'
   - Remove 'dupecount'
   - Remove 'dupemaster'
   - Remove 'security'
   - Remove 'hide'
   - Remove 'icon'
   - Add 'build'
   - Add 'classification'

  - Updated code to reflect the above design changes.
  - Updated code to remove references to 'backtrace' column.
    But not from the table because data migration from
    'backtrace' to 'payload' should be done first.

 - Added migration code to update the table data based on the above
   design changes.

 - Added migration code to update the Views based on the above design
   changes.

 - Removed the 'collector' app.

 - Modified database name to 'telemetry'.

 - Added code to filter out build id >= 100000.
This commit is contained in:
Reagan Lopez
2018-08-31 09:51:11 -07:00
committed by ajch
parent 4df418f580
commit 6a2b1dd185
40 changed files with 520 additions and 1991 deletions
+1
View File
@@ -1,3 +1,4 @@
*.pyc
*.log
*.swp
*.idea
-11
View File
@@ -1,11 +0,0 @@
language: python
python:
3.5.2
install:
pip3 install -r requirements.txt
script:
make run-tests
services:
postgresql
before_script:
psql -c 'create database testdb;' -U postgres
-11
View File
@@ -17,21 +17,10 @@ release:
git tag -a -m "Release $(VERSION)" v$(VERSION); \
printf "\nNew release $(VERSION) tagged!\n\n"
run-collector:
@echo $(WARNING)
FLASK_APP=collector/collector/report_handler.py flask run --host $(HOST) ${PORT}
run-telemetryui:
@echo $(WARNING)
FLASK_APP=telemetryui/telemetryui/views.py flask run --host $(HOST) ${PORT}
run-tests:
PYTHONPATH=/$(shell pwd)/collector python3 collector/collector/tests/headers.py
PYTHONPATH=/$(shell pwd)/collector python3 collector/collector/tests/api.py
PYTHONPATH=/$(shell pwd)/collector python3 collector/collector/tests/purging.py
PYTHONPATH=/$(shell pwd)/collector python3 collector/collector/tests/validation.py
PYTHONPATH=/$(shell pwd)/collector python3 collector/collector/tests/parsers.py
tag-production:
@TODAY=`date "+%F-%H%M%S"`; \
git rev-parse production-$$TODAY &> /dev/null; \
+7 -63
View File
@@ -7,8 +7,7 @@ This project provides the server-side component of a complete telemetrics
client-side component source repository lives at
https://github.com/clearlinux/telemetrics-client.
It consists of two Flask applications: an ingestion app, `collector`, for
records received from telemetrics-client probes; and a web app, `telemetryui`,
It consists of a Flask application `telemetryui`,
that exposes several views to visualize the telemetry data. The `telemetryui`
app also provides a REST API to perform queries on the data.
@@ -39,8 +38,8 @@ applications.
To control access to the applications, it is recommended that system
administrators leverage web server authentication.
Regarding alternate deployment scenarios, one might want to host the collector,
telemetryui, and database on three separate servers/VMs; and implement network
Regarding alternate deployment scenarios, one might want to host the telemetryui
and database on three separate servers/VMs; and implement network
access controls for these systems. The in-tree deployment script does not
support these types of deployments, but with minimal modification to the
source, they should be possible.
@@ -99,7 +98,7 @@ $ ./deploy.sh -H localhost -d clr -a install
During script execution, you will be prompted for user input:
* The first prompt begins with "DB password:", asking for a password to set for
the `telemdb` database. If you do not enter a value before pressing ENTER,
the `telemetry` database. If you do not enter a value before pressing ENTER,
the password will be `postgres`.
* If your sudo access requires a password, you will be prompted for that
@@ -125,32 +124,6 @@ working with. Its default value is the upstream repo location on
github.com/clearlinux. The `-s` option lets you select a different git branch
to install/deploy from rather than "master", the default value.
## `collector` operation
The `collector` app is an ingestion app that handles POST requests to `/`
or `/v2/collector` at the web server location where telemetrics-backend has
been installed. The requests are analyzed to make sure required header fields
are present and that values are correct.
Generally, the POST requests are sent by probes from the telemetrics-client,
since these probes use the client library, libtelemetry, to create well-formed
records capable of being processed by telemetrics-backend.
At minimum, make sure that your telemetrics-client configuration in
`/etc/telemetrics/telemetrics.conf` specifies the correct server URL for the
`server` config option. For example, if telemetrics-backend is hosted at
`example.com`, the client config should contain `server=http://example.com/` or
`server=http://example.com/v2/collector`.
## `collector` plugable payload parsers
By default `collector` will save the payload record from telemetry messages as is
received, there are cases when is desirable to apply additional transformations to
payload on selected classifications. This use case is covered by custom payload
parsers/(or transformations). For more information on this feature read specific
[documentation](/collector/collector/parsers/README.md)
## `telemetryui` views
The `telemetryui` app is a web app that exposes several views to visualize the
@@ -158,7 +131,7 @@ telemetry data and also provides a REST API to perform queries on record data.
The current views are:
* Records view - a paginated list of all records in the `telemdb` database that
* Records view - a paginated list of all records in the `telemetry` database that
have been accepted by the `collector`. The records are presented in tabular
format and the columns map to select fields from the `records` database table.
At the top of the view, an HTML form can be used for "advanced searches",
@@ -189,8 +162,7 @@ The current views are:
reports are grouped by "guilties"; a guilty is a frame from a crash backtrace
chosen as the best candidate for the cause of the crash. The logic for
determining crash record guilty frames accepts user input; the user can
identify which frames in a backtrace are never guilty. Guilty processing occurs
asychronously in a uWSGI spooler process for the `collector` app.
identify which frames in a backtrace are never guilty.
* MCE view - charts that display MCE (machine check exception) data from a
patched version of `mcelog` that uses libtelemetry to create and send
@@ -228,24 +200,6 @@ go to relevant documentation.
## Special configuration
### Configuring the `collector` TID
The `collector` requires a Telemetry ID (TID) header value set with the HTTP
header `X-Telemetry-TID`. The telemetrics-client daemon adds this header to
telemetry records, and its default value is used for identifying records from
Clear Linux OS systems.
However, when you are deploying your own instance of telemetrics-backend and
have deployed telemetrics-client to systems configured to send records to this
instance, it is recommended to generate your own random TID (e.g. using the
`uuidgen` program). Once you have generated a TID, the following steps are needed:
* Configure telemetrics-client to add this TID to records by modifying the
`tidheader` value in `/etc/telemetrics/telemetrics.conf` on systems sending
the telemetry data to your `collector`.
* Configure your `collector` app to accept records with this TID by modifying
`collector/collector/config.py`.
### Configuring nginx for TLS
The `sites_nginx.conf` config file is already enabled to accept TLS connections
@@ -262,16 +216,6 @@ comment out TLS-related nginx configuration. Specifically, it will comment out
the `listen 443 ssl`, `ssl_certificate`, `ssl_certificate_key`,
`ssl_protocols`, and `ssl_ciphers` directives.
### Configuring record retention time
The `collector` app uses the uWSGI cron interface to purge records in the
database on a daily basis that are older than a certain age. By default, a
record will only be kept in the database for 5 weeks (starting from the
timestamp the `collector` received the record). To modify the retention
time, update the `MAX_DAYS_KEEP_UNFILTERED_RECORDS` value in
`collector/collector/config.py` after installation, and restart
uWSGI for the new setting to take effect.
## Using the REST API
A REST API for querying records is available at "/api/records". The API returns
@@ -351,7 +295,7 @@ Database migrations are managed using
[Flask-Migrate](https://flask-migrate.readthedocs.io/en/latest/). Upon initial
install of telemetrics-backend, the first migration will be applied, and any
additional migrations in the `telemetryui/migrations/versions/` directory will
be applied in sequence and upgrade the `telemdb` schema to the latest version.
be applied in sequence and upgrade the `telemetry` schema to the latest version.
To create a new migration, you can follow the steps below:
-47
View File
@@ -1,47 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from flask import Flask
from . import config
from flask_sqlalchemy import SQLAlchemy
from logging.handlers import RotatingFileHandler
def configure_app(config_object, app):
app.config.from_object(config_object)
db = SQLAlchemy(app)
app = Flask(__name__)
db = SQLAlchemy()
app.config.from_object(config.Config)
try:
# try importing from the local dev configuration if it exists
from . import config_local
app.config.from_object(config_local.Config)
except Exception as e:
print(e)
pass
from .model import *
from . import report_handler
handler = RotatingFileHandler(app.config['LOG_FILE'], maxBytes=10000, backupCount=1)
handler.setLevel(app.config['LOG_LEVEL'])
app.logger.addHandler(handler)
# vi: ts=4 et sw=4 sts=4
-55
View File
@@ -1,55 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
class Config(object):
DEBUG = False
TESTING = False
LOG_LEVEL = logging.ERROR
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:@@db_password@@@localhost/telemdb'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'handler.log'
# When PURGE_OLD_RECORDS == True then a purging system of old records will
# be triggered daily. If this variable is not present, then no purging will be done.
# If the purging system is enabled, then the following two variables must be set
PURGE_OLD_RECORDS = True
# The maximum retention time in days for records stored in the database
# which do not match the filters in PURGE_FILTERED_RECORDS.
# Use 0 to avoid deletion of all unfiltered records.
MAX_DAYS_KEEP_UNFILTERED_RECORDS = 35
# See config_example.py for details about PURGE_FILTERED_RECORDS
PURGE_FILTERED_RECORDS = {
"classification": {
"org.clearlinux/hello/world": 1,
}
}
# The Telemetry ID (TID) accepted by this `collector` app. The ID should be a
# random UUID, generated with (for example) `uuidgen`. The default value
# set here is used for records from the Clear Linux OS for Intel
# Architecture.
TELEMETRY_ID = "6907c830-eed9-4ce9-81ae-76daf8d88f0f"
class Testing(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:@@db_password@@@localhost/testdb'
# vi: ts=4 et sw=4 sts=4
-78
View File
@@ -1,78 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
class Config(object):
DEBUG = False
TESTING = False
LOG_LEVEL = logging.ERROR
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:@@db_password@@@localhost/telemdb'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'handler.log'
# When PURGE_OLD_RECORDS == True then a purging system of old records will
# be triggered daily. If this variable is not present, then no purging will be done.
# If the purging system is enabled, then the following two variables must be set
PURGE_OLD_RECORDS = True
# The maximum retention time in days for records stored in the database
# which do not match the filters in PURGE_FILTERED_RECORDS.
# Use 0 to avoid deletion of all unfiltered records.
MAX_DAYS_KEEP_UNFILTERED_RECORDS = 35
# A dictionary in the following format:
# {
# "<field_name>": {
# "<field_value>": <max_days_to_keep>,
# ...
# },
# ...
# }
# Currently supported fields to filter:
# [
# 'severity',
# 'classification',
# 'machine_id'
# ]
# Use 0 to avoid deletion of records that matches the filter.
# If you do not want to filter records to delete, just set an empty dict '{}'
PURGE_FILTERED_RECORDS = {
"severity": {
1: 5,
4: 0
},
"classification": {
"org.clearlinux/mce/*": 0,
"org.clearlinux/hello/world": 1,
"org.clearlinux/heartbeat/ping": 1,
}
}
# The Telemetry ID (TID) accepted by this `collector` app. The ID should be a
# random UUID, generated with (for example) `uuidgen`. The default value
# set here is used for records from the Clear Linux OS for Intel
# Architecture.
TELEMETRY_ID = "6907c830-eed9-4ce9-81ae-76daf8d88f0f"
class Testing(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:@@db_password@@@localhost/testdb'
SQLALCHEMY_TRACK_MODIFICATIONS = True
PURGE_OLD_RECORDS = True
# vi: ts=4 et sw=4 sts=4
-61
View File
@@ -1,61 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This configuration file can be used for a local development debug server
# at localhost:5000. Overrides the config module.
import logging
class Config(object):
DEBUG = True
TESTING = False
LOG_LEVEL = logging.ERROR
# If your telemdb database password is not 'postgres', update this line.
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:postgres@localhost/telemdb'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'handler.log'
# When PURGE_OLD_RECORDS == True then a purging system of old records will
# be triggered daily. If this variable is not present, then no purging will be done.
# If the purging system is enabled, then the following two variables must be set
PURGE_OLD_RECORDS = True
# The maximum retention time in days for records stored in the database
# which do not match the filters in PURGE_FILTERED_RECORDS.
# Use 0 to avoid deletion of all unfiltered records.
MAX_DAYS_KEEP_UNFILTERED_RECORDS = 35
# See config_example.py for details about PURGE_FILTERED_RECORDS
PURGE_FILTERED_RECORDS = {
"classification": {
"org.clearlinux/hello/world": 1,
}
}
# Custom Payload transformations
# POST_PROCESSING_PARSERS = ["demo"]
class Testing(Config):
TESTING = True
# If your testdb database password is not 'postgres', update this line.
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:postgres@localhost/testdb'
SQLALCHEMY_TRACK_MODIFICATIONS = True
# vi: ts=4 et sw=4 sts=4
-1
View File
@@ -1 +0,0 @@
../../shared/crash.py
-6
View File
@@ -1,6 +0,0 @@
class PlugablePayloadParserException(Exception):
def __init__(self, message):
self.__str__ = message
-28
View File
@@ -1,28 +0,0 @@
#
# Copyright 2018 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
try:
from uwsgidecorators import spool
except ImportError:
def spool(f):
f.spool = f
return f
# Alias for uwsgi spool feature. Using this feature to async data processing from
# different probes.
# Aliasing this feature will make it easier in the future to move to a different
# product if scaling becomes a problem.
parser_spooler = spool
-213
View File
@@ -1,213 +0,0 @@
#
# Copyright 2015-2018 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import string
from .. import app
MAX_NUM_RECORDS = '1000'
REQUIRED_HEADERS_V1 = (
'Arch',
'Build',
'Creation-Timestamp',
'Classification',
'Host-Type',
'Kernel-Version',
'Machine-Id',
'Severity',
'Record-Format-Version',
)
REQUIRED_HEADERS_V2 = REQUIRED_HEADERS_V1 + (
'Payload-Format-Version',
'System-Name',
'X-Telemetry-Tid',
)
REQUIRED_HEADERS_V3 = REQUIRED_HEADERS_V2 + (
'Board-Name',
'Bios-Version',
'Cpu-Model',
)
REQUIRED_HEADERS_V4 = REQUIRED_HEADERS_V3 + (
'Event-Id',
)
# see config.py for the meaning of this value
TELEMETRY_ID = app.config.get("TELEMETRY_ID", "6907c830-eed9-4ce9-81ae-76daf8d88f0f")
SEVERITY_VALUES = [1, 2, 3, 4]
VALID_RECORD_FORMAT_VERSIONS = [1, 2, 3, 4]
POSTGRES_INT_MAX = 2147483647
MAXLEN_PRINTABLE = 200
class InvalidUsage(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
app.logger.error("InvalidUsage ({}): {}".format(self.status_code, self.message))
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
def __str__(self):
return self.message
def validate_headers(headers, required_headers):
""" Check for every single required header to be
in the request """
req_headers = dict(headers)
req_headers_keys = req_headers.keys()
for header in required_headers:
if header not in req_headers_keys:
raise InvalidUsage("Record-Format-Version headers are invalid, {} missing".format(header), 400)
return True
def is_not_none(v):
return v is not None
def is_a_number(n):
return str(n).isdigit()
def value_is_printable(a_value):
return all([x in string.printable for x in a_value])
def record_format_version_validation(record_format_version):
return all([is_not_none(record_format_version), is_a_number(record_format_version)]) and int(record_format_version) in VALID_RECORD_FORMAT_VERSIONS
def record_format_version_headers_validation(record_format_version, headers):
# Validate required headers based on Record Version
req_headrs = {
'1': REQUIRED_HEADERS_V1,
'2': REQUIRED_HEADERS_V2,
'3': REQUIRED_HEADERS_V3,
'4': REQUIRED_HEADERS_V4,
}
reqs = req_headrs.get(record_format_version, None)
if reqs is None:
return False
return validate_headers(headers, reqs)
def validation_tid_header(tid):
return tid == TELEMETRY_ID
def validate_severity(severity):
return is_a_number(severity) and int(severity) in SEVERITY_VALUES
def validate_classification(classification):
return is_not_none(classification) and len(classification.split('/')) == 3
def validate_machine_id(machine_id):
return is_not_none(machine_id) and len(machine_id) <= 32
def validate_timestamp(timestamp):
return all([is_not_none(timestamp), is_a_number(timestamp)])
def validate_architecture(arch):
return arch in ["armv7l", "armv6l", "aarch64", "amd64", "sparc64", "ppc64", "i686", "i386", "x86_64", "ppc"]
def validate_host_type(host_type):
return is_not_none(host_type) and len(host_type) < 250
def validate_kernel_version(kernel_version):
""" makes sure that the kernel version string has at least 2 numbers
version, major and minor revision
"""
try:
version, major_revision, _ = str(kernel_version).split('.', maxsplit=2)
return all([is_a_number(x) for x in [version, major_revision]])
except ValueError as e:
print(e)
return False
def validate_payload_format_version(payload_format_version):
return int(payload_format_version) < POSTGRES_INT_MAX
def validate_x_header(header_value):
return is_not_none(header_value) and len(header_value) < MAXLEN_PRINTABLE and value_is_printable(header_value)
def validate_created(created):
return is_a_number(created)
def validate_record_limit(limit):
return is_a_number(limit) and limit <= MAX_NUM_RECORDS
def validate_event_id(header_value):
return len(header_value) == 32 and len([v for v in header_value if v in "0123456789abcdef"]) == 32
def validate_header(name, value, expected=None):
if expected is None:
return {
'tid_header': validation_tid_header,
'record_format_version': record_format_version_validation,
'payload_format_version': validate_payload_format_version,
'severity': validate_severity,
'classification': validate_classification,
'machine_id': validate_machine_id,
'timestamp': validate_timestamp,
'architecture': validate_architecture,
'host_type': validate_host_type,
'kernel_version': validate_kernel_version,
'board_name': validate_x_header,
'cpu_model': validate_x_header,
'bios_version': validate_x_header,
'build': validate_x_header,
'event_id': validate_event_id,
}.get(name, lambda x: False)(value)
else:
return value == expected
def validate_query(name, value):
return {
'id': is_a_number,
'ts_capture': is_a_number,
'severity': validate_severity,
'classification': validate_classification,
'build': validate_x_header,
'limit': validate_record_limit,
'machine_id': validate_machine_id,
'created_in_days': validate_created,
'created_in_sec': validate_created,
}.get(name, lambda x: False)(value)
-36
View File
@@ -1,36 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from flask import request, current_app, app
import logging
@app.before_request
def before_request():
headers = request.headers
payload = request.data
print('Before request')
current_app.logger.info('\t'.join([
datetime.datetime.today().ctime(),
request.remote_addr,
request.method,
request.url,
str(request.data),
', '.join([': '.join(x) for x in request.headers])])
)
# vi: ts=4 et sw=4 sts=4
-1
View File
@@ -1 +0,0 @@
../../shared/model.py
-68
View File
@@ -1,68 +0,0 @@
## Plugable payload parsers
### Overview
A plugable parser is a python module that encapsulates a data transformation.
This transformation is applied to telemetry message payload if the classification
matches one of the classifications that the parser defines during plugin
registration.
Plugin registration occurs during collector initialization, to enable a plugin
the plugin needs to be listed in the array ```POST_PROCESSING_PARSERS``` in
collector configuration.
For example to enable the ```demo``` parser plugin that comes with the telemetry
backend we need to add a line like the following to collector configuration:
```python
class Config(object):
# ... some configuration values
POST_PROCESSING_PARSERS = ["demo"]
# ... more configuration values
```
### Plugable parser `installation`
The plugable parser module needs to be located under parsers directory, see
the following plugin parser source tree:
```bash
<telemetry-root>/collector/collector/
parsers/
<parser_name_dir>/
__init__.py
main.py
```
* ```<parser_name_dir>``` is the parser module name and should live under
collector/parsers
* ```main.py``` is the entry point for plugin registration.
The parser entry point `must have` the following members:
* An array called ```CLASSIFICATIONS``` with a list of message classifications. Any message
that matches one of these classifications will be parsed by this plugin.
* A function signature ```parse_payload(kwargs)```
which is the transformation that will be applied to the payload.
### Other considerations
Plugable parsers are applied asynchronously and no major consideration is required for
most use cases. When in doubt keep in mind the following:
* Payload transformations are applied after the message record (including payload)
is safely stored.
* It is a correct assumption to think that the entire record can
be queried from storage.
* It is not possible to register multiple plugins for the same classification,
the correct way to go about doing this is to encapsulate the multiple
transformations in one plugin. This is by design, remember that transformations
are applied asynchronously therefore a given plugin execution order is not
guaranteed.
-35
View File
@@ -1,35 +0,0 @@
#
# Copyright 2018 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import time
import base64
from collector.lib.parser import parser_spooler
CLASSIFICATIONS = ['org.clearlinux/telemetry/b64payload']
@parser_spooler
def parse_payload(**kwargs):
"""
:param kwargs: classification=<value>,
record_id=<value>,
payload=<value>
:return: None
"""
print("Processing data")
print(base64.b64decode(kwargs.get('payload')))
print("Data processed")
@@ -1,26 +0,0 @@
#
# Copyright 2018 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from collector.models import db
class Message(db.Model):
__tablename__ = 'demo_message'
record_id = db.Column(db.Integer, db.ForeignKey('records.id'))
payload = buildstamp = db.Column(db.String, default='')
def __init__(self, **kwargs):
self.record_id = kwargs.get('record_id', None)
self.payload = kwargs.get('payload', None)
-38
View File
@@ -1,38 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from .model import Record
from . import app
try:
import uwsgi
from uwsgidecorators import cron
PURGE_OLD_RECORDS = app.config.get("PURGE_OLD_RECORDS", True)
# Runs cron job at 4:30 every day
@cron(30, 4, -1, -1, -1, target='spooler')
def purge_task(signum):
if PURGE_OLD_RECORDS:
app.logger.info("Running cron job for purging records")
with app.app_context():
Record.delete_records()
except ImportError:
app.logger.info("Import error for uwsgi")
# vi: ts=4 et sw=4 sts=4
-346
View File
@@ -1,346 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import re
import time
import datetime
import importlib
from flask import request
from flask import jsonify
from flask import redirect
from .lib.exceptions import PlugablePayloadParserException
from .lib.validation import (
validate_header,
validate_query,
MAX_NUM_RECORDS,
record_format_version_headers_validation,
InvalidUsage)
from .model import (
Classification,
Build)
from .crash import (
process_guilties,
is_crash_classification)
from .purge import *
tm_version_regex = re.compile("^[0-9]+\.[0-9]+$")
client_id_regex = re.compile(
"^[0-9]+[ \t]+[-/.:_+*a-zA-Z0-9]+[ \t]+[-/.:_+*a-zA-Z0-9]+$")
ts_v3_regex = re.compile("^[0-9]+$")
# FIXME: configurable limits
max_payload_len_inline = 30 * 1024 # 30k
max_payload_len = 300 * 1024 # 300k
MAX_INTERVAL_SEC = 24 * 60 * 60 * 30 # 30 days in seconds
# This variable is loaded during app initialization from
# values on config.POST_PROCESSING_PARSERS
POST_PROCESSING_PARSERS = {}
@app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
@app.before_request
def before_request():
headers = request.headers
payload = request.data
app.logger.info('\t'.join([
datetime.datetime.today().ctime(),
request.method,
request.url,
str(request.data),
', '.join([': '.join(x) for x in request.headers])])
)
def clean_build_n_value(build):
# It is common to see the build numbers quoted in os-release. We don't
# need the quotes, so strip them from the semantic value.
_build = build.replace('"', '').replace("'", "")
return _build
def validate_header_value(header_value, record_name, err_msg):
try:
if validate_header(record_name, header_value) is True:
return header_value
except Exception as e:
err_msg = "Error parsing {}, {}".format(record_name, e)
raise InvalidUsage(err_msg, 400)
def validate_record_v3_headers(board_name, cpu_model, bios_version):
validate_header_value(board_name, "board_name", "board name is invalid")
validate_header_value(cpu_model, "cpu_model", "cpu model is invalid")
validate_header_value(bios_version, "bios_version", "BIOS version is invalid")
def collector_post_handler():
# The collector only accepts records with the configured TID value.
# Make sure the TID in the collector config.py matches the TID
# configured for telemetrics-client on the systems from which this
# collector receives records.
tid_header = request.headers.get("X-Telemetry-TID")
validate_header_value(tid_header, "tid_header", "Telemetry ID mismatch")
record_format_version = request.headers.get("Record-Format-Version")
validate_header_value(record_format_version, "record_format_version", "Record-Format-Version is invalid")
record_format_version_headers_validation(record_format_version, request.headers)
severity = request.headers.get("Severity")
validate_header_value(severity, "severity", "severity value is out of range")
classification = request.headers.get("Classification")
validate_header_value(classification, "classification", "Classification value is invalid")
machine_id = request.headers.get("Machine-Id")
validate_header_value(machine_id, "machine_id", "Machine id value is invalid")
timestamp = request.headers.get("Creation-Timestamp")
validate_header_value(timestamp, "timestamp", "timestamp is invalid")
ts_capture = int(timestamp)
ts_reception = time.time()
architecture = request.headers.get("Arch")
validate_header_value(architecture, "architecture", "architecture is invalid")
host_type = request.headers.get("Host-Type")
validate_header_value(host_type, "host_type", "host type is invalid")
kernel_version = request.headers.get("Kernel-Version")
validate_header_value(kernel_version, "kernel_version", "kernel version is invalid")
# Record V3 format headers
board_name = "N/A"
cpu_model = "N/A"
bios_version = "N/A"
if record_format_version >= '3':
board_name = request.headers.get("Board-Name")
cpu_model = request.headers.get("Cpu-Model")
bios_version = request.headers.get("Bios-Version")
validate_record_v3_headers(board_name, cpu_model, bios_version)
# Record V4 format headers
event_id = "N/A"
if record_format_version >= '4':
event_id = request.headers.get("Event-Id")
validate_header_value(event_id, "event_id", "Event id is invalid")
os_name = request.headers.get('System-Name')
os_name = os_name.replace('"', '').replace("'", "")
build = request.headers.get('Build')
build = clean_build_n_value(build)
# The build number is stored as a string in the database, but if this
# record is from a Clear Linux OS system, only accept an integer.
# Otherwise, loosen the restriction to the characters listed for
# VERSION_ID in os-release(5) in addition to the capital letters A-Z.
if os_name == 'clear-linux-os':
build_regex = re.compile(r"^[0-9]+$")
if not build_regex.match(build):
raise InvalidUsage("Clear Linux OS build version has invalid characters")
else:
build_regex = re.compile(r"^[-_a-zA-Z0-9.]+$")
if not build_regex.match(build):
raise InvalidUsage("Build version has invalid characters")
payload_format_version = request.headers.get("Payload-Format-Version")
validate_header_value(payload_format_version, "payload_format_version",
"Payload format version outside of range supported")
external = request.headers.get('X-CLR-External')
if external and external == "true":
external = True
else:
external = False
try:
# prefer UTF-8, if possible
payload = request.data.decode('utf-8')
except UnicodeError:
# fallback to Latin-1, since it accepts all byte values
payload = request.data.decode('latin-1')
db_class = Classification.query.filter_by(classification=classification).first()
if db_class is None:
db_class = Classification(classification)
db_build = Build.query.filter_by(build=build).first()
if db_build is None:
db_build = Build(build)
db_rec = Record.create(machine_id, host_type, severity, db_class, db_build, architecture, kernel_version,
record_format_version, ts_capture, ts_reception, payload_format_version, os_name,
board_name, bios_version, cpu_model, event_id, external, payload)
# TODO: This should become a plugable parser
if is_crash_classification(classification):
# must pass args as bytes to uwsgi under Python 3
process_guilties(klass=classification.encode(), id=str(db_rec.id).encode())
if classification in POST_PROCESSING_PARSERS.keys():
POST_PROCESSING_PARSERS[classification](classification=classification.encode(),
record_id=str(db_rec.id).encode(),
payload=payload.encode())
resp = jsonify(db_rec.to_dict())
resp.status_code = 201
return resp
def validate_query_value(query_value, query_name, err_msg):
try:
if validate_query(query_name, query_value) is True:
return query_value
except Exception as e:
err_msg = "Error parsing {}, {}".format(query_name, e)
raise InvalidUsage(err_msg, 400)
def get_records_api_handler():
# Validate query parameters correctness
severity = request.args.get("severity", None)
if severity is not None:
validate_query_value(severity, "severity", "Severity should be a numeric value")
classification = request.args.get('classification', None)
if classification is not None:
validate_query_value(classification, "classification", "Classification value is invalid")
build = request.args.get('build', None)
if build is not None:
build = clean_build_n_value(build)
validate_query_value(build, "build", "Build value is invalid")
machine_id = request.args.get('machine_id', None)
if machine_id is not None:
validate_query_value(machine_id, "machine_id", "Machine id value is invalid")
created_in_days = request.args.get('created_in_days', None)
if created_in_days is not None:
validate_query_value(created_in_days, "created_in_days", "Created (in days) value is invalid")
created_in_sec = request.args.get('created_in_sec', None)
if created_in_sec is not None:
validate_query_value(created_in_sec, "created_in_sec", "Created (in seconds) value is invalid")
from_id = request.args.get('from_id', None)
if from_id is not None:
validate_query_value(from_id, "id", "Provided record id value is invalid")
ts_capture = request.args.get('ts_capture', None)
if ts_capture is not None:
validate_query_value(ts_capture, "ts_capture", "Time stamp from record capture")
limit = request.args.get('limit', MAX_NUM_RECORDS)
if limit != MAX_NUM_RECORDS:
validate_query_value(limit, "limit", "Record limit value is invalid")
# Transform days interval to seconds
if created_in_days is not None:
created_in_days = int(created_in_days)
interval_sec = 24 * 60 * 60 * created_in_days
elif created_in_sec is not None:
interval_sec = int(created_in_sec)
else:
interval_sec = MAX_INTERVAL_SEC
if interval_sec is not None and interval_sec > MAX_INTERVAL_SEC:
interval_sec = MAX_INTERVAL_SEC
records = Record.query_records(build, classification, severity, machine_id, limit, interval_sec,
from_id=from_id, ts_capture=ts_capture)
record_list = [Record.to_dict(rec) for rec in records]
return jsonify(records=record_list)
# ########## Routes ###########
@app.route("/", methods=['GET', 'POST'])
@app.route("/v2/collector", methods=['GET', 'POST'])
def handler():
if request.method == 'POST':
return collector_post_handler()
else:
return redirect("/telemetryui", code=302)
@app.route("/api/records", methods=['GET'])
def records_api_handler():
"""
query filters for simple query:
classification
severity
build
machine_id
created_in_days - records created after given days
created_in_sec - records created after given seconds
TODO: Advanced query with pagination and following parameters?
client_created_after - timestamp
client_created_before - timestamp
server_created_after - timestamp
server_created_before - timestamp
"""
return get_records_api_handler()
def verify_parser_module(parser_module):
if getattr(parser_module, 'CLASSIFICATIONS', None) is None:
raise PlugablePayloadParserException('Parser {} should have a CLASSIFICATIONS field')
if getattr(parser_module, 'parse_payload', None) is None:
raise PlugablePayloadParserException('Parser {} should have a parse_payload method')
def load_parser(parser_name):
global POST_PROCESSING_PARSERS
try:
parser_module = importlib.import_module("collector.parsers.{}.main".format(parser_name))
verify_parser_module(parser_module)
for parser_classification in parser_module.CLASSIFICATIONS:
if parser_classification in POST_PROCESSING_PARSERS.keys():
raise PlugablePayloadParserException("Parser plugin for class"
" {} is already registered".format(parser_classification))
POST_PROCESSING_PARSERS[parser_classification] = parser_module.parse_payload
app.logger.info(" * Parser: {} registered for class: {}".format(parser_name, parser_classification))
except PlugablePayloadParserException as e:
print(e.str())
except ImportError as ie:
print(ie)
def load_parsers():
pp_parsers = app.config.get('POST_PROCESSING_PARSERS', [])
for pp_parser in pp_parsers:
load_parser(pp_parser)
load_parsers()
# vi: ts=4 et sw=4 sts=4
-48
View File
@@ -1,48 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import unittest
from collector.tests.testcase import (
RecordTestCases,
get_record)
class TestHandler(RecordTestCases):
def test_query_report(self):
rec = get_record()
response = self.client.post('/', headers=rec, data='test')
self.assertTrue(response.status_code == 201)
filters = {
'severity': 1,
}
response = self.client.get('/api/records', query_string=filters)
resp_obj = json.loads(response.data.decode('utf-8'))
self.assertEqual(len(resp_obj['records']), 1)
filters1 = {
'build': '17780',
}
response = self.client.get('/api/records', query_string=filters1)
resp_obj = json.loads(response.data.decode('utf-8'))
self.assertEqual(len(resp_obj['records']), 0)
if __name__ == '__main__':
unittest.main()
# vi: ts=4 et sw=4 sts=4
-173
View File
@@ -1,173 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
import json
from collector.tests.testcase import (
RecordTestCases,
classification,
severity,
kernel_version,
record_version,
machine_id,
host_type,
arch,
build,
timestamp,
tid,
board_name,
cpu_model,
bios_version,
system_name,
payload_version,
event_id,
get_record_v1,
get_record_v2,
get_record_v3,
get_record_v4,)
class TestHandlerRecordV2(RecordTestCases):
"""
Tests record creation for record version 2 and headers validation
"""
@staticmethod
def get_version_records():
return get_record_v2()
def test_record_created(self):
headers = get_record_v2()
data = "hello"
response = self.client.post('/', headers=headers, data=data)
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
json_resp = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_resp['classification'] == headers[classification])
self.assertTrue(str(json_resp['severity']) == str(headers[severity]))
self.assertTrue(json_resp['kernel_version'] == headers[kernel_version])
self.assertTrue(str(json_resp['record_format_version']) == str(headers[record_version]))
self.assertTrue(json_resp['machine_id'] == headers[machine_id])
self.assertTrue(json_resp['machine_type'] == headers[host_type])
self.assertTrue(json_resp['arch'] == headers[arch])
self.assertTrue(json_resp['build'] == headers[build])
self.assertTrue(json_resp['payload'] == data)
def missing_header(self, header, header_name):
headers = get_record_v2()
del headers[header_name]
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 400, response.data.decode('utf-8'))
def test_post_fail_missing_classifiction(self):
self.missing_header('Classification', severity)
def test_post_fail_missing_severity(self):
self.missing_header('Severity', severity)
def test_post_fail_missing_kernel_version(self):
self.missing_header('Kernel-Version', kernel_version)
def test_post_fail_missing_host_type(self):
self.missing_header('Host-Type', host_type)
def test_post_fail_missing_machine_id(self):
self.missing_header('Machine-Id', machine_id)
def test_post_fail_missing_arch(self):
self.missing_header('Arch', arch)
def test_post_fail_missing_build(self):
self.missing_header('Build', build)
def test_post_fail_missing_record_version(self):
self.missing_header('Record-Format-Version', record_version)
class TestHandlerRecordTransitionV2toV3(RecordTestCases):
"""
This test case tests a transition where the client is in record
version 2 though is sending v3 headers, make sure the server
understands the record as v2 and do not have problems creating it
"""
def test_record_created(self):
headers = get_record_v3()
data = "hello"
response = self.client.post('/', headers=headers, data=data)
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
json_resp = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_resp['classification'] == headers[classification])
self.assertTrue(int(json_resp['severity']) == int(headers[severity]))
self.assertTrue(json_resp['kernel_version'] == headers[kernel_version])
self.assertTrue(int(json_resp['record_format_version']) == int(headers[record_version]))
self.assertTrue(json_resp['machine_id'] == headers[machine_id])
self.assertTrue(json_resp['machine_type'] == headers[host_type])
self.assertTrue(json_resp['arch'] == headers[arch])
self.assertTrue(json_resp['build'] == headers[build])
class TestHandlerRecordV3(RecordTestCases):
"""
Test record v3 making sure to validate expected headers for v3
if record version is properly set to 3.
"""
@staticmethod
def get_version_records():
return get_record_v3()
def test_post_record_v3_with_headers_v2(self):
headers = get_record_v2()
headers.update({record_version: 3, })
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 400, response.data.decode('utf-8'))
def test_post_record_v3(self):
headers = get_record_v3()
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
def test_post_missing_cpu_model(self):
self.missing_header('Cpu-Model', cpu_model)
def test_post_missing_board_name(self):
self.missing_header('Board-Name', board_name)
def test_post_missing_bios_version(self):
self.missing_header('Bios-Version', bios_version)
class TestHandlerRecordV4(RecordTestCases):
"""
Test record v4
"""
@staticmethod
def get_version_records():
return get_record_v4()
def test_post_record_v4(self):
headers = get_record_v4()
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
def test_post_missing_eid(self):
self.missing_header('Event-Id', event_id)
if __name__ == '__main__' and __package__ is None:
from os import sys, path
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
unittest.main()
# vi: ts=4 et sw=4 sts=4
-53
View File
@@ -1,53 +0,0 @@
#
# Copyright 2018 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import base64
import unittest
from collector import app
from collector.parsers.demo import main
from collector.report_handler import load_parsers
from collector.tests.testcase import (
RecordTestCases,
get_record_v3,)
class TestCasesParserPlugin(RecordTestCases):
"""
Load parsers programmatically for test
"""
def setUp(self):
RecordTestCases.setUp(self)
app.config['POST_PROCESSING_PARSERS'] = ["demo"]
load_parsers()
class TestParserPlugin(TestCasesParserPlugin):
"""
Simple test to make sure that plugins parsers are working
"""
def test_record_created(self):
headers = get_record_v3()
data = b'Hello World'
_data = base64.b64encode(data)
headers['classification'] = main.CLASSIFICATIONS
response = self.client.post('/', headers=headers, data=_data)
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
if __name__ == '__main__' and __package__ is None:
from os import sys, path
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
unittest.main()
-115
View File
@@ -1,115 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
import time
from collector import db
from flask import current_app
from collector.model import (
app,
Record,
Classification,
Build)
from collector.tests.testcase import (
RecordTestCases,
get_record,)
def get_insert_params(days_old, severity, classification):
record = get_record()
record["classification"] = classification
record["severity"] = severity
db_class = Classification.query.filter_by(classification=record["classification"]).first()
if db_class is None:
db_class = Classification(record["classification"])
db_build = Build.query.filter_by(build=record["build"]).first()
if db_build is None:
db_build = Build(record["build"])
return [
record["machine_id"],
record["host_type"],
record["severity"],
db_class,
db_build,
record["arch"],
record["kernel_version"],
record["record_format_version"],
int(time.time()-3600*24*days_old),
int(time.time()-3600*24*days_old),
record["payload_format_version"],
record["system_name"],
record["board_name"],
record["bios_version"],
record["cpu_model"],
"39cc109a1079df96376693ebc7a0f632",
False,
"Test"
]
class TestPurging(RecordTestCases):
""" Generic object for telemetry record tests """
def setUp(self):
app.testing = True
app.config.from_object('config_local.Testing')
app.config["MAX_DAYS_KEEP_UNFILTERED_RECORDS"] = 5
app.config["PURGE_FILTERED_RECORDS"] = {
"severity": {
1: 1,
4: 0
},
"classification": {
"test/keep/one": 0,
"test/discard/*": 1,
}
}
app.debug = False
self.app_context = app.app_context()
self.app_context.push()
db.init_app(current_app)
db.create_all()
self.client = app.test_client()
def test_purge_delete(self):
Record.create(*get_insert_params(2, 1, "test/discard/one"))
Record.create(*get_insert_params(2, 2, "test/discard/two"))
Record.create(*get_insert_params(2, 2, "test/discard/three"))
Record.create(*get_insert_params(2, 4, "test/discard/two"))
Record.create(*get_insert_params(2, 4, "test/discard/three"))
Record.create(*get_insert_params(6, 2, "test/test/one"))
Record.create(*get_insert_params(2, 1, "test/keep/one"))
self.assertTrue(Record.query.count() == 7)
Record.delete_records()
self.assertTrue(Record.query.count() == 0)
def test_purge_keep(self):
Record.create(*get_insert_params(6, 2, "test/keep/one"))
Record.create(*get_insert_params(3, 2, "test/test/one"))
Record.create(*get_insert_params(6, 4, "test/test/one"))
Record.create(*get_insert_params(2, 4, "test/discard/three"))
Record.create(*get_insert_params(6, 2, "test/test/one"))
Record.create(*get_insert_params(2, 1, "test/keep/one"))
self.assertTrue(Record.query.count() == 6)
Record.delete_records()
self.assertTrue(len(Record.query.all()) == 3)
if __name__ == '__main__' and __package__ is None:
from os import sys, path
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
unittest.main()
-143
View File
@@ -1,143 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
from collector import (
app,
db,
report_handler,)
from flask import current_app
classification = 'classification'
severity = 'severity'
kernel_version = 'kernel_version'
record_version = 'record_format_version'
machine_id = 'machine_id'
host_type = 'host_type'
arch = 'arch'
build = 'build'
timestamp = 'creation_timestamp'
tid = 'X-Telemetry-Tid'
board_name = 'Board-Name'
cpu_model = 'Cpu-Model'
bios_version = 'Bios-Version'
system_name = 'System-Name'
payload_version = 'Payload-Format-Version'
event_id = 'Event-Id'
REQUIRED_HEADERS_V1 = (
'Arch',
'Build',
'Creation-Timestamp',
'Classification',
'Host-Type',
'Kernel-Version',
'Machine-Id',
'Severity',
'Record-Format-Version',
)
def get_record_v1():
return {
arch: 'x86_64',
build: '550',
timestamp: 1483232401,
classification: 'a/b/c',
host_type: 'LenovoT20',
kernel_version: '3.16.4-123.generic',
machine_id: '1234',
severity: 2,
record_version: 1,
}
def get_record_v2():
v2 = get_record_v1()
v2.update({
record_version: 2,
tid: '6907c830-eed9-4ce9-81ae-76daf8d88f0f',
system_name: 'clear-linux-os',
payload_version: 1
})
return v2
def get_record_v3():
v3 = get_record_v2()
v3.update({
record_version: 3,
board_name: 'D54250WYK|Intel Corporation',
cpu_model: 'Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz',
bios_version: 'WYLPT10H.86A.0041.2015.0720.1108',
})
return v3
def get_record_v4():
v4 = get_record_v3()
v4.update({
record_version: 4,
event_id: '39cc109a1079df96376693ebc7a0f632',
})
return v4
def get_record():
return {
"X-Telemetry-TID": "6907c830-eed9-4ce9-81ae-76daf8d88f0f",
"record_format_version": "2",
"severity": "1",
"classification": "org.clearlinux/hello/world",
"machine_id": "clr-linux-avj01",
"creation_timestamp": "1505235249",
"arch": "x86_64",
"host_type": "blank|blank|blank",
"kernel_version": "4.12.5-374.native",
"system_name": "clear-linux-os",
"build": "17700",
"payload_format_version": "1",
"board_name": "D54250WYK|Intel Corporation",
"cpu_model": "Intel(R) Core(TM) i5-4250U CPU @ 1.30GHz",
"bios_version": "WYLPT10H.86A.0041.2015.0720.1108"
}
class RecordTestCases(unittest.TestCase):
""" Generic object for telemetry record tests """
def setUp(self):
app.testing = True
app.config.from_object('collector.config_local.Testing')
app.debug = False
self.app_context = app.app_context()
self.app_context.push()
db.init_app(current_app)
db.create_all()
self.client = app.test_client()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def missing_header(self, header, header_name):
headers = self.get_version_records()
del headers[header_name]
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 400)
-45
View File
@@ -1,45 +0,0 @@
#
# Copyright 2015-2017 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
from collector.tests.testcase import (
RecordTestCases,
get_record_v3,
kernel_version)
class TestHandlerRecordValidation(RecordTestCases):
"""
Test record validation
"""
@staticmethod
def get_version_records():
return get_record_v3()
def test_post_kernel_version_validation_1(self):
headers = get_record_v3()
headers.update({kernel_version: '3.16.generic'})
response = self.client.post('/', headers=headers, data='test')
self.assertTrue(response.status_code == 201, response.data.decode('utf-8'))
if __name__ == '__main__' and __package__ is None:
from os import sys, path
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
unittest.main()
# vi: ts=4 et sw=4 sts=4
-50
View File
@@ -1,50 +0,0 @@
[uwsgi]
#application's base folder
chdir = @@install_path@@collector/
#python module to import
virtualenv = @@install_path@@venv/
module = collector:app
plugins = python3
#socket file's location
socket = @@install_path@@collector/%n.sock
#permissions for the socket file
chmod-socket = 644
processes = 4
threads = 2
#enable thread support.Need to figure out the optimal no of threads.
enable-threads = true
#location of log files
logto = /var/log/uwsgi/%n.log
#maximum size of log file before rotation (100MB)
log-maxsize = 104857600
#backup log file (created after rotation)
log-backupname = /var/log/uwsgi/%n.log.bk
#if request takes more than this parameter(in sec), request will be dropped
harakiri = 60
#respawn processes after serving 5000 requests
max-requests = 5000
#get verbose logs when a process gets stuck
harakiri-verbose = true
#http://uwsgi-docs.readthedocs.org/en/latest/Tracebacker.html#combining-the-tracebacker-with-harakiri
#Traceback is automatically logged during harakiri phase.
py-tracebacker=collectorsocket
#enable stats. Use this for fine-tuning no of processes.
#connect uwsgitop to the stats socket as: uwsgitop /tmp/collectorstats.socket
stats=/tmp/collectorstats.socket
#for asynchronous processing (e.g. updating guilty data for crash records)
spooler = %(chdir)/uwsgi-spool
+6 -26
View File
@@ -23,7 +23,6 @@ DB_PASSWORD=""
NGINX_USER=""
NGINX_GROUP=""
REPO_NAME="telemetrics-backend"
COLLECTOR_INI="collector_uwsgi.ini"
TELEMETRYUI_INI="telemetryui_uwsgi.ini"
SPOOL_DIR="uwsgi-spool"
APT_GET_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::=\"--force-confnew\""
@@ -321,7 +320,6 @@ _stage_from_git() {
cd $path
rm -rf $REPO_NAME
git clone -b $branch $REPO_LOCATION
rm -f $REPO_NAME/collector/collector/config_local.py
rm -f $REPO_NAME/telemetryui/telemetryui/config_local.py
tar -czf backend_local.tar.gz $REPO_NAME
)
@@ -359,7 +357,6 @@ EOF
_get_db_pass() {
local pass
local conf=$REMOTE_APP_DIR/collector/collector/config.py
if [ ! -f $conf ]; then
error "missing config.py file; must run the 'install' action first"
@@ -372,7 +369,7 @@ _get_db_pass() {
exit 1
fi
pass=$(sed -n -e 's|^.*SQLALCHEMY_DATABASE_URI.*postgres://postgres:\(.*\)@localhost/telemdb.*|\1|p' $conf)
pass=$(sed -n -e 's|^.*SQLALCHEMY_DATABASE_URI.*postgres://postgres:\(.*\)@localhost/telemetry.*|\1|p' $conf)
echo "$pass"
}
@@ -449,7 +446,6 @@ _get_flask_key() {
_deploy() {
local repo="/tmp/$REPO_NAME"
local collector_path="$repo/collector"
local telemetryui_path="$repo/telemetryui"
local shared_path="$repo/shared"
local scripts_path="$repo/scripts"
@@ -469,8 +465,7 @@ _deploy() {
mv /tmp/backend_local.tar.gz /tmp/backend.tar.gz
tar -C /tmp/ -xf /tmp/backend.tar.gz
# Finalize configuration for collector and telemetryui
_subst_config "$scripts_path/$COLLECTOR_INI" "$collector_path/"
# Finalize configuration for telemetryui
_subst_config "$scripts_path/$TELEMETRYUI_INI" "$telemetryui_path/"
_subst_config "$scripts_path/uwsgi.conf"
_subst_config "$scripts_path/sites_nginx.conf"
@@ -479,13 +474,8 @@ _deploy() {
_subst_config "$scripts_path/uwsgi.service"
# Finalize configuration for postgres
_subst_config "$collector_path/collector/config.py"
_subst_config "$telemetryui_path/telemetryui/config.py"
# Install collector + spooldir
sudo cp -af $collector_path $REMOTE_APP_DIR/
sudo mkdir -pv $REMOTE_APP_DIR/collector/$SPOOL_DIR
# Install telemetryui + spooldir
sudo cp -af $telemetryui_path $REMOTE_APP_DIR/
sudo mkdir -pv $REMOTE_APP_DIR/telemetryui/$SPOOL_DIR
@@ -500,7 +490,6 @@ _deploy() {
_config_uwsgi_${DISTRO}
# Fix up permissions
sudo chown -R $NGINX_USER:$NGINX_GROUP $REMOTE_APP_DIR/collector
sudo chown -R $NGINX_USER:$NGINX_GROUP $REMOTE_APP_DIR/telemetryui
# Modify existing nginx config
@@ -509,7 +498,6 @@ _deploy() {
# Misc uwsgi config
sudo mkdir -pv /etc/uwsgi/vassals
sudo ln -sf $REMOTE_APP_DIR/telemetryui/telemetryui_uwsgi.ini /etc/uwsgi/vassals/
sudo ln -sf $REMOTE_APP_DIR/collector/collector_uwsgi.ini /etc/uwsgi/vassals/
sudo systemctl enable uwsgi
# Cert configuration
@@ -542,14 +530,14 @@ _set_db_pass() {
}
_drop_db() {
sudo -u postgres dropdb telemdb
sudo -u postgres dropdb telemetry
}
_create_db() {
local scripts_path="/tmp/$REPO_NAME/scripts"
# First, detect if the database already exists. If so, nothing to do here.
sudo -u postgres psql --list --pset format=unaligned | grep -q '^telemdb|'
sudo -u postgres psql --list --pset format=unaligned | grep -q '^telemetry|'
if [ $? -eq 0 ]; then
return 0
fi
@@ -559,20 +547,18 @@ _create_db() {
export DB_PASSWORD
fi
sudo -u postgres createdb telemdb > /dev/null 2>&1
sudo -u postgres createdb telemetry > /dev/null 2>&1
if [ $? -eq 0 ]; then
sudo -u postgres psql template1 -c "ALTER USER postgres with encrypted password '$DB_PASSWORD';"
else
error "failed to create telemdb Postgres database"
error "failed to create telemetry Postgres database"
_drop_db
exit 1
fi
}
_update_db_pass() {
local src_collector_path="/tmp/$REPO_NAME/collector"
local src_telemetryui_path="/tmp/$REPO_NAME/telemetryui"
local dest_collector_path="$REMOTE_APP_DIR/collector"
local dest_telemetryui_path="$REMOTE_APP_DIR/telemetryui"
if [ -z "$DB_PASSWORD" ]; then
@@ -581,15 +567,9 @@ _update_db_pass() {
fi
# Restore the template files from the backups first
if [ -f $src_collector_path/config.py.backup ]; then
mv $src_collector_path/config.py.backup $src_collector_path/config.py
fi
if [ -f $src_telemetryui_path/config.py.backup ]; then
mv $src_telemetryui_path/config.py.backup $src_telemetryui_path/config.py
fi
_subst_config "$src_collector_path/config.py" "$dest_collector_path/"
_subst_config "$src_telemetryui_path/config.py" "$dest_telemetryui_path/"
}
do_install() {
-8
View File
@@ -12,14 +12,6 @@ server {
ssl_protocols TLSv1.2;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDH-ECDSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:AES128-GCM-SHA256:ECDHE-ECDSA-AES128-CCM:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-CCM:AES128-CCM:AES128-SHA256:AES256-CCM:AES256-SHA256:DHE-RSA-AES128-CCM:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-CCM:DHE-RSA-AES256-SHA256:DHE-DSS-AES128-GCM-SHA256:DHE-DSS-AES128-SHA256:DHE-DSS-AES256-SHA256";
location / { try_files $uri @collectorapp; }
location /v2/collector { try_files $uri @collectorapp; }
location @collectorapp {
include uwsgi_params;
uwsgi_pass unix:@@install_path@@collector/collector_uwsgi.sock;
error_log /var/log/nginx/collector_error.log;
}
location /telemetryui { try_files $uri @uiapp; }
location @uiapp {
include uwsgi_params;
+4 -4
View File
@@ -232,12 +232,12 @@ def _process_guilties(args):
crashes = Record.get_new_crash_records(classes=get_backtrace_classes(), id=record_id)
filters = GuiltyBlacklist.get_guilties()
for rec in crashes:
if rec.backtrace:
new_bt = demangle_backtrace(rec.backtrace)
rec.backtrace = new_bt
if rec.payload:
new_bt = demangle_backtrace(rec.payload)
rec.payload = new_bt
# TODO: update the rec.payload field as well
Record.commit_guilty_changes()
g, match = find_guilty(rec.backtrace)
g, match = find_guilty(rec.payload)
if match:
function = g['function']
module = g['module']
+119 -160
View File
@@ -19,7 +19,6 @@ from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql.expression import cast
from sqlalchemy.sql.expression import desc
from sqlalchemy.sql.expression import case
from sqlalchemy.sql import text
from time import time, localtime, strftime, mktime, strptime, gmtime
from distutils.version import LooseVersion
@@ -28,27 +27,6 @@ from . import app
db = SQLAlchemy(app)
class Classification(db.Model):
__tablename__ = 'classification'
id = db.Column(db.Integer, primary_key=True)
classification = db.Column(db.String)
domain = db.Column(db.String)
probe = db.Column(db.String)
def __init__(self, class_str):
self.classification = class_str
class Build(db.Model):
__tablename__ = 'build'
id = db.Column(db.Integer, primary_key=True)
build = db.Column(db.String)
def __init__(self, build_num):
self.build = build_num
class Guilty(db.Model):
__tablename__ = 'guilty'
id = db.Column(db.Integer, primary_key=True)
@@ -99,56 +77,46 @@ class Guilty(db.Model):
class Record(db.Model):
__tablename__ = 'records'
id = db.Column(db.Integer, primary_key=True)
severity = db.Column(db.Integer)
machine = db.Column(db.String, default='')
machine_id = db.Column(db.String, default='')
architecture = db.Column(db.String)
kernel_version = db.Column(db.String, default=0)
os_name = db.Column(db.String)
record_format_version = db.Column(db.Integer, default=0)
payload_format_version = db.Column(db.Integer)
payload = db.Column(db.LargeBinary)
tsp = db.Column(db.Integer)
tsp_server = db.Column(db.Integer)
buildstamp = db.Column(db.String, default='')
backtrace = db.Column(db.String, default='')
guilty = db.Column(db.String, default='')
dupe_of = db.Column(db.Integer, default=0)
dupecount = db.Column(db.Integer, default=0)
dupemaster = db.Column(db.Boolean, default=False)
security = db.Column(db.Boolean, default=False)
hide = db.Column(db.Boolean, default=False)
processed = db.Column(db.Boolean, default=False)
icon = db.Column(db.String, default='')
classification_id = db.Column(db.Integer, db.ForeignKey('classification.id'))
build_id = db.Column(db.Integer, db.ForeignKey('build.id'))
guilty_id = db.Column(db.Integer, db.ForeignKey('guilty.id'))
architecture = db.Column(db.Text)
bios_version = db.Column(db.Text, default='')
board_name = db.Column(db.Text, default='')
build = db.Column(db.Text, nullable=False)
classification = db.Column(db.Text, nullable=False)
cpu_model = db.Column(db.Text, default='')
event_id = db.Column(db.Text, default='')
external = db.Column(db.Boolean, default=False)
board_name = db.Column(db.String, default='')
bios_version = db.Column(db.String, default='')
cpu_model = db.Column(db.String, default='')
event_id = db.Column(db.String, default='')
host_type = db.Column(db.Text, default='')
kernel_version = db.Column(db.Text, default=0)
machine_id = db.Column(db.Text, default='')
payload_version = db.Column(db.Integer)
record_version = db.Column(db.Integer, default=0)
severity = db.Column(db.Integer)
system_name = db.Column(db.Text)
timestamp_client = db.Column(db.Numeric)
timestamp_server = db.Column(db.Numeric, nullable=False)
payload = db.Column(db.Text, nullable=False)
classification = db.relationship('Classification', backref=db.backref('records', lazy='dynamic'), lazy='joined')
build = db.relationship('Build', backref=db.backref('records', lazy='dynamic'), lazy='joined')
processed = db.Column(db.Boolean, default=False)
guilty_id = db.Column(db.Integer, db.ForeignKey('guilty.id'))
guilty = db.Column(db.Text, default='')
guilty = db.relationship('Guilty', backref=db.backref('records', lazy='dynamic'), lazy='joined')
def __init__(self, machine_id, host_type, severity, classification, build, architecture, kernel_version,
record_format_version, ts_capture, ts_reception, payload_format_version, os_name,
record_version, ts_capture, ts_reception, payload_version, system_name,
board_name, bios_version, cpu_model, event_id, external, payload):
self.machine_id = machine_id
self.machine = host_type
self.host_type = host_type
self.architecture = architecture
self.classification = classification
self.backtrace = payload
self.build = build
self.kernel_version = kernel_version
self.record_format_version = record_format_version
self.record_version = record_version
self.severity = severity
self.tsp = ts_capture
self.tsp_server = ts_reception
self.payload_format_version = payload_format_version
self.os_name = os_name
self.timestamp_client = ts_capture
self.timestamp_server = ts_reception
self.payload_version = payload_version
self.system_name = system_name
self.external = external
self.board_name = board_name
self.bios_version = bios_version
@@ -161,7 +129,7 @@ class Record(db.Model):
self.payload = payload.encode('latin-1')
def __repr__(self):
return "<Record(id='{}', class='{}', build='{}', created='{}')>".format(self.id, self.classification, self.build, strftime("%a, %d %b %Y %H:%M:%S", localtime(self.tsp)))
return "<Record(id='{}', class='{}', build='{}', created='{}')>".format(self.id, self.classification, self.build, strftime("%a, %d %b %Y %H:%M:%S", localtime(self.timestamp_client)))
def __str__(self):
return str(self.to_dict())
@@ -170,16 +138,16 @@ class Record(db.Model):
record = {
'id': self.id,
'machine_id': self.machine_id,
'machine_type': self.machine,
'machine_type': self.host_type,
'arch': self.architecture,
'build': self.build.build,
'build': self.build,
'kernel_version': self.kernel_version,
'ts_capture': strftime('%Y-%m-%d %H:%M:%S UTC', gmtime(self.tsp)),
'ts_reception': strftime('%Y-%m-%d %H:%M:%S UTC', gmtime(self.tsp_server)),
'ts_capture': strftime('%Y-%m-%d %H:%M:%S UTC', gmtime(self.timestamp_client)),
'ts_reception': strftime('%Y-%m-%d %H:%M:%S UTC', gmtime(self.timestamp_server)),
'severity': self.severity,
'classification': self.classification.classification,
'record_format_version': self.record_format_version,
'payload': self.backtrace,
'classification': self.classification,
'record_version': self.record_version,
'payload': self.payload,
'board_name': self.board_name,
'bios_version': self.bios_version,
'cpu_model': self.cpu_model,
@@ -193,12 +161,12 @@ class Record(db.Model):
record = [
self.id,
self.external,
self.tsp_server,
self.timestamp_server,
self.severity,
self.classification.classification,
self.build.build,
self.classification,
self.build,
self.machine_id,
self.backtrace
self.payload
]
return record
@@ -208,11 +176,11 @@ class Record(db.Model):
@staticmethod
def create(machine_id, host_type, severity, classification, build, architecture, kernel_version,
record_format_version, ts_capture, ts_reception, payload_format_version, os_name,
record_version, ts_capture, ts_reception, payload_version, system_name,
board_name, bios_version, cpu_model, event_id, external, payload):
try:
record = Record(machine_id, host_type, severity, classification, build, architecture, kernel_version,
record_format_version, ts_capture, ts_reception, payload_format_version, os_name,
record_version, ts_capture, ts_reception, payload_version, system_name,
board_name, bios_version, cpu_model, event_id, external, payload)
db.session.add(record)
db.session.commit()
@@ -226,9 +194,9 @@ class Record(db.Model):
interval_sec=None, ts_capture=None, from_id=None):
records = Record.query
if build is not None:
records = records.join(Record.build).filter_by(build=build)
records = records.filter_by(build=build)
if classification is not None:
records = records.join(Record.classification).filter_by(classification=classification)
records = records.filter_by(classification=classification)
if severity is not None:
records = records.filter(Record.severity == severity)
if machine_id is not None:
@@ -236,15 +204,15 @@ class Record(db.Model):
if from_id is not None:
records = records.filter(Record.id >= from_id)
if ts_capture is not None:
records = records.filter(Record.tsp > ts_capture)
records = records.filter(Record.timestamp_client > ts_capture)
if interval_sec is not None:
current_time = time()
secs_in_past = current_time - interval_sec
# Due to time skew on client systems, delayed sends due to
# spooling, etc, tsp_server works better as the reference
# spooling, etc, timestamp_server works better as the reference
# timestamp.
records = records.filter(Record.tsp_server > secs_in_past)
records = records.filter(Record.timestamp_server > secs_in_past)
records = records.order_by(Record.id.desc())
@@ -259,32 +227,32 @@ class Record(db.Model):
return record
@staticmethod
def filter_records(build, classification, severity, machine_id=None, os_name=None, limit=None, from_date=None,
def filter_records(build, classification, severity, machine_id=None, system_name=None, limit=None, from_date=None,
to_date=None, payload=None, not_payload=None, data_source=None):
records = Record.query
if build is not None:
records = records.join(Record.build).filter_by(build=build)
records = records.filter_by(build=build)
if classification is not None:
if isinstance(classification, list):
records = records.join(Record.classification).filter(Classification.classification.in_(classification))
records = records.filter(Record.classification.in_(classification))
else:
records = records.join(Record.classification).filter(Classification.classification.like(classification))
records = records.filter(Record.classification.like(classification))
if severity is not None:
records = records.filter(Record.severity == severity)
if os_name is not None:
records = records.filter(Record.os_name == os_name)
if system_name is not None:
records = records.filter(Record.system_name == system_name)
if machine_id is not None:
records = records.filter(Record.machine_id == machine_id)
if from_date is not None:
from_date = mktime(strptime(from_date, "%Y-%m-%d"))
records = records.filter(Record.tsp >= from_date)
records = records.filter(Record.timestamp_client >= from_date)
if to_date is not None:
to_date = mktime(strptime(to_date, "%Y-%m-%d"))
records = records.filter(Record.tsp < to_date)
records = records.filter(Record.timestamp_client < to_date)
if payload is not None:
records = records.filter(Record.backtrace.op('~')(payload))
records = records.filter(Record.payload.op('~')(payload))
if not_payload is not None:
records = records.filter(~Record.backtrace.op('~')(not_payload))
records = records.filter(~Record.payload.op('~')(not_payload))
if data_source is not None:
if data_source == "external":
records = records.filter(Record.external == True)
@@ -309,11 +277,10 @@ class Record(db.Model):
age = time() - PURGE_FILTERED_RECORDS[field][name] * 24 * 60 * 60
q = db.session.query(Record)
if field == 'classification':
q = q.join(Record.classification)
q = q.filter(Classification.classification.like(name.replace("*", "%")))
q = q.filter(Record.classification.like(name.replace("*", "%")))
else:
q = q.filter(getattr(Record, field) == name)
q = q.filter(Record.tsp_server < age)
q = q.filter(Record.timestamp_server < age)
if q.all():
count = db.session.query(Record).filter(Record.id.in_([x.id for x in q.all()])).delete(synchronize_session=False)
print("Deleted {} {} records".format(count, name))
@@ -324,13 +291,12 @@ class Record(db.Model):
q = db.session.query(Record.id)
for field in PURGE_FILTERED_RECORDS.keys():
if field == 'classification':
q = q.join(Record.classification)
for classification in PURGE_FILTERED_RECORDS[field].keys():
q = q.filter(~Classification.classification.like(classification.replace("*", "%")))
q = q.filter(~Record.classification.like(classification.replace("*", "%")))
else:
for name in PURGE_FILTERED_RECORDS[field].keys():
q = q.filter(getattr(Record, field) != name)
q = q.filter(Record.tsp_server < unfiltered_age)
q = q.filter(Record.timestamp_server < unfiltered_age)
if q.all():
count = db.session.query(Record).filter(Record.id.in_([x.id for x in q.all()])).delete(synchronize_session=False)
print("Deleted {} old records".format(count))
@@ -342,22 +308,21 @@ class Record(db.Model):
@staticmethod
def get_recordcnts_by_build():
q = db.session.query(Build.build, db.func.count(Record.id)).join(Record.build)
q = q.filter(Build.build.op('~')('^[0-9]+$'))
q = q.group_by(Build.build).order_by(cast(Build.build, db.Integer)).all()
q = db.session.query(Record.build, db.func.count(Record.id))
q = q.filter(Record.build.op('~')('^[0-9]+$'))
q = q.group_by(Record.build).order_by(cast(Record.build, db.Integer)).all()
return q
@staticmethod
def get_builds():
q = db.session.query(Build.build)
q = q.order_by(Build.build)
q = db.session.query(Record.build).distinct()
q = q.order_by(Record.build)
return sorted(q.all(), key=lambda x: LooseVersion(x[0]), reverse=True)
@staticmethod
def get_recordcnts_by_classification():
q = db.session.query(Classification.classification, db.func.count(Record.id).label('total'))
q = q.join(Record.classification)
q = q.group_by(Classification.classification)
q = db.session.query(Record.classification, db.func.count(Record.id).label('total'))
q = q.group_by(Record.classification)
q = q.order_by(desc('total'))
return q.all()
@@ -368,7 +333,7 @@ class Record(db.Model):
@staticmethod
def get_classifications(with_regex=False):
q = db.session.query(Classification.classification)
q = db.session.query(Record.classification).distinct()
if with_regex:
classes = [Record.expand_class(c[0].split('/')) for c in q.all()]
return sorted(set(itertools.chain(*classes)))
@@ -377,7 +342,7 @@ class Record(db.Model):
@staticmethod
def get_os_map():
q = db.session.query(Record.os_name, Build.build).join(Record.build).order_by(Record.os_name).group_by(Record.os_name, Build.build).all()
q = db.session.query(Record.system_name, Record.build).order_by(Record.system_name).group_by(Record.system_name, Record.build).all()
result = {}
for x in q:
result.setdefault(x[0], []).append(x[1])
@@ -385,8 +350,8 @@ class Record(db.Model):
@staticmethod
def get_recordcnts_by_machine_type():
q = db.session.query(Record.machine, db.func.count(Record.id).label('total'))
q = q.group_by(Record.machine)
q = db.session.query(Record.host_type, db.func.count(Record.id).label('total'))
q = q.group_by(Record.host_type)
q = q.order_by(desc('total'))
return q.all()
@@ -397,55 +362,53 @@ class Record(db.Model):
@staticmethod
def get_crashcnts_by_class(classes=None):
q = db.session.query(Classification.classification, db.func.count(Record.id))
q = q.join(Record)
q = db.session.query(Record.classification, db.func.count(Record.id))
if classes:
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Record.classification.in_(classes))
else:
q = q.filter(Classification.classification.like('org.clearlinux/crash/%'))
q = q.group_by(Classification.classification)
q = q.filter(Record.classification.like('org.clearlinux/crash/%'))
q = q.group_by(Record.classification)
return q.all()
@staticmethod
def get_crashcnts_by_build(classes=None):
q = db.session.query(Build.build, db.func.count(Record.id)).join(Record).join(Classification)
q = db.session.query(Record.build, db.func.count(Record.id))
if not classes:
classes = ['org.clearlinux/crash/clr']
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Build.build.op('~')('^[0-9]+$'))
q = q.group_by(Build.build)
q = q.order_by(desc(cast(Build.build, db.Integer)))
q = q.filter(Record.classification.in_(classes))
q = q.filter(Record.build.op('~')('^[0-9]+$'))
q = q.group_by(Record.build)
q = q.order_by(desc(cast(Record.build, db.Integer)))
q = q.limit(10)
return q.all()
@staticmethod
def get_top_crash_guilties(classes=None):
q = db.session.query(Guilty.function, Guilty.module, Build.build, db.func.count(Record.id).label('total'), Guilty.id, Guilty.comment)
q = db.session.query(Guilty.function, Guilty.module, Record.build, db.func.count(Record.id).label('total'), Guilty.id, Guilty.comment)
q = q.join(Record)
q = q.join(Build)
q = q.join(Classification).filter(Record.classification_id == Classification.id)
if not classes:
classes = ['org.clearlinux/crash/clr']
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Build.build.op('~')('^[0-9][0-9]+$'))
q = q.filter(Record.classification.in_(classes))
q = q.filter(Record.build.op('~')('^[0-9][0-9]+$'))
q = q.filter(cast(Record.build, db.Integer) <= 100000)
q = q.filter(Guilty.hide == False)
q = q.group_by(Guilty.function, Guilty.module, Guilty.comment, Guilty.id, Build.build)
q = q.order_by(desc(cast(Build.build, db.Integer)), desc('total'))
q = q.group_by(Guilty.function, Guilty.module, Guilty.comment, Guilty.id, Record.build)
q = q.order_by(desc(cast(Record.build, db.Integer)), desc('total'))
# query for records created in the last week (~ 10 Clear builds)
q = q.filter(Build.build.in_(sorted(tuple(set([x[2] for x in q.all()])), key=lambda x: int(x))[-8:]))
q = q.filter(Record.build.in_(sorted(tuple(set([x[2] for x in q.all()])), key=lambda x: int(x))[-8:]))
interval_sec = 24 * 60 * 60 * 7
current_time = time()
sec_in_past = current_time - interval_sec
q = q.filter(Record.tsp > sec_in_past)
q = q.filter(Record.timestamp_client > sec_in_past)
return q.all()
@staticmethod
def get_new_crash_records(classes=None, id=None):
q = db.session.query(Record).join(Classification)
q = db.session.query(Record)
if not classes:
classes = ['org.clearlinux/crash/clr']
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Record.os_name == 'clear-linux-os')
q = q.filter(Record.classification.in_(classes))
q = q.filter(Record.system_name == 'clear-linux-os')
q = q.filter(Record.processed == False)
if id:
q = q.filter(Record.id == id)
@@ -481,18 +444,17 @@ class Record(db.Model):
@staticmethod
def get_crash_backtraces(classes=None, guilty_id=None, machine_id=None, build=None, most_recent=None, record_id=None):
q = db.session.query(Record.backtrace, Record.id).join(Classification)
q = db.session.query(Record.payload, Record.id)
# Short circuit if we know the record ID
if record_id:
q = q.filter(Record.id == record_id)
return q.first()
if build:
q = q.join(Build)
q = q.filter(Build.build == build)
q = q.filter(Record.build == build)
if not classes:
classes = ['org.clearlinux/crash/clr']
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Record.os_name == 'clear-linux-os')
q = q.filter(Record.classification.in_(classes))
q = q.filter(Record.system_name == 'clear-linux-os')
if guilty_id:
q = q.filter(Record.guilty_id == guilty_id)
if machine_id:
@@ -501,16 +463,16 @@ class Record(db.Model):
interval_sec = 24 * 60 * 60 * int(most_recent)
current_time = time()
sec_in_past = current_time - interval_sec
q = q.filter(Record.tsp > sec_in_past)
q = q.filter(Record.timestamp_client > sec_in_past)
return q.all()
@staticmethod
def reset_processed_records(classes=None, id=None):
q = db.session.query(Record).join(Classification).join(Build)
q = db.session.query(Record)
if not classes:
classes = ['org.clearlinux/crash/clr']
q = q.filter(Classification.classification.in_(classes))
q = q.filter(Record.os_name == 'clear-linux-os')
q = q.filter(Record.classification.in_(classes))
q = q.filter(Record.system_name == 'clear-linux-os')
if id:
q = q.filter(Record.id == id)
records = q.all()
@@ -520,45 +482,44 @@ class Record(db.Model):
@staticmethod
def get_machine_ids_for_guilty(id, most_recent=None):
q = db.session.query(Build.build, Record.machine_id, db.func.count(Record.id).label('total'), Record.guilty_id)
q = q.join(Record)
q = db.session.query(Record.build, Record.machine_id, db.func.count(Record.id).label('total'), Record.guilty_id)
q = q.filter(Record.guilty_id == id)
q = q.filter(Record.os_name == 'clear-linux-os')
q = q.filter(Build.build.op('~')('^[0-9][0-9]+$'))
q = q.group_by(Build.build, Record.machine_id, Record.guilty_id)
q = q.order_by(desc(cast(Build.build, db.Integer)), desc('total'))
q = q.filter(Record.system_name == 'clear-linux-os')
q = q.filter(Record.build.op('~')('^[0-9][0-9]+$'))
q = q.group_by(Record.build, Record.machine_id, Record.guilty_id)
q = q.order_by(desc(cast(Record.build, db.Integer)), desc('total'))
if most_recent:
interval_sec = 24 * 60 * 60 * int(most_recent)
current_time = time()
sec_in_past = current_time - interval_sec
q = q.filter(Record.tsp > sec_in_past)
q = q.filter(Record.timestamp_client > sec_in_past)
return q.all()
@staticmethod
def get_update_msgs():
q = db.session.query(Record.backtrace).join(Classification)
q = q.filter(Classification.classification == "org.clearlinux/swupd-client/update")
q = db.session.query(Record.payload)
q = q.filter(Record.classification == "org.clearlinux/swupd-client/update")
sec_2_weeks = 24 * 60 * 60 * 7
current_time = time()
time_2_weeks_ago = current_time - sec_2_weeks
# query for records created in that last 2 weeks
q = q.filter(Record.tsp > time_2_weeks_ago)
q = q.filter(Record.timestamp_client > time_2_weeks_ago)
return q
@staticmethod
def get_swupd_msgs(most_recent=None):
q = db.session.query(Record.tsp, Record.machine_id, Record.backtrace).join(Classification)
q = q.filter(Classification.classification.like('org.clearlinux/swupd-client/%'))
q = db.session.query(Record.timestamp_client, Record.machine_id, Record.payload)
q = q.filter(Record.classification.like('org.clearlinux/swupd-client/%'))
if most_recent:
interval_sec = 24 * 60 * 60 * int(most_recent)
current_time = time()
sec_in_past = current_time - interval_sec
q = q.filter(Record.tsp > sec_in_past)
q = q.filter(Record.timestamp_client > sec_in_past)
q = q.order_by(desc(Record.tsp))
q = q.order_by(desc(Record.timestamp_client))
return q
@staticmethod
@@ -568,19 +529,18 @@ class Record(db.Model):
internal_expr = case([(Record.external == False, Record.machine_id), ]).label('internal_count')
external_expr = case([(Record.external == True, Record.machine_id), ]).label('external_count')
q = db.session.query(Build.build, db.func.count(db.distinct(internal_expr)), db.func.count(db.distinct(external_expr)))
q = q.join(Record).join(Classification)
q = q.filter(Classification.classification == "org.clearlinux/heartbeat/ping")
q = q.filter(Record.os_name == 'clear-linux-os')
q = q.group_by(Build.build)
q = db.session.query(Record.build, db.func.count(db.distinct(internal_expr)), db.func.count(db.distinct(external_expr)))
q = q.filter(Record.classification == "org.clearlinux/heartbeat/ping")
q = q.filter(Record.system_name == 'clear-linux-os')
q = q.group_by(Record.build)
if most_recent:
interval_sec = 24 * 60 * 60 * int(most_recent)
current_time = time()
sec_in_past = current_time - interval_sec
q = q.filter(Record.tsp > sec_in_past)
q = q.filter(Record.timestamp_client > sec_in_past)
q = q.order_by(cast(Build.build, db.Integer))
q = q.order_by(cast(Record.build, db.Integer))
return q.all()
@@ -656,5 +616,4 @@ class GuiltyBlacklist(db.Model):
db.session.rollback()
raise
# vi: ts=4 et sw=4 sts=4
@@ -0,0 +1,341 @@
"""empty message
Revision ID: 18e790856b53
Revises: cb72058cf4e4
Create Date: 2018-07-20 02:51:24.952915
"""
# revision identifiers, used by Alembic.
revision = '18e790856b53'
down_revision = 'cb72058cf4e4'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
connection = op.get_bind()
connection.execute("""
DROP VIEW IF EXISTS machine_ids_24h CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS top_builds CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS crashes CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS record_stream CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS population_external CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS population_internal CASCADE
""")
with op.batch_alter_table('records') as bop:
bop.alter_column('record_format_version', new_column_name='record_version')
bop.alter_column('payload_format_version', new_column_name='payload_version')
bop.alter_column('tsp', new_column_name='timestamp_client', type_=sa.NUMERIC())
bop.alter_column('tsp_server', new_column_name='timestamp_server', type_=sa.NUMERIC(), nullable=False,
server_default=sa.text("(date_part('epoch'::text, timezone('utc'::text, now())))::integer"))
bop.alter_column('os_name', new_column_name='system_name', type_=sa.TEXT())
bop.alter_column('machine', new_column_name='host_type')
bop.alter_column('payload', type_=sa.TEXT(), nullable=False)
bop.alter_column('bios_version', type_=sa.TEXT())
bop.alter_column('board_name', type_=sa.TEXT())
bop.alter_column('cpu_model', type_=sa.TEXT())
bop.alter_column('event_id', type_=sa.TEXT())
op.add_column('records', sa.Column('build', sa.TEXT(), nullable=False, server_default=''))
op.add_column('records', sa.Column('classification', sa.TEXT(), nullable=False, server_default=''))
op.drop_column('records', 'dupe_of')
op.drop_column('records', 'icon')
op.drop_column('records', 'buildstamp')
op.drop_column('records', 'hide')
op.drop_column('records', 'dupecount')
op.drop_column('records', 'security')
op.drop_column('records', 'dupemaster')
connection.execute("""
UPDATE records SET build = build.build FROM build WHERE build.id = records.build_id
""")
connection.execute("""
UPDATE records SET classification = classification.classification FROM classification WHERE classification.id = records.classification_id
""")
connection.execute("""
UPDATE records SET payload = backtrace
""")
connection.execute("""
CREATE VIEW machine_ids_24h AS
(
SELECT records.machine_id
FROM records
WHERE records.timestamp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision)
GROUP BY records.machine_id
)
""")
connection.execute("""
CREATE VIEW top_builds AS
(
SELECT a.build::numeric AS build_no
FROM (SELECT DISTINCT(records.build) FROM records WHERE records.build::numeric < 100000::numeric) a
ORDER BY (a.build::numeric) DESC
LIMIT 10
)
""")
connection.execute("""
CREATE VIEW crashes AS
(
SELECT g.function,
count(g.function) AS count,
r.build
FROM records r,
guilty g
WHERE r.guilty_id = g.id
AND (r.classification::text = ANY (ARRAY['org.clearlinux/crash/clr'::character varying::text, 'org.clearlinux/kernel/bug'::character varying::text, 'org.clearlinux/kernel/stackoverflow'::character varying::text, 'org.clearlinux/kernel/warning'::character varying::text]))
AND r.build::numeric < 100000::numeric
AND r.build::numeric >
(
SELECT top_builds.build_no
FROM top_builds
OFFSET 9
LIMIT 1
)
GROUP BY g.function, r.build
)
""")
connection.execute("""
CREATE VIEW record_stream AS
(
SELECT to_timestamp(records.timestamp_server::double precision) AS to_timestamp,
records.external,
CASE
WHEN records.external = true THEN 'external'::text
ELSE 'internal'::text
END AS source,
records.machine_id,
records.severity,
records.classification,
records.build
FROM records
ORDER BY records.id DESC
LIMIT 1000
)
""")
connection.execute("""
CREATE VIEW population_external AS
(
SELECT 1 AS value,
c.build AS metric,
c.time
FROM (
SELECT max(a.build) AS build,
max(a.timestamp_server) AS time
FROM (
SELECT records.machine_id,
records.build,
records.timestamp_server AS timestamp_server
FROM records
WHERE records.timestamp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision)
AND records.external = true
AND records.build::numeric < 100000::numeric
AND records.build::numeric > 12000::numeric
) a
GROUP BY a.machine_id
) c
)
""")
connection.execute("""
CREATE VIEW population_internal AS
(
SELECT 1 AS value,
c.build AS metric,
c.time
FROM (
SELECT max(a.build) AS build,
max(a.timestamp_server) AS time
FROM (
SELECT records.machine_id,
records.build,
records.timestamp_server AS timestamp_server
FROM records
WHERE records.timestamp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision)
AND records.external = false
AND records.build::numeric < 100000::numeric
AND records.build::numeric > 12000::numeric
) a
GROUP BY a.machine_id
) c
)
""")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
connection = op.get_bind()
connection.execute("""
DROP VIEW IF EXISTS machine_ids_24h CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS top_builds CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS crashes CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS record_stream CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS population_external CASCADE
""")
connection.execute("""
DROP VIEW IF EXISTS population_internal CASCADE
""")
with op.batch_alter_table('records') as bop:
bop.alter_column('record_version', new_column_name='record_format_version')
bop.alter_column('payload_version', new_column_name='payload_format_version')
bop.alter_column('timestamp_client', new_column_name='tsp', type_=sa.INTEGER())
bop.alter_column('timestamp_server', new_column_name='tsp_server', type_=sa.INTEGER())
bop.alter_column('system_name', new_column_name='os_name')
bop.alter_column('host_type', new_column_name='machine')
#bop.alter_column('payload', type_=sa.LargeBinary(), nullable=True)
op.drop_column('records', 'classification')
op.drop_column('records', 'build')
op.add_column('records', sa.Column('dupemaster', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('security', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('dupecount', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('hide', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('buildstamp', sa.TEXT(), server_default=sa.text("''::text"), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('icon', sa.TEXT(), server_default=sa.text("''::text"), autoincrement=False, nullable=True))
op.add_column('records', sa.Column('dupe_of', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True))
connection.execute("""
ALTER TABLE records ALTER COLUMN payload TYPE BYTEA USING convert_to(payload, 'LATIN1')
""")
connection.execute("""
CREATE VIEW machine_ids_24h AS
(
SELECT records.machine_id
FROM records
WHERE records.tsp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision)
GROUP BY records.machine_id
)
""")
connection.execute("""
CREATE VIEW top_builds AS
(
SELECT build.id,
build.build::numeric AS build_no
FROM build
WHERE build.build::numeric < 100000::numeric
ORDER BY (build.build::numeric) DESC
LIMIT 10
)
""")
connection.execute("""
CREATE VIEW crashes AS
(
SELECT g.function,
count(g.function) AS count,
b.build
FROM records,
guilty g,
build b,
classification c
WHERE records.guilty_id = g.id AND records.classification_id = c.id AND (c.classification::text = ANY (ARRAY['org.clearlinux/crash/clr'::character varying::text, 'org.clearlinux/kernel/bug'::character varying::text, 'org.clearlinux/kernel/stackoverflow'::character varying::text, 'org.clearlinux/kernel/warning'::character varying::text])) AND b.id = records.build_id AND b.build::numeric < 100000::numeric AND b.build::numeric > (( SELECT top_builds.build_no
FROM top_builds
OFFSET 9
LIMIT 1))
GROUP BY g.function, b.build
)
""")
connection.execute("""
CREATE VIEW record_stream AS
(
SELECT to_timestamp(records.tsp_server::double precision) AS to_timestamp,
records.external,
CASE
WHEN records.external = true THEN 'external'::text
ELSE 'internal'::text
END AS source,
records.machine_id,
records.severity,
c.classification,
b.build
FROM records,
classification c,
build b
WHERE c.id = records.classification_id AND b.id = records.build_id
ORDER BY records.id DESC
LIMIT 1000
)
""")
connection.execute("""
CREATE VIEW population_external AS
(
SELECT 1 AS value,
b.build AS metric,
c."time"
FROM ( SELECT max(a.build_id) AS build_id,
max(a.tsp_server) AS "time"
FROM ( SELECT records.machine_id,
records.build_id,
records.tsp_server
FROM records
WHERE records.tsp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision) AND records.external = true) a
GROUP BY a.machine_id) c,
build b
WHERE c.build_id = b.id AND b.build::numeric < 100000::numeric AND b.build::numeric > 12000::numeric
)
""")
connection.execute("""
CREATE VIEW population_internal AS
(
SELECT 1 AS value,
b.build AS metric,
c."time"
FROM ( SELECT max(a.build_id) AS build_id,
max(a.tsp_server) AS "time"
FROM ( SELECT records.machine_id,
records.build_id,
records.tsp_server
FROM records
WHERE records.tsp_server::double precision >= (date_part('epoch'::text, now()) - (24 * 3600)::double precision) AND records.external = false) a
GROUP BY a.machine_id) c,
build b
WHERE c.build_id = b.id AND b.build::numeric < 100000::numeric AND b.build::numeric > 12000::numeric
)
""")
# ### end Alembic commands ###
+1 -1
View File
@@ -21,7 +21,7 @@ class Config(object):
DEBUG = False
TESTING = False
LOG_LEVEL = logging.ERROR
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:@@db_password@@@localhost/telemdb'
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:@@db_password@@@localhost/telemetry'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'handler.log'
WTF_CSRF_ENABLED = True
+2 -2
View File
@@ -25,8 +25,8 @@ class Config(object):
TESTING = False
LOG_LEVEL = logging.ERROR
# If your telemdb database password is not 'postgres', update this line.
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:postgres@localhost/telemdb'
# If your telemetry database password is not 'postgres', update this line.
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:postgres@localhost/telemetry'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'handler.log'
+1 -1
View File
@@ -33,7 +33,7 @@ from wtforms.widgets import (
class RecordFilterForm(FlaskForm):
os_name = SelectField('OS Name', coerce=str, validators=[InputRequired()])
system_name = SelectField('OS Name', coerce=str, validators=[InputRequired()])
build = SelectField('Version', coerce=str, validators=[InputRequired()])
classification = SelectField('Classification', coerce=str, validators=[InputRequired()])
severity = SelectField('Severity', coerce=str, validators=[InputRequired()])
+1 -1
View File
@@ -118,7 +118,7 @@ class Config(object):
DEBUG = True
TESTING = False
LOG_LEVEL = logging.ERROR
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:postgres@localhost/telemdb'
SQLALCHEMY_DATABASE_URI = 'postgres://postgres:postgres@localhost/telemetry'
SQLALCHEMY_TRACK_MODIFICATIONS = True
LOG_FILE = 'telemetryui.log'
WTF_CSRF_ENABLED = True
@@ -29,10 +29,10 @@
<dd><a href="{{ url_for('records', page=1, machine_id=record.machine_id) }}">{{ record.machine_id }}</a></dd>
<dt>Machine Type</dt>
<dd>{{ record.machine }}</dd>
<dd>{{ record.host_type }}</dd>
<dt>Operating System</dt>
<dd>{{ record.os_name }}</dd>
<dd>{{ record.system_name }}</dd>
<dt>Kernel Version</dt>
<dd>{{ record.kernel_version }}</dd>
@@ -50,19 +50,19 @@
<dd>{{ record.cpu_model if record.cpu_model else 'N/A'}}</dd>
<dt>Record Time</dt>
<dd>{{ record.tsp|local_datetime_since }}</dd>
<dd>{{ record.timestamp_client|local_datetime_since }}</dd>
<dt>Server Time</dt>
<dd>{{ record.tsp_server|local_datetime_since }} </dd>
<dd>{{ record.timestamp_server|local_datetime_since }} </dd>
<dt>Build</dt>
<dd><a href="{{ url_for('records', page=1, build=record.build.build) }}">{{ record.build.build }}</a></dd>
<dd><a href="{{ url_for('records', page=1, build=record.build) }}">{{ record.build }}</a></dd>
<dt>Severity</dt>
<dd><a href="{{ url_for('records', page=1, severity=record.severity) }}">{{ record.severity }}</a></dd>
<dt>Classification</dt>
<dd><a id="class_link" href="{{ url_for('records', page=1, classification=record.classification.classification) }}">{{ record.classification.classification }}</a></dd>
<dd><a id="class_link" href="{{ url_for('records', page=1, classification=record.classification) }}">{{ record.classification }}</a></dd>
<dt>Guilty Id</dt>
<dd>
@@ -73,14 +73,14 @@
{%- endif %}
</dd>
<dt>Payload Format Version</dt>
<dd>{{ record.payload_format_version }}</dd>
<dt>Payload Version</dt>
<dd>{{ record.payload_version }}</dd>
<dt>External</dt>
<dd>{{ record.external }}</dd>
<dt>Payload</dt>
<dd> <pre class="prettyprint">{{ record.backtrace }}</pre></dd>
<dd> <pre class="prettyprint">{{ record.payload }}</pre></dd>
</dl>
{% endblock %}
@@ -22,8 +22,8 @@
{{ form.hidden_tag() }}
<div class="form-group row">
<div class="col-sm-3">
{{ form.os_name.label(class="control-label", for="os_name") }}
{{ form.os_name(id="os_name", class="form-control") }}
{{ form.system_name.label(class="control-label", for="system_name") }}
{{ form.system_name(id="system_name", class="form-control") }}
</div>
<div class="col-sm-3" >
{{ form.build.label(class="control-label" , for="build_id") }}
@@ -86,11 +86,11 @@
}
});
}
$(document).on('change', 'select#os_name', function () {
$(document).on('change', 'select#system_name', function () {
$('select#build_id').val("All")
selectBuilds($(this).val());
});
selectBuilds($('select#os_name').val());
selectBuilds($('select#system_name').val());
</script>
{% endblock %}
{% include "records_template.html" %}
@@ -41,14 +41,14 @@
<td>internal</td>
{%- endif %}
<td><span data-toggle="tooltip" data-placement="top" title="{{ rec.machine_id }}"><div class="machine_id">{{ rec.machine_id }}</div></span></td>
<td>{{ rec.tsp_server | timesince }}</td>
<td>{{ rec.timestamp_server | int | timesince }}</td>
<td><span class="label label-{{ (rec.severity | get_severity_label).0 }}">{{ (rec.severity | get_severity_label).1 }}</span></td>
<td class="ellipsize">{{ rec.classification.classification|replace("org.clearlinux/", "") }}</td>
<td>{{ rec.os_name }}</td>
<td>{{ rec.build.build }}</td>
<td class="ellipsize row-payload">{{ rec.backtrace }}</td>
<td class="ellipsize">{{ rec.classification|replace("org.clearlinux/", "") }}</td>
<td>{{ rec.system_name }}</td>
<td>{{ rec.build }}</td>
<td class="ellipsize row-payload">{{ rec.payload }}</td>
<td>
<button id="more_button" type="button" data-toggle="modal" data-target="#myModal" data-whatever="{{ rec.backtrace }}">...</button>
<button id="more_button" type="button" data-toggle="modal" data-target="#myModal" data-whatever="{{ rec.payload }}">...</button>
</td>
</tr>
{% endfor %}
+18 -18
View File
@@ -62,7 +62,7 @@ def records(page=1):
cl = session.get('severity')
os_map = Record.get_os_map()
form.os_name.choices = [("All", "All")] + [(n, n) for n in os_map.keys()]
form.system_name.choices = [("All", "All")] + [(n, n) for n in os_map.keys()]
form.machine_id.default = ""
@@ -81,7 +81,7 @@ def records(page=1):
return render_template('records.html', records=out_records, form=form)
else:
classification = request.form.get('classification')
os_name = request.form.get('os_name')
system_name = request.form.get('system_name')
build = request.form.get('build')
severity = request.form.get('severity')
page_size = request.form.get('page_size')
@@ -94,7 +94,7 @@ def records(page=1):
redirect_args = {
"page_size": page_size if page_size != "" else None,
"os_name": os_name if os_name != "All" else None,
"system_name": system_name if system_name != "All" else None,
"build": build if build != "All" else None,
"severity": severity if severity != "All" else None,
"classification": classification if classification != "All" else None,
@@ -122,7 +122,7 @@ def records(page=1):
elif request.method == 'GET':
classification = request.args.get('classification')
os_name = request.args.get('os_name')
system_name = request.args.get('system_name')
build = request.args.get('build')
severity = request.args.get('severity')
page_size = request.args.get('page_size')
@@ -137,8 +137,8 @@ def records(page=1):
form.classification.default = classification
if build is not None:
form.build.default = build
if os_name is not None:
form.os_name.default = os_name
if system_name is not None:
form.system_name.default = system_name
if severity is not None:
form.severity.default = severity
if machine_id is not None:
@@ -160,7 +160,7 @@ def records(page=1):
page_size = RECORDS_PER_PAGE
elif int(page_size) > MAX_RECORDS_PER_PAGE:
page_size = MAX_RECORDS_PER_PAGE
out_records = Record.filter_records(build, classification, severity, machine_id, os_name=os_name,
out_records = Record.filter_records(build, classification, severity, machine_id, system_name=system_name,
payload=payload, not_payload=not_payload, data_source=data_source,
from_date=from_date, to_date=to_date).paginate(page, int(page_size), False)
return render_template('records.html', records=out_records, form=form, os_map=json.dumps(os_map))
@@ -466,17 +466,17 @@ def mce():
week_rec_map = {}
class_rec_map = {}
for record in records:
week = time.strftime("%U", time.localtime(int(record.tsp)))
week_rec_map.setdefault(record.classification.classification, {}).setdefault(week, 0)
week_rec_map[record.classification.classification][week] += 1
class_rec_map.setdefault(record.classification.classification, 0)
class_rec_map[record.classification.classification] += 1
week = time.strftime("%U", time.localtime(int(record.timestamp_client)))
week_rec_map.setdefault(record.classification, {}).setdefault(week, 0)
week_rec_map[record.classification][week] += 1
class_rec_map.setdefault(record.classification, 0)
class_rec_map[record.classification] += 1
by_machine_id.setdefault(record.machine_id, {"builds": {}, "recordscnt": 0})
by_machine_id[record.machine_id]["builds"].setdefault(record.build.build, 0)
by_machine_id[record.machine_id]["builds"][record.build.build] += 1
by_machine_id[record.machine_id]["builds"].setdefault(record.build, 0)
by_machine_id[record.machine_id]["builds"][record.build] += 1
by_machine_id[record.machine_id]["recordscnt"] += 1
by_builds.setdefault(record.build.build, 0)
by_builds[record.build.build] += 1
by_builds.setdefault(record.build, 0)
by_builds[record.build] += 1
for machine_id in list(by_machine_id.keys()):
builds_cnt = by_machine_id[machine_id]["builds"]
maxcnt = max(list(builds_cnt.values()) + [maxcnt])
@@ -501,8 +501,8 @@ def thermal():
week_rec_map = {}
year_start = time.mktime(time.strptime(current_year + "000", "%Y%U%w"))
for r in records:
if r.tsp >= year_start:
week = str(int((r.tsp - year_start) / 604800) + ((r.tsp - year_start) % 604800 > 0))
if r.timestamp_client >= year_start:
week = str(int((r.timestamp_client - year_start) / 604800) + ((r.timestamp_client - year_start) % 604800 > 0))
if week in week_rec_map:
week_rec_map[week] += 1
else: