diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py index 37b6ad5714..e037cb5fc7 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/client.py @@ -55,6 +55,7 @@ from cgtsclient.v1 import istor from cgtsclient.v1 import isystem from cgtsclient.v1 import iuser from cgtsclient.v1 import kube_cluster +from cgtsclient.v1 import kube_cmd_version from cgtsclient.v1 import kube_host_upgrade from cgtsclient.v1 import kube_upgrade from cgtsclient.v1 import kube_version @@ -167,6 +168,7 @@ class Client(http.HTTPClient): self.host_fs = host_fs.HostFsManager(self) self.kube_cluster = kube_cluster.KubeClusterManager(self) self.kube_version = kube_version.KubeVersionManager(self) + self.kube_cmd_version = kube_cmd_version.KubeCmdVersionManager(self) self.kube_upgrade = kube_upgrade.KubeUpgradeManager(self) self.kube_host_upgrade = kube_host_upgrade.KubeHostUpgradeManager(self) self.device_image = device_image.DeviceImageManager(self) diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/kube_cmd_version.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/kube_cmd_version.py new file mode 100644 index 0000000000..fa666da47e --- /dev/null +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/kube_cmd_version.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from cgtsclient.common import base + + +class KubeCmdVersion(base.Resource): + def __repr__(self): + return "" % self._info + + +class KubeCmdVersionManager(base.Manager): + resource_class = KubeCmdVersion + + @staticmethod + def _path(): + return '/v1/kube_cmd_versions' + + def get(self): + try: + return self._list(self._path())[0] + except IndexError: + return None + + def update(self, patch): + return self._update(self._path(), patch) diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py index 5d90fb23a7..60e76e9452 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/__init__.py @@ -46,6 +46,7 @@ from sysinv.api.controllers.v1 import kube_host_upgrade from sysinv.api.controllers.v1 import kube_rootca_update from sysinv.api.controllers.v1 import kube_upgrade from sysinv.api.controllers.v1 import kube_version +from sysinv.api.controllers.v1 import kube_cmd_version from sysinv.api.controllers.v1 import label from sysinv.api.controllers.v1 import interface from sysinv.api.controllers.v1 import interface_network @@ -260,6 +261,9 @@ class V1(base.APIBase): kube_versions = [link.Link] "Links to the kube_version resource" + kube_cmd_versions = [link.Link] + "Links to the kube_cmd_version resource" + kube_upgrade = [link.Link] "Links to the kube_upgrade resource" @@ -815,6 +819,13 @@ class V1(base.APIBase): 'kube_versions', '', bookmark=True)] + v1.kube_cmd_versions = [link.Link.make_link('self', pecan.request.host_url, + 'kube_cmd_versions', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'kube_cmd_versions', '', + bookmark=True)] + v1.kube_upgrade = [link.Link.make_link('self', pecan.request.host_url, 'kube_upgrade', ''), link.Link.make_link('bookmark', @@ -935,6 +946,7 @@ class Controller(rest.RestController): host_fs = host_fs.HostFsController() kube_clusters = kube_cluster.KubeClusterController() kube_versions = kube_version.KubeVersionController() + kube_cmd_versions = kube_cmd_version.KubeCmdVersionController() kube_upgrade = kube_upgrade.KubeUpgradeController() kube_rootca_update = kube_rootca_update.KubeRootCAUpdateController() kube_host_upgrades = kube_host_upgrade.KubeHostUpgradeController() diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/kube_cmd_version.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/kube_cmd_version.py new file mode 100644 index 0000000000..6811c1317e --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/kube_cmd_version.py @@ -0,0 +1,88 @@ +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import jsonpatch +import pecan +from pecan import rest +import wsme +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from oslo_log import log +from sysinv._i18n import _ +from sysinv.api.controllers.v1 import base +from sysinv.api.controllers.v1 import types +from sysinv.api.controllers.v1 import utils +from sysinv.common import exception +from sysinv.common import utils as cutils +from sysinv import objects + +LOG = log.getLogger(__name__) +LOCK_NAME = 'KubeCmdVersionController' + + +class KubeCmdVersionPatchType(types.JsonPatchType): + + @staticmethod + def mandatory_attrs(): + return ['/kubeadm_version', '/kubelet_version'] + + +class KubeCmdVersion(base.APIBase): + """API representation of a k8s cmd version.""" + + kubeadm_version = wtypes.text + "Kubeadm version for this entry" + + kubelet_version = wtypes.text + "Kubelet version for this entry" + + def __init__(self, **kwargs): + self.fields = objects.kube_cmd_version.fields + for k in self.fields: + if not hasattr(self, k): + continue + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @classmethod + def convert_with_links(cls, rpc_kube_cmd_version): + kube_version = KubeCmdVersion(**rpc_kube_cmd_version.as_dict()) + return kube_version + + +class KubeCmdVersionController(rest.RestController): + """REST controller for Kubernetes Cmd Versions.""" + + @wsme_pecan.wsexpose(KubeCmdVersion) + def get(self): + """Get the kube cmd version object""" + kube_cmd_version = objects.kube_cmd_version.get(pecan.request.context) + return KubeCmdVersion.convert_with_links(kube_cmd_version) + + @cutils.synchronized(LOCK_NAME) + @wsme_pecan.wsexpose(KubeCmdVersion, body=[KubeCmdVersionPatchType]) + def patch(self, patch): + """Modify the kube cmd version object""" + try: + utils.validate_patch(patch) + patch_obj = jsonpatch.JsonPatch(patch) + kube_cmd_version = objects.kube_cmd_version.get(pecan.request.context) + kube_cmd_version_patched = KubeCmdVersion(**jsonpatch.apply_patch( + kube_cmd_version.as_dict(), + patch_obj)) + # Update only the fields that have changed + for field in objects.kube_cmd_version.fields: + if kube_cmd_version[field] != getattr(kube_cmd_version_patched, field): + kube_cmd_version[field] = getattr(kube_cmd_version_patched, field) + kube_cmd_version.save() + + except utils.JSONPATCH_EXCEPTIONS as e: + raise exception.PatchError(patch=patch, reason=e) + except Exception as e: + LOG.exception(e) + raise wsme.exc.ClientSideError(_( + "Unable modify the KubeCmdVersion object.")) + return KubeCmdVersion.convert_with_links(kube_cmd_version) diff --git a/sysinv/sysinv/sysinv/sysinv/common/kubernetes.py b/sysinv/sysinv/sysinv/sysinv/common/kubernetes.py index 3043f2e99c..d2f635bb09 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/kubernetes.py +++ b/sysinv/sysinv/sysinv/sysinv/common/kubernetes.py @@ -110,6 +110,9 @@ KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA = 'updating-host-trustNewCA' KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA = 'updated-host-trustNewCA' KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA_FAILED = 'updating-host-trustNewCA-failed' +# Kubeadm and Kubelet default versions +KUBERNETES_DEFAULT_VERSION = '1.18.1' + # Kubernetes constants MANIFEST_APPLY_TIMEOUT = 60 * 15 MANIFEST_APPLY_INTERVAL = 10 diff --git a/sysinv/sysinv/sysinv/sysinv/db/api.py b/sysinv/sysinv/sysinv/sysinv/db/api.py index ed3bf8c353..4c9ce4e67f 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/api.py @@ -4691,3 +4691,14 @@ class Connection(object): :param rootca_update_id: id of the kubernetes rootca update entry to be deleted from database. """ + + @abc.abstractmethod + def kube_cmd_version_get(self): + """ Get the kubernetes cmd version entry""" + + @abc.abstractmethod + def kube_cmd_version_update(self, values): + """ Update the kubernetes cmd version entry. + + :param values: a dictionary with the respective fields and values to be updated in the db entry. + """ diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py index d856b2f6a2..3e440d21c9 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/api.py @@ -8941,3 +8941,20 @@ class Connection(api.Connection): except NoResultFound: raise exception.KubeRootCAUpdateNotFound(rootca_update_id=rootca_update_id) query.delete() + + @objects.objectify(objects.kube_cmd_version) + def kube_cmd_version_get(self): + query = model_query(models.KubeCmdVersions) + try: + return query.one() + except NoResultFound: + raise exception.NotFound() + + @objects.objectify(objects.kube_cmd_version) + def kube_cmd_version_update(self, values): + with _session_for_write() as session: + query = model_query(models.KubeCmdVersions, session=session) + count = query.update(values) + if count != 1: + raise exception.NotFound() + return query.one() diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/118_kube_cmd_versions_table.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/118_kube_cmd_versions_table.py new file mode 100644 index 0000000000..b711a0ef2e --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/migrate_repo/versions/118_kube_cmd_versions_table.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from migrate.changeset import UniqueConstraint +from sqlalchemy import Column +from sqlalchemy import DateTime +from sqlalchemy import Integer +from sqlalchemy import MetaData +from sqlalchemy import String +from sqlalchemy import Table + +from sysinv.common import kubernetes + +ENGINE = 'InnoDB' +CHARSET = 'utf8' + + +def _insert_default_kube_cmd_version(kube_cmd_versions): + kube_cmd_versions_insert = kube_cmd_versions.insert() + values = { + 'kubeadm_version': kubernetes.KUBERNETES_DEFAULT_VERSION, + 'kubelet_version': kubernetes.KUBERNETES_DEFAULT_VERSION + } + kube_cmd_versions_insert.execute(values) + + +def upgrade(migrate_engine): + """ + This database upgrade creates a new kube_cmd_versions table + """ + + meta = MetaData() + meta.bind = migrate_engine + + # Define and create the kube_cmd_versions table. + kube_cmd_versions = Table( + 'kube_cmd_versions', + meta, + Column('created_at', DateTime), + Column('updated_at', DateTime), + Column('deleted_at', DateTime), + Column('id', Integer, primary_key=True, + unique=True, nullable=False), + Column('kubeadm_version', String(255), nullable=False), + Column('kubelet_version', String(255), nullable=False), + UniqueConstraint('kubeadm_version', 'kubelet_version', + name='u_kubeadm_version_kubelet_version'), + + mysql_engine=ENGINE, + mysql_charset=CHARSET, + ) + kube_cmd_versions.create() + + _insert_default_kube_cmd_version(kube_cmd_versions) + + +def downgrade(migrate_engine): + # Downgrade is unsupported in this release. + raise NotImplementedError('SysInv database downgrade is unsupported.') diff --git a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py index bec664981d..51731df026 100644 --- a/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py +++ b/sysinv/sysinv/sysinv/sysinv/db/sqlalchemy/models.py @@ -1958,3 +1958,13 @@ class KubeRootCAHostUpdate(Base): reserved_3 = Column(String(255)) host = relationship("ihost", lazy="joined", join_depth=1) + + +class KubeCmdVersions(Base): + __tablename__ = 'kube_cmd_versions' + + id = Column(Integer, primary_key=True) + kubeadm_version = Column(String(255), nullable=False) + kubelet_version = Column(String(255), nullable=False) + UniqueConstraint('kubeadm_version', 'kubelet_version', + name='u_kubeadm_version_kubelet_version') diff --git a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py index 018df2c4cf..0710da5c69 100644 --- a/sysinv/sysinv/sysinv/sysinv/objects/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/objects/__init__.py @@ -45,6 +45,7 @@ from sysinv.objects import kube_app_releases from sysinv.objects import kube_host_upgrade from sysinv.objects import kube_upgrade from sysinv.objects import kube_version +from sysinv.objects import kube_cmd_version from sysinv.objects import interface from sysinv.objects import interface_ae from sysinv.objects import interface_ethernet @@ -199,6 +200,7 @@ kube_app_releases = kube_app_releases.KubeAppReleases kube_host_upgrade = kube_host_upgrade.KubeHostUpgrade kube_upgrade = kube_upgrade.KubeUpgrade kube_version = kube_version.KubeVersion +kube_cmd_version = kube_cmd_version.KubeCmdVersion datanetwork = datanetwork.DataNetwork host_fs = host_fs.HostFS device_image = device_image.DeviceImage @@ -278,6 +280,7 @@ __all__ = ("system", "kube_host_upgrade", "kube_upgrade", "kube_version", + "kube_cmd_version", "datanetwork", "interface_network", "host_fs", diff --git a/sysinv/sysinv/sysinv/sysinv/objects/kube_cmd_version.py b/sysinv/sysinv/sysinv/sysinv/objects/kube_cmd_version.py new file mode 100644 index 0000000000..561415eec5 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/objects/kube_cmd_version.py @@ -0,0 +1,26 @@ +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from sysinv.db import api as db_api +from sysinv.objects import base +from sysinv.objects import utils + + +class KubeCmdVersion(base.SysinvObject): + + dbapi = db_api.get_instance() + + fields = { + 'kubeadm_version': utils.str_or_none, + 'kubelet_version': utils.str_or_none, + } + + @base.remotable_classmethod + def get(cls, context): + return cls.dbapi.kube_cmd_version_get() + + def save_changes(self, context, updates): + self.dbapi.kube_cmd_version_update(updates) diff --git a/sysinv/sysinv/sysinv/sysinv/tests/api/test_kube_cmd_version.py b/sysinv/sysinv/sysinv/sysinv/tests/api/test_kube_cmd_version.py new file mode 100644 index 0000000000..63b5db73e2 --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/tests/api/test_kube_cmd_version.py @@ -0,0 +1,61 @@ +# +# Copyright (c) 2021 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from six.moves import http_client +from sysinv.tests.api import base +from sysinv.common import kubernetes + + +class TestKubeCmdVersion(base.FunctionalTest): + API_HEADERS = {'User-Agent': 'sysinv-test'} + API_PREFIX = '/kube_cmd_versions' + expected_api_fields = ['kubeadm_version', + 'kubelet_version'] + + def setUp(self): + super(TestKubeCmdVersion, self).setUp() + + def assert_fields(self, api_object): + # Verify that expected attributes are returned + for field in self.expected_api_fields: + self.assertIn(field, api_object) + + def test_show_kube_cmd_version_success(self): + response = self.get_json(self.API_PREFIX, + headers=self.API_HEADERS) + self.assert_fields(response) + self.assertEqual(response['kubeadm_version'], + kubernetes.KUBERNETES_DEFAULT_VERSION) + self.assertEqual(response['kubelet_version'], + kubernetes.KUBERNETES_DEFAULT_VERSION) + + def test_patch_kube_cmd_version_success(self): + values = { + 'kubeadm_version': '1.5.1', + 'kubelet_version': '1.5.2' + } + response = self.patch_dict_json(self.API_PREFIX, + headers=self.API_HEADERS, + **values) + self.assertEqual(http_client.OK, response.status_int) + self.assert_fields(response.json) + for k in values: + self.assertEqual(values[k], response.json[k]) + get_response = self.get_json(self.API_PREFIX, + headers=self.API_HEADERS) + for k in values: + self.assertEqual(values[k], get_response[k]) + + def test_patch_kube_cmd_version_failure(self): + wrong_value = { + 'kube_version': '1.5.1' + } + response = self.patch_dict_json(self.API_PREFIX, + headers=self.API_HEADERS, + expect_errors=True, + **wrong_value) + self.assertEqual(http_client.BAD_REQUEST, response.status_int) + self.assertTrue(response.json.get('error_message'))