Fix pylinters

Change-Id: I3b17d87f821cadab39b9bf4e82043e79f20456ef
This commit is contained in:
okozachenko 2020-06-17 17:22:56 +03:00
parent 26719e3c48
commit ebbb899953
9 changed files with 127 additions and 72 deletions

View File

@ -19,15 +19,24 @@
parent: vexxhost-promote-docker-image parent: vexxhost-promote-docker-image
vars: *atmosphere_images vars: *atmosphere_images
- job:
name: atmosphere:linters:tox
parent: tox-linters
vars:
python_version: 3.7
- project: - project:
check: check:
jobs: jobs:
- atmosphere:linters:tox
- tox-py37 - tox-py37
- atmosphere:image:build - atmosphere:image:build
gate: gate:
jobs: jobs:
- atmosphere:linters:tox
- tox-py37 - tox-py37
- atmosphere:image:upload - atmosphere:image:upload
promote: promote:
jobs: jobs:
- atmosphere:linters:tox
- atmosphere:image:promote - atmosphere:image:promote

View File

@ -12,11 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Ingress
"""
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
from flask import abort from flask import abort
from flask import jsonify from flask import jsonify
from dateutil.relativedelta import relativedelta
from atmosphere.app import create_app from atmosphere.app import create_app
from atmosphere import exceptions from atmosphere import exceptions
@ -27,6 +30,7 @@ blueprint = Blueprint('ingress', __name__)
def init_application(config=None): def init_application(config=None):
"""init_application"""
app = create_app(config) app = create_app(config)
app.register_blueprint(blueprint) app.register_blueprint(blueprint)
return app return app
@ -34,20 +38,17 @@ def init_application(config=None):
@blueprint.route('/v1/event', methods=['POST']) @blueprint.route('/v1/event', methods=['POST'])
def event(): def event():
"""event"""
if request.json is None: if request.json is None:
abort(400) abort(400)
for event in request.json: for event_data in request.json:
print(jsonify(event).get_data(True)) print(jsonify(event_data).get_data(True))
event = utils.normalize_event(event) event_data = utils.normalize_event(event_data)
try: try:
resource = models.Resource.get_or_create(event) models.Resource.get_or_create(event_data)
except (exceptions.EventTooOld, exceptions.IgnoredEvent): except (exceptions.EventTooOld, exceptions.IgnoredEvent):
return '', 202 return '', 202
# TODO(mnaser): Drop this logging eventually...
print(jsonify(event).get_data(True))
print(jsonify(resource.serialize).get_data(True))
return '', 204 return '', 204

View File

@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""App
"""
import os import os
from flask import Flask from flask import Flask
@ -20,6 +23,7 @@ from atmosphere import models
def create_app(config=None): def create_app(config=None):
"""create_app"""
app = Flask(__name__) app = Flask(__name__)
if config is not None: if config is not None:
@ -39,5 +43,3 @@ def create_app(config=None):
models.migrate.init_app(app, models.db, directory=migrations_path) models.migrate.init_app(app, models.db, directory=migrations_path)
return app return app

View File

@ -12,20 +12,27 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Exceptions
"""
from werkzeug import exceptions from werkzeug import exceptions
class UnsupportedEventType(exceptions.BadRequest): class UnsupportedEventType(exceptions.BadRequest):
"""UnsupportedEventType"""
description = 'Unsupported event type' description = 'Unsupported event type'
class MultipleOpenPeriods(exceptions.Conflict): class MultipleOpenPeriods(exceptions.Conflict):
"""MultipleOpenPeriods"""
description = 'Multiple open periods' description = 'Multiple open periods'
class IgnoredEvent(Exception): class IgnoredEvent(Exception):
"""IgnoredEvent"""
description = 'Ignored event type' description = 'Ignored event type'
class EventTooOld(Exception): class EventTooOld(Exception):
pass """EventTooOld"""

View File

@ -12,18 +12,23 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Models
"""
# pylint: disable=R0903
# pylint: disable=W0223
# pylint: disable=no-member
# pylint: disable=not-an-iterable
from datetime import datetime from datetime import datetime
from dateutil.relativedelta import relativedelta
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from sqlalchemy import func
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy.orm import exc as orm_exc from sqlalchemy.orm import exc as orm_exc
from dateutil.relativedelta import relativedelta
from sqlalchemy.types import TypeDecorator from sqlalchemy.types import TypeDecorator
from atmosphere import exceptions from atmosphere import exceptions
from atmosphere import utils
db = SQLAlchemy() db = SQLAlchemy()
migrate = Migrate() migrate = Migrate()
@ -32,11 +37,44 @@ migrate = Migrate()
MONTH_START = relativedelta(day=1, hour=0, minute=0, second=0, microsecond=0) MONTH_START = relativedelta(day=1, hour=0, minute=0, second=0, microsecond=0)
def get_model_type_from_event(event):
"""get_model_type_from_event"""
if event.startswith('compute.instance'):
return Instance, InstanceSpec
if event.startswith('aggregate.'):
raise exceptions.IgnoredEvent
if event.startswith('compute_task.'):
raise exceptions.IgnoredEvent
if event.startswith('compute.'):
raise exceptions.IgnoredEvent
if event.startswith('flavor.'):
raise exceptions.IgnoredEvent
if event.startswith('keypair.'):
raise exceptions.IgnoredEvent
if event.startswith('libvirt.'):
raise exceptions.IgnoredEvent
if event.startswith('metrics.'):
raise exceptions.IgnoredEvent
if event.startswith('scheduler.'):
raise exceptions.IgnoredEvent
if event.startswith('server_group.'):
raise exceptions.IgnoredEvent
if event.startswith('service.'):
raise exceptions.IgnoredEvent
if event == 'volume.usage':
raise exceptions.IgnoredEvent
raise exceptions.UnsupportedEventType
class GetOrCreateMixin: class GetOrCreateMixin:
"""GetOrCreateMixin"""
@classmethod @classmethod
def get_or_create(self, event): def get_or_create(cls, event):
query = self.query_from_event(event) """get_or_create"""
new_instance = self.from_event(event) query = cls.query_from_event(event)
new_instance = cls.from_event(event)
db_instance = query.first() db_instance = query.first()
if db_instance is None: if db_instance is None:
@ -54,6 +92,8 @@ class GetOrCreateMixin:
class Resource(db.Model, GetOrCreateMixin): class Resource(db.Model, GetOrCreateMixin):
"""Resource"""
uuid = db.Column(db.String(36), primary_key=True) uuid = db.Column(db.String(36), primary_key=True)
type = db.Column(db.String(32), nullable=False) type = db.Column(db.String(32), nullable=False)
project = db.Column(db.String(32), nullable=False) project = db.Column(db.String(32), nullable=False)
@ -66,8 +106,9 @@ class Resource(db.Model, GetOrCreateMixin):
} }
@classmethod @classmethod
def from_event(self, event): def from_event(cls, event):
cls, _ = utils.get_model_type_from_event(event['event_type']) """from_event"""
cls, _ = get_model_type_from_event(event['event_type'])
return cls( return cls(
uuid=event['traits']['resource_id'], uuid=event['traits']['resource_id'],
@ -76,8 +117,9 @@ class Resource(db.Model, GetOrCreateMixin):
) )
@classmethod @classmethod
def query_from_event(self, event): def query_from_event(cls, event):
cls, _ = utils.get_model_type_from_event(event['event_type']) """query_from_event"""
cls, _ = get_model_type_from_event(event['event_type'])
return cls.query.filter_by( return cls.query.filter_by(
uuid=event['traits']['resource_id'], uuid=event['traits']['resource_id'],
@ -85,8 +127,9 @@ class Resource(db.Model, GetOrCreateMixin):
).with_for_update() ).with_for_update()
@classmethod @classmethod
def get_or_create(self, event): def get_or_create(cls, event):
resource = super(Resource, self).get_or_create(event) """get_or_create"""
resource = super(Resource, cls).get_or_create(event)
# If the last update is newer than our last update, we assume that # If the last update is newer than our last update, we assume that
# another event has been processed that is newer (so we should ignore # another event has been processed that is newer (so we should ignore
@ -137,6 +180,7 @@ class Resource(db.Model, GetOrCreateMixin):
return resource return resource
def get_open_period(self): def get_open_period(self):
"""get_open_period"""
open_periods = list(filter(lambda p: p.ended_at is None, self.periods)) open_periods = list(filter(lambda p: p.ended_at is None, self.periods))
if len(open_periods) > 1: if len(open_periods) > 1:
raise exceptions.MultipleOpenPeriods raise exceptions.MultipleOpenPeriods
@ -158,12 +202,15 @@ class Resource(db.Model, GetOrCreateMixin):
class Instance(Resource): class Instance(Resource):
"""Instance"""
__mapper_args__ = { __mapper_args__ = {
'polymorphic_identity': 'OS::Nova::Server' 'polymorphic_identity': 'OS::Nova::Server'
} }
@classmethod @classmethod
def is_event_ignored(self, event): def is_event_ignored(cls, event):
"""is_event_ignored"""
vm_state_is_deleted = (event['traits']['state'] == 'deleted') vm_state_is_deleted = (event['traits']['state'] == 'deleted')
no_deleted_at = ('deleted_at' not in event['traits']) no_deleted_at = ('deleted_at' not in event['traits'])
@ -174,21 +221,27 @@ class Instance(Resource):
class BigIntegerDateTime(TypeDecorator): class BigIntegerDateTime(TypeDecorator):
"""BigIntegerDateTime"""
impl = db.BigInteger impl = db.BigInteger
def process_bind_param(self, value, _): def process_bind_param(self, value, _):
"""process_bind_param"""
if value is None: if value is None:
return None return None
assert isinstance(value, datetime) assert isinstance(value, datetime)
return value.timestamp() * 1000 return value.timestamp() * 1000
def process_result_value(self, value, _): def process_result_value(self, value, _):
"""process_result_value"""
if value is None: if value is None:
return None return None
return datetime.fromtimestamp(value / 1000) return datetime.fromtimestamp(value / 1000)
class Period(db.Model): class Period(db.Model):
"""Period"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
resource_uuid = db.Column(db.String(36), db.ForeignKey('resource.uuid'), resource_uuid = db.Column(db.String(36), db.ForeignKey('resource.uuid'),
nullable=False) nullable=False)
@ -200,6 +253,7 @@ class Period(db.Model):
@property @property
def seconds(self): def seconds(self):
"""seconds"""
ended_at = self.ended_at ended_at = self.ended_at
if ended_at is None: if ended_at is None:
ended_at = datetime.now() ended_at = datetime.now()
@ -218,6 +272,8 @@ class Period(db.Model):
class Spec(db.Model, GetOrCreateMixin): class Spec(db.Model, GetOrCreateMixin):
"""Spec"""
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(32)) type = db.Column(db.String(32))
@ -226,16 +282,18 @@ class Spec(db.Model, GetOrCreateMixin):
} }
@classmethod @classmethod
def from_event(self, event): def from_event(cls, event):
_, cls = utils.get_model_type_from_event(event['event_type']) """from_event"""
_, cls = get_model_type_from_event(event['event_type'])
spec = {c.name: event['traits'][c.name] spec = {c.name: event['traits'][c.name]
for c in cls.__table__.columns if c.name != 'id'} for c in cls.__table__.columns if c.name != 'id'}
return cls(**spec) return cls(**spec)
@classmethod @classmethod
def query_from_event(self, event): def query_from_event(cls, event):
_, cls = utils.get_model_type_from_event(event['event_type']) """query_from_event"""
_, cls = get_model_type_from_event(event['event_type'])
spec = {c.name: event['traits'][c.name] spec = {c.name: event['traits'][c.name]
for c in cls.__table__.columns if c.name != 'id'} for c in cls.__table__.columns if c.name != 'id'}
@ -243,6 +301,8 @@ class Spec(db.Model, GetOrCreateMixin):
class InstanceSpec(Spec): class InstanceSpec(Spec):
"""InstanceSpec"""
id = db.Column(db.Integer, db.ForeignKey('spec.id'), primary_key=True) id = db.Column(db.Integer, db.ForeignKey('spec.id'), primary_key=True)
instance_type = db.Column(db.String(255)) instance_type = db.Column(db.String(255))
state = db.Column(db.String(255)) state = db.Column(db.String(255))

View File

@ -44,18 +44,18 @@ class TestNormalizeEvent:
class TestModelTypeDetection: class TestModelTypeDetection:
def test_compute_instance(self): def test_compute_instance(self):
assert utils.get_model_type_from_event('compute.instance.exists') == \ assert models.get_model_type_from_event('compute.instance.exists') == \
(models.Instance, models.InstanceSpec) (models.Instance, models.InstanceSpec)
def test_ignored_resource(self, ignored_event): def test_ignored_resource(self, ignored_event):
with pytest.raises(exceptions.IgnoredEvent) as e: with pytest.raises(exceptions.IgnoredEvent) as e:
utils.get_model_type_from_event(ignored_event) models.get_model_type_from_event(ignored_event)
assert e.value.description == "Ignored event type" assert e.value.description == "Ignored event type"
def test_unknown_resource(self): def test_unknown_resource(self):
with pytest.raises(exceptions.UnsupportedEventType) as e: with pytest.raises(exceptions.UnsupportedEventType) as e:
utils.get_model_type_from_event('foobar') models.get_model_type_from_event('foobar')
assert e.value.code == 400 assert e.value.code == 400
assert e.value.description == "Unsupported event type" assert e.value.description == "Unsupported event type"

View File

@ -12,14 +12,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""Utils
"""
from ceilometer.event import models as ceilometer_models from ceilometer.event import models as ceilometer_models
from dateutil import parser from dateutil import parser
from atmosphere import exceptions
from atmosphere import models
def normalize_event(event): def normalize_event(event):
"""normalize_event"""
event['generated'] = parser.parse(event['generated']) event['generated'] = parser.parse(event['generated'])
event['traits'] = { event['traits'] = {
k: ceilometer_models.Trait.convert_value(t, v) k: ceilometer_models.Trait.convert_value(t, v)
@ -27,34 +29,3 @@ def normalize_event(event):
} }
return event return event
def get_model_type_from_event(event):
if event.startswith('compute.instance'):
return models.Instance, models.InstanceSpec
if event.startswith('aggregate.'):
raise exceptions.IgnoredEvent
if event.startswith('compute_task.'):
raise exceptions.IgnoredEvent
if event.startswith('compute.'):
raise exceptions.IgnoredEvent
if event.startswith('flavor.'):
raise exceptions.IgnoredEvent
if event.startswith('keypair.'):
raise exceptions.IgnoredEvent
if event.startswith('libvirt.'):
raise exceptions.IgnoredEvent
if event.startswith('metrics.'):
raise exceptions.IgnoredEvent
if event.startswith('scheduler.'):
raise exceptions.IgnoredEvent
if event.startswith('server_group.'):
raise exceptions.IgnoredEvent
if event.startswith('service.'):
raise exceptions.IgnoredEvent
if event == 'volume.usage':
raise exceptions.IgnoredEvent
raise exceptions.UnsupportedEventType

View File

@ -2,6 +2,8 @@ before_after
flake8 flake8
freezegun freezegun
pylint pylint
pylint-flask
pylint-flask-sqlalchemy
pytest pytest
pytest-cov pytest-cov
pytest-flask pytest-flask

View File

@ -21,8 +21,11 @@ commands = {posargs}
[testenv:linters] [testenv:linters]
commands = commands =
pylint atmosphere pylint atmosphere \
flake8 atmosphere --load-plugins pylint_flask,pylint_flask_sqlalchemy \
--ignore migrations,tests
flake8 atmosphere \
--exclude .tox,atmosphere/migrations,atmosphere/tests
[testenv:docs] [testenv:docs]
deps = deps =