From 8110d18e404574c91ccd7c5c7a88551c737db7e1 Mon Sep 17 00:00:00 2001 From: LF Jenkins CI Date: Thu, 14 Oct 2021 13:32:47 +0000 Subject: [PATCH 02/11] Automation adds .gitreview Change-Id: I3d94569dc7e6d2fadd8375a44330f75190f3715b Signed-off-by: lf-jobbuilder --- .gitreview | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .gitreview diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..0c747aa --- /dev/null +++ b/.gitreview @@ -0,0 +1,7 @@ + + [gerrit] + host=gerrit.o-ran-sc.org + port=29418 + project=pti/o2 + defaultbranch=master + \ No newline at end of file -- 2.16.6 From 2ff453c321f9c7edb255c414d0cb4d0e22558b80 Mon Sep 17 00:00:00 2001 From: LF Jenkins CI Date: Thu, 14 Oct 2021 13:32:54 +0000 Subject: [PATCH 03/11] Automation adds INFO.yaml Change-Id: Ib402fc93bb947777970c20550d97e106dea2ebac Signed-off-by: lf-jobbuilder --- INFO.yaml | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 INFO.yaml diff --git a/INFO.yaml b/INFO.yaml new file mode 100644 index 0000000..e0a0aef --- /dev/null +++ b/INFO.yaml @@ -0,0 +1,59 @@ +--- +project: 'inf_pti_o2' +project_creation_date: '2021-10-14' +project_category: '' +lifecycle_state: 'Incubation' +project_lead: &inf_ptl + name: 'Jackie Huang' + email: 'jackie.huang@windriver.com' + company: 'Wind River' + id: 'jackiehjm' + timezone: 'Asia/Shanghai' +primary_contact: *inf_ptl +issue_tracking: + type: 'jira' + url: 'https://jira.o-ran-sc.org/projects/INF' + key: 'INF' +mailing_list: + type: 'groups.io' + url: 'https://lists.o-ran-sc.org/g/main' + tag: '<[inf]>' +realtime_discussion: '' +meetings: + - type: 'zoom' + agenda: 'https://wiki.o-ran-sc.org/display/IN/INF+project+meetings' + url: 'https://wiki.o-ran-sc.org/display/IN/INF+project+meetings' + server: 'n/a' + channel: 'n/a' + repeats: 'bi-weekly' + time: 'Tuesday, 12:00 UTC' +repositories: + - 'pti/o2' +committers: + - name: 'Bin Yang' + email: 'bin.yang@windriver.com' + company: 'Wind River' + id: biny993 + timezone: 'Asia/Shanghai' + - name: 'Litao Gao' + email: 'litao.gao@windriver.com' + company: 'Wind River' + id: 'gaolitaowrs' + timezone: 'Asia/Shanghai' + - name: 'Xiaohua Zhang' + email: 'zhangxiaohua@chinamobile.com' + company: 'China Mobile' + id: 'Xiaohua626' + timezone: 'Asia/Shanghai' + - name: 'Jackie Huang' + email: 'jackie.huang@windriver.com' + company: 'Wind River' + id: 'jackiehjm' + timezone: 'Asia/Shanghai' +tsc: + # yamllint disable rule:line-length + approval: 'https://wiki.o-ran-sc.org/display/TOC#ORANSCTechnicalOversightCommittee(TOC)-20210929' + changes: + - type: '' + name: '' + link: '' -- 2.16.6 From 40728ef923af791f42582a657c17abdaeec82b6d Mon Sep 17 00:00:00 2001 From: Jackie Huang Date: Fri, 22 Oct 2021 11:02:53 +0800 Subject: [PATCH 04/11] Fix the format of .gitreview Fix the format of .gitreview to avoid the following error: ConfigParser.MissingSectionHeaderError: File contains no section headers. Signed-off-by: Jackie Huang Change-Id: Ib81ad65fa197b19bfa77f9f39cd1a89174f1da3d --- .gitreview | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitreview b/.gitreview index 0c747aa..8b13fe1 100644 --- a/.gitreview +++ b/.gitreview @@ -1,7 +1,5 @@ - - [gerrit] - host=gerrit.o-ran-sc.org - port=29418 - project=pti/o2 - defaultbranch=master - \ No newline at end of file +[gerrit] +host=gerrit.o-ran-sc.org +port=29418 +project=pti/o2 +defaultbranch=master -- 2.16.6 From 8339c9a882a586578b37f44a504e21c5208611c0 Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Thu, 21 Oct 2021 19:10:40 +0800 Subject: [PATCH 05/11] Add framework and apiserver Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: I3e3022662a8d7e0158657811c0422f3503cb7883 --- .gitignore | 4 ++ Dockerfile | 15 +++++ README.md | 31 +++++++++ docker-compose.yml | 60 +++++++++++++++++ mypy.ini | 7 ++ requirements-test.txt | 8 +++ requirements.txt | 4 ++ src/__init__.py | 13 ++++ src/o2common/__init__.py | 13 ++++ src/o2dms/__init__.py | 13 ++++ src/o2dms/setup.py | 19 ++++++ src/o2ims/__init__.py | 13 ++++ src/o2ims/adapter/notifications.py | 20 ++++++ src/o2ims/adapter/ocloud_repository.py | 77 ++++++++++++++++++++++ src/o2ims/adapter/orm.py | 97 ++++++++++++++++++++++++++++ src/o2ims/adapter/redis_eventpublisher.py | 30 +++++++++ src/o2ims/bootstrap.py | 63 ++++++++++++++++++ src/o2ims/config.py | 41 ++++++++++++ src/o2ims/domain/__init__.py | 13 ++++ src/o2ims/domain/commands.py | 25 +++++++ src/o2ims/domain/events.py | 23 +++++++ src/o2ims/domain/ocloud.py | 83 ++++++++++++++++++++++++ src/o2ims/domain/resource_type.py | 6 ++ src/o2ims/entrypoints/__init__.py | 13 ++++ src/o2ims/entrypoints/flask_application.py | 29 +++++++++ src/o2ims/entrypoints/redis_eventconsumer.py | 45 +++++++++++++ src/o2ims/service/__init__.py | 13 ++++ src/o2ims/service/handlers.py | 32 +++++++++ src/o2ims/service/messagebus.py | 69 ++++++++++++++++++++ src/o2ims/service/unit_of_work.py | 78 ++++++++++++++++++++++ src/o2ims/views/ocloud_view.py | 36 +++++++++++ src/setup.py | 7 ++ tests/__init__.py | 13 ++++ tests/conftest.py | 88 +++++++++++++++++++++++++ tests/integration/__init__.py | 13 ++++ tests/integration/test_ocloud_repository.py | 93 ++++++++++++++++++++++++++ tests/pytest.ini | 4 ++ tests/unit/__init__.py | 13 ++++ tests/unit/test_ocloud.py | 39 +++++++++++ 39 files changed, 1263 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 mypy.ini create mode 100644 requirements-test.txt create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/o2common/__init__.py create mode 100644 src/o2dms/__init__.py create mode 100644 src/o2dms/setup.py create mode 100644 src/o2ims/__init__.py create mode 100644 src/o2ims/adapter/notifications.py create mode 100644 src/o2ims/adapter/ocloud_repository.py create mode 100644 src/o2ims/adapter/orm.py create mode 100644 src/o2ims/adapter/redis_eventpublisher.py create mode 100644 src/o2ims/bootstrap.py create mode 100644 src/o2ims/config.py create mode 100644 src/o2ims/domain/__init__.py create mode 100644 src/o2ims/domain/commands.py create mode 100644 src/o2ims/domain/events.py create mode 100644 src/o2ims/domain/ocloud.py create mode 100644 src/o2ims/domain/resource_type.py create mode 100644 src/o2ims/entrypoints/__init__.py create mode 100644 src/o2ims/entrypoints/flask_application.py create mode 100644 src/o2ims/entrypoints/redis_eventconsumer.py create mode 100644 src/o2ims/service/__init__.py create mode 100644 src/o2ims/service/handlers.py create mode 100644 src/o2ims/service/messagebus.py create mode 100644 src/o2ims/service/unit_of_work.py create mode 100644 src/o2ims/views/ocloud_view.py create mode 100644 src/setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_ocloud_repository.py create mode 100644 tests/pytest.ini create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_ocloud.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea29b71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv +.mypy_cache +__pycache__ +*.egg-info diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e1afbcb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10-slim-buster + +COPY requirements.txt /tmp/ +RUN pip install -r /tmp/requirements.txt + +COPY requirements-test.txt /tmp/ +RUN pip install -r /tmp/requirements-test.txt + +RUN mkdir -p /src +COPY src/ /src/ +RUN pip install -e /src + +COPY tests/ /tests/ + +WORKDIR /src diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb6900a --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +## Building containers + + +```sh +docker-compose build +``` + + +## Creating a local virtualenv (optional) + +```sh +python3.8 -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +pip install -e src/ +``` + +## Running the tests + +```sh +docker-compose up -d +docker-compose run --rm --no-deps --entrypoint=pytest api /tests/unit /tests/integration +pytest tests/unit +pytest tests/integration +pytest tests/e2e +``` + +## Tear down containers + +```sh +docker-compose down --remove-orphans +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..91d3ec8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3" + +services: + + redis_pubsub: + build: + context: . + dockerfile: Dockerfile + image: o2imsdms-image + depends_on: + - postgres + - redis + environment: + - DB_HOST=postgres + - DB_PASSWORD=o2ims123 + - REDIS_HOST=redis + - PYTHONDONTWRITEBYTECODE=1 + volumes: + - ./src:/src + - ./tests:/tests + entrypoint: + - python + - /src/o2ims/entrypoints/redis_eventconsumer.py + + api: + image: o2imsdms-image + depends_on: + - redis_pubsub + environment: + - DB_HOST=postgres + - DB_PASSWORD=o2ims123 + - API_HOST=api + - REDIS_HOST=redis + - PYTHONDONTWRITEBYTECODE=1 + - FLASK_APP=o2ims/entrypoints/flask_application.py + - FLASK_DEBUG=1 + - PYTHONUNBUFFERED=1 + volumes: + - ./src:/src + - ./tests:/tests + entrypoint: + - flask + - run + - --host=0.0.0.0 + - --port=80 + ports: + - "5005:80" + + postgres: + image: postgres:9.6 + environment: + - POSTGRES_USER=o2ims + - POSTGRES_PASSWORD=o2ims123 + ports: + - "54321:5432" + + redis: + image: redis:alpine + ports: + - "63791:6379" diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..31ee9f6 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +ignore_missing_imports = False +mypy_path = ./src +check_untyped_defs = True + +[mypy-pytest.*,sqlalchemy.*,redis.*] +ignore_missing_imports = True diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..251bb2b --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,8 @@ +pylint +mypy +requests + +pytest +pytest-icdiff + +tenacity diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..985e50e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +sqlalchemy +redis +psycopg2-binary diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2common/__init__.py b/src/o2common/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2common/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2dms/__init__.py b/src/o2dms/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2dms/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2dms/setup.py b/src/o2dms/setup.py new file mode 100644 index 0000000..feafb7a --- /dev/null +++ b/src/o2dms/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup + +setup( + name="o2common", + version="1.0", + packages=["o2common"], +) + +setup( + name="o2ims", + version="1.0", + packages=["o2ims"], +) + +setup( + name="o2dms", + version="1.0", + packages=["o2dms"], +) diff --git a/src/o2ims/__init__.py b/src/o2ims/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2ims/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2ims/adapter/notifications.py b/src/o2ims/adapter/notifications.py new file mode 100644 index 0000000..e2ad375 --- /dev/null +++ b/src/o2ims/adapter/notifications.py @@ -0,0 +1,20 @@ +# pylint: disable=too-few-public-methods +import abc +import smtplib +from o2ims import config + + +class AbstractNotifications(abc.ABC): + @abc.abstractmethod + def send(self, message): + raise NotImplementedError + + +SMO_O2_ENDPOINT = config.get_smo_o2endpoint() + +class SmoO2Notifications(AbstractNotifications): + def __init__(self, smoO2Endpoint=SMO_O2_ENDPOINT): + self.smoO2Endpoint = smoO2Endpoint + + def send(self, message): + pass diff --git a/src/o2ims/adapter/ocloud_repository.py b/src/o2ims/adapter/ocloud_repository.py new file mode 100644 index 0000000..6d3d221 --- /dev/null +++ b/src/o2ims/adapter/ocloud_repository.py @@ -0,0 +1,77 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 abc +from typing import Set +from o2ims.adapter import orm +from o2ims.domain import ocloud + +class OcloudRepository(abc.ABC): + def __init__(self): + self.seen = set() # type: Set[ocloud.Ocloud] + + def add(self, ocloud: ocloud.Ocloud): + self._add(ocloud) + self.seen.add(ocloud) + + def get(self, ocloudid) -> ocloud.Ocloud: + ocloud = self._get(ocloudid) + if ocloud: + self.seen.add(ocloud) + return ocloud + + def update(self, ocloud: ocloud.Ocloud): + self._update(ocloud) + + # def update_fields(self, ocloudid: str, updatefields: dict): + # self._update(ocloudid, updatefields) + + @abc.abstractmethod + def _add(self, ocloud: ocloud.Ocloud): + raise NotImplementedError + + @abc.abstractmethod + def _get(self, ocloudid) -> ocloud.Ocloud: + raise NotImplementedError + + @abc.abstractmethod + def _update(self, ocloud: ocloud.Ocloud): + raise NotImplementedError + + +class OcloudSqlAlchemyRepository(OcloudRepository): + def __init__(self, session): + super().__init__() + self.session = session + + def _add(self, ocloud: ocloud.Ocloud): + self.session.add(ocloud) + # self.session.add_all(ocloud.deploymentManagers) + + def _get(self, ocloudid) -> ocloud.Ocloud: + return self.session.query(ocloud.Ocloud).filter_by(oCloudId=ocloudid).first() + + def _update(self, ocloud: ocloud.Ocloud): + self.session.add(ocloud) + + # def _update_fields(self, ocloudid: str, updatefields: dict): + # dmslist = updatefields.pop("deploymentManagers", None) + # if dmslist: + # self._update_dms_list(dmslist) + # if updatefields: + # self.session.query(ocloud.Ocloud).filter_by(oCloudId=ocloudid).update(updatefields) + + # def _update_dms_list(self, dms_list: list): + # for dms in dms_list or []: + # self.session.query(ocloud.DeploymentManager).filter_by(deploymentManagerId=dms.deploymentManagerId).update(dms) diff --git a/src/o2ims/adapter/orm.py b/src/o2ims/adapter/orm.py new file mode 100644 index 0000000..6cf9847 --- /dev/null +++ b/src/o2ims/adapter/orm.py @@ -0,0 +1,97 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from sqlalchemy import ( + Table, + MetaData, + Column, + Integer, + String, + Date, + ForeignKey, + event, +) + +from sqlalchemy.orm import mapper, relationship +from sqlalchemy.sql.expression import true + +from o2ims.domain import ocloud as ocloudModel + +logger = logging.getLogger(__name__) + +metadata = MetaData() + +ocloud = Table( + "ocloud", + metadata, + Column("oCloudId", String(255), primary_key=True), + Column("name", String(255)), + Column("description", String(255)), + Column("infrastructureManagementServiceEndpoint", String(255)) +) + +resourcepool = Table( + "resourcepool", + metadata, + Column("resourcePoolId", String(255), primary_key=True), + Column("name", String(255)), + Column("location", String(255)), + Column("oCloudId", ForeignKey("ocloud.oCloudId")), + # Column("extensions", String(1024)) +) + +resourcetype = Table( + "resourcetype", + metadata, + Column("resourceTypeId", String(255), primary_key=True), + Column("oCloudId", ForeignKey("ocloud.oCloudId")), + Column("name", String(255)), +) + +resource = Table( + "resource", + metadata, + Column("resourceId", String(255), primary_key=True), + Column("parentId", String(255)), + Column("resourceTypeId", ForeignKey("resourcetype.resourceTypeId")), + Column("resourcePoolId", ForeignKey("resourcepool.resourcePoolId")), + Column("oCloudId", ForeignKey("ocloud.oCloudId")) +) + +deploymentmanager = Table( + "deploymentmanager", + metadata, + Column("deploymentManagerId", String(255), primary_key=True), + Column("name", String(255)), + Column("deploymentManagementServiceEndpoint", String(255)), + Column("oCloudId", ForeignKey("ocloud.oCloudId")) +) + + +def start_o2ims_mappers(): + logger.info("Starting O2 IMS mappers") + dm_mapper = mapper(ocloudModel.DeploymentManager, deploymentmanager) + resourcepool_mapper = mapper(ocloudModel.ResourcePool, resourcepool) + resourcetype_mapper = mapper(ocloudModel.ResourceType, resourcetype) + resource_mapper = mapper(ocloudModel.Resource, resource) + ocloud_mapper = mapper( + ocloudModel.Ocloud, + ocloud, + properties={ + "deploymentManagers": relationship(dm_mapper), + "resourceTypes": relationship(resourcetype_mapper), + "resourcePools": relationship(resourcepool_mapper) + }) diff --git a/src/o2ims/adapter/redis_eventpublisher.py b/src/o2ims/adapter/redis_eventpublisher.py new file mode 100644 index 0000000..835c72c --- /dev/null +++ b/src/o2ims/adapter/redis_eventpublisher.py @@ -0,0 +1,30 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +from dataclasses import asdict +import redis + +from o2ims import config +from o2ims.domain import events + +logger = logging.getLogger(__name__) + +r = redis.Redis(**config.get_redis_host_and_port()) + + +def publish(channel, event: events.Event): + logging.info("publishing: channel=%s, event=%s", channel, event) + r.publish(channel, json.dumps(asdict(event))) diff --git a/src/o2ims/bootstrap.py b/src/o2ims/bootstrap.py new file mode 100644 index 0000000..00cdc4a --- /dev/null +++ b/src/o2ims/bootstrap.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 inspect +from typing import Callable +from o2ims.adapter import orm, redis_eventpublisher +from o2ims.adapter.notifications import AbstractNotifications, SmoO2Notifications + +from o2ims.service import handlers, messagebus, unit_of_work + + +def bootstrap( + start_orm: bool = True, + uow: unit_of_work.AbstractUnitOfWork = unit_of_work.SqlAlchemyUnitOfWork(), + notifications: AbstractNotifications = None, + publish: Callable = redis_eventpublisher.publish, +) -> messagebus.MessageBus: + + if notifications is None: + notifications = SmoO2Notifications() + + if start_orm: + orm.start_o2ims_mappers() + + dependencies = {"uow": uow, "notifications": notifications, "publish": publish} + injected_event_handlers = { + event_type: [ + inject_dependencies(handler, dependencies) + for handler in event_handlers + ] + for event_type, event_handlers in handlers.EVENT_HANDLERS.items() + } + injected_command_handlers = { + command_type: inject_dependencies(handler, dependencies) + for command_type, handler in handlers.COMMAND_HANDLERS.items() + } + + return messagebus.MessageBus( + uow=uow, + event_handlers=injected_event_handlers, + command_handlers=injected_command_handlers, + ) + + +def inject_dependencies(handler, dependencies): + params = inspect.signature(handler).parameters + deps = { + name: dependency + for name, dependency in dependencies.items() + if name in params + } + return lambda message: handler(message, **deps) diff --git a/src/o2ims/config.py b/src/o2ims/config.py new file mode 100644 index 0000000..798ea79 --- /dev/null +++ b/src/o2ims/config.py @@ -0,0 +1,41 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 os + +def get_postgres_uri(): + host = os.environ.get("DB_HOST", "localhost") + port = 54321 if host == "localhost" else 5432 + password = os.environ.get("DB_PASSWORD", "o2ims123") + user, db_name = "o2ims", "o2ims" + return f"postgresql://{user}:{password}@{host}:{port}/{db_name}" + + +def get_api_url(): + host = os.environ.get("API_HOST", "localhost") + port = 5005 if host == "localhost" else 80 + return f"http://{host}:{port}" + +def get_o2ims_api_base(): + return '/o2ims_infrastructureInventory/v1' + +def get_redis_host_and_port(): + host = os.environ.get("REDIS_HOST", "localhost") + port = 63791 if host == "localhost" else 6379 + return dict(host=host, port=port) + + +def get_smo_o2endpoint(): + smo_o2endpoint = os.environ.get("SMO_O2_ENDPOINT", "http://localhost/smo_sim") + return smo_o2endpoint diff --git a/src/o2ims/domain/__init__.py b/src/o2ims/domain/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2ims/domain/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2ims/domain/commands.py b/src/o2ims/domain/commands.py new file mode 100644 index 0000000..9df6cff --- /dev/null +++ b/src/o2ims/domain/commands.py @@ -0,0 +1,25 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# pylint: disable=too-few-public-methods +from datetime import date +from typing import Optional +from dataclasses import dataclass + + +class Command: + pass + +class UpdateDms(Command): + ref: str \ No newline at end of file diff --git a/src/o2ims/domain/events.py b/src/o2ims/domain/events.py new file mode 100644 index 0000000..755f65e --- /dev/null +++ b/src/o2ims/domain/events.py @@ -0,0 +1,23 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# pylint: disable=too-few-public-methods +from dataclasses import dataclass + +class Event: + pass + +@dataclass +class OcloudUpdated(Event): + oCloudId: str diff --git a/src/o2ims/domain/ocloud.py b/src/o2ims/domain/ocloud.py new file mode 100644 index 0000000..ddd2646 --- /dev/null +++ b/src/o2ims/domain/ocloud.py @@ -0,0 +1,83 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 annotations +from dataclasses import dataclass +from datetime import date +from typing import Optional, List, Set +from .resource_type import ResourceTypeEnum +# from uuid import UUID + +class Ocloud: + def __init__( + self, ocloudid: str, name: str, imsendpoint: str, + description: str = '', version_number: int = 0) -> None: + + self.oCloudId = ocloudid + self.version_number = version_number + self.name = name + self.description = description + self.infrastructureManagementServiceEndpoint = imsendpoint + self.resourcePools = [] + self.deploymentManagers = [] + self.resourceTypes = [] + self.extensions = [] + self.events = [] + + def addDeploymentManager(self, deploymentManager: DeploymentManager) -> None: + deploymentManager.oCloudId = self.oCloudId + old = filter( + lambda x: x.deploymentManagerId == deploymentManager.deploymentManagerId, + self.deploymentManagers) + for o in old or []: + self.deploymentManagers.remove(o) + self.deploymentManagers.append(deploymentManager) + +class DeploymentManager: + def __init__(self, id: str, name: str, ocloudid: str, dmsendpoint: str) -> None: + self.deploymentManagerId = id + self.name = name + self.oCloudId = ocloudid + self.deploymentManagementServiceEndpoint = dmsendpoint + self.extensions = [] + + +class ResourcePool: + def __init__(self, id: str, name: str, location: str, ocloudid: str) -> None: + self.resourcePoolId = id + self.name = name + self.location = location + self.oCloudId = ocloudid + self.extensions = [] + + +class ResourceType: + def __init__(self, typeid: str, name:str, typeEnum: ResourceTypeEnum, ocloudid: str) -> None: + self.resourceTypeId = typeid + self.resourceTypeEnum = typeEnum.value + self.name = name + self.oCloudId = ocloudid + self.extensions = [] + + +class Resource: + def __init__(self, resourceId:str, resourceTypeId: str, resourcePoolId: str) -> None: + self.resourceId = resourceId + self.oCloudId = None # tbd + self.resourceTypeId = resourceTypeId + self.resourcePoolId = resourcePoolId + self.parentId = None + self.elements = [] + self.extensions = [] + diff --git a/src/o2ims/domain/resource_type.py b/src/o2ims/domain/resource_type.py new file mode 100644 index 0000000..faff8ea --- /dev/null +++ b/src/o2ims/domain/resource_type.py @@ -0,0 +1,6 @@ +from enum import Enum + +class ResourceTypeEnum(Enum): + PSERVER = 1 + PSERVER_CPU = 2 + PSERVER_RAM = 3 diff --git a/src/o2ims/entrypoints/__init__.py b/src/o2ims/entrypoints/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2ims/entrypoints/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2ims/entrypoints/flask_application.py b/src/o2ims/entrypoints/flask_application.py new file mode 100644 index 0000000..933301c --- /dev/null +++ b/src/o2ims/entrypoints/flask_application.py @@ -0,0 +1,29 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 datetime import datetime +from flask import Flask, jsonify, request +from o2ims.domain import commands +from o2ims.service.handlers import InvalidResourceType +from o2ims import bootstrap, config +from o2ims.views import ocloud_view + +app = Flask(__name__) +bus = bootstrap.bootstrap() +apibase = config.get_o2ims_api_base() + +@app.route(apibase, methods=["GET"]) +def oclouds(): + result = ocloud_view.oclouds(bus.uow) + return jsonify(result), 200 diff --git a/src/o2ims/entrypoints/redis_eventconsumer.py b/src/o2ims/entrypoints/redis_eventconsumer.py new file mode 100644 index 0000000..a22b61c --- /dev/null +++ b/src/o2ims/entrypoints/redis_eventconsumer.py @@ -0,0 +1,45 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +import redis + +from o2ims import bootstrap, config +from o2ims.domain import commands + +logger = logging.getLogger(__name__) + +r = redis.Redis(**config.get_redis_host_and_port()) + + +def main(): + logger.info("Redis pubsub starting") + bus = bootstrap.bootstrap() + pubsub = r.pubsub(ignore_subscribe_messages=True) + pubsub.subscribe("dms_changed") + + for m in pubsub.listen(): + handle_dms_changed(m, bus) + + +def handle_dms_changed(m, bus): + logger.info("handling %s", m) + data = json.loads(m["data"]) + cmd = commands.UpdateDms(ref=data["dmsid"]) + bus.handle(cmd) + + +if __name__ == "__main__": + main() diff --git a/src/o2ims/service/__init__.py b/src/o2ims/service/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/src/o2ims/service/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/src/o2ims/service/handlers.py b/src/o2ims/service/handlers.py new file mode 100644 index 0000000..c75dc07 --- /dev/null +++ b/src/o2ims/service/handlers.py @@ -0,0 +1,32 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# pylint: disable=unused-argument +from __future__ import annotations +from dataclasses import asdict +from typing import List, Dict, Callable, Type, TYPE_CHECKING +from o2ims.domain import commands, events, ocloud + +if TYPE_CHECKING: + from . import unit_of_work + +class InvalidResourceType(Exception): + pass + + +EVENT_HANDLERS = { +} # type: Dict[Type[events.Event], List[Callable]] + +COMMAND_HANDLERS = { +} # type: Dict[Type[commands.Command], Callable] diff --git a/src/o2ims/service/messagebus.py b/src/o2ims/service/messagebus.py new file mode 100644 index 0000000..bff45ab --- /dev/null +++ b/src/o2ims/service/messagebus.py @@ -0,0 +1,69 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# pylint: disable=broad-except, attribute-defined-outside-init +from __future__ import annotations +import logging +from typing import Callable, Dict, List, Union, Type, TYPE_CHECKING +from o2ims.domain import commands, events + +if TYPE_CHECKING: + from . import unit_of_work + +logger = logging.getLogger(__name__) + +Message = Union[commands.Command, events.Event] + + +class MessageBus: + def __init__( + self, + uow: unit_of_work.AbstractUnitOfWork, + event_handlers: Dict[Type[events.Event], List[Callable]], + command_handlers: Dict[Type[commands.Command], Callable], + ): + self.uow = uow + self.event_handlers = event_handlers + self.command_handlers = command_handlers + + def handle(self, message: Message): + self.queue = [message] + while self.queue: + message = self.queue.pop(0) + if isinstance(message, events.Event): + self.handle_event(message) + elif isinstance(message, commands.Command): + self.handle_command(message) + else: + raise Exception(f"{message} was not an Event or Command") + + def handle_event(self, event: events.Event): + for handler in self.event_handlers[type(event)]: + try: + logger.debug("handling event %s with handler %s", event, handler) + handler(event) + self.queue.extend(self.uow.collect_new_events()) + except Exception: + logger.exception("Exception handling event %s", event) + continue + + def handle_command(self, command: commands.Command): + logger.debug("handling command %s", command) + try: + handler = self.command_handlers[type(command)] + handler(command) + self.queue.extend(self.uow.collect_new_events()) + except Exception: + logger.exception("Exception handling command %s", command) + raise diff --git a/src/o2ims/service/unit_of_work.py b/src/o2ims/service/unit_of_work.py new file mode 100644 index 0000000..4714729 --- /dev/null +++ b/src/o2ims/service/unit_of_work.py @@ -0,0 +1,78 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# pylint: disable=attribute-defined-outside-init +from __future__ import annotations +import abc +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm.session import Session + + +from o2ims import config +from o2ims.adapter import ocloud_repository + + +class AbstractUnitOfWork(abc.ABC): + oclouds: ocloud_repository.OcloudRepository + + def __enter__(self) -> AbstractUnitOfWork: + return self + + def __exit__(self, *args): + self.rollback() + + def commit(self): + self._commit() + + def collect_new_events(self): + for ocloud in self.oclouds.seen: + while ocloud.events: + yield ocloud.events.pop(0) + + @abc.abstractmethod + def _commit(self): + raise NotImplementedError + + @abc.abstractmethod + def rollback(self): + raise NotImplementedError + + +DEFAULT_SESSION_FACTORY = sessionmaker( + bind=create_engine( + config.get_postgres_uri(), + isolation_level="REPEATABLE READ", + ) +) + + +class SqlAlchemyUnitOfWork(AbstractUnitOfWork): + def __init__(self, session_factory=DEFAULT_SESSION_FACTORY): + self.session_factory = session_factory + + def __enter__(self): + self.session = self.session_factory() # type: Session + self.oclouds = ocloud_repository.OcloudSqlAlchemyRepository(self.session) + return super().__enter__() + + def __exit__(self, *args): + super().__exit__(*args) + self.session.close() + + def _commit(self): + self.session.commit() + + def rollback(self): + self.session.rollback() diff --git a/src/o2ims/views/ocloud_view.py b/src/o2ims/views/ocloud_view.py new file mode 100644 index 0000000..7005b5a --- /dev/null +++ b/src/o2ims/views/ocloud_view.py @@ -0,0 +1,36 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 o2ims.service import unit_of_work + + +def ocloud_one(ocloudid: str, uow: unit_of_work.SqlAlchemyUnitOfWork): + with uow: + results = uow.session.execute( + """ + SELECT oCloudId, name FROM ocloud WHERE oCloudId = :ocloudid + """, + dict(ocloudid=ocloudid), + ) + return dict(results[0]) if len(results) > 0 else None + + +def oclouds(uow: unit_of_work.SqlAlchemyUnitOfWork): + with uow: + results = uow.session.execute( + """ + SELECT oCloudId, name FROM ocloud + """, + ) + return [dict(r) for r in results] diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..7558373 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup + +setup( + name="o2imsdms", + version="1.0", + packages=["o2ims", "o2dms", "o2common"], +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7744dbd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,88 @@ +# pylint: disable=redefined-outer-name +import shutil +import subprocess +import time +from pathlib import Path + +import pytest +import redis +import requests +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, clear_mappers +from tenacity import retry, stop_after_delay + +from o2ims.adapter.orm import metadata, start_o2ims_mappers +from o2ims import config + + +@pytest.fixture +def in_memory_sqlite_db(): + engine = create_engine("sqlite:///:memory:") + # engine = create_engine("sqlite:///:memory:", echo=True) + metadata.create_all(engine) + return engine + + +@pytest.fixture +def sqlite_session_factory(in_memory_sqlite_db): + yield sessionmaker(bind=in_memory_sqlite_db) + + +@pytest.fixture +def mappers(): + start_o2ims_mappers() + yield + clear_mappers() + + +@retry(stop=stop_after_delay(10)) +def wait_for_postgres_to_come_up(engine): + return engine.connect() + + +@retry(stop=stop_after_delay(10)) +def wait_for_webapp_to_come_up(): + return requests.get(config.get_api_url()) + + +@retry(stop=stop_after_delay(10)) +def wait_for_redis_to_come_up(): + r = redis.Redis(**config.get_redis_host_and_port()) + return r.ping() + + +@pytest.fixture(scope="session") +def postgres_db(): + engine = create_engine(config.get_postgres_uri(), isolation_level="SERIALIZABLE") + wait_for_postgres_to_come_up(engine) + metadata.create_all(engine) + return engine + + +@pytest.fixture +def postgres_session_factory(postgres_db): + yield sessionmaker(bind=postgres_db) + + +@pytest.fixture +def postgres_session(postgres_session_factory): + return postgres_session_factory() + + +@pytest.fixture +def restart_api(): + (Path(__file__).parent / "../src/o2ims/entrypoints/flask_application.py").touch() + time.sleep(0.5) + wait_for_webapp_to_come_up() + + +@pytest.fixture +def restart_redis_pubsub(): + wait_for_redis_to_come_up() + if not shutil.which("docker-compose"): + print("skipping restart, assumes running in container") + return + subprocess.run( + ["docker-compose", "restart", "-t", "0", "redis_pubsub"], + check=True, + ) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/tests/integration/test_ocloud_repository.py b/tests/integration/test_ocloud_repository.py new file mode 100644 index 0000000..36919b3 --- /dev/null +++ b/tests/integration/test_ocloud_repository.py @@ -0,0 +1,93 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 pytest +from o2ims.adapter import ocloud_repository as repository +from o2ims.domain import ocloud +from o2ims import config +import uuid + +pytestmark = pytest.mark.usefixtures("mappers") + + +def setup_ocloud(): + ocloudid1 = str(uuid.uuid4()) + ocloud1 = ocloud.Ocloud(ocloudid1, "ocloud1", config.get_api_url(), "ocloud 1 for integration test", 1) + return ocloud1 + +def setup_ocloud_and_save(sqlite_session_factory): + session = sqlite_session_factory() + repo = repository.OcloudSqlAlchemyRepository(session) + ocloudid1 = str(uuid.uuid4()) + ocloud1 = ocloud.Ocloud(ocloudid1, "ocloud1", config.get_api_url(), "ocloud for integration test", 1) + repo.add(ocloud1) + assert repo.get(ocloudid1) == ocloud1 + session.flush() + return ocloud1 + +def test_add_ocloud(sqlite_session_factory): + session = sqlite_session_factory() + repo = repository.OcloudSqlAlchemyRepository(session) + ocloudid1 = str(uuid.uuid4()) + ocloud1 = ocloud.Ocloud(ocloudid1, "ocloud1", config.get_api_url(), "ocloud for integration test", 1) + repo.add(ocloud1) + assert repo.get(ocloudid1) == ocloud1 + +def test_get_ocloud(sqlite_session_factory): + ocloud1 = setup_ocloud_and_save(sqlite_session_factory) + session = sqlite_session_factory() + repo = repository.OcloudSqlAlchemyRepository(session) + ocloud2 = repo.get(ocloud1.oCloudId) + assert ocloud2 != ocloud1 and ocloud2.oCloudId == ocloud1.oCloudId + +def test_add_ocloud_with_dms(sqlite_session_factory): + session = sqlite_session_factory() + repo = repository.OcloudSqlAlchemyRepository(session) + ocloud1 = setup_ocloud() + dmsid = str(uuid.uuid4()) + dms = ocloud.DeploymentManager( + dmsid, "k8s1", ocloud1.oCloudId, config.get_api_url()+"/k8s1") + ocloud1.addDeploymentManager(dms) + repo.add(ocloud1) + session.flush() + # seperate session to confirm ocloud is updated into repo + session2 = sqlite_session_factory() + repo2 = repository.OcloudSqlAlchemyRepository(session2) + ocloud2 = repo2.get(ocloud1.oCloudId) + assert ocloud2 is not None + assert ocloud2 != ocloud1 and ocloud2.oCloudId == ocloud1.oCloudId + assert len(ocloud2.deploymentManagers) == 1 + + +def test_update_ocloud_with_dms(sqlite_session_factory): + session = sqlite_session_factory() + repo = repository.OcloudSqlAlchemyRepository(session) + ocloud1 = setup_ocloud() + repo.add(ocloud1) + session.flush() + dmsid = str(uuid.uuid4()) + dms = ocloud.DeploymentManager( + dmsid, "k8s1", ocloud1.oCloudId, config.get_api_url()+"/k8s1") + ocloud1.addDeploymentManager(dms) + repo.update(ocloud1) + # repo.update(ocloud1.oCloudId, {"deploymentManagers": ocloud1.deploymentManagers}) + session.flush() + + # seperate session to confirm ocloud is updated into repo + session2 = sqlite_session_factory() + repo2 = repository.OcloudSqlAlchemyRepository(session2) + ocloud2 = repo2.get(ocloud1.oCloudId) + assert ocloud2 is not None + assert ocloud2 != ocloud1 and ocloud2.oCloudId == ocloud1.oCloudId + assert len(ocloud2.deploymentManagers) == 1 diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..3fd8685 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --tb=short +filterwarnings = + ignore::DeprecationWarning diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/tests/unit/test_ocloud.py b/tests/unit/test_ocloud.py new file mode 100644 index 0000000..baff737 --- /dev/null +++ b/tests/unit/test_ocloud.py @@ -0,0 +1,39 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 pytest +from o2ims.domain import ocloud +from o2ims import config +import uuid + + +def setup_ocloud(): + ocloudid1 = str(uuid.uuid4()) + ocloud1 = ocloud.Ocloud(ocloudid1, "ocloud1", config.get_api_url(), "ocloud for unit test", 1) + return ocloud1 + +def test_new_ocloud(): + ocloudid1 = str(uuid.uuid4()) + ocloud1 = ocloud.Ocloud(ocloudid1, "ocloud1", config.get_api_url(), "ocloud for unit test", 1) + assert ocloudid1 is not None and ocloud1.oCloudId == ocloudid1 + +def test_add_ocloud_with_dms(): + ocloud1 = setup_ocloud() + dmsid = str(uuid.uuid4()) + dms = ocloud.DeploymentManager( + dmsid, "k8s1", ocloud1.oCloudId, config.get_api_url()+"/k8s1") + ocloud1.addDeploymentManager(dms) + ocloud1.addDeploymentManager(dms) + assert len(ocloud1.deploymentManagers) == 1 + # repo.update(ocloud1.oCloudId, {"deploymentManagers": ocloud1.deploymentManagers}) -- 2.16.6 From 81e3575a77366f30c2049f98c48a3087db0ea992 Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Tue, 26 Oct 2021 18:28:10 +0800 Subject: [PATCH 06/11] Add tox Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: I668e72886de29f894ababa99f3ac0a84f552f87c --- .gitignore | 2 + Dockerfile | 11 +- LICENSE | 201 +++++++++++++++++++++ README.md | 25 +-- constraints.txt | 4 + docker-compose.yml | 15 +- {src => o2common}/__init__.py | 0 {src/o2common => o2dms}/__init__.py | 0 {src/o2dms => o2ims}/__init__.py | 0 {src/o2ims => o2ims/adapter/clients}/__init__.py | 0 o2ims/adapter/clients/ocloud_sa_client.py | 94 ++++++++++ o2ims/adapter/clients/orm_stx.py | 51 ++++++ {src/o2ims => o2ims}/adapter/notifications.py | 6 +- {src/o2ims => o2ims}/adapter/ocloud_repository.py | 14 +- {src/o2ims => o2ims}/adapter/orm.py | 12 +- .../adapter/redis_eventpublisher.py | 0 {src/o2ims => o2ims}/bootstrap.py | 6 +- {src/o2ims => o2ims}/config.py | 14 +- {src/o2ims => o2ims}/domain/__init__.py | 0 {src/o2ims => o2ims}/domain/commands.py | 8 +- {src/o2ims => o2ims}/domain/events.py | 2 + {src/o2ims => o2ims}/domain/ocloud.py | 71 ++++---- {src/o2ims => o2ims}/domain/resource_type.py | 1 + o2ims/domain/stx_object.py | 28 +++ {src/o2ims => o2ims}/entrypoints/__init__.py | 0 .../entrypoints/flask_application.py | 10 +- .../entrypoints/redis_eventconsumer.py | 0 {src/o2ims => o2ims}/service/__init__.py | 0 o2ims/service/client/__init__.py | 13 ++ o2ims/service/client/base_client.py | 37 ++++ {src/o2ims => o2ims}/service/handlers.py | 14 +- {src/o2ims => o2ims}/service/messagebus.py | 3 +- {src/o2ims => o2ims}/service/unit_of_work.py | 6 +- {src/o2ims => o2ims}/views/ocloud_view.py | 0 requirements-test.txt | 5 + requirements.txt | 5 + setup.py | 17 ++ src/o2dms/setup.py | 19 -- src/setup.py | 7 - tests/e2e/__init__.py | 13 ++ tox.ini | 35 ++++ 41 files changed, 640 insertions(+), 109 deletions(-) create mode 100644 LICENSE create mode 100644 constraints.txt rename {src => o2common}/__init__.py (100%) rename {src/o2common => o2dms}/__init__.py (100%) rename {src/o2dms => o2ims}/__init__.py (100%) rename {src/o2ims => o2ims/adapter/clients}/__init__.py (100%) create mode 100644 o2ims/adapter/clients/ocloud_sa_client.py create mode 100644 o2ims/adapter/clients/orm_stx.py rename {src/o2ims => o2ims}/adapter/notifications.py (96%) rename {src/o2ims => o2ims}/adapter/ocloud_repository.py (88%) rename {src/o2ims => o2ims}/adapter/orm.py (91%) rename {src/o2ims => o2ims}/adapter/redis_eventpublisher.py (100%) rename {src/o2ims => o2ims}/bootstrap.py (91%) rename {src/o2ims => o2ims}/config.py (77%) rename {src/o2ims => o2ims}/domain/__init__.py (100%) rename {src/o2ims => o2ims}/domain/commands.py (89%) rename {src/o2ims => o2ims}/domain/events.py (99%) rename {src/o2ims => o2ims}/domain/ocloud.py (68%) rename {src/o2ims => o2ims}/domain/resource_type.py (93%) create mode 100644 o2ims/domain/stx_object.py rename {src/o2ims => o2ims}/entrypoints/__init__.py (100%) rename {src/o2ims => o2ims}/entrypoints/flask_application.py (81%) rename {src/o2ims => o2ims}/entrypoints/redis_eventconsumer.py (100%) rename {src/o2ims => o2ims}/service/__init__.py (100%) create mode 100644 o2ims/service/client/__init__.py create mode 100644 o2ims/service/client/base_client.py rename {src/o2ims => o2ims}/service/handlers.py (81%) rename {src/o2ims => o2ims}/service/messagebus.py (98%) rename {src/o2ims => o2ims}/service/unit_of_work.py (94%) rename {src/o2ims => o2ims}/views/ocloud_view.py (100%) create mode 100644 setup.py delete mode 100644 src/o2dms/setup.py delete mode 100644 src/setup.py create mode 100644 tests/e2e/__init__.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index ea29b71..addeb00 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .mypy_cache __pycache__ *.egg-info +*.pyc +.tox \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e1afbcb..593e21f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,20 @@ FROM python:3.10-slim-buster +RUN apt-get update; apt-get install -y git gcc COPY requirements.txt /tmp/ -RUN pip install -r /tmp/requirements.txt +COPY constraints.txt /tmp/ + +RUN pip install -r /tmp/requirements.txt -c /tmp/constraints.txt COPY requirements-test.txt /tmp/ RUN pip install -r /tmp/requirements-test.txt RUN mkdir -p /src -COPY src/ /src/ +COPY o2ims/ /src/o2ims/ +COPY o2dms/ /src/o2dms/ +COPY o2common/ /src/o2common/ +COPY setup.py /src/ + RUN pip install -e /src COPY tests/ /tests/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index eb6900a..e4c75d9 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,11 @@ docker-compose build ``` - -## Creating a local virtualenv (optional) - -```sh -python3.8 -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -pip install -e src/ -``` - ## Running the tests ```sh docker-compose up -d docker-compose run --rm --no-deps --entrypoint=pytest api /tests/unit /tests/integration -pytest tests/unit -pytest tests/integration -pytest tests/e2e ``` ## Tear down containers @@ -29,3 +17,16 @@ pytest tests/e2e ```sh docker-compose down --remove-orphans ``` + +## Test with local virtualenv + +```sh +python3.8 -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt -c constraints.txt +pip install -r requirements-test.txt +pip install -e o2ims +# pip install -e o2dms -e o2common +pytest tests/unit +pytest tests/integration +pytest tests/e2e +``` diff --git a/constraints.txt b/constraints.txt new file mode 100644 index 0000000..a1cc510 --- /dev/null +++ b/constraints.txt @@ -0,0 +1,4 @@ +# -e git+https://opendev.org/starlingx/distcloud-client.git@master#egg=distributedcloud-client&subdirectory=distributedcloud-client +# -e git+https://opendev.org/starlingx/config.git@master#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client +cryptography==3.3.2 +python-keystoneclient==3.21.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 91d3ec8..255fbd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,11 +16,13 @@ services: - REDIS_HOST=redis - PYTHONDONTWRITEBYTECODE=1 volumes: - - ./src:/src + - ./o2ims:/o2ims + - ./o2dms:/o2dms + - ./o2common:/o2common - ./tests:/tests entrypoint: - python - - /src/o2ims/entrypoints/redis_eventconsumer.py + - /o2ims/entrypoints/redis_eventconsumer.py api: image: o2imsdms-image @@ -32,11 +34,16 @@ services: - API_HOST=api - REDIS_HOST=redis - PYTHONDONTWRITEBYTECODE=1 - - FLASK_APP=o2ims/entrypoints/flask_application.py + - FLASK_APP=/o2ims/entrypoints/flask_application.py - FLASK_DEBUG=1 - PYTHONUNBUFFERED=1 + - STX_AUTH_URL=http://192.168.204.1:5000/v3 + - STX_USERNAME=admin + - STX_PASSWORD=password1 volumes: - - ./src:/src + - ./o2ims:/o2ims + - ./o2dms:/o2dms + - ./o2common:/o2common - ./tests:/tests entrypoint: - flask diff --git a/src/__init__.py b/o2common/__init__.py similarity index 100% rename from src/__init__.py rename to o2common/__init__.py diff --git a/src/o2common/__init__.py b/o2dms/__init__.py similarity index 100% rename from src/o2common/__init__.py rename to o2dms/__init__.py diff --git a/src/o2dms/__init__.py b/o2ims/__init__.py similarity index 100% rename from src/o2dms/__init__.py rename to o2ims/__init__.py diff --git a/src/o2ims/__init__.py b/o2ims/adapter/clients/__init__.py similarity index 100% rename from src/o2ims/__init__.py rename to o2ims/adapter/clients/__init__.py diff --git a/o2ims/adapter/clients/ocloud_sa_client.py b/o2ims/adapter/clients/ocloud_sa_client.py new file mode 100644 index 0000000..b8607ef --- /dev/null +++ b/o2ims/adapter/clients/ocloud_sa_client.py @@ -0,0 +1,94 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. + +# client talking to Stx standalone + +from service.client.base_client import BaseClient +from typing import List +# Optional, Set +from o2ims.domain import stx_object as ocloudModel +from o2ims import config + + +class StxSaOcloudClient(BaseClient): + def __init__(self): + super().__init__() + self.driver = StxSaClientImp() + + # def list(self) -> List[ocloudModel.StxGenericModel]: + # return self._list() + + # def get(self, id) -> ocloudModel.StxGenericModel: + # return self._get(id) + + def _get(self, id) -> ocloudModel.StxGenericModel: + raise self.driver.getInstanceInfo() + + def _list(self): + return [self.driver.getInstanceInfo()] + + +class StxSaResourcePoolClient(BaseClient): + def __init__(self): + super().__init__() + self.driver = StxSaClientImp() + + def _get(self, id) -> ocloudModel.StxGenericModel: + return self.driver.getInstanceInfo() + + def _list(self): + return [self.driver.getInstanceInfo()] + + +class StxSaDmsClient(BaseClient): + def __init__(self): + super().__init__() + self.driver = StxSaClientImp() + + def _get(self, id) -> ocloudModel.StxGenericModel: + return self.driver.getK8sDetail(id) + + def _list(self): + return self.driver.getK8sList() + +# internal driver which implement client call to Stx Standalone instance + +# from keystoneauth1.identity import v3 +# from keystoneauth1 import session +# # from keystoneclient.v3 import ksclient +# from starlingxclient.v3 import stxclient + + +class StxSaClientImp(object): + def __init__(self, access_info=None) -> None: + super().__init__() + self.access_info = access_info + if self.access_info is None: + self.access_info = config.get_stx_access_info() + # self.auth = auth = v3.Password( + # auth_url="http://example.com:5000/v3", username="admin", + # password="password", project_name="admin", + # user_domain_id="default", project_domain_id="default") + # self.session = sess = session.Session(auth=auth) + # # self.keystone = ksclient.Client(session=sess) + # self.stx = stxclient.Client(session=sess) + + def getInstanceInfo(self) -> ocloudModel.StxGenericModel: + raise NotImplementedError + + def getK8sList(self) -> List[ocloudModel.StxGenericModel]: + raise NotImplementedError + + def getK8sDetail(self, id) -> ocloudModel.StxGenericModel: + raise NotImplementedError diff --git a/o2ims/adapter/clients/orm_stx.py b/o2ims/adapter/clients/orm_stx.py new file mode 100644 index 0000000..02f9bec --- /dev/null +++ b/o2ims/adapter/clients/orm_stx.py @@ -0,0 +1,51 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 datetime import datetime +import logging + +from sqlalchemy import ( + Table, + MetaData, + Column, + # Integer, + String, + # Date, + DateTime, + # ForeignKey, + # event, +) + +from sqlalchemy.orm import mapper +# from sqlalchemy.sql.expression import true + +from o2ims.domain import stx_object as ocloudModel + +logger = logging.getLogger(__name__) + +metadata = MetaData() + +stxobject = Table( + "stxcache", + metadata, + Column("id", String(255), primary_key=True), + Column("name", String(255)), + Column("lastupdate", DateTime), + Column("content", String(255)) +) + + +def start_o2ims_stx_mappers(): + logger.info("Starting O2 IMS Stx mappers") + mapper(ocloudModel.StxGenericModel, stxobject) diff --git a/src/o2ims/adapter/notifications.py b/o2ims/adapter/notifications.py similarity index 96% rename from src/o2ims/adapter/notifications.py rename to o2ims/adapter/notifications.py index e2ad375..4c49c1b 100644 --- a/src/o2ims/adapter/notifications.py +++ b/o2ims/adapter/notifications.py @@ -1,17 +1,17 @@ # pylint: disable=too-few-public-methods import abc -import smtplib from o2ims import config +SMO_O2_ENDPOINT = config.get_smo_o2endpoint() + + class AbstractNotifications(abc.ABC): @abc.abstractmethod def send(self, message): raise NotImplementedError -SMO_O2_ENDPOINT = config.get_smo_o2endpoint() - class SmoO2Notifications(AbstractNotifications): def __init__(self, smoO2Endpoint=SMO_O2_ENDPOINT): self.smoO2Endpoint = smoO2Endpoint diff --git a/src/o2ims/adapter/ocloud_repository.py b/o2ims/adapter/ocloud_repository.py similarity index 88% rename from src/o2ims/adapter/ocloud_repository.py rename to o2ims/adapter/ocloud_repository.py index 6d3d221..8a547fe 100644 --- a/src/o2ims/adapter/ocloud_repository.py +++ b/o2ims/adapter/ocloud_repository.py @@ -14,9 +14,10 @@ import abc from typing import Set -from o2ims.adapter import orm +# from o2ims.adapter import orm from o2ims.domain import ocloud + class OcloudRepository(abc.ABC): def __init__(self): self.seen = set() # type: Set[ocloud.Ocloud] @@ -30,7 +31,7 @@ class OcloudRepository(abc.ABC): if ocloud: self.seen.add(ocloud) return ocloud - + def update(self, ocloud: ocloud.Ocloud): self._update(ocloud) @@ -60,7 +61,8 @@ class OcloudSqlAlchemyRepository(OcloudRepository): # self.session.add_all(ocloud.deploymentManagers) def _get(self, ocloudid) -> ocloud.Ocloud: - return self.session.query(ocloud.Ocloud).filter_by(oCloudId=ocloudid).first() + return self.session.query(ocloud.Ocloud).filter_by( + oCloudId=ocloudid).first() def _update(self, ocloud: ocloud.Ocloud): self.session.add(ocloud) @@ -70,8 +72,10 @@ class OcloudSqlAlchemyRepository(OcloudRepository): # if dmslist: # self._update_dms_list(dmslist) # if updatefields: - # self.session.query(ocloud.Ocloud).filter_by(oCloudId=ocloudid).update(updatefields) + # self.session.query(ocloud.Ocloud).filter_by( + # oCloudId=ocloudid).update(updatefields) # def _update_dms_list(self, dms_list: list): # for dms in dms_list or []: - # self.session.query(ocloud.DeploymentManager).filter_by(deploymentManagerId=dms.deploymentManagerId).update(dms) + # self.session.query(ocloud.DeploymentManager).filter_by( + # deploymentManagerId=dms.deploymentManagerId).update(dms) diff --git a/src/o2ims/adapter/orm.py b/o2ims/adapter/orm.py similarity index 91% rename from src/o2ims/adapter/orm.py rename to o2ims/adapter/orm.py index 6cf9847..2028538 100644 --- a/src/o2ims/adapter/orm.py +++ b/o2ims/adapter/orm.py @@ -18,15 +18,15 @@ from sqlalchemy import ( Table, MetaData, Column, - Integer, + # Integer, String, - Date, + # Date, ForeignKey, - event, + # event, ) from sqlalchemy.orm import mapper, relationship -from sqlalchemy.sql.expression import true +# from sqlalchemy.sql.expression import true from o2ims.domain import ocloud as ocloudModel @@ -86,8 +86,8 @@ def start_o2ims_mappers(): dm_mapper = mapper(ocloudModel.DeploymentManager, deploymentmanager) resourcepool_mapper = mapper(ocloudModel.ResourcePool, resourcepool) resourcetype_mapper = mapper(ocloudModel.ResourceType, resourcetype) - resource_mapper = mapper(ocloudModel.Resource, resource) - ocloud_mapper = mapper( + # resource_mapper = mapper(ocloudModel.Resource, resource) + mapper( ocloudModel.Ocloud, ocloud, properties={ diff --git a/src/o2ims/adapter/redis_eventpublisher.py b/o2ims/adapter/redis_eventpublisher.py similarity index 100% rename from src/o2ims/adapter/redis_eventpublisher.py rename to o2ims/adapter/redis_eventpublisher.py diff --git a/src/o2ims/bootstrap.py b/o2ims/bootstrap.py similarity index 91% rename from src/o2ims/bootstrap.py rename to o2ims/bootstrap.py index 00cdc4a..524f325 100644 --- a/src/o2ims/bootstrap.py +++ b/o2ims/bootstrap.py @@ -15,7 +15,8 @@ import inspect from typing import Callable from o2ims.adapter import orm, redis_eventpublisher -from o2ims.adapter.notifications import AbstractNotifications, SmoO2Notifications +from o2ims.adapter.notifications import AbstractNotifications,\ + SmoO2Notifications from o2ims.service import handlers, messagebus, unit_of_work @@ -33,7 +34,8 @@ def bootstrap( if start_orm: orm.start_o2ims_mappers() - dependencies = {"uow": uow, "notifications": notifications, "publish": publish} + dependencies = {"uow": uow, "notifications": notifications, + "publish": publish} injected_event_handlers = { event_type: [ inject_dependencies(handler, dependencies) diff --git a/src/o2ims/config.py b/o2ims/config.py similarity index 77% rename from src/o2ims/config.py rename to o2ims/config.py index 798ea79..f42ec72 100644 --- a/src/o2ims/config.py +++ b/o2ims/config.py @@ -14,6 +14,7 @@ import os + def get_postgres_uri(): host = os.environ.get("DB_HOST", "localhost") port = 54321 if host == "localhost" else 5432 @@ -27,9 +28,11 @@ def get_api_url(): port = 5005 if host == "localhost" else 80 return f"http://{host}:{port}" + def get_o2ims_api_base(): return '/o2ims_infrastructureInventory/v1' + def get_redis_host_and_port(): host = os.environ.get("REDIS_HOST", "localhost") port = 63791 if host == "localhost" else 6379 @@ -37,5 +40,14 @@ def get_redis_host_and_port(): def get_smo_o2endpoint(): - smo_o2endpoint = os.environ.get("SMO_O2_ENDPOINT", "http://localhost/smo_sim") + smo_o2endpoint = os.environ.get( + "SMO_O2_ENDPOINT", "http://localhost/smo_sim") return smo_o2endpoint + + +def get_stx_access_info(): + authurl = os.environ.get("STX_AUTH_URL", "http://192.168.204.1:5000/v3") + username = os.environ.get("STX_USERNAME", "admin") + pswd = os.environ.get("STX_PASSWORD", "passwd1") + stx_access_info = (authurl, username, pswd) + return stx_access_info diff --git a/src/o2ims/domain/__init__.py b/o2ims/domain/__init__.py similarity index 100% rename from src/o2ims/domain/__init__.py rename to o2ims/domain/__init__.py diff --git a/src/o2ims/domain/commands.py b/o2ims/domain/commands.py similarity index 89% rename from src/o2ims/domain/commands.py rename to o2ims/domain/commands.py index 9df6cff..869bf52 100644 --- a/src/o2ims/domain/commands.py +++ b/o2ims/domain/commands.py @@ -13,13 +13,15 @@ # limitations under the License. # pylint: disable=too-few-public-methods -from datetime import date -from typing import Optional +# from datetime import date +# from typing import Optional from dataclasses import dataclass class Command: pass + +@dataclass class UpdateDms(Command): - ref: str \ No newline at end of file + ref: str diff --git a/src/o2ims/domain/events.py b/o2ims/domain/events.py similarity index 99% rename from src/o2ims/domain/events.py rename to o2ims/domain/events.py index 755f65e..591662f 100644 --- a/src/o2ims/domain/events.py +++ b/o2ims/domain/events.py @@ -15,9 +15,11 @@ # pylint: disable=too-few-public-methods from dataclasses import dataclass + class Event: pass + @dataclass class OcloudUpdated(Event): oCloudId: str diff --git a/src/o2ims/domain/ocloud.py b/o2ims/domain/ocloud.py similarity index 68% rename from src/o2ims/domain/ocloud.py rename to o2ims/domain/ocloud.py index ddd2646..68e304e 100644 --- a/src/o2ims/domain/ocloud.py +++ b/o2ims/domain/ocloud.py @@ -13,39 +13,16 @@ # limitations under the License. from __future__ import annotations -from dataclasses import dataclass -from datetime import date -from typing import Optional, List, Set +# from dataclasses import dataclass +# from datetime import date +# from typing import Optional, List, Set from .resource_type import ResourceTypeEnum # from uuid import UUID -class Ocloud: - def __init__( - self, ocloudid: str, name: str, imsendpoint: str, - description: str = '', version_number: int = 0) -> None: - - self.oCloudId = ocloudid - self.version_number = version_number - self.name = name - self.description = description - self.infrastructureManagementServiceEndpoint = imsendpoint - self.resourcePools = [] - self.deploymentManagers = [] - self.resourceTypes = [] - self.extensions = [] - self.events = [] - - def addDeploymentManager(self, deploymentManager: DeploymentManager) -> None: - deploymentManager.oCloudId = self.oCloudId - old = filter( - lambda x: x.deploymentManagerId == deploymentManager.deploymentManagerId, - self.deploymentManagers) - for o in old or []: - self.deploymentManagers.remove(o) - self.deploymentManagers.append(deploymentManager) class DeploymentManager: - def __init__(self, id: str, name: str, ocloudid: str, dmsendpoint: str) -> None: + def __init__(self, id: str, name: str, ocloudid: str, + dmsendpoint: str) -> None: self.deploymentManagerId = id self.name = name self.oCloudId = ocloudid @@ -54,7 +31,8 @@ class DeploymentManager: class ResourcePool: - def __init__(self, id: str, name: str, location: str, ocloudid: str) -> None: + def __init__(self, id: str, name: str, location: str, + ocloudid: str) -> None: self.resourcePoolId = id self.name = name self.location = location @@ -63,7 +41,8 @@ class ResourcePool: class ResourceType: - def __init__(self, typeid: str, name:str, typeEnum: ResourceTypeEnum, ocloudid: str) -> None: + def __init__(self, typeid: str, name: str, typeEnum: ResourceTypeEnum, + ocloudid: str) -> None: self.resourceTypeId = typeid self.resourceTypeEnum = typeEnum.value self.name = name @@ -72,12 +51,40 @@ class ResourceType: class Resource: - def __init__(self, resourceId:str, resourceTypeId: str, resourcePoolId: str) -> None: + def __init__(self, resourceId: str, resourceTypeId: str, + resourcePoolId: str) -> None: self.resourceId = resourceId - self.oCloudId = None # tbd + self.oCloudId = None # tbd self.resourceTypeId = resourceTypeId self.resourcePoolId = resourcePoolId self.parentId = None self.elements = [] self.extensions = [] + +class Ocloud: + def __init__(self, ocloudid: str, name: str, imsendpoint: str, + description: str = '', version_number: int = 0) -> None: + + self.oCloudId = ocloudid + self.version_number = version_number + self.name = name + self.description = description + self.infrastructureManagementServiceEndpoint = imsendpoint + self.resourcePools = [] + self.deploymentManagers = [] + self.resourceTypes = [] + self.extensions = [] + self.events = [] + + def addDeploymentManager(self, + deploymentManager: DeploymentManager): + + deploymentManager.oCloudId = self.oCloudId + old = filter( + lambda x: x.deploymentManagerId == + deploymentManager.deploymentManagerId, + self.deploymentManagers) + for o in old or []: + self.deploymentManagers.remove(o) + self.deploymentManagers.append(deploymentManager) diff --git a/src/o2ims/domain/resource_type.py b/o2ims/domain/resource_type.py similarity index 93% rename from src/o2ims/domain/resource_type.py rename to o2ims/domain/resource_type.py index faff8ea..72f0db0 100644 --- a/src/o2ims/domain/resource_type.py +++ b/o2ims/domain/resource_type.py @@ -1,5 +1,6 @@ from enum import Enum + class ResourceTypeEnum(Enum): PSERVER = 1 PSERVER_CPU = 2 diff --git a/o2ims/domain/stx_object.py b/o2ims/domain/stx_object.py new file mode 100644 index 0000000..a3adaf2 --- /dev/null +++ b/o2ims/domain/stx_object.py @@ -0,0 +1,28 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 dataclasses import dataclass +import datetime + + +class StxGenericModel: + def __init__(self, id: str, name: str, + lastupdate: datetime, content: str) -> None: + self.id = id + self.name = name + self.lastupdate = lastupdate + self.content = content + + def isChanged(self, updatetime: datetime) -> bool: + return True if self.lastupdate > updatetime else False diff --git a/src/o2ims/entrypoints/__init__.py b/o2ims/entrypoints/__init__.py similarity index 100% rename from src/o2ims/entrypoints/__init__.py rename to o2ims/entrypoints/__init__.py diff --git a/src/o2ims/entrypoints/flask_application.py b/o2ims/entrypoints/flask_application.py similarity index 81% rename from src/o2ims/entrypoints/flask_application.py rename to o2ims/entrypoints/flask_application.py index 933301c..8965a30 100644 --- a/src/o2ims/entrypoints/flask_application.py +++ b/o2ims/entrypoints/flask_application.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime -from flask import Flask, jsonify, request -from o2ims.domain import commands -from o2ims.service.handlers import InvalidResourceType +# from datetime import datetime +from flask import Flask, jsonify +# request +# from o2ims.domain import commands +# from o2ims.service.handlers import InvalidResourceType from o2ims import bootstrap, config from o2ims.views import ocloud_view @@ -23,6 +24,7 @@ app = Flask(__name__) bus = bootstrap.bootstrap() apibase = config.get_o2ims_api_base() + @app.route(apibase, methods=["GET"]) def oclouds(): result = ocloud_view.oclouds(bus.uow) diff --git a/src/o2ims/entrypoints/redis_eventconsumer.py b/o2ims/entrypoints/redis_eventconsumer.py similarity index 100% rename from src/o2ims/entrypoints/redis_eventconsumer.py rename to o2ims/entrypoints/redis_eventconsumer.py diff --git a/src/o2ims/service/__init__.py b/o2ims/service/__init__.py similarity index 100% rename from src/o2ims/service/__init__.py rename to o2ims/service/__init__.py diff --git a/o2ims/service/client/__init__.py b/o2ims/service/client/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/o2ims/service/client/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/o2ims/service/client/base_client.py b/o2ims/service/client/base_client.py new file mode 100644 index 0000000..6057ab3 --- /dev/null +++ b/o2ims/service/client/base_client.py @@ -0,0 +1,37 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 abc +# from typing import Optional, List, Set +from typing import List +from o2ims.domain import stx_object as ocloudModel + + +class BaseClient(abc.ABC): + def __init__(self): + pass + + def list(self) -> List[ocloudModel.StxGenericModel]: + return self._list() + + def get(self, id) -> ocloudModel.StxGenericModel: + return self._get(id) + + @abc.abstractmethod + def _get(self, id) -> ocloudModel.StxGenericModel: + raise NotImplementedError + + @abc.abstractmethod + def _list(self): + raise NotImplementedError diff --git a/src/o2ims/service/handlers.py b/o2ims/service/handlers.py similarity index 81% rename from src/o2ims/service/handlers.py rename to o2ims/service/handlers.py index c75dc07..c80ea6f 100644 --- a/src/o2ims/service/handlers.py +++ b/o2ims/service/handlers.py @@ -14,12 +14,15 @@ # pylint: disable=unused-argument from __future__ import annotations -from dataclasses import asdict -from typing import List, Dict, Callable, Type, TYPE_CHECKING -from o2ims.domain import commands, events, ocloud +# from dataclasses import asdict +from typing import List, Dict, Callable, Type +# TYPE_CHECKING +from o2ims.domain import commands, events +# ocloud + +# if TYPE_CHECKING: +# from . import unit_of_work -if TYPE_CHECKING: - from . import unit_of_work class InvalidResourceType(Exception): pass @@ -28,5 +31,6 @@ class InvalidResourceType(Exception): EVENT_HANDLERS = { } # type: Dict[Type[events.Event], List[Callable]] + COMMAND_HANDLERS = { } # type: Dict[Type[commands.Command], Callable] diff --git a/src/o2ims/service/messagebus.py b/o2ims/service/messagebus.py similarity index 98% rename from src/o2ims/service/messagebus.py rename to o2ims/service/messagebus.py index bff45ab..0758529 100644 --- a/src/o2ims/service/messagebus.py +++ b/o2ims/service/messagebus.py @@ -51,7 +51,8 @@ class MessageBus: def handle_event(self, event: events.Event): for handler in self.event_handlers[type(event)]: try: - logger.debug("handling event %s with handler %s", event, handler) + logger.debug("handling event %s with handler %s", + event, handler) handler(event) self.queue.extend(self.uow.collect_new_events()) except Exception: diff --git a/src/o2ims/service/unit_of_work.py b/o2ims/service/unit_of_work.py similarity index 94% rename from src/o2ims/service/unit_of_work.py rename to o2ims/service/unit_of_work.py index 4714729..40e0f76 100644 --- a/src/o2ims/service/unit_of_work.py +++ b/o2ims/service/unit_of_work.py @@ -19,7 +19,6 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session - from o2ims import config from o2ims.adapter import ocloud_repository @@ -27,7 +26,7 @@ from o2ims.adapter import ocloud_repository class AbstractUnitOfWork(abc.ABC): oclouds: ocloud_repository.OcloudRepository - def __enter__(self) -> AbstractUnitOfWork: + def __enter__(self): return self def __exit__(self, *args): @@ -64,7 +63,8 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork): def __enter__(self): self.session = self.session_factory() # type: Session - self.oclouds = ocloud_repository.OcloudSqlAlchemyRepository(self.session) + self.oclouds = ocloud_repository\ + .OcloudSqlAlchemyRepository(self.session) return super().__enter__() def __exit__(self, *args): diff --git a/src/o2ims/views/ocloud_view.py b/o2ims/views/ocloud_view.py similarity index 100% rename from src/o2ims/views/ocloud_view.py rename to o2ims/views/ocloud_view.py diff --git a/requirements-test.txt b/requirements-test.txt index 251bb2b..e22107b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,13 @@ +flake8 pylint mypy requests +tox pytest pytest-icdiff tenacity + +# -e git+https://opendev.org/starlingx/distcloud-client.git@master#egg=distributedcloud-client&subdirectory=distributedcloud-client +# -e git+https://opendev.org/starlingx/config.git@master#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client diff --git a/requirements.txt b/requirements.txt index 985e50e..acbfe10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,8 @@ flask sqlalchemy redis psycopg2-binary + +Cython>=3.0a1 + +# -e git+https://opendev.org/starlingx/distcloud-client.git@master#egg=distributedcloud-client&subdirectory=distributedcloud-client +# -e git+https://opendev.org/starlingx/config.git@master#egg=cgtsclient&subdirectory=sysinv/cgts-client/cgts-client# diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..553dd19 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup +from setuptools import find_packages + +setup( + name="o2imsdms", + version="1.0", + packages=find_packages(), + license="LICENSE", + description="Represent O2 IMS and O2 DMS", + install_requires=[ + 'httplib2', + # 'distributedcloud-client', + # 'cgtsclient', + 'babel', # Required by distributedcloud-client + 'PrettyTable<0.8,>=0.7.2', # Required by distributedcloud-client + ] +) diff --git a/src/o2dms/setup.py b/src/o2dms/setup.py deleted file mode 100644 index feafb7a..0000000 --- a/src/o2dms/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup - -setup( - name="o2common", - version="1.0", - packages=["o2common"], -) - -setup( - name="o2ims", - version="1.0", - packages=["o2ims"], -) - -setup( - name="o2dms", - version="1.0", - packages=["o2dms"], -) diff --git a/src/setup.py b/src/setup.py deleted file mode 100644 index 7558373..0000000 --- a/src/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup - -setup( - name="o2imsdms", - version="1.0", - packages=["o2ims", "o2dms", "o2common"], -) diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/tests/e2e/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3480ef8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist=flake8,code + +minversion = 1.6 +skipsdist = True + +[testenv] +basepython = + code: python3.8 + flake8: python3.8 +setenv = + VIRTUAL_ENV={envdir} + +# NOTE: relative paths were used due to '-w' flag for nosetests util + +usedevelop = True +install_command = pip install -U {opts} {packages} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-test.txt +whitelist_externals = bash, flake8, pytest + +[testenv:flake8] +commands = + flake8 o2ims + flake8 o2dms + flake8 o2common + +[testenv:code] +commands = + pytest tests + +[testenv:nosetests] +commands = + pytest tests -- 2.16.6 From f10107e78b324fc607fe06486764588f38fc220d Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Thu, 28 Oct 2021 12:08:54 +0800 Subject: [PATCH 07/11] Add wrcp aio client test with real ocloud leverage openrc script to inject real wrcp access info Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: Ia4da4e49768e15e42034f46f7542304b348c2be7 --- README.md | 7 +++ docker-compose.yml | 9 ++-- o2ims/adapter/clients/ocloud_sa_client.py | 68 ++++++++++++++++++--------- o2ims/adapter/clients/orm_stx.py | 8 ++-- o2ims/config.py | 40 ++++++++++++++-- o2ims/domain/stx_object.py | 22 ++++++--- tests/conftest.py | 5 ++ tests/integration/test_clientdriver_stx_sa.py | 64 +++++++++++++++++++++++++ tox.ini | 4 +- 9 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 tests/integration/test_clientdriver_stx_sa.py diff --git a/README.md b/README.md index e4c75d9..ea2bdb4 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,16 @@ docker-compose build ## Running the tests +Prerequisite: in case of testing against real ocloud, download openrc file from ocloud dashboard, e.g. admin_openrc.sh + ```sh +source ./admin_openrc.sh +export |grep OS_AUTH_URL +export |grep OS_USERNAME +export |grep OS_PASSWORD docker-compose up -d docker-compose run --rm --no-deps --entrypoint=pytest api /tests/unit /tests/integration + ``` ## Tear down containers diff --git a/docker-compose.yml b/docker-compose.yml index 255fbd9..4ffc2f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,9 @@ services: - DB_PASSWORD=o2ims123 - REDIS_HOST=redis - PYTHONDONTWRITEBYTECODE=1 + - OS_AUTH_URL=${OS_AUTH_URL} + - OS_USERNAME=${OS_USERNAME} + - OS_PASSWORD=${OS_PASSWORD} volumes: - ./o2ims:/o2ims - ./o2dms:/o2dms @@ -37,9 +40,9 @@ services: - FLASK_APP=/o2ims/entrypoints/flask_application.py - FLASK_DEBUG=1 - PYTHONUNBUFFERED=1 - - STX_AUTH_URL=http://192.168.204.1:5000/v3 - - STX_USERNAME=admin - - STX_PASSWORD=password1 + - OS_AUTH_URL=${OS_AUTH_URL} + - OS_USERNAME=${OS_USERNAME} + - OS_PASSWORD=${OS_PASSWORD} volumes: - ./o2ims:/o2ims - ./o2dms:/o2dms diff --git a/o2ims/adapter/clients/ocloud_sa_client.py b/o2ims/adapter/clients/ocloud_sa_client.py index b8607ef..5d1a5f9 100644 --- a/o2ims/adapter/clients/ocloud_sa_client.py +++ b/o2ims/adapter/clients/ocloud_sa_client.py @@ -14,17 +14,22 @@ # client talking to Stx standalone -from service.client.base_client import BaseClient +from o2ims.service.client.base_client import BaseClient from typing import List # Optional, Set from o2ims.domain import stx_object as ocloudModel from o2ims import config +# from dcmanagerclient.api import client +from cgtsclient.client import get_client +import logging +logger = logging.getLogger(__name__) + class StxSaOcloudClient(BaseClient): - def __init__(self): + def __init__(self, driver=None): super().__init__() - self.driver = StxSaClientImp() + self.driver = driver if driver else StxSaClientImp() # def list(self) -> List[ocloudModel.StxGenericModel]: # return self._list() @@ -33,7 +38,7 @@ class StxSaOcloudClient(BaseClient): # return self._get(id) def _get(self, id) -> ocloudModel.StxGenericModel: - raise self.driver.getInstanceInfo() + return self.driver.getInstanceInfo() def _list(self): return [self.driver.getInstanceInfo()] @@ -62,33 +67,54 @@ class StxSaDmsClient(BaseClient): def _list(self): return self.driver.getK8sList() -# internal driver which implement client call to Stx Standalone instance -# from keystoneauth1.identity import v3 -# from keystoneauth1 import session -# # from keystoneclient.v3 import ksclient -# from starlingxclient.v3 import stxclient +class StxPserverClient(BaseClient): + def __init__(self): + super().__init__() + self.driver = StxSaClientImp() + + def _get(self, id) -> ocloudModel.StxGenericModel: + return self.driver.getPserver(id) + + def _list(self) -> List[ocloudModel.StxGenericModel]: + return self.driver.getPserverList() + +# internal driver which implement client call to Stx Standalone instance class StxSaClientImp(object): - def __init__(self, access_info=None) -> None: + def __init__(self, stx_client=None): super().__init__() - self.access_info = access_info - if self.access_info is None: - self.access_info = config.get_stx_access_info() - # self.auth = auth = v3.Password( - # auth_url="http://example.com:5000/v3", username="admin", - # password="password", project_name="admin", - # user_domain_id="default", project_domain_id="default") - # self.session = sess = session.Session(auth=auth) - # # self.keystone = ksclient.Client(session=sess) - # self.stx = stxclient.Client(session=sess) + self.stxclient = stx_client if stx_client else self.getStxClient() + + def getStxClient(): + os_client_args = config.get_stx_access_info() + config_client = get_client(**os_client_args) + return config_client def getInstanceInfo(self) -> ocloudModel.StxGenericModel: - raise NotImplementedError + systems = self.stxclient.isystem.list() + logger.debug("systems:" + str(systems[0].to_dict())) + return ocloudModel.StxGenericModel(systems[0]) if systems else None + + def getPserverList(self) -> List[ocloudModel.StxGenericModel]: + hosts = self.stxclient.ihost.list() + logger.debug("host 1:" + str(hosts[0].to_dict())) + return [ocloudModel.StxGenericModel(self._hostconverter(host)) + for host in hosts if host] + + def getPserver(self, id) -> ocloudModel.StxGenericModel: + host = self.stxclient.ihost.get(id) + logger.debug("host:" + str(host.to_dict())) + return ocloudModel.StxGenericModel(self._hostconverter(host)) def getK8sList(self) -> List[ocloudModel.StxGenericModel]: raise NotImplementedError def getK8sDetail(self, id) -> ocloudModel.StxGenericModel: raise NotImplementedError + + @staticmethod + def _hostconverter(host): + setattr(host, "name", host.hostname) + return host diff --git a/o2ims/adapter/clients/orm_stx.py b/o2ims/adapter/clients/orm_stx.py index 02f9bec..4b825c4 100644 --- a/o2ims/adapter/clients/orm_stx.py +++ b/o2ims/adapter/clients/orm_stx.py @@ -17,7 +17,7 @@ import logging from sqlalchemy import ( Table, - MetaData, + # MetaData, Column, # Integer, String, @@ -31,17 +31,19 @@ from sqlalchemy.orm import mapper # from sqlalchemy.sql.expression import true from o2ims.domain import stx_object as ocloudModel +from o2ims.adapter.orm import metadata logger = logging.getLogger(__name__) -metadata = MetaData() +# metadata = MetaData() stxobject = Table( "stxcache", metadata, Column("id", String(255), primary_key=True), Column("name", String(255)), - Column("lastupdate", DateTime), + Column("updatetime", DateTime), + Column("createtime", DateTime), Column("content", String(255)) ) diff --git a/o2ims/config.py b/o2ims/config.py index f42ec72..e55d00b 100644 --- a/o2ims/config.py +++ b/o2ims/config.py @@ -13,6 +13,8 @@ # limitations under the License. import os +import sys +import logging def get_postgres_uri(): @@ -46,8 +48,36 @@ def get_smo_o2endpoint(): def get_stx_access_info(): - authurl = os.environ.get("STX_AUTH_URL", "http://192.168.204.1:5000/v3") - username = os.environ.get("STX_USERNAME", "admin") - pswd = os.environ.get("STX_PASSWORD", "passwd1") - stx_access_info = (authurl, username, pswd) - return stx_access_info + # authurl = os.environ.get("STX_AUTH_URL", "http://192.168.204.1:5000/v3") + # username = os.environ.get("STX_USERNAME", "admin") + # pswd = os.environ.get("STX_PASSWORD", "passwd1") + # stx_access_info = (authurl, username, pswd) + try: + client_args = dict( + auth_url=os.environ.get('OS_AUTH_URL', + "http://192.168.204.1:5000/v3"), + username=os.environ.get('OS_USERNAME', "admin"), + api_key=os.environ.get('OS_PASSWORD', "fakepasswd1"), + project_name=os.environ.get('OS_PROJECT_NAME', "admin"), + ) + # dc_client_args = dict( + # auth_url=os.environ['OS_AUTH_URL'], + # username=os.environ['OS_USERNAME'], + # api_key=os.environ['OS_PASSWORD'], + # project_name=os.environ['OS_PROJECT_NAME'], + # user_domain_name=os.environ['OS_USER_DOMAIN_NAME'], + # project_domain_name=os.environ['OS_PROJECT_NAME'], + # project_domain_id=os.environ['OS_PROJECT_DOMAIN_ID'] + # ) + except KeyError: + logging.error('Please source your RC file before execution, ' + 'e.g.: `source ~/downloads/admin-rc.sh`') + sys.exit(1) + + os_client_args = {} + for key, val in client_args.items(): + os_client_args['os_{key}'.format(key=key)] = val + os_client_args['os_password'] = os_client_args.pop('os_api_key') + os_client_args['os_region_name'] = 'RegionOne' + os_client_args['api_version'] = 1 + return os_client_args diff --git a/o2ims/domain/stx_object.py b/o2ims/domain/stx_object.py index a3adaf2..ee4f718 100644 --- a/o2ims/domain/stx_object.py +++ b/o2ims/domain/stx_object.py @@ -14,15 +14,23 @@ # from dataclasses import dataclass import datetime +import json class StxGenericModel: - def __init__(self, id: str, name: str, - lastupdate: datetime, content: str) -> None: - self.id = id - self.name = name - self.lastupdate = lastupdate - self.content = content + def __init__(self, api_response: dict) -> None: + self.id = api_response.uuid + self.content = json.dumps(api_response.to_dict()) + self.updatetime = api_response.updated_at + self.createtime = api_response.created_at + self.name = api_response.name + + # def __init__(self, id: str, name: str, + # lastupdate: datetime, content: str) -> None: + # self.id = id + # self.name = name + # self.lastupdate = lastupdate + # self.content = content def isChanged(self, updatetime: datetime) -> bool: - return True if self.lastupdate > updatetime else False + return True if self.updatetime > updatetime else False diff --git a/tests/conftest.py b/tests/conftest.py index 7744dbd..c6b0904 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ from sqlalchemy.orm import sessionmaker, clear_mappers from tenacity import retry, stop_after_delay from o2ims.adapter.orm import metadata, start_o2ims_mappers +from o2ims.adapter.clients.orm_stx import start_o2ims_stx_mappers from o2ims import config @@ -31,9 +32,13 @@ def sqlite_session_factory(in_memory_sqlite_db): @pytest.fixture def mappers(): start_o2ims_mappers() + start_o2ims_stx_mappers() yield clear_mappers() +@pytest.fixture +def fake_stx_client(): + pass @retry(stop=stop_after_delay(10)) def wait_for_postgres_to_come_up(engine): diff --git a/tests/integration/test_clientdriver_stx_sa.py b/tests/integration/test_clientdriver_stx_sa.py new file mode 100644 index 0000000..551a4c5 --- /dev/null +++ b/tests/integration/test_clientdriver_stx_sa.py @@ -0,0 +1,64 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 sys +import pytest +from o2ims.adapter import ocloud_repository as repository +from o2ims.domain import ocloud +from o2ims import config +import uuid +from o2ims.adapter.clients.ocloud_sa_client import StxSaClientImp +from cgtsclient.client import get_client + +import logging + + +@pytest.fixture +def real_stx_aio_client(): + os_client_args = config.get_stx_access_info() + config_client = get_client(**os_client_args) + yield config_client + +# pytestmark = pytest.mark.usefixtures("mappers") + + +def test_get_instanceinfo(real_stx_aio_client): + logger = logging.getLogger(__name__) + stxclientimp = StxSaClientImp(real_stx_aio_client) + assert stxclientimp is not None + systeminfo = stxclientimp.getInstanceInfo() + assert systeminfo is not None + assert systeminfo.id is not None + assert systeminfo.name is not None + assert systeminfo.content is not None + + +def test_get_pserverlist(real_stx_aio_client): + stxSaClientImp = StxSaClientImp(real_stx_aio_client) + assert stxSaClientImp is not None + hosts = stxSaClientImp.getPserverList() + assert hosts is not None + assert len(hosts) > 0 + + +def test_get_pserver(real_stx_aio_client): + stxSaClientImp = StxSaClientImp(real_stx_aio_client) + assert stxSaClientImp is not None + hosts = stxSaClientImp.getPserverList() + assert hosts is not None + assert len(hosts) > 0 + host1 = hosts[0] + host2 = stxSaClientImp.getPserver(host1.id) + assert host1 != host2 + assert host1.id == host2.id diff --git a/tox.ini b/tox.ini index 3480ef8..39016e9 100644 --- a/tox.ini +++ b/tox.ini @@ -28,8 +28,8 @@ commands = [testenv:code] commands = - pytest tests + pytest tests/unit [testenv:nosetests] commands = - pytest tests + pytest tests/unit -- 2.16.6 From 249a0dc9a44d8d515f186093bbd05bec330b7d09 Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Thu, 28 Oct 2021 16:11:46 +0800 Subject: [PATCH 08/11] Add test with fake stx client implementation Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: Iaf71af39270c5d17ac7cabeed1d9ea91eb85f9d2 --- o2ims/domain/stx_object.py | 13 ++--- tests/integration/test_clientdriver_fake_stx_sa.py | 57 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 tests/integration/test_clientdriver_fake_stx_sa.py diff --git a/o2ims/domain/stx_object.py b/o2ims/domain/stx_object.py index ee4f718..0a17092 100644 --- a/o2ims/domain/stx_object.py +++ b/o2ims/domain/stx_object.py @@ -18,12 +18,13 @@ import json class StxGenericModel: - def __init__(self, api_response: dict) -> None: - self.id = api_response.uuid - self.content = json.dumps(api_response.to_dict()) - self.updatetime = api_response.updated_at - self.createtime = api_response.created_at - self.name = api_response.name + def __init__(self, api_response: dict = None) -> None: + if api_response: + self.id = api_response.uuid + self.content = json.dumps(api_response.to_dict()) + self.updatetime = api_response.updated_at + self.createtime = api_response.created_at + self.name = api_response.name # def __init__(self, id: str, name: str, # lastupdate: datetime, content: str) -> None: diff --git a/tests/integration/test_clientdriver_fake_stx_sa.py b/tests/integration/test_clientdriver_fake_stx_sa.py new file mode 100644 index 0000000..177546c --- /dev/null +++ b/tests/integration/test_clientdriver_fake_stx_sa.py @@ -0,0 +1,57 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 datetime import date, datetime +import sys +import pytest +from o2ims.adapter import ocloud_repository as repository +from o2ims.domain import ocloud +from o2ims import config +import logging +import uuid +import json +from o2ims.adapter.clients.ocloud_sa_client import StxSaOcloudClient +from o2ims.domain import stx_object as ocloudModel + +# pytestmark = pytest.mark.usefixtures("mappers") + +class FakeStxSaClientImp(object): + def __init__(self): + super().__init__() + + def getInstanceInfo(self) -> ocloudModel.StxGenericModel: + model = ocloudModel.StxGenericModel() + model.id = uuid.uuid4() + model.name = "stx1" + model.updatetime = datetime.now + model.createtime = datetime.now + model.content = json.dumps({}) + return model + + +@pytest.fixture +def fake_driver_imp(): + fakedriver = FakeStxSaClientImp() + yield fakedriver + + +def test_get_instanceinfo(fake_driver_imp): + logger = logging.getLogger(__name__) + stxclientimp = StxSaOcloudClient(fake_driver_imp) + assert stxclientimp is not None + systeminfo = stxclientimp.get(None) + assert systeminfo is not None + assert systeminfo.id is not None + assert systeminfo.name == "stx1" + assert systeminfo.content == json.dumps({}) -- 2.16.6 From f1946a1e90036bb8a758b49f94ac4d3b40bae66e Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Thu, 28 Oct 2021 19:55:47 +0800 Subject: [PATCH 09/11] Add ocloud watcher and tests Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: I3c0fb09913f5a064e84cecc3f6fdb6072c58be5a --- o2ims/adapter/ocloud_repository.py | 21 +++---- o2ims/domain/stx_object.py | 24 +++++--- o2ims/service/auditor/__init__.py | 13 ++++ o2ims/service/auditor/base.py | 13 ++++ o2ims/service/watcher/__init__.py | 13 ++++ o2ims/service/watcher/base.py | 68 +++++++++++++++++++++ tests/conftest.py | 4 +- tests/integration/test_clientdriver_fake_stx_sa.py | 1 + tests/unit/test_watcher.py | 71 ++++++++++++++++++++++ 9 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 o2ims/service/auditor/__init__.py create mode 100644 o2ims/service/auditor/base.py create mode 100644 o2ims/service/watcher/__init__.py create mode 100644 o2ims/service/watcher/base.py create mode 100644 tests/unit/test_watcher.py diff --git a/o2ims/adapter/ocloud_repository.py b/o2ims/adapter/ocloud_repository.py index 8a547fe..ca90209 100644 --- a/o2ims/adapter/ocloud_repository.py +++ b/o2ims/adapter/ocloud_repository.py @@ -13,7 +13,7 @@ # limitations under the License. import abc -from typing import Set +from typing import List, Set # from o2ims.adapter import orm from o2ims.domain import ocloud @@ -32,6 +32,9 @@ class OcloudRepository(abc.ABC): self.seen.add(ocloud) return ocloud + def list(self) -> List[ocloud.Ocloud]: + return self._list() + def update(self, ocloud: ocloud.Ocloud): self._update(ocloud) @@ -64,18 +67,8 @@ class OcloudSqlAlchemyRepository(OcloudRepository): return self.session.query(ocloud.Ocloud).filter_by( oCloudId=ocloudid).first() + def _list(self) -> List[ocloud.Ocloud]: + return self.session.query() + def _update(self, ocloud: ocloud.Ocloud): self.session.add(ocloud) - - # def _update_fields(self, ocloudid: str, updatefields: dict): - # dmslist = updatefields.pop("deploymentManagers", None) - # if dmslist: - # self._update_dms_list(dmslist) - # if updatefields: - # self.session.query(ocloud.Ocloud).filter_by( - # oCloudId=ocloudid).update(updatefields) - - # def _update_dms_list(self, dms_list: list): - # for dms in dms_list or []: - # self.session.query(ocloud.DeploymentManager).filter_by( - # deploymentManagerId=dms.deploymentManagerId).update(dms) diff --git a/o2ims/domain/stx_object.py b/o2ims/domain/stx_object.py index 0a17092..7345694 100644 --- a/o2ims/domain/stx_object.py +++ b/o2ims/domain/stx_object.py @@ -13,10 +13,14 @@ # limitations under the License. # from dataclasses import dataclass -import datetime +# import datetime import json +class MismatchedModel(Exception): + pass + + class StxGenericModel: def __init__(self, api_response: dict = None) -> None: if api_response: @@ -26,12 +30,14 @@ class StxGenericModel: self.createtime = api_response.created_at self.name = api_response.name - # def __init__(self, id: str, name: str, - # lastupdate: datetime, content: str) -> None: - # self.id = id - # self.name = name - # self.lastupdate = lastupdate - # self.content = content + def is_outdated(self, newmodel) -> bool: + return self.updatetime < newmodel.updatetime + + def update_by(self, newmodel) -> None: + if self.id != newmodel.id: + raise MismatchedModel("Mismatched model") + self.name = newmodel.name - def isChanged(self, updatetime: datetime) -> bool: - return True if self.updatetime > updatetime else False + self.content = newmodel.content + self.createtime = newmodel.createtime + self.updatetime = newmodel.updatetime diff --git a/o2ims/service/auditor/__init__.py b/o2ims/service/auditor/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/o2ims/service/auditor/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/o2ims/service/auditor/base.py b/o2ims/service/auditor/base.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/o2ims/service/auditor/base.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/o2ims/service/watcher/__init__.py b/o2ims/service/watcher/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/o2ims/service/watcher/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/o2ims/service/watcher/base.py b/o2ims/service/watcher/base.py new file mode 100644 index 0000000..4ea23ff --- /dev/null +++ b/o2ims/service/watcher/base.py @@ -0,0 +1,68 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 o2ims.service.client.base_client import BaseClient +from o2ims.domain.stx_object import StxGenericModel +from o2ims.adapter.ocloud_repository import OcloudRepository + + +class InvalidOcloudState(Exception): + pass + + +class BaseWatcher(object): + def __init__(self, client: BaseClient) -> None: + super().__init__() + self._client = client + + def probe(self): + self._probe() + + def _probe(self): + pass + + +class OcloudWather(BaseWatcher): + def __init__(self, ocloud_client: BaseClient, + repo: OcloudRepository) -> None: + super().__init__(ocloud_client) + self._repo = repo + + def _probe(self): + ocloudmodel = self._client.get(None) + if ocloudmodel: + self._compare_and_update(ocloudmodel) + + def _compare_and_update(self, ocloudmodel: StxGenericModel) -> bool: + # localmodel = self._repo.get(ocloudmodel.id) + oclouds = self._repo.list() + if len(oclouds) > 1: + raise InvalidOcloudState("More than 1 ocloud is found") + if len(oclouds) == 0: + self._repo.add(ocloudmodel) + else: + localmodel = oclouds.pop() + if localmodel.is_outdated(ocloudmodel): + localmodel.update_by(ocloudmodel) + self._repo.update(localmodel) + + +class ResourcePoolWatcher(object): + def __init__(self) -> None: + super().__init__() + + +class ResourceWatcher(object): + def __init__(self) -> None: + super().__init__() diff --git a/tests/conftest.py b/tests/conftest.py index c6b0904..211ad3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from tenacity import retry, stop_after_delay from o2ims.adapter.orm import metadata, start_o2ims_mappers from o2ims.adapter.clients.orm_stx import start_o2ims_stx_mappers from o2ims import config +from o2ims.domain import stx_object as ocloudModel @pytest.fixture @@ -36,9 +37,6 @@ def mappers(): yield clear_mappers() -@pytest.fixture -def fake_stx_client(): - pass @retry(stop=stop_after_delay(10)) def wait_for_postgres_to_come_up(engine): diff --git a/tests/integration/test_clientdriver_fake_stx_sa.py b/tests/integration/test_clientdriver_fake_stx_sa.py index 177546c..8f262b8 100644 --- a/tests/integration/test_clientdriver_fake_stx_sa.py +++ b/tests/integration/test_clientdriver_fake_stx_sa.py @@ -26,6 +26,7 @@ from o2ims.domain import stx_object as ocloudModel # pytestmark = pytest.mark.usefixtures("mappers") + class FakeStxSaClientImp(object): def __init__(self): super().__init__() diff --git a/tests/unit/test_watcher.py b/tests/unit/test_watcher.py new file mode 100644 index 0000000..ec28519 --- /dev/null +++ b/tests/unit/test_watcher.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 datetime import datetime +import json +from typing import List +from o2ims.service.client.base_client import BaseClient +import pytest +from o2ims.domain import ocloud +from o2ims import config +import uuid +from o2ims.service.watcher.base import OcloudWather +from o2ims.domain import stx_object as ocloudModel +from o2ims.adapter.ocloud_repository import OcloudRepository + +class FakeOcloudClient(BaseClient): + def __init__(self): + super().__init__() + fakeCloud = ocloudModel.StxGenericModel() + fakeCloud.id = uuid.uuid4() + fakeCloud.name = 'stx1' + fakeCloud.content = json.dumps({}) + fakeCloud.createtime = datetime.now() + fakeCloud.updatetime = datetime.now + self.fakeCloud = fakeCloud + + def _get(self, id) -> ocloudModel.StxGenericModel: + return self.fakeCloud + + def _list(self): + return [self.fakeCloud] + +class FakeOcloudRepo(OcloudRepository): + def __init__(self): + super().__init__() + self.oclouds = [] + + def _add(self, ocloud: ocloud.Ocloud): + self.oclouds.append(ocloud) + + def _get(self, ocloudid) -> ocloud.Ocloud: + filtered = [o for o in self.oclouds if o.id == ocloudid] + return filtered.pop() + + def _list(self) -> List[ocloud.Ocloud]: + return [x for x in self.oclouds] + + def _update(self, ocloud: ocloud.Ocloud): + filtered = [o for o in self.oclouds if o.id == ocloud.id] + assert len(filtered) == 1 + ocloud1 = filtered.pop() + ocloud1.update_by(ocloud) + +def test_probe_new_ocloud(): + fakeRepo = FakeOcloudRepo() + fakeClient = FakeOcloudClient() + ocloudwatcher = OcloudWather(fakeClient, fakeRepo) + ocloudwatcher.probe() + assert len(fakeRepo.oclouds) == 1 + assert fakeRepo.oclouds[0].name == "stx1" -- 2.16.6 From 23735ebe8b295ea1c77730896cd61a89536b13b6 Mon Sep 17 00:00:00 2001 From: Bin Yang Date: Thu, 28 Oct 2021 22:21:42 +0800 Subject: [PATCH 10/11] Extract ocloud integration test Issue-ID: INF-196 Signed-off-by: Bin Yang Change-Id: I8f62bb344e407a9b13d9d3fb37729b28ff807fc6 --- README.md | 10 +++++++++- tests/integration-ocloud/__init__.py | 13 +++++++++++++ .../test_clientdriver_stx_sa.py | 0 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/integration-ocloud/__init__.py rename tests/{integration => integration-ocloud}/test_clientdriver_stx_sa.py (100%) diff --git a/README.md b/README.md index ea2bdb4..edd927e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ docker-compose build ## Running the tests -Prerequisite: in case of testing against real ocloud, download openrc file from ocloud dashboard, e.g. admin_openrc.sh ```sh source ./admin_openrc.sh @@ -16,6 +15,15 @@ export |grep OS_USERNAME export |grep OS_PASSWORD docker-compose up -d docker-compose run --rm --no-deps --entrypoint=pytest api /tests/unit /tests/integration +``` + +## Running the tests with a O-Cloud + +Prerequisite: in case of testing against real ocloud, download openrc file from ocloud dashboard, e.g. + +```sh +admin_openrc.sh +docker-compose run --rm --no-deps --entrypoint=pytest api /tests/unit /tests/integration-ocloud ``` diff --git a/tests/integration-ocloud/__init__.py b/tests/integration-ocloud/__init__.py new file mode 100644 index 0000000..b514342 --- /dev/null +++ b/tests/integration-ocloud/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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. diff --git a/tests/integration/test_clientdriver_stx_sa.py b/tests/integration-ocloud/test_clientdriver_stx_sa.py similarity index 100% rename from tests/integration/test_clientdriver_stx_sa.py rename to tests/integration-ocloud/test_clientdriver_stx_sa.py -- 2.16.6 From e2d086012940d43ca06b8ecd369ed6f376a81574 Mon Sep 17 00:00:00 2001 From: Jackie Huang Date: Tue, 26 Oct 2021 09:34:57 +0800 Subject: [PATCH 11/11] INFO.yaml: add quotation marks for committer id Signed-off-by: Jackie Huang Change-Id: I4c5ad2cb7ad9c6c1de5589f51b1e37efaffbe65e --- INFO.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INFO.yaml b/INFO.yaml index e0a0aef..3518949 100644 --- a/INFO.yaml +++ b/INFO.yaml @@ -33,7 +33,7 @@ committers: - name: 'Bin Yang' email: 'bin.yang@windriver.com' company: 'Wind River' - id: biny993 + id: 'biny993' timezone: 'Asia/Shanghai' - name: 'Litao Gao' email: 'litao.gao@windriver.com' -- 2.16.6