From 88d8d53d46ad158a732eeccd3ce46bd15a4f882d Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 22 Nov 2022 10:52:23 +0000 Subject: [PATCH] coe: Add support for clusters This allows us to start migrating the coe cloud layer to this newly introduced proxy layer. Change-Id: Ia9c622dc284234bb618c2caf3d035097bba58ec5 Signed-off-by: Stephen Finucane --- openstack/_services_mixin.py | 5 +- .../__init__.py | 0 ...ainer_infrastructure_management_service.py | 24 +++ .../v1/__init__.py | 0 .../v1/_proxy.py | 109 ++++++++++++ .../v1/cluster.py | 167 ++++++++++++++++++ .../__init__.py | 0 .../v1/__init__.py | 0 .../v1/test_cluster.py | 56 ++++++ tools/print-services.py | 19 +- 10 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 openstack/container_infrastructure_management/__init__.py create mode 100644 openstack/container_infrastructure_management/container_infrastructure_management_service.py create mode 100644 openstack/container_infrastructure_management/v1/__init__.py create mode 100644 openstack/container_infrastructure_management/v1/_proxy.py create mode 100644 openstack/container_infrastructure_management/v1/cluster.py create mode 100644 openstack/tests/unit/container_infrastructure_management/__init__.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/__init__.py create mode 100644 openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py diff --git a/openstack/_services_mixin.py b/openstack/_services_mixin.py index 0e90577b0..13c5358c7 100644 --- a/openstack/_services_mixin.py +++ b/openstack/_services_mixin.py @@ -6,6 +6,7 @@ from openstack.baremetal_introspection import baremetal_introspection_service from openstack.block_storage import block_storage_service from openstack.clustering import clustering_service from openstack.compute import compute_service +from openstack.container_infrastructure_management import container_infrastructure_management_service from openstack.database import database_service from openstack.dns import dns_service from openstack.identity import identity_service @@ -55,9 +56,9 @@ class ServicesMixin: application_catalog = service_description.ServiceDescription(service_type='application-catalog') - container_infrastructure_management = service_description.ServiceDescription(service_type='container-infrastructure-management') - container_infrastructure = container_infrastructure_management + container_infrastructure_management = container_infrastructure_management_service.ContainerInfrastructureManagementService(service_type='container-infrastructure-management') container_infra = container_infrastructure_management + container_infrastructure = container_infrastructure_management search = service_description.ServiceDescription(service_type='search') diff --git a/openstack/container_infrastructure_management/__init__.py b/openstack/container_infrastructure_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/container_infrastructure_management/container_infrastructure_management_service.py b/openstack/container_infrastructure_management/container_infrastructure_management_service.py new file mode 100644 index 000000000..e71676d08 --- /dev/null +++ b/openstack/container_infrastructure_management/container_infrastructure_management_service.py @@ -0,0 +1,24 @@ +# 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 openstack.container_infrastructure_management.v1 import _proxy +from openstack import service_description + + +class ContainerInfrastructureManagementService( + service_description.ServiceDescription, +): + """The container infrastructure management service.""" + + supported_versions = { + '1': _proxy.Proxy, + } diff --git a/openstack/container_infrastructure_management/v1/__init__.py b/openstack/container_infrastructure_management/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/container_infrastructure_management/v1/_proxy.py b/openstack/container_infrastructure_management/v1/_proxy.py new file mode 100644 index 000000000..7d3d2783b --- /dev/null +++ b/openstack/container_infrastructure_management/v1/_proxy.py @@ -0,0 +1,109 @@ +# 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 openstack.container_infrastructure_management.v1 import ( + cluster as _cluster +) +from openstack import proxy + + +class Proxy(proxy.Proxy): + + _resource_registry = { + "cluster": _cluster.Cluster, + } + + def create_cluster(self, **attrs): + """Create a new cluster from attributes + + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster`, + comprised of the properties on the Cluster class. + :returns: The results of cluster creation + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._create(_cluster.Cluster, **attrs) + + def delete_cluster(self, cluster, ignore_missing=True): + """Delete a cluster + + :param cluster: The value can be either the ID of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the cluster does not exist. When set to ``True``, no exception will + be set when attempting to delete a nonexistent cluster. + :returns: ``None`` + """ + self._delete(_cluster.Cluster, cluster, ignore_missing=ignore_missing) + + def find_cluster(self, name_or_id, ignore_missing=True): + """Find a single cluster + + :param name_or_id: The name or ID of a cluster. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the resource does not exist. + When set to ``True``, None will be returned when + attempting to find a nonexistent resource. + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + or None + """ + return self._find( + _cluster.Cluster, + name_or_id, + ignore_missing=ignore_missing, + ) + + def get_cluster(self, cluster): + """Get a single cluster + + :param cluster: The value can be the ID of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + + :returns: One + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_cluster.Cluster, cluster) + + def clusters(self, **query): + """Return a generator of clusters + + :param kwargs query: Optional query parameters to be sent to limit + the resources being returned. + + :returns: A generator of cluster objects + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._list(_cluster.Cluster, **query) + + def update_cluster(self, cluster, **attrs): + """Update a cluster + + :param cluster: Either the id of a cluster or a + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + instance. + :param attrs: The attributes to update on the cluster represented + by ``cluster``. + + :returns: The updated cluster + :rtype: + :class:`~openstack.container_infrastructure_management.v1.cluster.Cluster` + """ + return self._update(_cluster.Cluster, cluster, **attrs) diff --git a/openstack/container_infrastructure_management/v1/cluster.py b/openstack/container_infrastructure_management/v1/cluster.py new file mode 100644 index 000000000..71c880437 --- /dev/null +++ b/openstack/container_infrastructure_management/v1/cluster.py @@ -0,0 +1,167 @@ +# 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 openstack import exceptions +from openstack import resource +from openstack import utils + + +class Cluster(resource.Resource): + + resources_key = 'clusters' + base_path = '/clusters' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + allow_patch = True + + #: The endpoint URL of COE API exposed to end-users. + api_address = resource.Body('api_address') + #: The UUID of the cluster template. + cluster_template_id = resource.Body('cluster_template_id') + #: Version info of chosen COE in bay/cluster for helping client in picking + #: the right version of client. + coe_version = resource.Body('coe_version') + #: The timeout for cluster creation in minutes. The value expected is a + #: positive integer. If the timeout is reached during cluster creation + #: process, the operation will be aborted and the cluster status will be + #: set to CREATE_FAILED. Defaults to 60. + create_timeout = resource.Body('create_timeout', type=int) + #: The date and time when the resource was created. The date and time stamp + #: format is ISO 8601:: + #: + #: CCYY-MM-DDThh:mm:ss±hh:mm + #: + #: For example, `2015-08-27T09:49:58-05:00`. The ±hh:mm value, if included, + #: is the time zone as an offset from UTC. + created_at = resource.Body('created_at') + #: The custom discovery url for node discovery. This is used by the COE to + #: discover the servers that have been created to host the containers. The + #: actual discovery mechanism varies with the COE. In some cases, the + #: service fills in the server info in the discovery service. In other + #: cases,if the discovery_url is not specified, the service will use the + #: public discovery service at https://discovery.etcd.io. In this case, the + #: service will generate a unique url here for each bay and store the info + #: for the servers. + discovery_url = resource.Body('discovery_url') + #: The name or ID of the network to provide connectivity to the internal + #: network for the bay/cluster. + fixed_network = resource.Body('fixed_network') + #: The fixed subnet to use when allocating network addresses for nodes in + #: bay/cluster. + fixed_subnet = resource.Body('fixed_subnet') + #: The flavor name or ID to use when booting the node servers. Defaults to + #: m1.small. + flavor_id = resource.Body('flavor_id') + #: Whether to enable using the floating IP of cloud provider. Some cloud + #: providers use floating IPs while some use public IPs. When set to true, + #: floating IPs will be used. If this value is not provided, the value of + #: ``floating_ip_enabled`` provided in the template will be used. + is_floating_ip_enabled = resource.Body('floating_ip_enabled', type=bool) + #: Whether to enable the master load balancer. Since multiple masters may + #: exist in a bay/cluster, a Neutron load balancer is created to provide + #: the API endpoint for the bay/cluster and to direct requests to the + #: masters. In some cases, such as when the LBaaS service is not available, + #: this option can be set to false to create a bay/cluster without the load + #: balancer. In this case, one of the masters will serve as the API + #: endpoint. The default is true, i.e. to create the load balancer for the + #: bay. + is_master_lb_enabled = resource.Body('master_lb_enabled', type=bool) + #: The name of the SSH keypair to configure in the bay/cluster servers for + #: SSH access. Users will need the key to be able to ssh to the servers in + #: the bay/cluster. The login name is specific to the bay/cluster driver. + #: For example, with fedora-atomic image the default login name is fedora. + keypair = resource.Body('keypair') + #: Arbitrary labels in the form of key=value pairs. The accepted keys and + #: valid values are defined in the bay/cluster drivers. They are used as a + #: way to pass additional parameters that are specific to a bay/cluster + #: driver. + labels = resource.Body('labels', type=list) + #: A list of floating IPs of all master nodes. + master_addresses = resource.Body('master_addresses', type=list) + #: The number of servers that will serve as master for the bay/cluster. Set + #: to more than 1 master to enable High Availability. If the option + #: master-lb-enabled is specified in the baymodel/cluster template, the + #: master servers will be placed in a load balancer pool. Defaults to 1. + master_count = resource.Body('master_count', type=int) + #: The flavor of the master node for this baymodel/cluster template. + master_flavor_id = resource.Body('master_flavor_id') + #: Name of the resource. + name = resource.Body('name') + #: The number of servers that will serve as node in the bay/cluster. + #: Defaults to 1. + node_count = resource.Body('node_count', type=int) + #: A list of floating IPs of all servers that serve as nodes. + node_addresses = resource.Body('node_addresses', type=list) + #: The reference UUID of orchestration stack from Heat orchestration + #: service. + stack_id = resource.Body('stack_id') + #: The current state of the bay/cluster. + status = resource.Body('status') + #: The reason of bay/cluster current status. + status_reason = resource.Body('reason') + #: The date and time when the resource was updated. The date and time stamp + #: format is ISO 8601:: + #: + #: CCYY-MM-DDThh:mm:ss±hh:mm + #: + #: For example, `2015-08-27T09:49:58-05:00`. The ±hh:mm value, if included, + #: is the time zone as an offset from UTC. If the updated_at date and time + #: stamp is not set, its value is null. + updated_at = resource.Body('updated_at') + #: The UUID of the cluster. + uuid = resource.Body('uuid', alternate_id=True) + + def resize(self, session, *, node_count, nodes_to_remove=None): + """Resize the cluster. + + :param node_count: The number of servers that will serve as node in the + bay/cluster. The default is 1. + :param nodes_to_remove: The server ID list will be removed if + downsizing the cluster. + :returns: The UUID of the resized cluster. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'resize') + headers = {'Accept': ''} + body = { + 'node_count': node_count, + 'nodes_to_remove': nodes_to_remove, + } + response = session.post(url, json=body, headers=headers) + exceptions.raise_from_response(response) + return response['uuid'] + + def upgrade(self, session, *, cluster_template, max_batch_size=None): + """Upgrade the cluster. + + :param cluster_template: The UUID of the cluster template. + :param max_batch_size: The max batch size each time when doing upgrade. + The default is 1 + :returns: The UUID of the updated cluster. + :raises: :exc:`~openstack.exceptions.ResourceNotFound` if + the resource was not found. + """ + url = utils.urljoin(Cluster.base_path, self.id, 'actions', 'upgrade') + headers = {'Accept': ''} + body = { + 'cluster_template': cluster_template, + 'max_batch_size': max_batch_size, + } + response = session.post(url, json=body, headers=headers) + exceptions.raise_from_response(response) + return response['uuid'] diff --git a/openstack/tests/unit/container_infrastructure_management/__init__.py b/openstack/tests/unit/container_infrastructure_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/container_infrastructure_management/v1/__init__.py b/openstack/tests/unit/container_infrastructure_management/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py new file mode 100644 index 000000000..8e370877b --- /dev/null +++ b/openstack/tests/unit/container_infrastructure_management/v1/test_cluster.py @@ -0,0 +1,56 @@ +# 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 openstack.container_infrastructure_management.v1 import cluster +from openstack.tests.unit import base + +EXAMPLE = { + "cluster_template_id": "0562d357-8641-4759-8fed-8173f02c9633", + "create_timeout": 60, + "discovery_url": None, + "flavor_id": None, + "keypair": "my_keypair", + "labels": [], + "master_count": 2, + "master_flavor_id": None, + "name": "k8s", + "node_count": 2, +} + + +class TestCluster(base.TestCase): + def test_basic(self): + sot = cluster.Cluster() + self.assertIsNone(sot.resource_key) + self.assertEqual('clusters', sot.resources_key) + self.assertEqual('/clusters', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = cluster.Cluster(**EXAMPLE) + self.assertEqual( + EXAMPLE['cluster_template_id'], + sot.cluster_template_id, + ) + self.assertEqual(EXAMPLE['create_timeout'], sot.create_timeout) + self.assertEqual(EXAMPLE['discovery_url'], sot.discovery_url) + self.assertEqual(EXAMPLE['flavor_id'], sot.flavor_id) + self.assertEqual(EXAMPLE['keypair'], sot.keypair) + self.assertEqual(EXAMPLE['labels'], sot.labels) + self.assertEqual(EXAMPLE['master_count'], sot.master_count) + self.assertEqual(EXAMPLE['master_flavor_id'], sot.master_flavor_id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['node_count'], sot.node_count) diff --git a/tools/print-services.py b/tools/print-services.py index 73dd3d09f..f30d29106 100644 --- a/tools/print-services.py +++ b/tools/print-services.py @@ -103,11 +103,17 @@ def _get_aliases(service_type, aliases=None): def _find_service_description_class(service_type): - package_name = 'openstack.{service_type}'.format( - service_type=service_type).replace('-', '_') + package_name = f'openstack.{service_type}'.replace('-', '_') module_name = service_type.replace('-', '_') + '_service' class_name = ''.join( - [part.capitalize() for part in module_name.split('_')]) + [part.capitalize() for part in module_name.split('_')] + ) + + # We have some exceptions :( + # This should have been called 'shared-filesystem' + if service_type == 'shared-file-system': + class_name = 'SharedFilesystemService' + try: import_name = '.'.join([package_name, module_name]) service_description_module = importlib.import_module(import_name) @@ -116,10 +122,11 @@ def _find_service_description_class(service_type): # as an opt-in for people trying to figure out why something # didn't work. warnings.warn( - "Could not import {service_type} service description: {e}".format( - service_type=service_type, e=str(e)), - ImportWarning) + f"Could not import {service_type} service description: {str(e)}", + ImportWarning, + ) return service_description.ServiceDescription + # There are no cases in which we should have a module but not the class # inside it. service_description_class = getattr(service_description_module, class_name)