diff --git a/almanach/api/v1/routes.py b/almanach/api/v1/routes.py index 99ff1fb..bd4ec98 100644 --- a/almanach/api/v1/routes.py +++ b/almanach/api/v1/routes.py @@ -49,7 +49,7 @@ def to_json(api_call): LOG.warning(e.message) return send_response({"error": e.message}, 400) except KeyError as e: - message = "The {param} param is mandatory for the request you have made.".format(param=e) + message = "The {} param is mandatory for the request you have made.".format(e) LOG.warning(message) return send_response({"error": message}, 400) except (TypeError, ValueError): @@ -124,18 +124,18 @@ def create_instance(project_id): .. literalinclude:: ../api_examples/input/create_instance-body.json :language: json """ - instance = jsonutils.loads(flask.request.data) - LOG.info("Creating instance for tenant %s with data %s", project_id, instance) + body = jsonutils.loads(flask.request.data) + LOG.info("Creating instance for tenant %s with data %s", project_id, body) + instance_ctl.create_instance( tenant_id=project_id, - instance_id=instance['id'], - create_date=instance['created_at'], - flavor=instance['flavor'], - os_type=instance['os_type'], - distro=instance['os_distro'], - version=instance['os_version'], - name=instance['name'], - metadata={} + instance_id=body['id'], + create_date=body['created_at'], + name=body['name'], + flavor=body['flavor'], + image_meta=dict(distro=body['os_distro'], + version=body['os_version'], + os_type=body['os_type']) ) return flask.Response(status=201) @@ -220,14 +220,14 @@ def rebuild_instance(instance_id): .. literalinclude:: ../api_examples/input/rebuild_instance-body.json :language: json """ - instance = jsonutils.loads(flask.request.data) - LOG.info("Rebuilding instance with id %s with data %s", instance_id, instance) + body = jsonutils.loads(flask.request.data) + LOG.info("Rebuilding instance with id %s with data %s", instance_id, body) instance_ctl.rebuild_instance( instance_id=instance_id, - distro=instance['distro'], - version=instance['version'], - os_type=instance['os_type'], - rebuild_date=instance['rebuild_date'], + rebuild_date=body['rebuild_date'], + image_meta=dict(distro=body['distro'], + version=body['version'], + os_type=body['os_type']) ) return flask.Response(status=200) diff --git a/almanach/collector/handlers/instance_handler.py b/almanach/collector/handlers/instance_handler.py index 1c32b87..e05efac 100644 --- a/almanach/collector/handlers/instance_handler.py +++ b/almanach/collector/handlers/instance_handler.py @@ -33,15 +33,13 @@ class InstanceHandler(base_handler.BaseHandler): def _on_instance_created(self, notification): self.controller.create_instance( - notification.payload.get("instance_id"), - notification.payload.get("tenant_id"), - notification.payload.get("created_at"), - notification.payload.get("instance_type"), - notification.payload.get("image_meta").get("os_type"), - notification.payload.get("image_meta").get("distro"), - notification.payload.get("image_meta").get("version"), - notification.payload.get("hostname"), - notification.payload.get("metadata", {}) + instance_id=notification.payload.get("instance_id"), + tenant_id=notification.payload.get("tenant_id"), + create_date=notification.payload.get("created_at"), + name=notification.payload.get("hostname"), + flavor=notification.payload.get("instance_type"), + image_meta=notification.payload.get("image_meta"), + metadata=notification.payload.get("metadata"), ) def _on_instance_deleted(self, notification): @@ -58,7 +56,8 @@ class InstanceHandler(base_handler.BaseHandler): def _on_instance_rebuilt(self, notification): date = notification.context.get("timestamp") instance_id = notification.payload.get("instance_id") - distro = notification.payload.get("image_meta").get("distro") - version = notification.payload.get("image_meta").get("version") - os_type = notification.payload.get("image_meta").get("os_type") - self.controller.rebuild_instance(instance_id, distro, version, os_type, date) + self.controller.rebuild_instance( + instance_id=instance_id, + rebuild_date=date, + image_meta=notification.payload.get("image_meta") + ) diff --git a/almanach/core/controllers/instance_controller.py b/almanach/core/controllers/instance_controller.py index f0c6d9b..31ec2f5 100644 --- a/almanach/core/controllers/instance_controller.py +++ b/almanach/core/controllers/instance_controller.py @@ -24,24 +24,30 @@ LOG = log.getLogger(__name__) class InstanceController(base_controller.BaseController): def __init__(self, config, database_adapter): + self.config = config self.database_adapter = database_adapter - self.metadata_whitelist = config.resources.device_metadata_whitelist - def create_instance(self, instance_id, tenant_id, create_date, flavor, os_type, distro, version, name, metadata): + def create_instance(self, instance_id, tenant_id, create_date, name, flavor, image_meta=None, metadata=None): create_date = self._validate_and_parse_date(create_date) - LOG.info("instance %s created in project %s (flavor %s; distro %s %s %s) on %s", - instance_id, tenant_id, flavor, os_type, distro, version, create_date) + image_meta = self._filter_image_meta(image_meta) + LOG.info("Instance %s created (tenant %s; flavor %s; image_meta %s) on %s", + instance_id, tenant_id, flavor, image_meta, create_date) if self._fresher_entity_exists(instance_id, create_date): LOG.warning("instance %s already exists with a more recent entry", instance_id) return - filtered_metadata = self._filter_metadata_with_whitelist(metadata) + entity = model.Instance( + entity_id=instance_id, + project_id=tenant_id, + last_event=create_date, + start=create_date, + end=None, + name=name, + flavor=flavor, + image_meta=image_meta, + metadata=self._filter_metadata(metadata)) - entity = model.Instance(instance_id, tenant_id, create_date, None, flavor, - {"os_type": os_type, "distro": distro, - "version": version}, - create_date, name, filtered_metadata) self.database_adapter.insert_entity(entity) def delete_instance(self, instance_id, delete_date): @@ -50,12 +56,12 @@ class InstanceController(base_controller.BaseController): "InstanceId: {0} Not Found".format(instance_id)) delete_date = self._validate_and_parse_date(delete_date) - LOG.info("instance %s deleted on %s", instance_id, delete_date) + LOG.info("Instance %s deleted on %s", instance_id, delete_date) self.database_adapter.close_active_entity(instance_id, delete_date) def resize_instance(self, instance_id, flavor, resize_date): resize_date = self._validate_and_parse_date(resize_date) - LOG.info("instance %s resized to flavor %s on %s", instance_id, flavor, resize_date) + LOG.info("Instance %s resized to flavor %s on %s", instance_id, flavor, resize_date) try: instance = self.database_adapter.get_active_entity(instance_id) if flavor != instance.flavor: @@ -69,18 +75,16 @@ class InstanceController(base_controller.BaseController): LOG.error("Trying to resize an instance with id '%s' not in the database yet.", instance_id) raise e - def rebuild_instance(self, instance_id, distro, version, os_type, rebuild_date): + def rebuild_instance(self, instance_id, rebuild_date, image_meta): rebuild_date = self._validate_and_parse_date(rebuild_date) instance = self.database_adapter.get_active_entity(instance_id) - LOG.info("instance %s rebuilded in project %s to os %s %s %s on %s", - instance_id, instance.project_id, os_type, distro, version, rebuild_date) + image_meta = self._filter_image_meta(image_meta) + LOG.info("Instance %s rebuilt for tenant %s with %s on %s", + instance_id, instance.project_id, image_meta, rebuild_date) - if instance.os.distro != distro or instance.os.version != version: + if instance.image_meta != image_meta: self.database_adapter.close_active_entity(instance_id, rebuild_date) - - instance.os.distro = distro - instance.os.version = version - instance.os.os_type = os_type + instance.image_meta = image_meta instance.start = rebuild_date instance.end = None instance.last_event = rebuild_date @@ -89,5 +93,14 @@ class InstanceController(base_controller.BaseController): def list_instances(self, project_id, start, end): return self.database_adapter.get_all_entities_by_project(project_id, start, end, model.Instance.TYPE) - def _filter_metadata_with_whitelist(self, metadata): - return {key: value for key, value in metadata.items() if key in self.metadata_whitelist} + def _filter_metadata(self, metadata): + return self._filter(metadata, self.config.entities.instance_metadata) + + def _filter_image_meta(self, image_meta): + return self._filter(image_meta, self.config.entities.instance_image_meta) + + @staticmethod + def _filter(d, whitelist): + if d: + return {key: value for key, value in d.items() if key in whitelist} + return {} diff --git a/almanach/core/controllers/volume_controller.py b/almanach/core/controllers/volume_controller.py index c79be6a..e93d16e 100644 --- a/almanach/core/controllers/volume_controller.py +++ b/almanach/core/controllers/volume_controller.py @@ -26,7 +26,7 @@ class VolumeController(base_controller.BaseController): def __init__(self, config, database_adapter): self.database_adapter = database_adapter - self.volume_existence_threshold = timedelta(0, config.resources.volume_existence_threshold) + self.volume_existence_threshold = timedelta(0, config.entities.volume_existence_threshold) def list_volumes(self, project_id, start, end): return self.database_adapter.get_all_entities_by_project(project_id, start, end, model.Volume.TYPE) diff --git a/almanach/core/model.py b/almanach/core/model.py index b0553bf..172ed46 100644 --- a/almanach/core/model.py +++ b/almanach/core/model.py @@ -12,12 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc import six from almanach.core import exception +@six.add_metaclass(abc.ABCMeta) class Entity(object): + def __init__(self, entity_id, project_id, start, end, last_event, name, entity_type): self.entity_id = entity_id self.project_id = project_id @@ -28,7 +31,19 @@ class Entity(object): self.entity_type = entity_type def as_dict(self): - return todict(self) + return dict( + entity_id=self.entity_id, + project_id=self.project_id, + start=self.start, + end=self.end, + last_event=self.last_event, + name=self.name, + entity_type=self.entity_type, + ) + + @staticmethod + def from_dict(d): + raise NotImplementedError def __eq__(self, other): return (other.entity_id == self.entity_id and @@ -44,49 +59,74 @@ class Entity(object): class Instance(Entity): - TYPE = "instance" + TYPE = 'instance' - def __init__(self, entity_id, project_id, start, end, flavor, os, last_event, name, metadata={}, entity_type=TYPE): - super(Instance, self).__init__(entity_id, project_id, start, end, last_event, name, entity_type) + def __init__(self, entity_id, project_id, start, end, flavor, last_event, name, image_meta=None, metadata=None): + super(Instance, self).__init__(entity_id, project_id, start, end, last_event, name, self.TYPE) self.flavor = flavor - self.metadata = metadata - self.os = OS(**os) + self.metadata = metadata or dict() + self.image_meta = image_meta or dict() - def as_dict(self): - _replace_metadata_name_with_dot_instead_of_circumflex(self) - return todict(self) + # TODO(fguillot): This attribute still used by the legacy API, + # that should be removed when the new API v2 will be implemented + self.os = self.image_meta def __eq__(self, other): return (super(Instance, self).__eq__(other) and other.flavor == self.flavor and - other.os == self.os and + other.image_meta == self.image_meta and other.metadata == self.metadata) - def __ne__(self, other): - return not self.__eq__(other) + def as_dict(self): + d = super(Instance, self).as_dict() + d['flavor'] = self.flavor + d['metadata'] = self.metadata + d['image_meta'] = self.image_meta + # NOTE(fguillot): we keep this key for backward compatibility + d['os'] = self.image_meta + return d -class OS(object): - def __init__(self, os_type, distro, version): - self.os_type = os_type - self.distro = distro - self.version = version + @staticmethod + def from_dict(d): + return Instance( + entity_id=d.get('entity_id'), + project_id=d.get('project_id'), + start=d.get('start'), + end=d.get('end'), + last_event=d.get('last_event'), + name=d.get('name'), + flavor=d.get('flavor'), + image_meta=d.get('os') or d.get('image_meta'), + metadata=Instance._unserialize_metadata(d), + ) - def __eq__(self, other): - return (other.os_type == self.os_type and - other.distro == self.distro and - other.version == self.version) + @staticmethod + def _unserialize_metadata(d): + metadata = d.get('metadata') + if metadata: + tmp = dict() + for key, value in metadata.items(): + if '^' in key: + key = key.replace('^', '.') + tmp[key] = value + metadata = tmp + return metadata - def __ne__(self, other): - return not self.__eq__(other) + def _serialize_metadata(self): + tmp = dict() + for key, value in self.metadata.items(): + if '.' in key: + key = key.replace('.', '^') + tmp[key] = value + return tmp class Volume(Entity): - TYPE = "volume" + TYPE = 'volume' - def __init__(self, entity_id, project_id, start, end, volume_type, size, last_event, name, attached_to=None, - entity_type=TYPE): - super(Volume, self).__init__(entity_id, project_id, start, end, last_event, name, entity_type) + def __init__(self, entity_id, project_id, start, end, volume_type, size, last_event, name, attached_to=None): + super(Volume, self).__init__(entity_id, project_id, start, end, last_event, name, self.TYPE) self.volume_type = volume_type self.size = size self.attached_to = attached_to or [] @@ -97,11 +137,30 @@ class Volume(Entity): other.size == self.size and other.attached_to == self.attached_to) - def __ne__(self, other): - return not self.__eq__(other) + def as_dict(self): + d = super(Volume, self).as_dict() + d['volume_type'] = self.volume_type + d['size'] = self.size + d['attached_to'] = self.attached_to + return d + + @staticmethod + def from_dict(d): + return Volume( + entity_id=d.get('entity_id'), + project_id=d.get('project_id'), + start=d.get('start'), + end=d.get('end'), + last_event=d.get('last_event'), + name=d.get('name'), + volume_type=d.get('volume_type'), + size=d.get('size'), + attached_to=d.get('attached_to'), + ) class VolumeType(object): + def __init__(self, volume_type_id, volume_type_name): self.volume_type_id = volume_type_id self.volume_type_name = volume_type_name @@ -109,52 +168,23 @@ class VolumeType(object): def __eq__(self, other): return other.__dict__ == self.__dict__ - def __ne__(self, other): - return not self.__eq__(other) - def as_dict(self): - return todict(self) + return dict( + volume_type_id=self.volume_type_id, + volume_type_name=self.volume_type_name, + ) + + @staticmethod + def from_dict(d): + return VolumeType(volume_type_id=d['volume_type_id'], + volume_type_name=d['volume_type_name']) -def build_entity_from_dict(entity_dict): - if entity_dict.get("entity_type") == Instance.TYPE: - _replace_metadata_name_with_circumflex_instead_of_dot(entity_dict) - return Instance(**entity_dict) - elif entity_dict.get("entity_type") == Volume.TYPE: - return Volume(**entity_dict) +def get_entity_from_dict(d): + entity_type = d.get('entity_type') + if entity_type == Instance.TYPE: + return Instance.from_dict(d) + elif entity_type == Volume.TYPE: + return Volume.from_dict(d) raise exception.EntityTypeNotSupportedException( - 'Unsupported entity type: "{}"'.format(entity_dict.get("entity_type"))) - - -def todict(obj): - if isinstance(obj, dict) or isinstance(obj, six.text_type): - return obj - elif hasattr(obj, "__iter__"): - return [todict(v) for v in obj] - elif hasattr(obj, "__dict__"): - return dict([(key, todict(value)) - for key, value in obj.__dict__.items() - if not callable(value) and not key.startswith('_')]) - else: - return obj - - -def _replace_metadata_name_with_dot_instead_of_circumflex(instance): - if instance.metadata: - cleaned_metadata = dict() - for key, value in instance.metadata.items(): - if '.' in key: - key = key.replace(".", "^") - cleaned_metadata[key] = value - instance.metadata = cleaned_metadata - - -def _replace_metadata_name_with_circumflex_instead_of_dot(entity_dict): - metadata = entity_dict.get("metadata") - if metadata: - dirty_metadata = dict() - for key, value in metadata.items(): - if '^' in key: - key = key.replace("^", ".") - dirty_metadata[key] = value - entity_dict["metadata"] = dirty_metadata + 'Unsupported entity type: "{}"'.format(entity_type)) diff --git a/almanach/core/opts.py b/almanach/core/opts.py index 65d628a..d391443 100644 --- a/almanach/core/opts.py +++ b/almanach/core/opts.py @@ -87,14 +87,16 @@ auth_opts = [ help='Private key for private key authentication'), ] -resource_opts = [ +entity_opts = [ cfg.IntOpt('volume_existence_threshold', default=60, help='Volume existence threshold'), - cfg.ListOpt('device_metadata_whitelist', + cfg.ListOpt('instance_metadata', default=[], - deprecated_for_removal=True, - help='Metadata to include in entity'), + help='List of instance metadata to include from notifications'), + cfg.ListOpt('instance_image_meta', + default=[], + help='List of instance image metadata to include from notifications'), ] CONF.register_opts(database_opts, group='database') @@ -102,7 +104,7 @@ CONF.register_opts(api_opts, group='api') CONF.register_opts(collector_opts, group='collector') CONF.register_opts(auth_opts, group='auth') CONF.register_opts(keystone_opts, group='keystone_authtoken') -CONF.register_opts(resource_opts, group='resources') +CONF.register_opts(entity_opts, group='entities') logging.register_options(CONF) logging.setup(CONF, DOMAIN) @@ -115,5 +117,5 @@ def list_opts(): ('collector', collector_opts), ('auth', auth_opts), ('keystone_authtoken', keystone_opts), - ('resources', resource_opts), + ('entities', entity_opts), ] diff --git a/almanach/storage/drivers/mongodb_driver.py b/almanach/storage/drivers/mongodb_driver.py index 321a8a1..83ba98f 100644 --- a/almanach/storage/drivers/mongodb_driver.py +++ b/almanach/storage/drivers/mongodb_driver.py @@ -17,7 +17,7 @@ import pymongo from almanach.core import exception from almanach.core import model -from almanach.core.model import build_entity_from_dict +from almanach.core.model import get_entity_from_dict from almanach.storage.drivers import base_driver LOG = log.getLogger(__name__) @@ -50,7 +50,7 @@ class MongoDbDriver(base_driver.BaseDriver): entity = self.db.entity.find_one({"entity_id": entity_id, "end": None}, {"_id": 0}) if not entity: raise exception.EntityNotFoundException("Entity {} not found".format(entity_id)) - return build_entity_from_dict(entity) + return get_entity_from_dict(entity) def get_all_entities_by_project(self, project_id, start, end, entity_type=None): args = { @@ -65,11 +65,11 @@ class MongoDbDriver(base_driver.BaseDriver): args["entity_type"] = entity_type entities = list(self.db.entity.find(args, {"_id": 0})) - return [build_entity_from_dict(entity) for entity in entities] + return [get_entity_from_dict(entity) for entity in entities] def get_all_entities_by_id(self, entity_id): entities = self.db.entity.find({"entity_id": entity_id}, {"_id": 0}) - return [build_entity_from_dict(entity) for entity in entities] + return [get_entity_from_dict(entity) for entity in entities] def get_all_entities_by_id_and_date(self, entity_id, start, end): entities = self.db.entity.find({ @@ -80,7 +80,7 @@ class MongoDbDriver(base_driver.BaseDriver): {"end": {"$lte": end}} ] }, {"_id": 0}) - return [build_entity_from_dict(entity) for entity in entities] + return [get_entity_from_dict(entity) for entity in entities] def close_active_entity(self, entity_id, end): self.db.entity.update({"entity_id": entity_id, "end": None}, {"$set": {"end": end, "last_event": end}}) @@ -110,9 +110,7 @@ class MongoDbDriver(base_driver.BaseDriver): volume_type = self.db.volume_type.find_one({"volume_type_id": volume_type_id}) if not volume_type: raise exception.VolumeTypeNotFoundException(volume_type_id=volume_type_id) - - return model.VolumeType(volume_type_id=volume_type["volume_type_id"], - volume_type_name=volume_type["volume_type_name"]) + return model.VolumeType.from_dict(volume_type) def delete_volume_type(self, volume_type_id): if volume_type_id is None: diff --git a/almanach/tests/tempest/tests/api/base.py b/almanach/tests/tempest/tests/api/base.py index c1cd7f3..8f99adb 100644 --- a/almanach/tests/tempest/tests/api/base.py +++ b/almanach/tests/tempest/tests/api/base.py @@ -11,12 +11,12 @@ # 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 uuid import uuid4 from oslo_serialization import jsonutils as json from tempest.common.utils import data_utils from tempest import config import tempest.test +from uuid import uuid4 from almanach.tests.tempest import clients @@ -24,6 +24,7 @@ CONF = config.CONF class BaseAlmanachTest(tempest.test.BaseTestCase): + @classmethod def skip_checks(cls): super(BaseAlmanachTest, cls).skip_checks() diff --git a/almanach/tests/tempest/tests/api/test_server_creation.py b/almanach/tests/tempest/tests/api/test_server_creation.py index 9b7ddfc..6c0128e 100644 --- a/almanach/tests/tempest/tests/api/test_server_creation.py +++ b/almanach/tests/tempest/tests/api/test_server_creation.py @@ -11,11 +11,12 @@ # 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_serialization import jsonutils as json +from tempest.lib import exceptions from uuid import uuid4 from almanach.tests.tempest.tests.api import base -from oslo_serialization import jsonutils as json -from tempest.lib import exceptions class TestServerCreation(base.BaseAlmanachTest): diff --git a/almanach/tests/tempest/tests/api/test_server_deletion.py b/almanach/tests/tempest/tests/api/test_server_deletion.py index ef90f0e..02783d8 100644 --- a/almanach/tests/tempest/tests/api/test_server_deletion.py +++ b/almanach/tests/tempest/tests/api/test_server_deletion.py @@ -11,13 +11,15 @@ # 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_serialization import jsonutils as json from uuid import uuid4 from almanach.tests.tempest.tests.api import base -from oslo_serialization import jsonutils as json class TestServerDeletion(base.BaseAlmanachTest): + def setUp(self): super(base.BaseAlmanachTest, self).setUp() diff --git a/almanach/tests/tempest/tests/api/test_server_rebuild.py b/almanach/tests/tempest/tests/api/test_server_rebuild.py index 8555842..e8e8547 100644 --- a/almanach/tests/tempest/tests/api/test_server_rebuild.py +++ b/almanach/tests/tempest/tests/api/test_server_rebuild.py @@ -11,13 +11,15 @@ # 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_serialization import jsonutils as json from uuid import uuid4 from almanach.tests.tempest.tests.api import base -from oslo_serialization import jsonutils as json class TestServerRebuild(base.BaseAlmanachTest): + def setUp(self): super(base.BaseAlmanachTest, self).setUp() @@ -30,15 +32,15 @@ class TestServerRebuild(base.BaseAlmanachTest): server = self.get_server_creation_payload() self.create_server_through_api(tenant_id, server) - rebuild_data = { - 'distro': 'Ubuntu', - 'version': '14.04', - 'os_type': 'Linux', + data = { + 'distro': 'debian', + 'version': '8.0', + 'os_type': 'linux', 'rebuild_date': '2016-01-01T18:50:00Z' } - data = json.dumps(rebuild_data) - self.almanach_client.rebuild(server['id'], data) + self.almanach_client.rebuild(server['id'], + json.dumps(data)) resp, response_body = self.almanach_client.get_tenant_entities(tenant_id) @@ -46,11 +48,26 @@ class TestServerRebuild(base.BaseAlmanachTest): self.assertIsInstance(entities, list) self.assertEqual(2, len(entities)) - rebuilded_server, initial_server = sorted(entities, key=lambda k: k['end'] if k['end'] is not None else '') + rebuilt_server, initial_server = sorted(entities, key=lambda k: k['end'] if k['end'] is not None else '') self.assertEqual(server['id'], initial_server['entity_id']) - self.assertEqual(server['os_version'], initial_server['os']['version']) self.assertIsNotNone(initial_server['end']) - self.assertEqual(server['id'], rebuilded_server['entity_id']) - self.assertEqual(rebuild_data['version'], rebuilded_server['os']['version']) - self.assertIsNone(rebuilded_server['end']) + + self.assertEqual(server['os_distro'], initial_server['os']['distro']) + self.assertEqual(server['os_version'], initial_server['os']['version']) + self.assertEqual(server['os_type'], initial_server['os']['os_type']) + + self.assertEqual(server['os_distro'], initial_server['image_meta']['distro']) + self.assertEqual(server['os_version'], initial_server['image_meta']['version']) + self.assertEqual(server['os_type'], initial_server['image_meta']['os_type']) + + self.assertEqual(server['id'], rebuilt_server['entity_id']) + self.assertIsNone(rebuilt_server['end']) + + self.assertEqual(data['distro'], rebuilt_server['os']['distro']) + self.assertEqual(data['version'], rebuilt_server['os']['version']) + self.assertEqual(data['os_type'], rebuilt_server['os']['os_type']) + + self.assertEqual(data['distro'], rebuilt_server['image_meta']['distro']) + self.assertEqual(data['version'], rebuilt_server['image_meta']['version']) + self.assertEqual(data['os_type'], rebuilt_server['image_meta']['os_type']) diff --git a/almanach/tests/tempest/tests/api/test_server_resize.py b/almanach/tests/tempest/tests/api/test_server_resize.py index ee03d14..dd210eb 100644 --- a/almanach/tests/tempest/tests/api/test_server_resize.py +++ b/almanach/tests/tempest/tests/api/test_server_resize.py @@ -11,13 +11,15 @@ # 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_serialization import jsonutils as json from uuid import uuid4 from almanach.tests.tempest.tests.api import base -from oslo_serialization import jsonutils as json class TestServerResize(base.BaseAlmanachTest): + def setUp(self): super(base.BaseAlmanachTest, self).setUp() diff --git a/almanach/tests/tempest/tests/api/test_server_update.py b/almanach/tests/tempest/tests/api/test_server_update.py index 3980715..2c288d3 100644 --- a/almanach/tests/tempest/tests/api/test_server_update.py +++ b/almanach/tests/tempest/tests/api/test_server_update.py @@ -11,13 +11,15 @@ # 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_serialization import jsonutils as json from uuid import uuid4 from almanach.tests.tempest.tests.api import base -from oslo_serialization import jsonutils as json class TestServerUpdate(base.BaseAlmanachTest): + def setUp(self): super(base.BaseAlmanachTest, self).setUp() diff --git a/almanach/tests/tempest/tests/api/test_volume_attach.py b/almanach/tests/tempest/tests/api/test_volume_attach.py index ab44276..c2dd3f4 100644 --- a/almanach/tests/tempest/tests/api/test_volume_attach.py +++ b/almanach/tests/tempest/tests/api/test_volume_attach.py @@ -11,14 +11,15 @@ # 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 uuid import uuid4 from oslo_serialization import jsonutils as json +from uuid import uuid4 from almanach.tests.tempest.tests.api import base class TestVolumeAttach(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeAttach, cls).resource_setup() @@ -38,8 +39,10 @@ class TestVolumeAttach(base.BaseAlmanachTest): self.assertEqual(resp.status, 200) self.assertIsInstance(response_body, list) self.assertEqual(2, len(response_body)) + un_attached_volume = [v for v in response_body if v['attached_to'] == []][0] attached_volume = [v for v in response_body if v['attached_to'] != []][0] + self.assertEqual(volume['volume_id'], attached_volume['entity_id']) self.assertEqual(volume['volume_id'], un_attached_volume['entity_id']) self.assertEqual('volume', attached_volume['entity_type']) diff --git a/almanach/tests/tempest/tests/api/test_volume_creation.py b/almanach/tests/tempest/tests/api/test_volume_creation.py index e3f94c3..a91388f 100644 --- a/almanach/tests/tempest/tests/api/test_volume_creation.py +++ b/almanach/tests/tempest/tests/api/test_volume_creation.py @@ -11,12 +11,14 @@ # 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_serialization import jsonutils as json from almanach.tests.tempest.tests.api import base class TestVolumeCreation(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeCreation, cls).resource_setup() diff --git a/almanach/tests/tempest/tests/api/test_volume_deletion.py b/almanach/tests/tempest/tests/api/test_volume_deletion.py index 83bc1e3..6027c08 100644 --- a/almanach/tests/tempest/tests/api/test_volume_deletion.py +++ b/almanach/tests/tempest/tests/api/test_volume_deletion.py @@ -11,6 +11,7 @@ # 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_serialization import jsonutils as json from oslo_utils import timeutils @@ -18,6 +19,7 @@ from almanach.tests.tempest.tests.api import base class TestVolumeDeletion(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeDeletion, cls).resource_setup() diff --git a/almanach/tests/tempest/tests/api/test_volume_detach.py b/almanach/tests/tempest/tests/api/test_volume_detach.py index 58b61c1..2220560 100644 --- a/almanach/tests/tempest/tests/api/test_volume_detach.py +++ b/almanach/tests/tempest/tests/api/test_volume_detach.py @@ -11,6 +11,7 @@ # 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_serialization import jsonutils as json from uuid import uuid4 @@ -18,6 +19,7 @@ from almanach.tests.tempest.tests.api import base class TestVolumeDetach(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeDetach, cls).resource_setup() @@ -43,8 +45,10 @@ class TestVolumeDetach(base.BaseAlmanachTest): self.assertEqual(resp.status, 200) self.assertIsInstance(response_body, list) self.assertEqual(3, len(response_body)) + un_attached_volumes = [v for v in response_body if v['attached_to'] == []] attached_volume = [v for v in response_body if v['attached_to'] != []][0] + self.assertEqual(volume['volume_id'], attached_volume['entity_id']) self.assertEqual(volume['volume_id'], un_attached_volumes[0]['entity_id']) self.assertEqual(volume['volume_id'], un_attached_volumes[1]['entity_id']) diff --git a/almanach/tests/tempest/tests/api/test_volume_resize.py b/almanach/tests/tempest/tests/api/test_volume_resize.py index f3f1bba..5e00534 100644 --- a/almanach/tests/tempest/tests/api/test_volume_resize.py +++ b/almanach/tests/tempest/tests/api/test_volume_resize.py @@ -11,12 +11,14 @@ # 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_serialization import jsonutils as json from almanach.tests.tempest.tests.api import base class TestVolumeResize(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeResize, cls).resource_setup() diff --git a/almanach/tests/tempest/tests/api/test_volume_type_creation.py b/almanach/tests/tempest/tests/api/test_volume_type_creation.py index 844b489..8800e80 100644 --- a/almanach/tests/tempest/tests/api/test_volume_type_creation.py +++ b/almanach/tests/tempest/tests/api/test_volume_type_creation.py @@ -11,12 +11,14 @@ # 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_serialization import jsonutils as json from almanach.tests.tempest.tests.api import base class TestVolumeCreation(base.BaseAlmanachTest): + @classmethod def resource_setup(cls): super(TestVolumeCreation, cls).resource_setup() diff --git a/almanach/tests/tempest/tests/scenario/test_server_rebuild.py b/almanach/tests/tempest/tests/scenario/test_server_rebuild.py index ac91ff9..eec44ef 100644 --- a/almanach/tests/tempest/tests/scenario/test_server_rebuild.py +++ b/almanach/tests/tempest/tests/scenario/test_server_rebuild.py @@ -32,7 +32,8 @@ class TestServerRebuildScenario(base.BaseAlmanachScenarioTest): self.assertEqual(flavor['name'], entities[0]['flavor']) self.assertIsNotNone(entities[0]['start']) self.assertIsNotNone(entities[0]['end']) - self.assertIsNone(entities[0]['os']['distro']) + self.assertEqual(dict(), entities[0]['os']) + self.assertEqual(dict(), entities[0]['image_meta']) self.assertEqual(server['id'], entities[1]['entity_id']) self.assertEqual('instance', entities[1]['entity_type']) @@ -40,6 +41,7 @@ class TestServerRebuildScenario(base.BaseAlmanachScenarioTest): self.assertEqual(flavor['name'], entities[1]['flavor']) self.assertIsNotNone(entities[1]['start']) self.assertIsNone(entities[1]['end']) + self.assertEqual('linux', entities[1]['image_meta']['distro']) self.assertEqual('linux', entities[1]['os']['distro']) def _rebuild_server(self): diff --git a/almanach/tests/tempest/tests/scenario/test_server_resize.py b/almanach/tests/tempest/tests/scenario/test_server_resize.py index d9fd4e1..45f4f30 100644 --- a/almanach/tests/tempest/tests/scenario/test_server_resize.py +++ b/almanach/tests/tempest/tests/scenario/test_server_resize.py @@ -32,6 +32,8 @@ class TestServerResizeScenario(base.BaseAlmanachScenarioTest): self.assertEqual(initial_flavor['name'], entities[0]['flavor']) self.assertIsNotNone(entities[0]['start']) self.assertIsNotNone(entities[0]['end']) + self.assertEqual(dict(), entities[0]['os']) + self.assertEqual(dict(), entities[0]['image_meta']) self.assertEqual(server['id'], entities[1]['entity_id']) self.assertEqual('instance', entities[1]['entity_type']) @@ -39,6 +41,8 @@ class TestServerResizeScenario(base.BaseAlmanachScenarioTest): self.assertEqual(resized_flavor['name'], entities[1]['flavor']) self.assertIsNotNone(entities[1]['start']) self.assertIsNone(entities[1]['end']) + self.assertEqual(dict(), entities[0]['os']) + self.assertEqual(dict(), entities[0]['image_meta']) def _resize_server(self): flavors = self.flavors_client.list_flavors()['flavors'] diff --git a/almanach/tests/unit/api/test_api_instance.py b/almanach/tests/unit/api/test_api_instance.py index b964815..8b21b3d 100644 --- a/almanach/tests/unit/api/test_api_instance.py +++ b/almanach/tests/unit/api/test_api_instance.py @@ -57,13 +57,12 @@ class ApiInstanceTest(base_api.BaseApi): .with_args(tenant_id="PROJECT_ID", instance_id=data["id"], create_date=data["created_at"], - flavor=data['flavor'], - os_type=data['os_type'], - distro=data['os_distro'], - version=data['os_version'], name=data['name'], - metadata={}) \ - .once() + flavor=data['flavor'], + image_meta=dict(os_type=data['os_type'], + distro=data['os_distro'], + version=data['os_version']) + ).once() code, result = self.api_post( '/project/PROJECT_ID/instance', @@ -105,11 +104,10 @@ class ApiInstanceTest(base_api.BaseApi): instance_id=data["id"], create_date=data["created_at"], flavor=data['flavor'], - os_type=data['os_type'], - distro=data['os_distro'], - version=data['os_version'], - name=data['name'], - metadata={}) \ + image_meta=dict(os_type=data['os_type'], + distro=data['os_distro'], + version=data['os_version']), + name=data['name']) \ .once() \ .and_raise(exception.DateFormatException) @@ -240,12 +238,12 @@ class ApiInstanceTest(base_api.BaseApi): } self.instance_ctl.should_receive('rebuild_instance') \ .with_args( - instance_id=instance_id, - distro=data.get('distro'), - version=data.get('version'), - os_type=data.get('os_type'), - rebuild_date=data.get('rebuild_date')) \ - .once() + instance_id=instance_id, + rebuild_date=data.get('rebuild_date'), + image_meta=dict(distro=data.get('distro'), + version=data.get('version'), + os_type=data.get('os_type')) + ).once() code, result = self.api_put( '/instance/INSTANCE_ID/rebuild', @@ -282,7 +280,11 @@ class ApiInstanceTest(base_api.BaseApi): } self.instance_ctl.should_receive('rebuild_instance') \ - .with_args(instance_id=instance_id, **data) \ + .with_args(instance_id=instance_id, + rebuild_date=data.get('rebuild_date'), + image_meta=dict(distro=data.get('distro'), + version=data.get('version'), + os_type=data.get('os_type'))) \ .once() \ .and_raise(exception.DateFormatException) diff --git a/almanach/tests/unit/builders/entity.py b/almanach/tests/unit/builders/entity.py index b12765e..86659db 100644 --- a/almanach/tests/unit/builders/entity.py +++ b/almanach/tests/unit/builders/entity.py @@ -29,7 +29,7 @@ class Builder(object): class EntityBuilder(Builder): def build(self): - return model.build_entity_from_dict(self.dict_object) + return model.get_entity_from_dict(self.dict_object) def with_id(self, entity_id): self.dict_object["entity_id"] = entity_id diff --git a/almanach/tests/unit/collector/handlers/test_instance_handler.py b/almanach/tests/unit/collector/handlers/test_instance_handler.py index 59b1f41..dd99f78 100644 --- a/almanach/tests/unit/collector/handlers/test_instance_handler.py +++ b/almanach/tests/unit/collector/handlers/test_instance_handler.py @@ -37,15 +37,13 @@ class InstanceHandlerTest(base.BaseTestCase): self.instance_handler.handle_events(notification) self.controller.create_instance.assert_called_once_with( - notification.payload['instance_id'], - notification.payload['tenant_id'], - notification.payload['created_at'], - notification.payload['instance_type'], - notification.payload['image_meta']['os_type'], - notification.payload['image_meta']['distro'], - notification.payload['image_meta']['version'], - notification.payload['hostname'], - notification.payload['metadata'], + instance_id=notification.payload['instance_id'], + tenant_id=notification.payload['tenant_id'], + create_date=notification.payload['created_at'], + name=notification.payload['hostname'], + flavor=notification.payload['instance_type'], + image_meta=notification.payload['image_meta'], + metadata=notification.payload['metadata'], ) def test_instance_deleted(self): @@ -87,9 +85,7 @@ class InstanceHandlerTest(base.BaseTestCase): self.instance_handler.handle_events(notification) self.controller.rebuild_instance.assert_called_once_with( - notification.payload['instance_id'], - notification.payload['image_meta']['distro'], - notification.payload['image_meta']['version'], - notification.payload['image_meta']['os_type'], - notification.context.get("timestamp") + instance_id=notification.payload['instance_id'], + rebuild_date=notification.context.get("timestamp"), + image_meta=notification.payload['image_meta'], ) diff --git a/almanach/tests/unit/core/controllers/test_instance_controller.py b/almanach/tests/unit/core/controllers/test_instance_controller.py index fa25503..b0ed08a 100644 --- a/almanach/tests/unit/core/controllers/test_instance_controller.py +++ b/almanach/tests/unit/core/controllers/test_instance_controller.py @@ -31,6 +31,7 @@ class InstanceControllerTest(base.BaseTestCase): def setUp(self): super(InstanceControllerTest, self).setUp() + self.config.entities.instance_image_meta = ['distro', 'version', 'os_type'] self.database_adapter = flexmock(base_driver.BaseDriver) self.controller = instance_controller.InstanceController(self.config, self.database_adapter) @@ -47,9 +48,13 @@ class InstanceControllerTest(base.BaseTestCase): .should_receive("insert_entity") .once()) - self.controller.create_instance(fake_instance.entity_id, fake_instance.project_id, fake_instance.start, - fake_instance.flavor, fake_instance.os.os_type, fake_instance.os.distro, - fake_instance.os.version, fake_instance.name, fake_instance.metadata) + self.controller.create_instance(fake_instance.entity_id, + fake_instance.project_id, + fake_instance.start, + fake_instance.flavor, + fake_instance.name, + fake_instance.image_meta, + fake_instance.metadata) def test_resize_instance(self): fake_instance = a(instance()) @@ -88,8 +93,7 @@ class InstanceControllerTest(base.BaseTestCase): self.controller.create_instance(fake_instance.entity_id, fake_instance.project_id, '2015-10-21T16:25:00.000000Z', - fake_instance.flavor, fake_instance.os.os_type, fake_instance.os.distro, - fake_instance.os.version, fake_instance.name, fake_instance.metadata) + fake_instance.flavor, fake_instance.image_meta, fake_instance.metadata) def test_instance_created_but_find_garbage(self): fake_instance = a(instance().with_all_dates_in_string()) @@ -105,8 +109,7 @@ class InstanceControllerTest(base.BaseTestCase): .once()) self.controller.create_instance(fake_instance.entity_id, fake_instance.project_id, fake_instance.start, - fake_instance.flavor, fake_instance.os.os_type, fake_instance.os.distro, - fake_instance.os.version, fake_instance.name, fake_instance.metadata) + fake_instance.flavor, fake_instance.image_meta, fake_instance.metadata) def test_instance_deleted(self): (flexmock(self.database_adapter) @@ -158,15 +161,11 @@ class InstanceControllerTest(base.BaseTestCase): self.controller.rebuild_instance( "an_instance_id", - "some_distro", - "some_version", - "some_type", - "2015-10-21T16:25:00.000000Z" + "2015-10-21T16:25:00.000000Z", + dict(distro="some_distro", version="some_version", os_type="some_type") ) self.controller.rebuild_instance( "an_instance_id", - i.os.distro, - i.os.version, - i.os.os_type, - "2015-10-21T16:25:00.000000Z" + "2015-10-21T16:25:00.000000Z", + dict(distro=i.image_meta['distro'], version=i.image_meta['version'], os_type=i.image_meta['os_type']) ) diff --git a/almanach/tests/unit/core/test_model.py b/almanach/tests/unit/core/test_model.py new file mode 100644 index 0000000..2d202ab --- /dev/null +++ b/almanach/tests/unit/core/test_model.py @@ -0,0 +1,334 @@ +# Copyright 2016 Internap. +# +# 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 almanach.core import exception +from almanach.core import model +from datetime import datetime +import pytz + +from almanach.tests.unit import base + + +class TestModel(base.BaseTestCase): + + def test_instance_serialize(self): + instance = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='flavor_id', + image_meta=dict(os_type='linux', distro='Ubuntu', version='16.04'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata={'some_key': 'some.value', 'another^key': 'another.value'}, + ) + + entry = instance.as_dict() + self.assertEqual('instance_id', entry['entity_id']) + self.assertEqual('project_id', entry['project_id']) + self.assertEqual('instance', entry['entity_type']) + self.assertEqual('hostname', entry['name']) + self.assertEqual('flavor_id', entry['flavor']) + self.assertEqual('linux', entry['os']['os_type']) + self.assertEqual('Ubuntu', entry['os']['distro']) + self.assertEqual('16.04', entry['os']['version']) + self.assertEqual('linux', entry['image_meta']['os_type']) + self.assertEqual('Ubuntu', entry['image_meta']['distro']) + self.assertEqual('16.04', entry['image_meta']['version']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), entry['last_event']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), entry['start']) + self.assertIsNone(entry['end']) + + def test_instance_unserialize(self): + entry = { + 'entity_id': 'instance_id', + 'entity_type': 'instance', + 'project_id': 'project_id', + 'start': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'end': None, + 'last_event': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'flavor': 'flavor_id', + 'image_meta': { + 'os_type': 'linux', + 'distro': 'Ubuntu', + 'version': '16.04', + }, + 'name': 'hostname' + } + + instance = model.get_entity_from_dict(entry) + self.assertEqual('instance_id', instance.entity_id) + self.assertEqual('project_id', instance.project_id) + self.assertEqual('instance', instance.entity_type) + self.assertEqual('hostname', instance.name) + self.assertEqual('flavor_id', instance.flavor) + self.assertEqual('linux', instance.image_meta['os_type']) + self.assertEqual('Ubuntu', instance.image_meta['distro']) + self.assertEqual('16.04', instance.image_meta['version']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.last_event) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.start) + self.assertIsNone(instance.end) + + def test_instance_unserialize_with_legacy_os(self): + entry = { + 'entity_id': 'instance_id', + 'entity_type': 'instance', + 'project_id': 'project_id', + 'start': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'end': None, + 'last_event': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'flavor': 'flavor_id', + 'os': { + 'os_type': 'linux', + 'distro': 'Ubuntu', + 'version': '16.04', + }, + 'name': 'hostname' + } + + instance = model.get_entity_from_dict(entry) + self.assertEqual('instance_id', instance.entity_id) + self.assertEqual('project_id', instance.project_id) + self.assertEqual('instance', instance.entity_type) + self.assertEqual('hostname', instance.name) + self.assertEqual('flavor_id', instance.flavor) + self.assertEqual('linux', instance.image_meta['os_type']) + self.assertEqual('Ubuntu', instance.image_meta['distro']) + self.assertEqual('16.04', instance.image_meta['version']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.last_event) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.start) + self.assertIsNone(instance.end) + + def test_instance_unserialize_with_both_keys(self): + entry = { + 'entity_id': 'instance_id', + 'entity_type': 'instance', + 'project_id': 'project_id', + 'start': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'end': None, + 'last_event': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'flavor': 'flavor_id', + 'os': { + 'os_type': 'linux', + 'distro': 'Ubuntu', + 'version': '16.04', + }, + 'image_meta': { + 'os_type': 'linux', + 'distro': 'Ubuntu', + 'version': '16.04', + }, + 'name': 'hostname' + } + + instance = model.get_entity_from_dict(entry) + self.assertEqual('instance_id', instance.entity_id) + self.assertEqual('project_id', instance.project_id) + self.assertEqual('instance', instance.entity_type) + self.assertEqual('hostname', instance.name) + self.assertEqual('flavor_id', instance.flavor) + self.assertEqual('linux', instance.image_meta['os_type']) + self.assertEqual('Ubuntu', instance.image_meta['distro']) + self.assertEqual('16.04', instance.image_meta['version']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.last_event) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), instance.start) + self.assertIsNone(instance.end) + + def test_instance_comparison(self): + instance1 = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='flavor_id', + image_meta=dict(os_type='linux', distro='Ubuntu', version='16.04'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata={'some_key': 'some.value', 'another^key': 'another.value'}, + ) + + instance2 = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='flavor_id', + image_meta=dict(os_type='linux', distro='Ubuntu', version='16.04'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata={'some_key': 'some.value', 'another^key': 'another.value'}, + ) + + # different image properties + instance3 = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='flavor_id', + image_meta=dict(os_type='linux', distro='Centos', version='7'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata={'some_key': 'some.value', 'another^key': 'another.value'}, + ) + + # different flavor + instance4 = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='another_flavor', + image_meta=dict(os_type='linux', distro='Ubuntu', version='16.04'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata={'some_key': 'some.value', 'another^key': 'another.value'}, + ) + + # different metadata + instance5 = model.Instance( + entity_id='instance_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + flavor='flavor_id', + image_meta=dict(os_type='linux', distro='Ubuntu', version='16.04'), + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='hostname', + metadata=dict(), + ) + + self.assertTrue(instance1 == instance2) + self.assertTrue(instance1 != instance3) + self.assertTrue(instance1 != instance4) + self.assertTrue(instance1 != instance5) + + def test_volume_serialize(self): + volume = model.Volume( + entity_id='volume_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='volume_name', + volume_type='volume_type_id', + size=2, + attached_to=['instance_id1', 'instance_id2'], + ) + + entry = volume.as_dict() + self.assertEqual('volume_id', entry['entity_id']) + self.assertEqual('project_id', entry['project_id']) + self.assertEqual('volume', entry['entity_type']) + self.assertEqual('volume_name', entry['name']) + self.assertEqual('volume_type_id', entry['volume_type']) + self.assertEqual(2, entry['size']) + self.assertEqual(['instance_id1', 'instance_id2'], entry['attached_to']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), entry['last_event']) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), entry['start']) + self.assertIsNone(entry['end']) + + def test_volume_unserialize(self): + entry = { + 'entity_id': 'volume_id', + 'entity_type': 'volume', + 'project_id': 'project_id', + 'start': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'end': None, + 'last_event': datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + 'volume_type': 'volume_type_id', + 'name': 'volume_name', + 'size': 2, + } + + volume = model.get_entity_from_dict(entry) + self.assertEqual('volume_id', volume.entity_id) + self.assertEqual('project_id', volume.project_id) + self.assertEqual('volume', volume.entity_type) + self.assertEqual('volume_name', volume.name) + self.assertEqual(2, volume.size) + self.assertEqual([], volume.attached_to) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), volume.last_event) + self.assertEqual(datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), volume.start) + self.assertIsNone(volume.end) + + def test_volume_comparison(self): + volume1 = model.Volume( + entity_id='volume_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='volume_name', + volume_type='volume_type_id', + size=2, + attached_to=['instance_id1', 'instance_id2'], + ) + + volume2 = model.Volume( + entity_id='volume_id', + project_id='project_id', + start=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + end=None, + last_event=datetime(2014, 1, 1, 0, 0, 0, 0, pytz.utc), + name='volume_name', + volume_type='volume_type_id', + size=2, + attached_to=['instance_id1', 'instance_id2'], + ) + + self.assertTrue(volume1 == volume2) + + volume2.volume_type = 'another_volume_type' + self.assertFalse(volume1 == volume2) + + volume2.volume_type = 'volume_type_id' + volume2.size = 3 + self.assertFalse(volume1 == volume2) + + volume2.volume_type = 'volume_type_id' + volume2.size = 2 + volume2.attached_to = [] + self.assertFalse(volume1 == volume2) + + def test_volume_type_serialize(self): + volume_type = model.VolumeType( + volume_type_id='id', + volume_type_name='name', + ) + + entry = volume_type.as_dict() + self.assertEqual('id', entry['volume_type_id']) + self.assertEqual('name', entry['volume_type_name']) + + def test_volume_type_unserialize(self): + entry = dict(volume_type_id='id', volume_type_name='name') + + volume_type = model.VolumeType.from_dict(entry) + self.assertEqual('id', volume_type.volume_type_id) + self.assertEqual('name', volume_type.volume_type_name) + + def test_volume_type_comparison(self): + volume_type1 = model.VolumeType(volume_type_id='id', volume_type_name='name') + volume_type2 = model.VolumeType(volume_type_id='id2', volume_type_name='name2') + volume_type3 = model.VolumeType(volume_type_id='id', volume_type_name='name') + + self.assertTrue(volume_type1 != volume_type2) + self.assertTrue(volume_type1 == volume_type3) + + def test_unserialize_unknown_entity(self): + self.assertRaises(exception.EntityTypeNotSupportedException, + model.get_entity_from_dict, + dict(entity_type='unknown')) diff --git a/almanach/tests/unit/storage/drivers/test_mongodb_driver.py b/almanach/tests/unit/storage/drivers/test_mongodb_driver.py index 278977f..202a686 100644 --- a/almanach/tests/unit/storage/drivers/test_mongodb_driver.py +++ b/almanach/tests/unit/storage/drivers/test_mongodb_driver.py @@ -59,14 +59,14 @@ class MongoDbDriverTest(base.BaseTestCase): def test_get_active_entity(self): fake_entity = a(instance().with_metadata({})) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) self.assertEqual(fake_entity, self.adapter.get_active_entity(fake_entity.entity_id)) def test_get_active_entity_with_special_metadata_characters(self): fake_entity = a(instance().with_metadata({"a_metadata_not_sanitize": "not.sanitize", "a_metadata^to_sanitize": "this.sanitize"})) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) entity = self.adapter.get_active_entity(fake_entity.entity_id) expected_entity = a(instance() @@ -87,7 +87,7 @@ class MongoDbDriverTest(base.BaseTestCase): fake_entity = a(instance()) fake_entity.entity_type = "will_raise_exception" - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) self.assertRaises(exception.EntityTypeNotSupportedException, self.adapter.get_active_entity, fake_entity.entity_id) @@ -115,7 +115,7 @@ class MongoDbDriverTest(base.BaseTestCase): ] all_entities = fake_active_entities + fake_inactive_entities - [self.db.entity.insert(model.todict(fake_entity)) for fake_entity in all_entities] + [self.db.entity.insert(fake_entity.as_dict()) for fake_entity in all_entities] self.assertEqual(4, self.adapter.count_entities()) self.assertEqual(2, self.adapter.count_active_entities()) @@ -124,7 +124,7 @@ class MongoDbDriverTest(base.BaseTestCase): def test_get_all_entities_by_id(self): fake_entity = a(instance().with_id("id1").with_start(2014, 1, 1, 8, 0, 0).with_no_end()) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) entries = self.adapter.get_all_entities_by_id(entity_id="id1") self.assertEqual(1, len(entries)) @@ -173,7 +173,7 @@ class MongoDbDriverTest(base.BaseTestCase): .with_project_id("project_id")), ] - [self.db.entity.insert(model.todict(fake_entity)) for fake_entity in fake_instances + fake_volumes] + [self.db.entity.insert(fake_entity.as_dict()) for fake_entity in fake_instances + fake_volumes] entities = self.adapter.get_all_entities_by_project("project_id", datetime(2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc), @@ -217,7 +217,7 @@ class MongoDbDriverTest(base.BaseTestCase): .with_metadata({"a_metadata.to_sanitize": "this.sanitize"})), ] - [self.db.entity.insert(model.todict(fake_entity)) for fake_entity in fake_instances] + [self.db.entity.insert(fake_entity.as_dict()) for fake_entity in fake_instances] entities = self.adapter.get_all_entities_by_project("project_id", datetime(2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc), @@ -265,16 +265,17 @@ class MongoDbDriverTest(base.BaseTestCase): .with_project_id("project_id")), ] - [self.db.entity.insert(model.todict(fake_entity)) + [self.db.entity.insert(fake_entity.as_dict()) for fake_entity in fake_entities_in_period + fake_entities_out_period] entities = self.adapter.get_all_entities_by_project("project_id", datetime(2014, 1, 1, 6, 0, 0, tzinfo=pytz.utc), datetime(2014, 1, 1, 9, 0, 0, tzinfo=pytz.utc)) self.assertEqual(3, len(entities)) - self.assertIn(fake_entities_in_period[0], entities) - self.assertIn(fake_entities_in_period[1], entities) - self.assertIn(fake_entities_in_period[2], entities) + entity_ids = [entity.entity_id for entity in entities] + self.assertIn(fake_entities_in_period[0].entity_id, entity_ids) + self.assertIn(fake_entities_in_period[1].entity_id, entity_ids) + self.assertIn(fake_entities_in_period[2].entity_id, entity_ids) def test_get_all_entities_by_id_and_date(self): start = datetime(2016, 3, 1, 0, 0, 0, 0, pytz.utc) @@ -292,7 +293,7 @@ class MongoDbDriverTest(base.BaseTestCase): .with_no_end()), ] - [self.db.entity.insert(model.todict(fake_instance)) for fake_instance in instances] + [self.db.entity.insert(fake_instance.as_dict()) for fake_instance in instances] entities = self.adapter.get_all_entities_by_id_and_date("id1", start, end) self.assertEqual(1, len(entities)) @@ -302,7 +303,7 @@ class MongoDbDriverTest(base.BaseTestCase): fake_entity = a(instance()) end_date = datetime(2015, 10, 21, 16, 29, 0) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) self.adapter.close_active_entity(fake_entity.entity_id, end_date) self.assertEqual(self.db.entity.find_one({"entity_id": fake_entity.entity_id})["end"], end_date) @@ -310,7 +311,7 @@ class MongoDbDriverTest(base.BaseTestCase): def test_update_closed_entity(self): fake_entity = a(instance().with_end(2016, 3, 2, 0, 0, 0)) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) fake_entity.flavor = "my_new_flavor" self.adapter.update_closed_entity(fake_entity, data={"flavor": fake_entity.flavor}) @@ -320,20 +321,22 @@ class MongoDbDriverTest(base.BaseTestCase): def test_update_active_entity(self): fake_entity = a(instance()) - fake_entity.os.distro = "Centos" + fake_entity.image_meta['distro'] = "Centos" - self.db.entity.insert(model.todict(fake_entity)) - fake_entity.os.distro = "Windows" + self.db.entity.insert(fake_entity.as_dict()) + fake_entity.image_meta['distro'] = "Windows" self.adapter.update_active_entity(fake_entity) self.assertEqual(self.db.entity.find_one({"entity_id": fake_entity.entity_id})["os"]["distro"], - fake_entity.os.distro) + fake_entity.image_meta['distro']) + self.assertEqual(self.db.entity.find_one({"entity_id": fake_entity.entity_id})["image_meta"]["distro"], + fake_entity.image_meta['distro']) def test_delete_active_entity(self): fake_entity = a(volume()) - self.db.entity.insert(model.todict(fake_entity)) + self.db.entity.insert(fake_entity.as_dict()) self.assertEqual(1, self.db.entity.count()) self.adapter.delete_active_entity(fake_entity.entity_id) @@ -348,7 +351,7 @@ class MongoDbDriverTest(base.BaseTestCase): def test_get_volume_type(self): fake_volume_type = a(volume_type()) - self.db.volume_type.insert(model.todict(fake_volume_type)) + self.db.volume_type.insert(fake_volume_type.as_dict()) self.assertEqual(self.adapter.get_volume_type(fake_volume_type.volume_type_id), fake_volume_type) def test_get_volume_type_that_does_not_exists(self): @@ -360,7 +363,7 @@ class MongoDbDriverTest(base.BaseTestCase): def test_delete_volume_type(self): fake_volume_type = a(volume_type()) - self.db.volume_type.insert(model.todict(fake_volume_type)) + self.db.volume_type.insert(fake_volume_type.as_dict()) self.assertEqual(1, self.db.volume_type.count()) self.adapter.delete_volume_type(fake_volume_type.volume_type_id) self.assertEqual(0, self.db.volume_type.count()) @@ -379,7 +382,7 @@ class MongoDbDriverTest(base.BaseTestCase): fake_volume_types = [a(volume_type()), a(volume_type())] for fake_volume_type in fake_volume_types: - self.db.volume_type.insert(model.todict(fake_volume_type)) + self.db.volume_type.insert(fake_volume_type.as_dict()) self.assertEqual(len(self.adapter.list_volume_types()), 2) diff --git a/devstack/plugin.sh b/devstack/plugin.sh index db83afb..3c18a72 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -46,6 +46,8 @@ function almanach_configure { iniset $ALMANACH_CONF collector transport_url rabbit://$RABBIT_USERID:$RABBIT_PASSWORD@$RABBIT_HOST:5672 iniset $ALMANACH_CONF database connection_url mongodb://localhost/almanach + + iniset $ALMANACH_CONF entities instance_image_meta distro,version,os_type } function almanach_configure_external_services {