Add Kosmos Objects lib

Change-Id: I3a188e6e326f61695e549ba99c8ff82bcbe2e215
This commit is contained in:
Graham Hayes 2015-12-10 16:47:48 +00:00
parent 8cc34a7d45
commit 5f61bbd440
19 changed files with 1238 additions and 7 deletions

8
.gitignore vendored
View File

@ -28,7 +28,7 @@ pip-log.txt
nosetests.xml
.testrepository
.venv
cover/*
# Translations
*.mo
@ -52,3 +52,9 @@ ChangeLog
*~
.*.swp
.*sw?
.idea
*.ipynb
.ipynb_checkpoints/*
*.sublime-*
# Flake8 Results
flake8_results.html

View File

@ -0,0 +1,23 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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.
def register_all():
__import__('kosmos.objects.pool_member_parameter')
__import__('kosmos.objects.pool_member')
__import__('kosmos.objects.pool')
__import__('kosmos.objects.monitor_parameter')
__import__('kosmos.objects.monitor')
__import__('kosmos.objects.base')
__import__('kosmos.objects.load_balancer')

63
kosmos/objects/base.py Normal file
View File

@ -0,0 +1,63 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 oslo_log import log as logging
from oslo_versionedobjects import base as ovoo_base
from kosmos.objects import fields
from kosmos import objects
LOG = logging.getLogger('kosmos')
class VersionedObjectRegistry(ovoo_base.VersionedObjectRegistry):
def registration_hook(self, cls, index):
setattr(objects, cls.obj_name(), cls)
class KosmosObject(ovoo_base.VersionedObject):
"""Base class and object factory.
This forms the base of all objects that can be remoted or instantiated
via RPC. Simply defining a class that inherits from this base class
will make it remotely instantiatable. Objects should implement the
necessary "get" classmethod routines as well as "save" object methods
as appropriate.
"""
OBJ_PROJECT_NAMESPACE = 'kosmos'
DB_TABLE = ''
class KosmosOwnedObject(object):
"""Mixin class for objects owned by users.
This adds the fields that we use in common for most object ownership.
"""
fields = {
'project_id': fields.StringField(),
'domain_id': fields.StringField(),
}
class KosmosPersistentObject(object):
"""Mixin class for Persistent objects.
This adds the fields that we use in common for most persistent objects.
"""
fields = {
'id': fields.UUIDField(read_only=True),
'version': fields.IntegerField(read_only=True),
'created_at': fields.DateTimeField(nullable=True),
'updated_at': fields.DateTimeField(nullable=True),
'deleted_at': fields.DateTimeField(nullable=True)
}

194
kosmos/objects/fields.py Normal file
View File

@ -0,0 +1,194 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos._i18n import _
import re
import uuid as uuid_tools
from oslo_versionedobjects import fields
# Import field errors from oslo.versionedobjects
KeyTypeError = fields.KeyTypeError
ElementTypeError = fields.ElementTypeError
# Import fields from oslo.versionedobjects
BooleanField = fields.BooleanField
UnspecifiedDefault = fields.UnspecifiedDefault
IntegerField = fields.IntegerField
FloatField = fields.FloatField
StringField = fields.StringField
EnumField = fields.EnumField
DateTimeField = fields.DateTimeField
DictOfStringsField = fields.DictOfStringsField
DictOfNullableStringsField = fields.DictOfNullableStringsField
DictOfIntegersField = fields.DictOfIntegersField
ListOfStringsField = fields.ListOfStringsField
SetOfIntegersField = fields.SetOfIntegersField
ListOfSetsOfIntegersField = fields.ListOfSetsOfIntegersField
ListOfDictOfNullableStringsField = fields.ListOfDictOfNullableStringsField
DictProxyField = fields.DictProxyField
ObjectField = fields.ObjectField
ListOfObjectsField = fields.ListOfObjectsField
# Ripped from designate/schema/format.py
RE_ZONENAME = r'^(?!.{255,})(?:(?!\-)[A-Za-z0-9_\-]{1,63}(?<!\-)\.)+\Z'
RE_HOSTNAME = r'^(?!.{255,})(?:(?:^\*|(?!\-)[A-Za-z0-9_\-]{1,63})(?<!\-)\.)+\Z'
# TODO(graham): Remove this when https://review.openstack.org/#/c/250493
# merges
class UUID(fields.FieldType):
def coerce(self, obj, attr, value):
msg = _("%(value)s is not a valid UUID for %(attr)s") % {
'attr': attr,
'value': value
}
try:
uuid_tools.UUID(value)
except ValueError:
raise ValueError(msg)
return str(value)
class UUIDField(fields.AutoTypedField):
AUTO_TYPE = UUID()
class DNSZoneName(StringField):
def coerce(self, obj, attr, value):
if not re.match(RE_ZONENAME, value):
msg = _("'%s' is not a valid DNS Zone name") % value
raise ValueError(msg)
return super(DNSZoneName, self).coerce(obj, attr, value)
class DNSFQDN(StringField):
def coerce(self, obj, attr, value):
if not re.match(RE_HOSTNAME, value):
msg = _("'%s' is not valid a valid DNS FQDN") % value
raise ValueError(msg)
return super(DNSFQDN, self).coerce(obj, attr, value)
class PreDefinedEnumType(EnumField):
_TYPES = ()
_msg = _("%(value)s is not a valid choice, choose from %(options)r")
def __init__(self, **kwargs):
super(PreDefinedEnumType, self).__init__(self._TYPES, **kwargs)
def coerce(self, obj, attr, value):
try:
return super(PreDefinedEnumType, self).coerce(obj, attr, value)
except ValueError:
msg = self._msg % {"value": value, "options": self._TYPES}
raise ValueError(msg)
class PoolMemberType(PreDefinedEnumType):
IP = 'IP'
NEUTRON_LBAAS_V2 = 'NEUTRON_LBAAS_V2'
NEUTRON_PORT = 'NEUTRON_PORT'
# TODO(graham): Dynamically Load this list of types from config + plugins
_TYPES = (IP, NEUTRON_LBAAS_V2, NEUTRON_PORT)
class MonitorType(PreDefinedEnumType):
TCP = 'TCP'
UDP = 'UDP'
ICMP = 'ICMP'
HTTP = 'HTTP'
HTTPS = 'HTTPS'
SSH = 'SSH'
# TODO(graham): Dynamically Load this list of types from config + plugins
_TYPES = (TCP, UDP, ICMP, HTTP, HTTPS, SSH)
_msg = _("%(value)s is not a valid Monitor Type, choose from %(options)r")
class StateMachineEnforce(object):
# This is dict of states, that have dicts of states an object is
# allowed to transition to
ALLOWED_TRANSITIONS = {}
def coerce(self, obj, attr, value):
super(StateMachineEnforce, self).coerce(obj, attr, value)
msg = _("%(object)s's are not allowed transition out of %(value)s "
"state")
# olso.versionedobjects do not create the field until it is first used
try:
current_value = getattr(obj, attr)
except NotImplementedError:
return value
if current_value in self.ALLOWED_TRANSITIONS:
if value in self.ALLOWED_TRANSITIONS[current_value]:
return value
else:
msg = _(
"%(object)s's are not allowed transition out of "
"'%(current_value)s' state to '%(value)s' state, choose "
"from %(options)r")
msg = msg % {
'object': obj.obj_name(),
'current_value': current_value,
'value': value,
'options': [x for x in self.ALLOWED_TRANSITIONS[current_value]]
}
raise ValueError(msg)
class KosmosActions(StateMachineEnforce, PreDefinedEnumType):
CREATE = 'create'
UPDATE = 'update'
DELETE = 'delete'
NONE = 'none'
ALLOWED_TRANSITIONS = {
CREATE: {
NONE,
UPDATE
},
UPDATE: {
NONE
},
NONE: {
UPDATE,
DELETE
},
DELETE: {
NONE
}
}
_TYPES = (CREATE, UPDATE, DELETE, NONE)
_msg = _("%(value)s is not a valid Action, choose from %(options)r")

View File

@ -0,0 +1,68 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos._i18n import _
from kosmos.objects import base
from kosmos.objects import fields
class LoadBalancerStatus(fields.StateMachineEnforce,
fields.PreDefinedEnumType):
ACTIVE = 'active'
PENDING = 'pending'
ERROR = 'error'
DELETED = 'deleted'
ALLOWED_TRANSITIONS = {
ACTIVE: {
ERROR,
PENDING,
DELETED
},
PENDING: {
ACTIVE,
ERROR,
DELETED
},
ERROR: {
PENDING,
ACTIVE,
DELETED
},
DELETED: {}
}
_TYPES = (ACTIVE, PENDING, ERROR, DELETED)
_msg = _("'%(value)s' is not a valid status, choose from %(options)r")
@base.VersionedObjectRegistry.register
class LoadBalancer(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'name': fields.StringField(),
'description': fields.StringField(nullable=True),
'fqdn': fields.DNSFQDN(),
'zone_name': fields.DNSZoneName(),
'flavor': fields.UUIDField(),
'appliance_id': fields.StringField(),
'pool': fields.ObjectField('Pool'),
'status': LoadBalancerStatus(),
'action': fields.KosmosActions(),
}

68
kosmos/objects/monitor.py Normal file
View File

@ -0,0 +1,68 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos._i18n import _
from kosmos.objects import base
from kosmos.objects import fields
class MonitorStatus(fields.StateMachineEnforce, fields.PreDefinedEnumType):
ACTIVE = 'active'
PENDING = 'pending'
ERROR = 'error'
DELETED = 'deleted'
ALLOWED_TRANSITIONS = {
ACTIVE: {
ERROR,
PENDING,
DELETED
},
PENDING: {
ACTIVE,
ERROR,
DELETED
},
ERROR: {
PENDING,
ACTIVE,
DELETED
},
DELETED: {}
}
_TYPES = (ACTIVE, PENDING, ERROR, DELETED)
_msg = _("'%(value)s' is not a valid status, choose from %(options)r")
@base.VersionedObjectRegistry.register
class Monitor(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'name': fields.StringField(),
'description': fields.StringField(nullable=True),
'type': fields.MonitorType(),
'target': fields.StringField(),
'auth': fields.BooleanField(),
'parameters': fields.ListOfObjectsField(
'MonitorParameter', default=[]),
'status': MonitorStatus(),
'action': fields.KosmosActions(),
}

View File

@ -0,0 +1,28 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos.objects import base
from kosmos.objects import fields
@base.VersionedObjectRegistry.register
class MonitorParameter(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'key': fields.StringField(nullable=False),
'value': fields.StringField(nullable=True),
}

79
kosmos/objects/pool.py Normal file
View File

@ -0,0 +1,79 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos._i18n import _
from kosmos.objects import base
from kosmos.objects import fields
class PoolStatus(fields.StateMachineEnforce, fields.PreDefinedEnumType):
ACTIVE = 'active'
PENDING = 'pending'
DEGRADED = 'degraded'
DOWN = 'down'
ERROR = 'error'
DELETED = 'deleted'
ALLOWED_TRANSITIONS = {
ACTIVE: {
DEGRADED,
DOWN,
ERROR,
PENDING,
DELETED
},
PENDING: {
ACTIVE,
ERROR,
DELETED
},
DEGRADED: {
ERROR,
ACTIVE,
DELETED
},
DOWN: {
ACTIVE,
ERROR,
DELETED
},
ERROR: {
PENDING,
ACTIVE,
DELETED
},
DELETED: {}
}
_TYPES = (ACTIVE, PENDING, DEGRADED, DOWN, ERROR, DELETED)
_msg = _("'%(value)s' is not a valid status, choose from %(options)r")
@base.VersionedObjectRegistry.register
class Pool(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'name': fields.StringField(),
'description': fields.StringField(nullable=True),
'members': fields.ListOfObjectsField(
'PoolMember',
default=[]),
'status': PoolStatus(),
'action': fields.KosmosActions(),
}

View File

@ -0,0 +1,79 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 kosmos._i18n import _
from kosmos.objects import base
from kosmos.objects import fields
class PoolMemberStatus(fields.StateMachineEnforce, fields.PreDefinedEnumType):
ACTIVE = 'active'
PENDING = 'pending'
DEGRADED = 'degraded'
DOWN = 'down'
ERROR = 'error'
DELETED = 'deleted'
ALLOWED_TRANSITIONS = {
ACTIVE: {
DEGRADED,
DOWN,
ERROR,
PENDING,
DELETED
},
PENDING: {
ACTIVE,
ERROR,
DELETED
},
DEGRADED: {
ERROR,
ACTIVE,
DELETED
},
DOWN: {
ACTIVE,
ERROR,
DELETED
},
ERROR: {
PENDING,
ACTIVE,
DELETED
},
DELETED: {}
}
_TYPES = (ACTIVE, PENDING, DEGRADED, DOWN, ERROR, DELETED)
_msg = _("'%(value)s' is not a valid status, choose from %(options)r")
@base.VersionedObjectRegistry.register
class PoolMember(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'name': fields.StringField(),
'description': fields.StringField(nullable=True),
'type': fields.PoolMemberType(nullable=False),
'parameters': fields.ListOfObjectsField(
'PoolMemberParameter',
default=[]),
'status': PoolMemberStatus(),
'action': fields.KosmosActions(),
}

View File

@ -0,0 +1,30 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 oslo_versionedobjects.base import VersionedObjectRegistry
from kosmos.objects import base
from kosmos.objects import fields
@VersionedObjectRegistry.register
class PoolMemberParameter(base.KosmosObject, base.KosmosOwnedObject,
base.KosmosPersistentObject):
VERSION = '1.0'
fields = {
'key': fields.StringField(nullable=False),
'value': fields.StringField(nullable=True),
}

View File

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# Copyright 2015-2016 Hewlett Packard Enterprise Development LP
#
# 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 kosmos.tests.unit import base as test
from kosmos import objects
class TestCase(test.TestCase):
def setUp(self):
objects.register_all()
super(TestCase, self).setUp()
"""Test case base class for all unit tests."""
@staticmethod
def compare_objects(test_case, obj_1, obj_2):
for field, value in obj_1.items():
test.assertEqual(getattr(obj_1, field), getattr(obj_2, field))

View File

@ -0,0 +1,40 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 oslo_versionedobjects import fixture
from kosmos.tests.unit.objects import base as test
from kosmos.objects import base
# NOTE: The hashes in this list should only be changed if they come with a
# corresponding version bump in the affected objects.
object_data = {
'LoadBalancer': '1.0-06538794c851e034ece73694cc4737ad',
'Monitor': '1.0-1e3dd244093ce14b4b8079ae4deea343',
'MonitorParameter': '1.0-5fae2adca64543db6c854f6418de7089',
'Pool': '1.0-fb284fb3826c2c984494a319f91d04b1',
'PoolMember': '1.0-0586ac99722f74080988aa49113452de',
'PoolMemberParameter': '1.0-5fae2adca64543db6c854f6418de7089'
}
class TestObjectVersions(test.TestCase):
def test_versions(self):
checker = fixture.ObjectVersionChecker(
base.VersionedObjectRegistry.obj_classes())
expected, actual = checker.test_hashes(object_data)
self.assertEqual(expected, actual,
'Some objects have changed; please make sure the '
'versions have been bumped, and then update their '
'hashes in the object_data map in this test module.')

View File

@ -0,0 +1,226 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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.
"""
test_kosmos
----------------------------------
Tests for `objects` module.
"""
import uuid
import testtools
from oslo_versionedobjects import exception
from kosmos.tests.unit.objects import base as test
from kosmos import objects
import kosmos.objects.fields as kosmos_fields
class TestPreDefinedEnumType(kosmos_fields.PreDefinedEnumType):
_TYPES = (
'Galactica',
'Pegasus',
'Athena',
'Atlantia',
'Columbia',
'Erasmus',
'Night Flight',
'Solaria',
'Triton',
'Uned',
'Universal',
'Valkyrie',
'Yashuman',
)
@objects.base.VersionedObjectRegistry.register_if(False)
class TestObject(objects.base.KosmosObject):
fields = {
'text': kosmos_fields.StringField(),
'uuid': kosmos_fields.UUIDField(),
'int': kosmos_fields.IntegerField(),
'read_only': kosmos_fields.StringField(read_only=True),
'dns_zone_name': kosmos_fields.DNSZoneName(),
'dns_fqdn': kosmos_fields.DNSFQDN(),
'pool_member_type': kosmos_fields.PoolMemberType(),
'monitor_type': kosmos_fields.MonitorType(),
'enum': TestPreDefinedEnumType()
}
@objects.base.VersionedObjectRegistry.register_if(False)
class TestOwnedObject(objects.base.KosmosObject,
objects.base.KosmosOwnedObject):
pass
@objects.base.VersionedObjectRegistry.register_if(False)
class TestPersistentObject(objects.base.KosmosObject,
objects.base.KosmosPersistentObject):
pass
@objects.base.VersionedObjectRegistry.register_if(False)
class TestPersistentOwnedObject(objects.base.KosmosObject,
objects.base.KosmosPersistentObject,
objects.base.KosmosOwnedObject):
pass
class TestKosmosObjectsBase(test.TestCase):
def test_basic(self):
test_object = TestObject()
self.assertEqual(test_object.OBJ_PROJECT_NAMESPACE, 'kosmos')
def test_owned_mixin(self):
test_object = TestOwnedObject()
set_fields = [x for x in test_object.fields.keys()]
required_fields = [
'project_id',
'domain_id'
]
set_fields.sort()
required_fields.sort()
self.assertEqual(required_fields, set_fields)
def test_persistant_mixin(self):
test_object = TestPersistentObject()
set_fields = [x for x in test_object.fields.keys()]
required_fields = [
'id',
'version',
'created_at',
'updated_at',
'deleted_at'
]
set_fields.sort()
required_fields.sort()
self.assertEqual(required_fields, set_fields)
def test_persistant_owned_mixin(self):
test_object = TestPersistentOwnedObject()
set_fields = [x for x in test_object.fields.keys()]
required_fields = [
'id',
'version',
'created_at',
'updated_at',
'deleted_at',
'project_id',
'domain_id'
]
set_fields.sort()
required_fields.sort()
self.assertEqual(required_fields, set_fields)
def test_to_dict(self):
test_object = TestPersistentOwnedObject()
test_object = TestObject()
test_object.text = 'test_text'
test_object.uuid = '77c1e75d-ba52-4ec9-a0a8-9d61bfb85407'
test_object.int = 42
test_object.dns_zone_name = 'dns.zone.tld.'
test_object.dns_fqdn = '*.dns.zone.tld.'
test_object.enum = 'Galactica'
test_object.pool_member_type = 'IP'
test_object.monitor_type = 'HTTP'
test_object.obj_reset_changes()
expected_output = {
'versioned_object.name': 'TestObject',
'versioned_object.namespace': 'kosmos',
'versioned_object.data': {
'int': 42,
'dns_fqdn': '*.dns.zone.tld.',
'dns_zone_name': 'dns.zone.tld.',
'text': 'test_text',
'uuid': '77c1e75d-ba52-4ec9-a0a8-9d61bfb85407',
'monitor_type': 'HTTP',
'enum': 'Galactica',
'pool_member_type': 'IP'
},
'versioned_object.version': '1.0'
}
self.assertDictEqual(test_object.obj_to_primitive(), expected_output)
class TestKosmosObjectsFields(test.TestCase):
def test_basic(self):
test_object = TestObject()
test_object.text = 'test_text'
test_object.uuid = str(uuid.uuid4())
test_object.int = 42
test_object.dns_zone_name = 'dns.zone.tld.'
test_object.dns_fqdn = '*.dns.zone.tld.'
def test_invalid_data(self):
test_object = TestObject()
with testtools.ExpectedException(ValueError):
test_object.text = TestOwnedObject()
with testtools.ExpectedException(ValueError):
test_object.uuid = 'fake_uuid'
with testtools.ExpectedException(ValueError):
test_object.int = 'one'
with testtools.ExpectedException(ValueError):
test_object.dns_zone_name = 'dns.zone.tld'
with testtools.ExpectedException(ValueError):
test_object.dns_fqdn = 'name'
def test_read_only(self):
test_object = TestObject()
# Set initial value - 'read_only' is write once, then read only
test_object.read_only = 'Initial Value'
with testtools.ExpectedException(exception.ReadOnlyFieldError):
test_object.read_only = 'Updated Value'
self.assertEqual(test_object.read_only, 'Initial Value')
def test_enums_base(self):
test_object = TestObject()
test_object.enum = 'Galactica'
test_object.enum = 'Athena'
with testtools.ExpectedException(ValueError):
test_object.enum = 'Enterprise'
def test_enums_pool_member(self):
test_object = TestObject()
test_object.pool_member_type = 'IP'
test_object.pool_member_type = 'NEUTRON_PORT'
with testtools.ExpectedException(ValueError):
test_object.pool_member_type = 'AMAZON_ELB'
def test_enums_monitor_type(self):
test_object = TestObject()
test_object.monitor_type = 'TCP'
test_object.monitor_type = 'HTTP'
with testtools.ExpectedException(ValueError):
test_object.monitor_type = 'XMPP'

View File

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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.
"""
test_kosmos
----------------------------------
Tests for `kosmos_objects` `Pool` class.
"""
from kosmos.tests.unit.objects import base as test
from kosmos import objects
import kosmos.objects.fields as kosmos_fields
class TestKosmosObjectsPool(test.TestCase):
def test_basic(self):
test_object = objects.pool.Pool()
test_object = objects.pool.Pool()
test_object.id = '568004ac-e41c-43bc-8a42-c284a7eaea25'
test_object.name = "Pool Name"
test_object.description = "Pool Description"
test_object.members = [
objects.base.KosmosObject.obj_class_from_name(
'PoolMember', '1.0')()
]
test_object.members[0].id = 'b47d8499-de07-4344-a9e1-9773375a66e0'
test_object.members[0].name = "Pool Member Name"
test_object.members[0].description = "Pool Member Description"
test_object.members[0].type = kosmos_fields.PoolMemberType.IP
test_object.members[0].parameters = [
objects.base.KosmosObject.obj_class_from_name(
'PoolMemberParameter', '1.0'
)()
]
test_object.members[0].parameters[0].key = 'address'
test_object.members[0].parameters[0].value = '10.0.0.1'
test_object.obj_reset_changes()
self.assertEqual(test_object.OBJ_PROJECT_NAMESPACE, 'kosmos')
def test_from_dict(self):
test_input = {
'versioned_object.namespace': 'kosmos',
'versioned_object.data': {
'id': '568004ac-e41c-43bc-8a42-c284a7eaea25',
'name': 'Pool Name',
'description': 'Pool Description',
'members': [
{
'versioned_object.namespace': 'kosmos',
'versioned_object.data': {
'id': 'b47d8499-de07-4344-a9e1-9773375a66e0',
'type': 'IP',
'name': 'Pool Member Name',
'description': 'Pool Member Description',
'parameters': [
{
'versioned_object.namespace': 'kosmos',
'versioned_object.data': {
'key': 'address',
'value': '10.0.0.1'
},
'versioned_object.version': '1.0',
'versioned_object.name':
'PoolMemberParameter',
}
]
},
'versioned_object.version': '1.0',
'versioned_object.name': 'PoolMember',
}
]
},
'versioned_object.version': '1.0',
'versioned_object.name': 'Pool',
}
objects.base.KosmosObject.obj_from_primitive(test_input)

View File

@ -0,0 +1,103 @@
# Copyright 2015 Hewlett Packard Enterprise Development LP
#
# 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 testtools
from kosmos.tests.unit.objects import base as test
from kosmos import objects
import kosmos.objects.fields as kosmos_fields
class TestStateMachineField(kosmos_fields.StateMachineEnforce,
kosmos_fields.PreDefinedEnumType):
ACTIVE = 'active'
PENDING = 'pending'
DEGRADED = 'degraded'
DOWN = 'down'
ERROR = 'error'
ALLOWED_TRANSITIONS = {
# This is dict of states, that have dicts of states an object is
# allowed to transition to
ACTIVE: {
DEGRADED,
DOWN,
ERROR,
PENDING
},
PENDING: {
ACTIVE,
ERROR
},
DEGRADED: {
ERROR,
DOWN,
ACTIVE
},
DOWN: {
ACTIVE,
ERROR
},
ERROR: {
PENDING,
ACTIVE
}
}
_TYPES = (
ACTIVE,
PENDING,
DEGRADED,
DOWN,
ERROR
)
@objects.base.VersionedObjectRegistry.register_if(False)
class StateMachineTestObject(objects.base.KosmosObject):
VERSION = '1.0'
fields = {
'status': TestStateMachineField()
}
class TestKosmosObjectsFields(test.TestCase):
def test_non_existant_states(self):
test_object = StateMachineTestObject()
with testtools.ExpectedException(
ValueError,
msg="missing is not a valid choice, choose from ('active', "
"'pending', 'degraded', 'down', 'error')"):
test_object.status = 'missing'
def test_allow_transitions(self):
test_object = StateMachineTestObject()
test_object.status = TestStateMachineField.PENDING
test_object.status = TestStateMachineField.ACTIVE
test_object.status = TestStateMachineField.DOWN
test_object.status = TestStateMachineField.ERROR
test_object.status = 'pending'
def test_disallow_transitions(self):
test_object = StateMachineTestObject()
test_object.status = TestStateMachineField.PENDING
with testtools.ExpectedException(
ValueError,
msg="StateMachineTestObject's are not allowed transition out "
"of 'pending' state to 'down' state, choose from "
"['error', 'active']"):
test_object.status = TestStateMachineField.DOWN

91
tools/pretty_flake8.py Executable file
View File

@ -0,0 +1,91 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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 __future__ import print_function
import re
import sys
import linecache
from prettytable import PrettyTable
PEP8_LINE = r'^((?P<file>.*):(?P<line>\d*):(?P<col>\d*):) ' \
'(?P<error>(?P<error_code>\w\d{1,3})(?P<error_desc>.*$))'
HTML = True
def main():
raw_errors = []
max_filename_len = 0
for line in sys.stdin:
m = re.match(PEP8_LINE, line)
if m:
m = m.groupdict()
raw_errors.append(m)
if len(m['file']) > max_filename_len:
max_filename_len = len(m['file'])
else:
print(line)
if len(raw_errors) > 0:
print('Flake8 Results')
ct = PrettyTable([
"File",
"Line",
"Column",
"Error Code",
"Error Message",
"Code"
])
ct.align["File"] = "l"
ct.align["Error Message"] = "l"
ct.align["Code"] = "l"
for line in raw_errors:
ct.add_row(format_dict(line))
print(ct)
with open('flake8_results.html', 'w') as f:
f.write('<html><head><style type="text/css">table a:link{color:#666;font-weight:700;text-decoration:none}table a:visited{color:#999;font-weight:700;text-decoration:none}table a:active,table a:hover{color:#bd5a35;text-decoration:underline}table{font-family:Arial,Helvetica,sans-serif;color:#666;font-size:12px;text-shadow:1px 1px 0 #fff;background:#eaebec;margin:20px;border:1px solid #ccc;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-moz-box-shadow:0 1px 2px #d1d1d1;-webkit-box-shadow:0 1px 2px #d1d1d1;box-shadow:0 1px 2px #d1d1d1}table th{padding:21px 25px 22px;border-top:1px solid #fafafa;border-bottom:1px solid #e0e0e0;background:#ededed;background:-webkit-gradient(linear,left top,left bottom,from(#ededed),to(#ebebeb));background:-moz-linear-gradient(top,#ededed,#ebebeb)}table th:first-child{text-align:left;padding-left:20px}table tr:first-child th:first-child{-moz-border-radius-topleft:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px}table tr:first-child th:last-child{-moz-border-radius-topright:3px;-webkit-border-top-right-radius:3px;border-top-right-radius:3px}table tr{text-align:left;padding-left:20px}table td:first-child{text-align:left;padding-left:20px;border-left:0}table td{padding:18px;border-top:1px solid #fff;border-bottom:1px solid #e0e0e0;border-left:1px solid #e0e0e0;background:#fafafa;background:-webkit-gradient(linear,left top,left bottom,from(#fbfbfb),to(#fafafa));background:-moz-linear-gradient(top,#fbfbfb,#fafafa)}table tr.even td{background:#f6f6f6;background:-webkit-gradient(linear,left top,left bottom,from(#f8f8f8),to(#f6f6f6));background:-moz-linear-gradient(top,#f8f8f8,#f6f6f6)}table tr:last-child td{border-bottom:0}table tr:last-child td:first-child{-moz-border-radius-bottomleft:3px;-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px}table tr:last-child td:last-child{-moz-border-radius-bottomright:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px}table tr:hover td{background:#f2f2f2;background:-webkit-gradient(linear,left top,left bottom,from(#f2f2f2),to(#f0f0f0));background:-moz-linear-gradient(top,#f2f2f2,#f0f0f0)}</style></head><body>%s</body</html>' % ct.get_html_string(attributes = {"cellspacing": 0})) # noqa
def format_dict(raw):
output = []
if raw['file'].startswith('./'):
output.append(raw['file'][2:])
else:
output.append(raw['file'])
output.append(raw['line'])
output.append(raw['col'])
output.append(raw['error_code'])
output.append(raw['error_desc'].lstrip())
code_string = linecache.getline(
output[0],
int(raw['line'])).lstrip().rstrip()
output.append(code_string)
return output
if __name__ == '__main__':
main()

6
tools/pretty_flake8.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
TESTARGS=$1
exec 3>&1
status=$(exec 4>&1 >&3; ( flake8 ; echo $? >&4 ) | python tools/pretty_flake8.py) && exit $status

14
tox.ini
View File

@ -10,9 +10,11 @@ setenv =
VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/test-requirements.txt
commands = python setup.py test --slowest --testr-args='{posargs}'
whitelist_externals = sh
[testenv:pep8]
commands = flake8
commands = sh tools/pretty_flake8.sh
[testenv:venv]
commands = {posargs}
@ -26,13 +28,13 @@ commands = python setup.py build_sphinx
[testenv:debug]
commands = oslo_debug_helper {posargs}
[hacking]
import_exceptions = kosmos._i18n
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
ignore = E123,E125,H302,H306,H402,H404,H405,H904
builtins = _
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
[hacking]
import_exceptions = kosmos._i18n
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools