diff -Nru python-magnumclient-2.11.0/CONTRIBUTING.rst python-magnumclient-3.0.1/CONTRIBUTING.rst --- python-magnumclient-2.11.0/CONTRIBUTING.rst 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/CONTRIBUTING.rst 2020-05-14 11:51:49.000000000 +0000 @@ -13,4 +13,4 @@ Bugs should be filed on Launchpad, not GitHub: - https://bugs.launchpad.net/python-magnumclient \ No newline at end of file + https://bugs.launchpad.net/python-magnumclient diff -Nru python-magnumclient-2.11.0/debian/changelog python-magnumclient-3.0.1/debian/changelog --- python-magnumclient-2.11.0/debian/changelog 2022-06-24 15:41:10.000000000 +0000 +++ python-magnumclient-3.0.1/debian/changelog 2023-06-29 23:01:16.000000000 +0000 @@ -1,3 +1,16 @@ +python-magnumclient (3.0.1-0ubuntu1) focal; urgency=medium + + [ Corey Bryant ] + * New stable point release for OpenStack Ussuri (LP: #1996229). + + [ Felipe Reyes ] + * d/p/fix-py37-compatibility.patch: Dropped. Fixed in new stable point + release. + * d/p/Fix-failing-to-parse-json-error-msg.patch: Dropped. Fixed in new + stable point release. + + -- Corey Bryant Thu, 29 Jun 2023 19:01:16 -0400 + python-magnumclient (2.11.0-0ubuntu6) focal; urgency=medium * d/p/Fix-failing-to-parse-json-error-msg.patch: Fix failing to parse json diff -Nru python-magnumclient-2.11.0/debian/patches/Fix-failing-to-parse-json-error-msg.patch python-magnumclient-3.0.1/debian/patches/Fix-failing-to-parse-json-error-msg.patch --- python-magnumclient-2.11.0/debian/patches/Fix-failing-to-parse-json-error-msg.patch 2022-06-24 15:41:10.000000000 +0000 +++ python-magnumclient-3.0.1/debian/patches/Fix-failing-to-parse-json-error-msg.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,38 +0,0 @@ -From f7551a6bac103070ff456098fe2631409620d492 Mon Sep 17 00:00:00 2001 -From: Tobias Urdin -Date: Wed, 18 Nov 2020 11:46:44 +0100 -Subject: [PATCH] Fix failing to parse json error msg - -It assumes its a requests response but could -be a HTTPResponse from urllib. - -Story: 2008789 -Task: 42183 - -Change-Id: I7306d167a17284c7f478ec1c1599a8d4b32040c2 ---- - magnumclient/common/httpclient.py | 3 +++ - 1 file changed, 3 insertions(+) - -Index: python-magnumclient-2.11.0/magnumclient/common/httpclient.py -=================================================================== ---- python-magnumclient-2.11.0.orig/magnumclient/common/httpclient.py -+++ python-magnumclient-2.11.0/magnumclient/common/httpclient.py -@@ -23,6 +23,7 @@ import socket - import ssl - - from keystoneauth1 import adapter -+from oslo_serialization import jsonutils - from oslo_utils import importutils - import six - import six.moves.urllib.parse as urlparse -@@ -65,6 +66,9 @@ def _extract_error_json(body, resp): - try: - body_json = resp.json() - return _extract_error_json_text(body_json) -+ except AttributeError: -+ body_json = jsonutils.loads(body) -+ return _extract_error_json_text(body_json) - except ValueError: - return {} - else: diff -Nru python-magnumclient-2.11.0/debian/patches/fix-py37-compatibility.patch python-magnumclient-3.0.1/debian/patches/fix-py37-compatibility.patch --- python-magnumclient-2.11.0/debian/patches/fix-py37-compatibility.patch 2022-06-24 15:41:10.000000000 +0000 +++ python-magnumclient-3.0.1/debian/patches/fix-py37-compatibility.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -From 5deb538930d98bf83e11bfb1dacb509982226540 Mon Sep 17 00:00:00 2001 -From: Michal Arbet -Date: Wed, 6 Feb 2019 14:11:07 +0100 -Subject: [PATCH] Fix py37 compatibility - -Unit tests are failing under python3.7. -Generators which explicitly raise StopIteration can generally be -changed to simply return instead. This will be compatible with -all existing Python versions. - -PEP Documentation for this change: -https://www.python.org/dev/peps/pep-0479/ - -Change-Id: I4ae2049d8a2469d0a37077bdc722481e68d7cc49 -Closes-Bug: #1814890 ---- - magnumclient/common/httpclient.py | 7 +++++-- - 1 file changed, 5 insertions(+), 2 deletions(-) - -diff --git a/magnumclient/common/httpclient.py b/magnumclient/common/httpclient.py -index ce9e4c9..038fb21 100644 ---- a/magnumclient/common/httpclient.py -+++ b/magnumclient/common/httpclient.py -@@ -406,7 +406,10 @@ class ResponseBodyIterator(object): - - def __iter__(self): - while True: -- yield self.next() -+ try: -+ yield self.next() -+ except StopIteration: -+ return - - def __bool__(self): - return hasattr(self, 'items') -@@ -418,7 +421,7 @@ class ResponseBodyIterator(object): - if chunk: - return chunk - else: -- raise StopIteration() -+ raise StopIteration - - - def _construct_http_client(*args, **kwargs): --- -2.31.1 - diff -Nru python-magnumclient-2.11.0/debian/patches/series python-magnumclient-3.0.1/debian/patches/series --- python-magnumclient-2.11.0/debian/patches/series 2022-06-24 15:41:10.000000000 +0000 +++ python-magnumclient-3.0.1/debian/patches/series 2023-06-29 23:01:16.000000000 +0000 @@ -1,2 +0,0 @@ -fix-py37-compatibility.patch -Fix-failing-to-parse-json-error-msg.patch diff -Nru python-magnumclient-2.11.0/doc/requirements.txt python-magnumclient-3.0.1/doc/requirements.txt --- python-magnumclient-2.11.0/doc/requirements.txt 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/doc/requirements.txt 2020-05-14 11:51:49.000000000 +0000 @@ -1,3 +1,4 @@ -sphinx!=1.6.6,>=1.6.2 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2;python_version>='3.4' # BSD openstackdocstheme>=1.18.1 # Apache-2.0 reno>=2.5.0 # Apache-2.0 diff -Nru python-magnumclient-2.11.0/.gitreview python-magnumclient-3.0.1/.gitreview --- python-magnumclient-2.11.0/.gitreview 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/.gitreview 2020-05-14 11:51:49.000000000 +0000 @@ -1,4 +1,5 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-magnumclient.git +defaultbranch=stable/ussuri diff -Nru python-magnumclient-2.11.0/magnumclient/common/cliutils.py python-magnumclient-3.0.1/magnumclient/common/cliutils.py --- python-magnumclient-2.11.0/magnumclient/common/cliutils.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/common/cliutils.py 2020-05-14 11:51:49.000000000 +0000 @@ -404,8 +404,8 @@ return getattr(f, 'service_type', None) -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) +def pretty_choice_list(lst): + return ', '.join("'%s'" % i for i in lst) def exit(msg=''): diff -Nru python-magnumclient-2.11.0/magnumclient/common/httpclient.py python-magnumclient-3.0.1/magnumclient/common/httpclient.py --- python-magnumclient-2.11.0/magnumclient/common/httpclient.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/common/httpclient.py 2020-05-14 11:51:49.000000000 +0000 @@ -16,13 +16,13 @@ # under the License. import copy -import json import logging import os import socket import ssl from keystoneauth1 import adapter +from oslo_serialization import jsonutils from oslo_utils import importutils import six import six.moves.urllib.parse as urlparse @@ -43,7 +43,7 @@ error_json = {} if 'error_message' in body_json: raw_msg = body_json['error_message'] - error_json = json.loads(raw_msg) + error_json = jsonutils.loads(raw_msg) elif 'error' in body_json: error_body = body_json['error'] error_json = {'faultstring': error_body['title'], @@ -69,7 +69,7 @@ return {} else: try: - body_json = json.loads(body) + body_json = jsonutils.loads(body) return _extract_error_json_text(body_json) except ValueError: return {} @@ -228,7 +228,7 @@ kwargs['headers'].setdefault('Accept', 'application/json') if 'body' in kwargs: - kwargs['body'] = json.dumps(kwargs['body']) + kwargs['body'] = jsonutils.dumps(kwargs['body']) resp, body_iter = self._http_request(url, method, **kwargs) content_type = resp.getheader('content-type', None) @@ -239,7 +239,7 @@ if 'application/json' in content_type: body = ''.join([chunk for chunk in body_iter]) try: - body = json.loads(body) + body = jsonutils.loads(body) except ValueError: LOG.error('Could not decode response body as JSON') else: @@ -373,7 +373,7 @@ kwargs['headers'].setdefault('Content-Type', 'application/json') kwargs['headers'].setdefault('Accept', 'application/json') if 'body' in kwargs: - kwargs['data'] = json.dumps(kwargs.pop('body')) + kwargs['data'] = jsonutils.dumps(kwargs.pop('body')) resp = self._http_request(url, method, **kwargs) body = resp.content @@ -406,7 +406,10 @@ def __iter__(self): while True: - yield self.next() + try: + yield self.next() + except StopIteration: + return def __bool__(self): return hasattr(self, 'items') @@ -418,7 +421,7 @@ if chunk: return chunk else: - raise StopIteration() + raise StopIteration def _construct_http_client(*args, **kwargs): diff -Nru python-magnumclient-2.11.0/magnumclient/common/utils.py python-magnumclient-3.0.1/magnumclient/common/utils.py --- python-magnumclient-2.11.0/magnumclient/common/utils.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/common/utils.py 2020-05-14 11:51:49.000000000 +0000 @@ -15,8 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -import base64 -import json import os from cryptography.hazmat.backends import default_backend @@ -25,6 +23,8 @@ from cryptography.hazmat.primitives import serialization from cryptography import x509 from cryptography.x509.oid import NameOID +from oslo_serialization import base64 +from oslo_serialization import jsonutils from magnumclient import exceptions as exc from magnumclient.i18n import _ @@ -64,7 +64,7 @@ raise exc.CommandError(_('Attributes must be a list of ' 'PATH=VALUE not "%s"') % string) try: - value = json.loads(value) + value = jsonutils.loads(value) except ValueError: pass @@ -100,7 +100,7 @@ if 'mesos_slave_executor_env_file' in labels: environment_variables_data = handle_json_from_file( labels['mesos_slave_executor_env_file']) - labels['mesos_slave_executor_env_variables'] = json.dumps( + labels['mesos_slave_executor_env_variables'] = jsonutils.dumps( environment_variables_data) return labels @@ -118,12 +118,12 @@ lbls = lbls[0].replace(';', ',').split(',') labels = {} - for l in lbls: + for lbl in lbls: try: - (k, v) = l.split(('='), 1) + (k, v) = lbl.split(('='), 1) except ValueError: raise exc.CommandError(_('labels must be a list of KEY=VALUE ' - 'not %s') % l) + 'not %s') % lbl) if k not in labels: labels[k] = v else: @@ -146,7 +146,7 @@ try: with open(json_arg, 'r') as f: json_arg = f.read().strip() - json_arg = json.loads(json_arg) + json_arg = jsonutils.loads(json_arg) except IOError as e: err = _("Cannot get JSON from file '%(file)s'. " "Error: %(err)s") % {'err': e, 'file': json_arg} @@ -160,11 +160,11 @@ def config_cluster(cluster, cluster_template, cfg_dir, force=False, - certs=None): + certs=None, use_keystone=False): """Return and write configuration for the given cluster.""" if cluster_template.coe == 'kubernetes': return _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, - force, certs) + force, certs, use_keystone) elif (cluster_template.coe == 'swarm' or cluster_template.coe == 'swarm-mode'): return _config_cluster_swarm(cluster, cluster_template, cfg_dir, @@ -172,7 +172,7 @@ def _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, - force=False, certs=None): + force=False, certs=None, use_keystone=False): """Return and write configuration for the given kubernetes cluster.""" cfg_file = "%s/config" % cfg_dir if cluster_template.tls_disabled or certs is None: @@ -193,30 +193,64 @@ "- name: %(name)s'\n" % {'name': cluster.name, 'api_address': cluster.api_address}) else: - cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" - " certificate-authority-data: %(ca)s\n" - " server: %(api_address)s\n" - " name: %(name)s\n" - "contexts:\n" - "- context:\n" - " cluster: %(name)s\n" - " user: admin\n" - " name: default\n" - "current-context: default\n" - "kind: Config\n" - "preferences: {}\n" - "users:\n" - "- name: admin\n" - " user:\n" - " client-certificate-data: %(cert)s\n" - " client-key-data: %(key)s\n" - % {'name': cluster.name, - 'api_address': cluster.api_address, - 'key': base64.b64encode(certs['key']), - 'cert': base64.b64encode(certs['cert']), - 'ca': base64.b64encode(certs['ca'])}) + if not use_keystone: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " certificate-authority-data: %(ca)s\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: admin\n" + " name: default\n" + "current-context: default\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: admin\n" + " user:\n" + " client-certificate-data: %(cert)s\n" + " client-key-data: %(key)s\n" + % {'name': cluster.name, + 'api_address': cluster.api_address, + 'key': base64.encode_as_text(certs['key']), + 'cert': base64.encode_as_text(certs['cert']), + 'ca': base64.encode_as_text(certs['ca'])}) + else: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " certificate-authority-data: %(ca)s\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: openstackuser\n" + " name: openstackuser@kubernetes\n" + "current-context: openstackuser@kubernetes\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: openstackuser\n" + " user:\n" + " exec:\n" + " command: /bin/bash\n" + " apiVersion: client.authentication.k8s.io/v1alpha1\n" + " args:\n" + " - -c\n" + " - >\n" + " if [ -z ${OS_TOKEN} ]; then\n" + " echo 'Error: Missing OpenStack credential from environment variable $OS_TOKEN' > /dev/stderr\n" # noqa + " exit 1\n" + " else\n" + " echo '{ \"apiVersion\": \"client.authentication.k8s.io/v1alpha1\", \"kind\": \"ExecCredential\", \"status\": { \"token\": \"'\"${OS_TOKEN}\"'\"}}'\n" # noqa + " fi\n" + % {'name': cluster.name, + 'api_address': cluster.api_address, + 'ca': base64.encode_as_text(certs['ca'])}) if os.path.exists(cfg_file) and not force: raise exc.CommandError("File %s exists, aborting." % cfg_file) diff -Nru python-magnumclient-2.11.0/magnumclient/exceptions.py python-magnumclient-3.0.1/magnumclient/exceptions.py --- python-magnumclient-2.11.0/magnumclient/exceptions.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/exceptions.py 2020-05-14 11:51:49.000000000 +0000 @@ -18,15 +18,15 @@ # NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards # compatibility. -InvalidEndpoint = EndpointException -CommunicationError = ConnectionRefused -HTTPBadRequest = BadRequest -HTTPInternalServerError = InternalServerError -HTTPNotFound = NotFound -HTTPServiceUnavailable = ServiceUnavailable +InvalidEndpoint = exceptions.EndpointException +CommunicationError = exceptions.ConnectionRefused +HTTPBadRequest = exceptions.BadRequest +HTTPInternalServerError = exceptions.InternalServerError +HTTPNotFound = exceptions.NotFound +HTTPServiceUnavailable = exceptions.ServiceUnavailable -class AmbiguousAuthSystem(ClientException): +class AmbiguousAuthSystem(exceptions.ClientException): """Could not obtain token and endpoint using provided credentials.""" pass @@ -35,7 +35,7 @@ AmbigiousAuthSystem = AmbiguousAuthSystem -class InvalidAttribute(ClientException): +class InvalidAttribute(exceptions.ClientException): pass diff -Nru python-magnumclient-2.11.0/magnumclient/osc/plugin.py python-magnumclient-3.0.1/magnumclient/osc/plugin.py --- python-magnumclient-2.11.0/magnumclient/osc/plugin.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/osc/plugin.py 2020-05-14 11:51:49.000000000 +0000 @@ -17,7 +17,8 @@ LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '1' +DEFAULT_MAJOR_API_VERSION = '1' +DEFAULT_MAGNUM_API_VERSION = 'latest' API_VERSION_OPTION = 'os_container_infra_api_version' API_NAME = 'container_infra' API_VERSIONS = { @@ -37,7 +38,8 @@ region_name=instance._region_name, interface=instance._interface, insecure=instance._insecure, - ca_cert=instance._cacert) + ca_cert=instance._cacert, + api_version=DEFAULT_MAGNUM_API_VERSION) return client @@ -49,8 +51,8 @@ metavar='', default=utils.env( 'OS_CONTAINER_INFRA_API_VERSION', - default=DEFAULT_API_VERSION), + default=DEFAULT_MAJOR_API_VERSION), help='Container-Infra API version, default=' + - DEFAULT_API_VERSION + + DEFAULT_MAJOR_API_VERSION + ' (Env: OS_CONTAINER_INFRA_API_VERSION)') return parser diff -Nru python-magnumclient-2.11.0/magnumclient/osc/v1/clusters.py python-magnumclient-3.0.1/magnumclient/osc/v1/clusters.py --- python-magnumclient-2.11.0/magnumclient/osc/v1/clusters.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/osc/v1/clusters.py 2020-05-14 11:51:49.000000000 +0000 @@ -24,6 +24,7 @@ CLUSTER_ATTRIBUTES = [ 'status', + 'health_status', 'cluster_template_id', 'node_addresses', 'uuid', @@ -33,6 +34,9 @@ 'updated_at', 'coe_version', 'labels', + 'labels_overridden', + 'labels_skipped', + 'labels_added', 'faults', 'keypair', 'api_address', @@ -45,6 +49,8 @@ 'name', 'master_flavor_id', 'flavor_id', + 'health_status_reason', + 'project_id', ] @@ -113,6 +119,38 @@ metavar='', help=_('The nova flavor name or UUID to use when launching the ' 'Cluster.')) + parser.add_argument( + '--fixed-network', + dest='fixed_network', + metavar='', + help=_('The private Neutron network name to connect to this ' + 'Cluster template.')) + parser.add_argument( + '--fixed-subnet', + dest='fixed_subnet', + metavar='', + help=_('The private Neutron subnet name to connect to Cluster.')) + parser.add_argument( + '--floating-ip-enabled', + dest='floating_ip_enabled', + default=[], + action='append_const', + const=True, + help=_('Indicates whether created Clusters should have a ' + 'floating ip.')) + parser.add_argument( + '--floating-ip-disabled', + dest='floating_ip_enabled', + action='append_const', + const=False, + help=_('Disables floating ip creation on the new Cluster')) + parser.add_argument( + '--merge-labels', + dest='merge_labels', + action='store_true', + default=False, + help=_('The labels provided will be merged with the labels ' + 'configured in the specified cluster template.')) return parser @@ -130,6 +168,15 @@ 'node_count': parsed_args.node_count, } + if len(parsed_args.floating_ip_enabled) > 1: + raise exceptions.InvalidAttribute( + '--floating-ip-enabled and ' + '--floating-ip-disabled are ' + 'mutually exclusive and ' + 'should be specified only once.') + elif len(parsed_args.floating_ip_enabled) == 1: + args['floating_ip_enabled'] = parsed_args.floating_ip_enabled[0] + if parsed_args.labels is not None: args['labels'] = magnum_utils.handle_labels(parsed_args.labels) @@ -142,6 +189,17 @@ if parsed_args.flavor is not None: args['flavor_id'] = parsed_args.flavor + if parsed_args.fixed_network is not None: + args["fixed_network"] = parsed_args.fixed_network + + if parsed_args.fixed_subnet is not None: + args["fixed_subnet"] = parsed_args.fixed_subnet + + if parsed_args.merge_labels: + # We are only sending this if it's True. This + # way we avoid breaking older APIs. + args["merge_labels"] = parsed_args.merge_labels + cluster = mag_client.clusters.create(**args) print("Request to create cluster %s accepted" % cluster.uuid) @@ -197,7 +255,8 @@ mag_client = self.app.client_manager.container_infra columns = [ - 'uuid', 'name', 'keypair', 'node_count', 'master_count', 'status'] + 'uuid', 'name', 'keypair', 'node_count', 'master_count', 'status', + 'health_status'] clusters = mag_client.clusters.list(limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir) @@ -305,6 +364,18 @@ dest='output_certs', default=False, help=_('Output certificates in separate files.')) + parser.add_argument( + '--use-certificate', + action='store_true', + dest='use_certificate', + default=True, + help=_('Use certificate in config files.')) + parser.add_argument( + '--use-keystone', + action='store_true', + dest='use_keystone', + default=False, + help=_('Use Keystone token in config files.')) return parser @@ -315,16 +386,21 @@ the corresponding COE configured to access the cluster. """ + if parsed_args.use_keystone: + parsed_args.use_certificate = False + if not parsed_args.use_certificate: + parsed_args.use_keystone = True + self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra parsed_args.dir = os.path.abspath(parsed_args.dir) cluster = mag_client.clusters.get(parsed_args.cluster) - if cluster.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE', - 'ROLLBACK_COMPLETE'): - raise exceptions.CommandError("cluster in status %s" % - cluster.status) + if cluster.api_address is None: + self.log.warning("WARNING: The cluster's api_address is" + " not known yet.") + cluster_template = mag_client.cluster_templates.get( cluster.cluster_template_id) opts = { @@ -346,8 +422,92 @@ with open(fname, "w") as f: f.write(tls[k]) - print(magnum_utils.config_cluster(cluster, - cluster_template, - parsed_args.dir, - force=parsed_args.force, - certs=tls)) + print(magnum_utils.config_cluster( + cluster, cluster_template, parsed_args.dir, + force=parsed_args.force, certs=tls, + use_keystone=parsed_args.use_keystone)) + + +class ResizeCluster(command.Command): + _description = _("Resize a Cluster") + + def get_parser(self, prog_name): + parser = super(ResizeCluster, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('The name or UUID of cluster to update')) + + parser.add_argument( + 'node_count', + type=int, + help=_("Desired node count of the cluser.")) + + parser.add_argument( + '--nodes-to-remove', + metavar='', + action='append', + help=_("Server ID of the nodes to be removed. Repeat to add" + "more server ID")) + + parser.add_argument( + '--nodegroup', + metavar='', + help=_('The name or UUID of the nodegroup of current cluster.')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + cluster = mag_client.clusters.get(parsed_args.cluster) + + mag_client.clusters.resize(cluster.uuid, + parsed_args.node_count, + parsed_args.nodes_to_remove, + parsed_args.nodegroup) + print("Request to resize cluster %s has been accepted." % + parsed_args.cluster) + + +class UpgradeCluster(command.Command): + _description = _("Upgrade a Cluster") + + def get_parser(self, prog_name): + parser = super(UpgradeCluster, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('The name or UUID of cluster to update')) + + parser.add_argument( + 'cluster_template', + help=_("The new cluster template ID will be upgraded to.")) + + parser.add_argument( + '--max-batch-size', + metavar='', + type=int, + default=1, + help=_("The max batch size for upgrading each time.")) + + parser.add_argument( + '--nodegroup', + metavar='', + help=_('The name or UUID of the nodegroup of current cluster.')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + cluster = mag_client.clusters.get(parsed_args.cluster) + + mag_client.clusters.upgrade(cluster.uuid, + parsed_args.cluster_template, + parsed_args.max_batch_size, + parsed_args.nodegroup) + print("Request to upgrade cluster %s has been accepted." % + parsed_args.cluster) diff -Nru python-magnumclient-2.11.0/magnumclient/osc/v1/cluster_templates.py python-magnumclient-3.0.1/magnumclient/osc/v1/cluster_templates.py --- python-magnumclient-2.11.0/magnumclient/osc/v1/cluster_templates.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/osc/v1/cluster_templates.py 2020-05-14 11:51:49.000000000 +0000 @@ -51,7 +51,8 @@ 'coe', 'flavor_id', 'master_lb_enabled', - 'dns_nameserver' + 'dns_nameserver', + 'hidden' ] @@ -221,6 +222,17 @@ action='append_const', const=False, help=_('Disables floating ip creation on the new Cluster')) + parser.add_argument( + '--hidden', + dest='hidden', + action='store_true', + default=False, + help=_('Indicates the cluster template should be hidden.')) + parser.add_argument( + '--visible', + dest='hidden', + action='store_false', + help=_('Indicates the cluster template should be visible.')) return parser @@ -252,6 +264,12 @@ 'server_type': parsed_args.server_type, 'master_lb_enabled': parsed_args.master_lb_enabled, } + + # NOTE (brtknr): Only supply hidden arg if it is True + # for backward compatibility + if parsed_args.hidden: + args['hidden'] = parsed_args.hidden + if len(parsed_args.floating_ip_enabled) > 1: raise InvalidAttribute('--floating-ip-enabled and ' '--floating-ip-disabled are ' @@ -336,6 +354,8 @@ mag_client = self.app.client_manager.container_infra columns = ['uuid', 'name'] + if parsed_args.fields: + columns += parsed_args.fields.split(',') cts = mag_client.cluster_templates.list(limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir) diff -Nru python-magnumclient-2.11.0/magnumclient/osc/v1/nodegroups.py python-magnumclient-3.0.1/magnumclient/osc/v1/nodegroups.py --- python-magnumclient-2.11.0/magnumclient/osc/v1/nodegroups.py 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/osc/v1/nodegroups.py 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,301 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 magnumclient.common import utils as magnum_utils +from magnumclient.i18n import _ + +from osc_lib.command import command +from osc_lib import utils + + +NODEGROUP_ATTRIBUTES = [ + 'uuid', + 'name', + 'cluster_id', + 'project_id', + 'docker_volume_size', + 'labels', + 'labels_overridden', + 'labels_skipped', + 'labels_added', + 'flavor_id', + 'image_id', + 'node_addresses', + 'node_count', + 'role', + 'max_node_count', + 'min_node_count', + 'is_default', + 'stack_id', + 'status', + 'status_reason', +] + + +class CreateNodeGroup(command.Command): + _description = _("Create a nodegroup") + + def get_parser(self, prog_name): + parser = super(CreateNodeGroup, self).get_parser(prog_name) + # NOTE: All arguments are positional and, if not provided + # with a default, required. + parser.add_argument('--docker-volume-size', + dest='docker_volume_size', + type=int, + metavar='', + help=('The size in GB for the docker volume to ' + 'use.')) + parser.add_argument('--labels', + metavar='', + action='append', + help=_('Arbitrary labels in the form of key=value' + 'pairs to associate with a nodegroup. ' + 'May be used multiple times.')) + parser.add_argument('cluster', + metavar='', + help='Name of the nodegroup to create.') + parser.add_argument('name', + metavar='', + help='Name of the nodegroup to create.') + parser.add_argument('--node-count', + dest='node_count', + type=int, + default=1, + metavar='', + help='The nodegroup node count.') + parser.add_argument('--min-nodes', + dest='min_node_count', + type=int, + default=1, + metavar='', + help='The nodegroup minimum node count.') + parser.add_argument('--max-nodes', + dest='max_node_count', + type=int, + default=None, + metavar='', + help='The nodegroup maximum node count.') + parser.add_argument('--role', + dest='role', + type=str, + default='worker', + metavar='', + help=('The role of the nodegroup')) + parser.add_argument( + '--image', + metavar='', + help=_('The name or UUID of the base image to customize for the ' + 'NodeGroup.')) + parser.add_argument( + '--flavor', + metavar='', + help=_('The nova flavor name or UUID to use when launching the ' + 'nodes in this NodeGroup.')) + parser.add_argument( + '--merge-labels', + dest='merge_labels', + action='store_true', + default=False, + help=_('The labels provided will be merged with the labels ' + 'configured in the specified cluster.')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + args = { + 'name': parsed_args.name, + 'node_count': parsed_args.node_count, + 'max_node_count': parsed_args.max_node_count, + 'min_node_count': parsed_args.min_node_count, + 'role': parsed_args.role, + } + + if parsed_args.labels is not None: + args['labels'] = magnum_utils.handle_labels(parsed_args.labels) + + if parsed_args.docker_volume_size is not None: + args['docker_volume_size'] = parsed_args.docker_volume_size + + if parsed_args.flavor is not None: + args['flavor_id'] = parsed_args.flavor + + if parsed_args.image is not None: + args['image_id'] = parsed_args.image + + if parsed_args.merge_labels: + # We are only sending this if it's True. This + # way we avoid breaking older APIs. + args["merge_labels"] = parsed_args.merge_labels + + cluster_id = parsed_args.cluster + nodegroup = mag_client.nodegroups.create(cluster_id, **args) + print("Request to create nodegroup %s accepted" + % nodegroup.uuid) + + +class DeleteNodeGroup(command.Command): + _description = _("Delete a nodegroup") + + def get_parser(self, prog_name): + parser = super(DeleteNodeGroup, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup(s) ' + 'belong(s).')) + parser.add_argument( + 'nodegroup', + nargs='+', + metavar='', + help='ID or name of the nodegroup(s) to delete.') + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + cluster_id = parsed_args.cluster + for ng in parsed_args.nodegroup: + mag_client.nodegroups.delete(cluster_id, ng) + print("Request to delete nodegroup %s has been accepted." % ng) + + +class ListNodeGroup(command.Lister): + _description = _("List nodegroups") + + def get_parser(self, prog_name): + parser = super(ListNodeGroup, self).get_parser(prog_name) + + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup belongs.')) + parser.add_argument( + '--limit', + metavar='', + type=int, + help=_('Maximum number of nodegroups to return')) + parser.add_argument( + '--sort-key', + metavar='', + help=_('Column to sort results by')) + parser.add_argument( + '--sort-dir', + metavar='', + choices=['desc', 'asc'], + help=_('Direction to sort. "asc" or "desc".')) + parser.add_argument( + '--role', + metavar='', + help=_('List the nodegroups in the cluster with this role')) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', + 'status', 'role'] + cluster_id = parsed_args.cluster + nodegroups = mag_client.nodegroups.list(cluster_id, + limit=parsed_args.limit, + sort_key=parsed_args.sort_key, + sort_dir=parsed_args.sort_dir, + role=parsed_args.role) + return ( + columns, + (utils.get_item_properties(n, columns) for n in nodegroups) + ) + + +class ShowNodeGroup(command.ShowOne): + _description = _("Show a nodegroup") + + def get_parser(self, prog_name): + parser = super(ShowNodeGroup, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup belongs.')) + parser.add_argument( + 'nodegroup', + metavar='', + help=_('ID or name of the nodegroup to show.') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + columns = NODEGROUP_ATTRIBUTES + + mag_client = self.app.client_manager.container_infra + cluster_id = parsed_args.cluster + nodegroup = mag_client.nodegroups.get(cluster_id, + parsed_args.nodegroup) + + return (columns, utils.get_item_properties(nodegroup, columns)) + + +class UpdateNodeGroup(command.Command): + _description = _("Update a Nodegroup") + + def get_parser(self, prog_name): + parser = super(UpdateNodeGroup, self).get_parser(prog_name) + parser.add_argument( + 'cluster', + metavar='', + help=_('ID or name of the cluster where the nodegroup belongs.')) + parser.add_argument( + 'nodegroup', + metavar='', + help=_('The name or UUID of cluster to update')) + + parser.add_argument( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help=_("Operations: one of 'add', 'replace' or 'remove'")) + + parser.add_argument( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help=_( + "Attributes to add/replace or remove (only PATH is necessary " + "on remove)")) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + mag_client = self.app.client_manager.container_infra + + patch = magnum_utils.args_array_to_patch(parsed_args.op, + parsed_args.attributes[0]) + + cluster_id = parsed_args.cluster + mag_client.nodegroups.update(cluster_id, parsed_args.nodegroup, + patch) + print("Request to update nodegroup %s has been accepted." % + parsed_args.nodegroup) diff -Nru python-magnumclient-2.11.0/magnumclient/shell.py python-magnumclient-3.0.1/magnumclient/shell.py --- python-magnumclient-2.11.0/magnumclient/shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -33,6 +33,13 @@ from oslo_utils import strutils import six +from magnumclient.common import cliutils +from magnumclient import exceptions as exc +from magnumclient.i18n import _ +from magnumclient.v1 import client as client_v1 +from magnumclient.v1 import shell as shell_v1 +from magnumclient import version + profiler = importutils.try_import("osprofiler.profiler") HAS_KEYRING = False @@ -51,12 +58,6 @@ except ImportError: pass -from magnumclient.common import cliutils -from magnumclient import exceptions as exc -from magnumclient.i18n import _ -from magnumclient.v1 import client as client_v1 -from magnumclient.v1 import shell as shell_v1 -from magnumclient import version LATEST_API_VERSION = ('1', 'latest') DEFAULT_INTERFACE = 'public' diff -Nru python-magnumclient-2.11.0/magnumclient/tests/osc/unit/osc_fakes.py python-magnumclient-3.0.1/magnumclient/tests/osc/unit/osc_fakes.py --- python-magnumclient-2.11.0/magnumclient/tests/osc/unit/osc_fakes.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/osc/unit/osc_fakes.py 2020-05-14 11:51:49.000000000 +0000 @@ -13,13 +13,12 @@ # under the License. # -import json import mock +from oslo_serialization import jsonutils import sys from keystoneauth1 import fixture import requests -import six AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" @@ -239,9 +238,7 @@ self.status_code = status_code self.headers.update(headers) - self._content = json.dumps(data) - if not isinstance(self._content, six.binary_type): - self._content = self._content.encode() + self._content = jsonutils.dump_as_bytes(data) class FakeModel(dict): diff -Nru python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/fakes.py python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/fakes.py --- python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/fakes.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/fakes.py 2020-05-14 11:51:49.000000000 +0000 @@ -62,14 +62,38 @@ pass +class FakeNodeGroupManager(object): + def list(self, cluster_id, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False): + pass + + def get(self, cluster_id, id): + pass + + def create(self, cluster_id, **kwargs): + pass + + def delete(self, cluster_id, id): + pass + + def update(self, cluster_id, id, patch): + pass + + +class FakeCertificatesModelManager(FakeBaseModelManager): + def get(self, cluster_uuid): + pass + + class MagnumFakeContainerInfra(object): def __init__(self): self.cluster_templates = FakeBaseModelManager() self.clusters = FakeBaseModelManager() self.mservices = FakeBaseModelManager() - self.certificates = FakeBaseModelManager() + self.certificates = FakeCertificatesModelManager() self.stats = FakeStatsModelManager() self.quotas = FakeQuotasModelManager() + self.nodegroups = FakeNodeGroupManager() class MagnumFakeClientManager(osc_fakes.FakeClientManager): @@ -160,7 +184,8 @@ 'coe': 'kubernetes', 'flavor_id': 'm1.medium', 'master_lb_enabled': False, - 'dns_nameserver': '8.8.8.8' + 'dns_nameserver': '8.8.8.8', + 'hidden': False } # Overwrite default attributes. @@ -205,12 +230,16 @@ # set default attributes. cluster_info = { 'status': 'CREATE_IN_PROGRESS', + 'health_status': 'HEALTHY', 'cluster_template_id': 'fake-ct', 'node_addresses': [], 'uuid': '3a369884-b6ba-484f-a206-919b4b718aff', 'stack_id': 'c4554582-77bd-4734-8f1a-72c3c40e5fb4', 'status_reason': None, 'labels': {}, + 'labels_overridden': {}, + 'labels_added': {}, + 'labels_skipped': {}, 'created_at': '2017-03-16T18:40:39+00:00', 'updated_at': '2017-03-16T18:40:45+00:00', 'coe_version': None, @@ -227,6 +256,7 @@ 'master_flavor_id': None, 'flavor_id': 'm1.medium', 'project_id': None, + 'health_status_reason': {'api': 'ok'} } # Overwrite default attributes. @@ -237,6 +267,11 @@ return cluster +class FakeCert(object): + def __init__(self, pem): + self.pem = pem + + class FakeQuota(object): """Fake one or more Quota""" @@ -266,3 +301,52 @@ quota = osc_fakes.FakeResource(info=copy.deepcopy(quota_info), loaded=True) return quota + + +class FakeNodeGroup(object): + """Fake one or more NodeGroup.""" + + @staticmethod + def create_one_nodegroup(attrs=None): + """Create a fake NodeGroup. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with flavor_id, image_id, and so on + """ + + attrs = attrs or {} + + # set default attributes. + nodegroup_info = { + 'created_at': '2017-03-16T18:40:39+00:00', + 'updated_at': '2017-03-16T18:40:45+00:00', + 'uuid': '3a369884-b6ba-484f-a206-919b4b718aff', + 'cluster_id': 'fake-cluster', + 'docker_volume_size': None, + 'node_addresses': [], + 'labels': {}, + 'labels_overridden': {}, + 'labels_added': {}, + 'labels_skipped': {}, + 'node_count': 1, + 'name': 'fake-nodegroup', + 'flavor_id': 'm1.medium', + 'image_id': 'fedora-latest', + 'project_id': None, + 'role': 'worker', + 'max_node_count': 10, + 'min_node_count': 1, + 'is_default': False, + 'stack_id': '3a369884-b6ba-484f-fake-stackb718aff', + 'status': 'CREATE_COMPLETE', + 'status_reason': 'None' + } + + # Overwrite default attributes. + nodegroup_info.update(attrs) + + nodegroup = osc_fakes.FakeResource(info=copy.deepcopy(nodegroup_info), + loaded=True) + return nodegroup diff -Nru python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_clusters.py python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_clusters.py --- python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_clusters.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_clusters.py 2020-05-14 11:51:49.000000000 +0000 @@ -37,6 +37,8 @@ super(TestCluster, self).setUp() self.clusters_mock = self.app.client_manager.container_infra.clusters + self.certificates_mock = \ + self.app.client_manager.container_infra.certificates class TestClusterCreate(TestCluster): @@ -181,7 +183,8 @@ 'keypair', 'node_count', 'master_count', - 'status' + 'status', + 'health_status' ] datalist = ( @@ -191,7 +194,8 @@ _cluster.keypair, _cluster.node_count, _cluster.master_count, - _cluster.status + _cluster.status, + _cluster.health_status, ), ) @@ -360,10 +364,15 @@ self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster + cert = magnum_fakes.FakeCert(pem='foo bar') + self.certificates_mock.create = mock.Mock() + self.certificates_mock.create.return_value = cert + self.certificates_mock.get = mock.Mock() + self.certificates_mock.get.return_value = cert + # Fake the cluster_template attr = dict() attr['name'] = 'fake-ct' - attr['tls_disabled'] = True self._cluster_template = \ magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr) @@ -459,14 +468,65 @@ self.clusters_mock.get.assert_called_with('fake-cluster') - def test_cluster_config_with_in_progress_status(self): - self._cluster.status = 'CREATE_IN_PROGRESS' - arglist = ['fake-cluster-1'] +class TestClusterResize(TestCluster): + + def setUp(self): + super(TestClusterResize, self).setUp() + self.cluster = mock.Mock() + self.cluster.uuid = "UUID1" + self.clusters_mock.resize = mock.Mock() + self.clusters_mock.resize.return_value = None + + self.clusters_mock.get = mock.Mock() + self.clusters_mock.get.return_value = self.cluster + + # Get the command object to test + self.cmd = osc_clusters.ResizeCluster(self.app, None) + + def test_cluster_resize_pass(self): + arglist = ['foo', '2'] verifylist = [ - ('cluster', 'fake-cluster-1') + ('cluster', 'foo'), + ('node_count', 2), + ('nodes_to_remove', None), + ('nodegroup', None) ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.clusters_mock.resize.assert_called_with( + "UUID1", 2, None, None + ) + + +class TestClusterUpgrade(TestCluster): + def setUp(self): + super(TestClusterUpgrade, self).setUp() + self.cluster = mock.Mock() + self.cluster.uuid = "UUID1" + self.clusters_mock.upgrade = mock.Mock() + self.clusters_mock.upgrade.return_value = None + + self.clusters_mock.get = mock.Mock() + self.clusters_mock.get.return_value = self.cluster + + # Get the command object to test + self.cmd = osc_clusters.UpgradeCluster(self.app, None) + + def test_cluster_upgrade_pass(self): + cluster_template_id = 'TEMPLATE_ID' + arglist = ['foo', cluster_template_id] + verifylist = [ + ('cluster', 'foo'), + ('cluster_template', cluster_template_id), + ('max_batch_size', 1), + ('nodegroup', None) + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises(exceptions.CommandError, - self.cmd.take_action, parsed_args) + + self.cmd.take_action(parsed_args) + self.clusters_mock.upgrade.assert_called_with( + "UUID1", cluster_template_id, 1, None + ) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_cluster_templates.py python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_cluster_templates.py --- python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_cluster_templates.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_cluster_templates.py 2020-05-14 11:51:49.000000000 +0000 @@ -48,7 +48,7 @@ 'registry_enabled': False, 'server_type': 'vm', 'tls_disabled': False, - 'volume_driver': None + 'volume_driver': None, } def setUp(self): @@ -271,22 +271,24 @@ '--limit', '1', '--sort-key', 'key', '--sort-dir', 'asc', - '--fields', 'fields' + '--fields', 'field1,field2' ] verifylist = [ ('limit', 1), ('sort_key', 'key'), ('sort_dir', 'asc'), - ('fields', 'fields'), + ('fields', 'field1,field2'), ] + verifycolumns = self.columns + ['field1', 'field2'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + columns, data = self.cmd.take_action(parsed_args) self.cluster_templates_mock.list.assert_called_with( limit=1, sort_dir='asc', sort_key='key', ) + self.assertEqual(verifycolumns, columns) def test_cluster_template_list_bad_sort_dir_fail(self): arglist = [ diff -Nru python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_nodegroups.py python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_nodegroups.py --- python-magnumclient-2.11.0/magnumclient/tests/osc/unit/v1/test_nodegroups.py 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/osc/unit/v1/test_nodegroups.py 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,333 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 copy +import mock +from mock import call + +from magnumclient.osc.v1 import nodegroups as osc_nodegroups +from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes + + +class TestNodeGroup(magnum_fakes.TestMagnumClientOSCV1): + + def setUp(self): + super(TestNodeGroup, self).setUp() + self.ng_mock = self.app.client_manager.container_infra.nodegroups + + +class TestNodeGroupCreate(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupCreate, self).setUp() + self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + + self.ng_mock.create = mock.Mock() + self.ng_mock.create.return_value = self.nodegroup + + self.ng_mock.get = mock.Mock() + self.ng_mock.get.return_value = copy.deepcopy(self.nodegroup) + + self.ng_mock.update = mock.Mock() + self.ng_mock.update.return_value = self.nodegroup + + self._default_args = { + 'name': 'fake-nodegroup', + 'node_count': 1, + 'role': 'worker', + 'min_node_count': 1, + 'max_node_count': None, + } + + # Get the command object to test + self.cmd = osc_nodegroups.CreateNodeGroup(self.app, None) + + self.data = tuple(map(lambda x: getattr(self.nodegroup, x), + osc_nodegroups.NODEGROUP_ATTRIBUTES)) + + def test_nodegroup_create_required_args_pass(self): + """Verifies required arguments.""" + + arglist = [ + self.nodegroup.cluster_id, + self.nodegroup.name + ] + verifylist = [ + ('cluster', self.nodegroup.cluster_id), + ('name', self.nodegroup.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, + **self._default_args) + + def test_nodegroup_create_missing_required_arg(self): + """Verifies missing required arguments.""" + + arglist = [ + self.nodegroup.name + ] + verifylist = [ + ('name', self.nodegroup.name) + ] + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_create_with_labels(self): + """Verifies labels are properly parsed when given as argument.""" + + expected_args = self._default_args + expected_args['labels'] = { + 'arg1': 'value1', 'arg2': 'value2' + } + + arglist = [ + '--labels', 'arg1=value1', + '--labels', 'arg2=value2', + self.nodegroup.cluster_id, + self.nodegroup.name + ] + verifylist = [ + ('labels', ['arg1=value1', 'arg2=value2']), + ('name', self.nodegroup.name), + ('cluster', self.nodegroup.cluster_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, + **expected_args) + + +class TestNodeGroupDelete(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupDelete, self).setUp() + + self.ng_mock.delete = mock.Mock() + self.ng_mock.delete.return_value = None + + # Get the command object to test + self.cmd = osc_nodegroups.DeleteNodeGroup(self.app, None) + + def test_nodegroup_delete_one(self): + arglist = ['foo', 'fake-nodegroup'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', ['fake-nodegroup']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.delete.assert_called_with('foo', 'fake-nodegroup') + + def test_nodegroup_delete_multiple(self): + arglist = ['foo', 'fake-nodegroup1', 'fake-nodegroup2'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', ['fake-nodegroup1', 'fake-nodegroup2']) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.delete.assert_has_calls( + [call('foo', 'fake-nodegroup1'), call('foo', 'fake-nodegroup2')] + ) + + def test_nodegroup_delete_no_args(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('nodegroup', []) + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + +class TestNodeGroupUpdate(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupUpdate, self).setUp() + + self.ng_mock.update = mock.Mock() + self.ng_mock.update.return_value = None + + # Get the command object to test + self.cmd = osc_nodegroups.UpdateNodeGroup(self.app, None) + + def test_nodegroup_update_pass(self): + arglist = ['foo', 'ng1', 'remove', 'bar'] + verifylist = [ + ('cluster', 'foo'), + ('nodegroup', 'ng1'), + ('op', 'remove'), + ('attributes', [['bar']]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.update.assert_called_with( + 'foo', 'ng1', + [{'op': 'remove', 'path': '/bar'}] + ) + + def test_nodegroup_update_bad_op(self): + arglist = ['cluster', 'ng1', 'foo', 'bar'] + verifylist = [ + ('cluster', 'cluster'), + ('nodegroup', 'ng1'), + ('op', 'foo'), + ('attributes', ['bar']) + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + +class TestNodeGroupShow(TestNodeGroup): + + def setUp(self): + super(TestNodeGroupShow, self).setUp() + + self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + self.ng_mock.get = mock.Mock() + self.ng_mock.get.return_value = self.nodegroup + + self.data = tuple(map(lambda x: getattr(self.nodegroup, x), + osc_nodegroups.NODEGROUP_ATTRIBUTES)) + + # Get the command object to test + self.cmd = osc_nodegroups.ShowNodeGroup(self.app, None) + + def test_nodegroup_show_pass(self): + arglist = ['fake-cluster', 'fake-nodegroup'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('nodegroup', 'fake-nodegroup') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.ng_mock.get.assert_called_with( + 'fake-cluster', 'fake-nodegroup') + self.assertEqual(osc_nodegroups.NODEGROUP_ATTRIBUTES, columns) + self.assertEqual(self.data, data) + + def test_nodegroup_show_no_nodegroup_fail(self): + arglist = ['fake-cluster'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('nodegroup', '') + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_show_no_args(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('nodegroup', '') + ] + + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + +class TestNodeGroupList(TestNodeGroup): + + nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() + + columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', + 'status', 'role'] + + datalist = ( + ( + nodegroup.uuid, + nodegroup.name, + nodegroup.flavor_id, + nodegroup.image_id, + nodegroup.node_count, + nodegroup.status, + nodegroup.role, + ), + ) + + def setUp(self): + super(TestNodeGroupList, self).setUp() + self.ng_mock.list = mock.Mock() + self.ng_mock.list.return_value = [self.nodegroup] + + # Get the command object to test + self.cmd = osc_nodegroups.ListNodeGroup(self.app, None) + + def test_nodegroup_list_no_options(self): + arglist = [] + verifylist = [ + ('cluster', ''), + ('limit', None), + ('sort_key', None), + ('sort_dir', None), + ] + self.assertRaises(magnum_fakes.MagnumParseException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_nodegroup_list_ok(self): + arglist = ['fake-cluster'] + verifylist = [ + ('cluster', 'fake-cluster'), + ('limit', None), + ('sort_key', None), + ('sort_dir', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.ng_mock.list.assert_called_with( + 'fake-cluster', + limit=None, + sort_dir=None, + sort_key=None, + role=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_nodegroup_list_options(self): + arglist = [ + 'fake-cluster', + '--limit', '1', + '--sort-key', 'key', + '--sort-dir', 'asc' + ] + verifylist = [ + ('cluster', 'fake-cluster'), + ('limit', 1), + ('sort_key', 'key'), + ('sort_dir', 'asc') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.ng_mock.list.assert_called_with( + 'fake-cluster', + limit=1, + sort_dir='asc', + sort_key='key', + role=None + ) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/test_httpclient.py python-magnumclient-3.0.1/magnumclient/tests/test_httpclient.py --- python-magnumclient-2.11.0/magnumclient/tests/test_httpclient.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/test_httpclient.py 2020-05-14 11:51:49.000000000 +0000 @@ -13,9 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import json - import mock +from oslo_serialization import jsonutils import six import socket @@ -37,7 +36,7 @@ 'faultstring': faultstring, 'debuginfo': debuginfo } - raw_error_body = json.dumps(error_body) + raw_error_body = jsonutils.dumps(error_body) body = {'error_message': raw_error_body} elif err_type == ERROR_DICT: body = {'error': {'title': faultstring, 'message': debuginfo}} @@ -47,7 +46,7 @@ elif err_type == ERROR_LIST_WITH_DESC: main_body = {'title': faultstring, 'description': debuginfo} body = {'errors': [main_body]} - raw_body = json.dumps(body) + raw_body = jsonutils.dumps(body) return raw_body @@ -347,7 +346,7 @@ resp, body = client.json_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) - self.assertEqual(json.dumps(body), err) + self.assertEqual(jsonutils.dumps(body), err) def test_raw_request(self): fake_resp = utils.FakeResponse( diff -Nru python-magnumclient-2.11.0/magnumclient/tests/test_utils.py python-magnumclient-3.0.1/magnumclient/tests/test_utils.py --- python-magnumclient-2.11.0/magnumclient/tests/test_utils.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/test_utils.py 2020-05-14 11:51:49.000000000 +0000 @@ -17,7 +17,7 @@ import collections import mock -from oslo_serialization import jsonutils as json +from oslo_serialization import jsonutils import six import six.moves.builtins as __builtin__ import tempfile @@ -123,7 +123,7 @@ self.assertEqual({}, utils.format_labels(None)) def test_format_labels(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1,K2=V2,' 'K3=V3,K4=V4,' 'K5=V5']) @@ -132,10 +132,10 @@ 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' - }, l) + }, la) def test_format_labels_semicolon(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1;K2=V2;' 'K3=V3;K4=V4;' 'K5=V5']) @@ -144,10 +144,10 @@ 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' - }, l) + }, la) def test_format_labels_mix_commas_semicolon(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1,K2=V2,' 'K3=V3;K4=V4,' 'K5=V5']) @@ -156,10 +156,10 @@ 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' - }, l) + }, la) def test_format_labels_split(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1,' 'K2=V22222222222222222222222222222' '222222222222222222222222222,' @@ -167,10 +167,10 @@ self.assertEqual({'K1': 'V1', 'K2': 'V22222222222222222222222222222' '222222222222222222222222222', - 'K3': '3.3.3.3'}, l) + 'K3': '3.3.3.3'}, la) def test_format_labels_multiple(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1', 'K2=V22222222222222222222222222222' '222222222222222222222222222', @@ -178,35 +178,35 @@ self.assertEqual({'K1': 'V1', 'K2': 'V22222222222222222222222222222' '222222222222222222222222222', - 'K3': '3.3.3.3'}, l) + 'K3': '3.3.3.3'}, la) def test_format_labels_multiple_colon_values(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1', 'K2=V2,V22,V222,V2222', 'K3=3.3.3.3']) self.assertEqual({'K1': 'V1', 'K2': 'V2,V22,V222,V2222', - 'K3': '3.3.3.3'}, l) + 'K3': '3.3.3.3'}, la) def test_format_labels_parse_comma_false(self): - l = utils.format_labels( + la = utils.format_labels( ['K1=V1,K2=2.2.2.2,K=V'], parse_comma=False) - self.assertEqual({'K1': 'V1,K2=2.2.2.2,K=V'}, l) + self.assertEqual({'K1': 'V1,K2=2.2.2.2,K=V'}, la) def test_format_labels_multiple_values_per_labels(self): - l = utils.format_labels([ + la = utils.format_labels([ 'K1=V1', 'K1=V2']) - self.assertEqual({'K1': 'V1,V2'}, l) + self.assertEqual({'K1': 'V1,V2'}, la) def test_format_label_special_label(self): labels = ['K1=V1,K22.2.2.2'] - l = utils.format_labels( + la = utils.format_labels( labels, parse_comma=True) - self.assertEqual({'K1': 'V1,K22.2.2.2'}, l) + self.assertEqual({'K1': 'V1,K22.2.2.2'}, la) def test_format_multiple_bad_label(self): labels = ['K1=V1', 'K22.2.2.2'] @@ -261,7 +261,7 @@ f.flush() steps = utils.handle_json_from_file(f.name) - self.assertEqual(json.loads(contents), steps) + self.assertEqual(jsonutils.loads(contents), steps) @mock.patch.object(__builtin__, 'open', autospec=True) def test_handle_json_from_file_open_fail(self, mock_open): diff -Nru python-magnumclient-2.11.0/magnumclient/tests/utils.py python-magnumclient-3.0.1/magnumclient/tests/utils.py --- python-magnumclient-2.11.0/magnumclient/tests/utils.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/utils.py 2020-05-14 11:51:49.000000000 +0000 @@ -15,8 +15,8 @@ import copy import datetime -import json as jsonlib import os +from oslo_serialization import jsonutils import sys import fixtures @@ -95,7 +95,7 @@ self.reason = reason def __getitem__(self, key): - if key is 'location': + if key == 'location': return 'fake_url' else: return None @@ -182,7 +182,7 @@ def json(self): if self.content is not None: - return jsonlib.loads(self.content) + return jsonutils.loads(self.content) else: return {} diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/shell_test_base.py python-magnumclient-3.0.1/magnumclient/tests/v1/shell_test_base.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/shell_test_base.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/shell_test_base.py 2020-05-14 11:51:49.000000000 +0000 @@ -28,56 +28,56 @@ class TestCommandLineArgument(utils.TestCase): _unrecognized_arg_error = [ - '.*?^usage: ', - '.*?^error: unrecognized arguments:', - ".*?^Try 'magnum help ' for more information.", + r'.*?^usage: ', + r'.*?^error: unrecognized arguments:', + r".*?^Try 'magnum help ' for more information.", ] _mandatory_group_arg_error = [ - '.*?^usage: ', - '.*?^error: one of the arguments', - ".*?^Try 'magnum help ", + r'.*?^usage: ', + r'.*?^error: one of the arguments', + r".*?^Try 'magnum help ", ] _too_many_group_arg_error = [ - '.*?^usage: ', - '.*?^error: (argument \-\-[a-z\-]*: not allowed with argument )', - ".*?^Try 'magnum help ", + r'.*?^usage: ', + r'.*?^error: (argument \-\-[a-z\-]*: not allowed with argument )', + r".*?^Try 'magnum help ", ] _mandatory_arg_error = [ - '.*?^usage: ', - '.*?^error: (the following arguments|argument)', - ".*?^Try 'magnum help ", - ] + r'.*?^usage: ', + r'.*?^error: (the following arguments|argument)', + r".*?^Try 'magnum help ", + ] _duplicate_arg_error = [ - '.*?^usage: ', - '.*?^error: (Duplicate "<.*>" arguments:)', - ".*?^Try 'magnum help ", - ] + r'.*?^usage: ', + r'.*?^error: (Duplicate "<.*>" arguments:)', + r".*?^Try 'magnum help ", + ] _deprecated_warning = [ - '.*(WARNING: The \-\-[a-z\-]* parameter is deprecated)+', - ('.*(Use the [\<\-a-z\-\>]* (positional )*parameter to avoid seeing ' + r'.*(WARNING: The \-\-[a-z\-]* parameter is deprecated)+', + (r'.*(Use the [\<\-a-z\-\>]* (positional )*parameter to avoid seeing ' 'this message)+') ] _few_argument_error = [ - '.*?^usage: magnum ', - '.*?^error: (the following arguments|too few arguments)', - ".*?^Try 'magnum help ", - ] + r'.*?^usage: magnum ', + r'.*?^error: (the following arguments|too few arguments)', + r".*?^Try 'magnum help ", + ] _invalid_value_error = [ - '.*?^usage: ', - '.*?^error: argument .*: invalid .* value:', - ".*?^Try 'magnum help ", - ] + r'.*?^usage: ', + r'.*?^error: argument .*: invalid .* value:', + r".*?^Try 'magnum help ", + ] _bay_status_error = [ - '.*?^Bay status for', - ] + r'.*?^Bay status for', + ] def setUp(self): super(TestCommandLineArgument, self).setUp() diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_bays_shell.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_bays_shell.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_bays_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_bays_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -329,15 +329,6 @@ self._test_arg_success('bay-config --dir /tmp --force xxx') mock_bay.assert_called_with('xxx') - @mock.patch('magnumclient.v1.baymodels.BayModelManager.get') - @mock.patch('magnumclient.v1.bays.BayManager.get') - def test_bay_config_failure_wrong_status(self, mock_bay, mock_baymodel): - mock_bay.return_value = FakeBay(status='CREATE_IN_PROGRESS') - self.assertRaises(exceptions.CommandError, - self._test_arg_failure, - 'bay-config xxx', - ['.*?^Bay in status: ']) - @mock.patch('magnumclient.v1.bays.BayManager.get') def test_bay_config_failure_no_arg(self, mock_bay): self._test_arg_failure('bay-config', self._few_argument_error) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_clusters.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_clusters.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_clusters.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_clusters.py 2020-05-14 11:51:49.000000000 +0000 @@ -54,6 +54,14 @@ NEW_NAME = 'newcluster' UPDATED_CLUSTER['name'] = NEW_NAME +RESIZED_CLUSTER = copy.deepcopy(CLUSTER1) +RESIZED_NODE_COUNT = 5 +UPDATED_CLUSTER['node_count'] = RESIZED_NODE_COUNT + +UPGRADED_CLUSTER = copy.deepcopy(CLUSTER1) +UPGRADED_TO_TEMPLATE = "eabbc463-0d3f-49dc-8519-cb6b59507bd6" +UPGRADED_CLUSTER['cluster_template_id'] = UPGRADED_TO_TEMPLATE + fake_responses = { '/v1/clusters': { @@ -145,6 +153,20 @@ {'clusters': [CLUSTER2, CLUSTER1]}, ), }, + '/v1/clusters/%s/actions/resize' % CLUSTER1['uuid']: + { + 'POST': ( + {}, + UPDATED_CLUSTER + ), + }, + '/v1/clusters/%s/actions/upgrade' % CLUSTER1['uuid']: + { + 'POST': ( + {}, + UPGRADED_CLUSTER + ), + } } @@ -355,3 +377,24 @@ ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_NAME, cluster.name) + + def test_cluster_resize(self): + body = {'node_count': RESIZED_NODE_COUNT} + cluster = self.mgr.resize(CLUSTER1["uuid"], **body) + expect = [ + ('POST', '/v1/clusters/%s/actions/resize' % CLUSTER1['uuid'], + {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(RESIZED_NODE_COUNT, cluster.node_count) + + def test_cluster_upgrade(self): + body = {'cluster_template': UPGRADED_TO_TEMPLATE, + 'max_batch_size': 1} + cluster = self.mgr.upgrade(CLUSTER1["uuid"], **body) + expect = [ + ('POST', '/v1/clusters/%s/actions/upgrade' % CLUSTER1['uuid'], + {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(UPGRADED_TO_TEMPLATE, cluster.cluster_template_id) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_clusters_shell.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_clusters_shell.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_clusters_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_clusters_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -447,17 +447,6 @@ self._test_arg_success('cluster-config --dir /tmp --force xxx') mock_cluster.assert_called_with('xxx') - @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') - @mock.patch('magnumclient.v1.clusters.ClusterManager.get') - def test_cluster_config_failure_wrong_status(self, - mock_cluster, - mock_clustertemplate): - mock_cluster.return_value = FakeCluster(status='CREATE_IN_PROGRESS') - self.assertRaises(exceptions.CommandError, - self._test_arg_failure, - 'cluster-config xxx', - ['.*?^Cluster in status: ']) - @mock.patch('magnumclient.v1.clusters.ClusterManager.get') def test_cluster_config_failure_no_arg(self, mock_cluster): self._test_arg_failure('cluster-config', self._few_argument_error) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_clustertemplates.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_clustertemplates.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_clustertemplates.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_clustertemplates.py 2020-05-14 11:51:49.000000000 +0000 @@ -46,7 +46,8 @@ 'public': False, 'registry_enabled': False, 'master_lb_enabled': True, - 'floating_ip_enabled': True + 'floating_ip_enabled': True, + 'hidden': False } CLUSTERTEMPLATE2 = { @@ -309,6 +310,8 @@ cluster_template.master_lb_enabled) self.assertEqual(CLUSTERTEMPLATE1['floating_ip_enabled'], cluster_template.floating_ip_enabled) + self.assertEqual(CLUSTERTEMPLATE1['hidden'], + cluster_template.hidden) def test_clustertemplate_show_by_name(self): cluster_template = self.mgr.get(CLUSTERTEMPLATE1['name']) @@ -355,6 +358,8 @@ cluster_template.master_lb_enabled) self.assertEqual(CLUSTERTEMPLATE1['floating_ip_enabled'], cluster_template.floating_ip_enabled) + self.assertEqual(CLUSTERTEMPLATE1['hidden'], + cluster_template.hidden) def test_clustertemplate_create(self): cluster_template = self.mgr.create(**CREATE_CLUSTERTEMPLATE) diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_clustertemplates_shell.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_clustertemplates_shell.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_clustertemplates_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_clustertemplates_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -32,6 +32,7 @@ self.coe = kwargs.get('coe', 'x') self.public = kwargs.get('public', False) self.name = kwargs.get('name', 'x') + self.hidden = kwargs.get('hidden', False) class ShellTest(shell_test_base.TestCommandLineArgument): @@ -58,7 +59,7 @@ tls_disabled=False, public=False, master_lb_enabled=False, server_type='vm', registry_enabled=False, - insecure_registry=None): + insecure_registry=None, hidden=False): expected_args = {} expected_args['image_id'] = image_id @@ -85,6 +86,7 @@ expected_args['server_type'] = server_type expected_args['registry_enabled'] = registry_enabled expected_args['insecure_registry'] = insecure_registry + expected_args['hidden'] = hidden return expected_args diff -Nru python-magnumclient-2.11.0/magnumclient/tests/v1/test_nodegroups.py python-magnumclient-3.0.1/magnumclient/tests/v1/test_nodegroups.py --- python-magnumclient-2.11.0/magnumclient/tests/v1/test_nodegroups.py 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/tests/v1/test_nodegroups.py 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,333 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 copy + +import testtools +from testtools import matchers + +from magnumclient import exceptions +from magnumclient.tests import utils +from magnumclient.v1 import nodegroups + + +NODEGROUP1 = { + 'id': 123, + 'uuid': '66666666-7777-8888-9999-000000000001', + 'cluster_id': '66666666-7777-8888-9999-000000000000', + 'name': 'test-worker', + 'node_addresses': ['172.17.2.3'], + 'node_count': 2, + 'project_id': 'fake_project', + 'labels': {}, + 'flavor_id': 'fake_flavor_1', + 'image_id': 'fake_image', + 'is_default': True, + 'role': 'worker', + 'max_node_count': 10, + 'min_node_count': 1 +} +NODEGROUP2 = { + 'id': 124, + 'uuid': '66666666-7777-8888-9999-000000000002', + 'cluster_id': '66666666-7777-8888-9999-000000000000', + 'name': 'test-master', + 'node_addresses': ['172.17.2.4'], + 'node_count': 2, + 'project_id': 'fake_project', + 'labels': {}, + 'flavor_id': 'fake_flavor_1', + 'image_id': 'fake_image', + 'is_default': True, + 'role': 'master', + 'max_node_count': 10, + 'min_node_count': 1 +} + +CREATE_NODEGROUP = copy.deepcopy(NODEGROUP1) +del CREATE_NODEGROUP['id'] +del CREATE_NODEGROUP['uuid'] +del CREATE_NODEGROUP['node_addresses'] +del CREATE_NODEGROUP['is_default'] +del CREATE_NODEGROUP['cluster_id'] + +UPDATED_NODEGROUP = copy.deepcopy(NODEGROUP1) +NEW_NODE_COUNT = 9 +UPDATED_NODEGROUP['node_count'] = NEW_NODE_COUNT + + +fake_responses = { + '/v1/clusters/test/nodegroups/': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + 'POST': ( + {}, + CREATE_NODEGROUP, + ), + }, + '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']: + { + 'GET': ( + {}, + NODEGROUP1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), + }, + '/v1/clusters/test/nodegroups/%s/?rollback=True' % NODEGROUP1['id']: + { + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), + }, + '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['name']: + { + 'GET': ( + {}, + NODEGROUP1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_NODEGROUP, + ), + }, + '/v1/clusters/test/nodegroups/?limit=2': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?marker=%s' % NODEGROUP2['uuid']: + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?limit=2&marker=%s' % NODEGROUP2['uuid']: + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_dir=asc': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_key=uuid': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP1, NODEGROUP2]}, + ), + }, + '/v1/clusters/test/nodegroups/?sort_key=uuid&sort_dir=desc': + { + 'GET': ( + {}, + {'nodegroups': [NODEGROUP2, NODEGROUP1]}, + ), + }, +} + + +class NodeGroupManagerTest(testtools.TestCase): + + def setUp(self): + super(NodeGroupManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = nodegroups.NodeGroupManager(self.api) + self.cluster_id = 'test' + self.base_path = '/v1/clusters/test/nodegroups/' + + def test_nodegroup_list(self): + clusters = self.mgr.list(self.cluster_id) + expect = [ + ('GET', self.base_path, {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(clusters, matchers.HasLength(2)) + + def _test_nodegroup_list_with_filters(self, cluster_id, limit=None, + marker=None, sort_key=None, + sort_dir=None, detail=False, + expect=[]): + nodegroup_filter = self.mgr.list(cluster_id, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + detail=detail) + self.assertEqual(expect, self.api.calls) + self.assertThat(nodegroup_filter, matchers.HasLength(2)) + + def test_nodegroup_list_with_limit(self): + expect = [ + ('GET', self.base_path + '?limit=2', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + limit=2, + expect=expect) + + def test_nodegroup_list_with_marker(self): + filter_ = '?marker=%s' % NODEGROUP2['uuid'] + expect = [ + ('GET', self.base_path + filter_, {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + marker=NODEGROUP2['uuid'], + expect=expect) + + def test_nodegroup_list_with_marker_limit(self): + filter_ = '?limit=2&marker=%s' % NODEGROUP2['uuid'] + expect = [ + ('GET', self.base_path + filter_, {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + limit=2, marker=NODEGROUP2['uuid'], + expect=expect) + + def test_nodegroup_list_with_sort_dir(self): + expect = [ + ('GET', '/v1/clusters/test/nodegroups/?sort_dir=asc', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_dir='asc', + expect=expect) + + def test_nodegroup_list_with_sort_key(self): + expect = [ + ('GET', '/v1/clusters/test/nodegroups/?sort_key=uuid', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_key='uuid', + expect=expect) + + def test_nodegroup_list_with_sort_key_dir(self): + expect = [ + ('GET', self.base_path + '?sort_key=uuid&sort_dir=desc', {}, None), + ] + self._test_nodegroup_list_with_filters( + self.cluster_id, + sort_key='uuid', sort_dir='desc', + expect=expect) + + def test_nodegroup_show_by_name(self): + nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['name']) + expect = [ + ('GET', self.base_path + '%s' % NODEGROUP1['name'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NODEGROUP1['name'], nodegroup.name) + + def test_nodegroup_show_by_id(self): + nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['id']) + expect = [ + ('GET', self.base_path + '%s' % NODEGROUP1['id'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NODEGROUP1['name'], nodegroup.name) + + def test_nodegroup_delete_by_id(self): + nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['id']) + expect = [ + ('DELETE', self.base_path + '%s' % NODEGROUP1['id'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(nodegroup) + + def test_nodegroup_delete_by_name(self): + nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['name']) + expect = [ + ('DELETE', self.base_path + '%s' % NODEGROUP1['name'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(nodegroup) + + def test_nodegroup_update(self): + patch = {'op': 'replace', + 'value': NEW_NODE_COUNT, + 'path': '/node_count'} + nodegroup = self.mgr.update(self.cluster_id, id=NODEGROUP1['id'], + patch=patch) + expect = [ + ('PATCH', self.base_path + '%s' % NODEGROUP1['id'], {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_NODE_COUNT, nodegroup.node_count) + + def test_nodegroup_create(self): + nodegroup = self.mgr.create(self.cluster_id, **CREATE_NODEGROUP) + expect = [ + ('POST', self.base_path, {}, CREATE_NODEGROUP), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_with_docker_volume_size(self): + ng_with_volume_size = dict() + ng_with_volume_size.update(CREATE_NODEGROUP) + ng_with_volume_size['docker_volume_size'] = 20 + nodegroup = self.mgr.create(self.cluster_id, **ng_with_volume_size) + expect = [ + ('POST', self.base_path, {}, ng_with_volume_size), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_with_labels(self): + ng_with_labels = dict() + ng_with_labels.update(CREATE_NODEGROUP) + ng_with_labels['labels'] = "key=val" + nodegroup = self.mgr.create(self.cluster_id, **ng_with_labels) + expect = [ + ('POST', self.base_path, {}, ng_with_labels), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(nodegroup) + + def test_nodegroup_create_fail(self): + CREATE_NODEGROUP_FAIL = copy.deepcopy(CREATE_NODEGROUP) + CREATE_NODEGROUP_FAIL["wrong_key"] = "wrong" + self.assertRaisesRegex(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(nodegroups.CREATION_ATTRIBUTES)), + self.mgr.create, self.cluster_id, + **CREATE_NODEGROUP_FAIL) + self.assertEqual([], self.api.calls) diff -Nru python-magnumclient-2.11.0/magnumclient/v1/basemodels.py python-magnumclient-3.0.1/magnumclient/v1/basemodels.py --- python-magnumclient-2.11.0/magnumclient/v1/basemodels.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/basemodels.py 2020-05-14 11:51:49.000000000 +0000 @@ -22,7 +22,7 @@ 'no_proxy', 'network_driver', 'tls_disabled', 'public', 'registry_enabled', 'volume_driver', 'server_type', 'docker_storage_driver', 'master_lb_enabled', - 'floating_ip_enabled'] + 'floating_ip_enabled', 'hidden'] OUTPUT_ATTRIBUTES = CREATION_ATTRIBUTES + ['apiserver_port', 'created_at', 'insecure_registry', 'links', diff -Nru python-magnumclient-2.11.0/magnumclient/v1/bays_shell.py python-magnumclient-3.0.1/magnumclient/v1/bays_shell.py --- python-magnumclient-2.11.0/magnumclient/v1/bays_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/bays_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -228,8 +228,8 @@ """ args.dir = os.path.abspath(args.dir) bay = cs.bays.get(args.bay) - if bay.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE'): - raise exceptions.CommandError("Bay in status %s" % bay.status) + if (hasattr(bay, 'api_address') and bay.api_address is None): + print("WARNING: The bay's api_address is not known yet.") baymodel = cs.baymodels.get(bay.baymodel_id) opts = { 'cluster_uuid': bay.uuid, diff -Nru python-magnumclient-2.11.0/magnumclient/v1/client.py python-magnumclient-3.0.1/magnumclient/v1/client.py --- python-magnumclient-2.11.0/magnumclient/v1/client.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/client.py 2020-05-14 11:51:49.000000000 +0000 @@ -25,6 +25,7 @@ from magnumclient.v1 import cluster_templates from magnumclient.v1 import clusters from magnumclient.v1 import mservices +from magnumclient.v1 import nodegroups from magnumclient.v1 import quotas from magnumclient.v1 import stats @@ -215,3 +216,4 @@ profiler.init(profile) self.stats = stats.StatsManager(self.http_client) self.quotas = quotas.QuotasManager(self.http_client) + self.nodegroups = nodegroups.NodeGroupManager(self.http_client) diff -Nru python-magnumclient-2.11.0/magnumclient/v1/clusters.py python-magnumclient-3.0.1/magnumclient/v1/clusters.py --- python-magnumclient-2.11.0/magnumclient/v1/clusters.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/clusters.py 2020-05-14 11:51:49.000000000 +0000 @@ -23,6 +23,10 @@ CREATION_ATTRIBUTES.append('labels') CREATION_ATTRIBUTES.append('master_flavor_id') CREATION_ATTRIBUTES.append('flavor_id') +CREATION_ATTRIBUTES.append('fixed_network') +CREATION_ATTRIBUTES.append('fixed_subnet') +CREATION_ATTRIBUTES.append('floating_ip_enabled') +CREATION_ATTRIBUTES.append('merge_labels') class Cluster(baseunit.BaseTemplate): @@ -32,3 +36,33 @@ class ClusterManager(baseunit.BaseTemplateManager): resource_class = Cluster template_name = 'clusters' + + def resize(self, cluster_uuid, node_count, + nodes_to_remove=[], nodegroup=None): + url = self._path(cluster_uuid) + "/actions/resize" + + post_body = {"node_count": node_count} + if nodes_to_remove: + post_body.update({"nodes_to_remove": nodes_to_remove}) + if nodegroup: + post_body.update({"nodegroup": nodegroup}) + + resp, resp_body = self.api.json_request("POST", url, body=post_body) + + if resp_body: + return self.resource_class(self, resp_body) + + def upgrade(self, cluster_uuid, cluster_template, + max_batch_size=1, nodegroup=None): + url = self._path(cluster_uuid) + "/actions/upgrade" + + post_body = {"cluster_template": cluster_template} + if max_batch_size: + post_body.update({"max_batch_size": max_batch_size}) + if nodegroup: + post_body.update({"nodegroup": nodegroup}) + + resp, resp_body = self.api.json_request("POST", url, body=post_body) + + if resp_body: + return self.resource_class(self, resp_body) diff -Nru python-magnumclient-2.11.0/magnumclient/v1/clusters_shell.py python-magnumclient-3.0.1/magnumclient/v1/clusters_shell.py --- python-magnumclient-2.11.0/magnumclient/v1/clusters_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/clusters_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -262,9 +262,8 @@ """ args.dir = os.path.abspath(args.dir) cluster = cs.clusters.get(args.cluster) - if cluster.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE', - 'ROLLBACK_COMPLETE'): - raise exceptions.CommandError("cluster in status %s" % cluster.status) + if (hasattr(cluster, 'api_address') and cluster.api_address is None): + print("WARNING: The cluster's api_address is not known yet.") cluster_template = cs.cluster_templates.get(cluster.cluster_template_id) opts = { 'cluster_uuid': cluster.uuid, diff -Nru python-magnumclient-2.11.0/magnumclient/v1/cluster_templates_shell.py python-magnumclient-3.0.1/magnumclient/v1/cluster_templates_shell.py --- python-magnumclient-2.11.0/magnumclient/v1/cluster_templates_shell.py 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/cluster_templates_shell.py 2020-05-14 11:51:49.000000000 +0000 @@ -187,6 +187,13 @@ @utils.arg('--insecure-registry', metavar='', help='url of docker registry') +@utils.arg('--hidden', + action='store_true', default=False, + help=_('Make cluster template hidden.')) +@utils.arg('--visible', + dest='hidden', + action='store_false', + help=_('Make cluster template visible.')) @utils.deprecated(utils.MAGNUM_CLIENT_DEPRECATION_WARNING) def do_cluster_template_create(cs, args): """Create a cluster template.""" @@ -219,6 +226,7 @@ opts['server_type'] = args.server_type opts['master_lb_enabled'] = args.master_lb_enabled opts['insecure_registry'] = args.insecure_registry + opts['hidden'] = args.hidden if len(args.floating_ip_enabled) > 1: raise InvalidAttribute('--floating-ip-enabled and ' diff -Nru python-magnumclient-2.11.0/magnumclient/v1/nodegroups.py python-magnumclient-3.0.1/magnumclient/v1/nodegroups.py --- python-magnumclient-2.11.0/magnumclient/v1/nodegroups.py 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/magnumclient/v1/nodegroups.py 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,84 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 magnumclient.common import utils +from magnumclient import exceptions +from magnumclient.v1 import baseunit + + +CREATION_ATTRIBUTES = ['docker_volume_size', 'labels', 'flavor_id', 'image_id', + 'project_id', 'node_count', 'name', 'role', + 'min_node_count', 'max_node_count', 'merge_labels'] + + +class NodeGroup(baseunit.BaseTemplate): + template_name = "NodeGroups" + + +class NodeGroupManager(baseunit.BaseTemplateManager): + resource_class = NodeGroup + template_name = 'nodegroups' + api_name = 'nodegroups' + + @classmethod + def _path(cls, cluster_id, id=None): + path = '/v1/clusters/%s/%s/' % (cluster_id, cls.template_name) + if id: + path += str(id) + return path + + def list(self, cluster_id, limit=None, marker=None, sort_key=None, + sort_dir=None, role=None, detail=False): + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + path = '' + if role: + filters.append('role=%s' % role) + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(cluster_id, id=path), + self.__class__.api_name) + else: + return self._list_pagination(self._path(cluster_id, id=path), + self.__class__.api_name, + limit=limit) + + def get(self, cluster_id, id): + try: + return self._list(self._path(cluster_id, id=id))[0] + except IndexError: + return None + + def create(self, cluster_id, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute( + "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) + return self._create(self._path(cluster_id), new) + + def delete(self, cluster_id, id): + return self._delete(self._path(cluster_id, id=id)) + + def update(self, cluster_id, id, patch): + return self._update(self._path(cluster_id, id=id), patch) diff -Nru python-magnumclient-2.11.0/README.rst python-magnumclient-3.0.1/README.rst --- python-magnumclient-2.11.0/README.rst 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/README.rst 2020-05-14 11:51:49.000000000 +0000 @@ -2,8 +2,8 @@ Team and repository tags ======================== -.. image:: http://governance.openstack.org/badges/python-magnumclient.svg - :target: http://governance.openstack.org/reference/tags/index.html +.. image:: https://governance.openstack.org/tc/badges/python-magnumclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on @@ -11,11 +11,11 @@ ================================= .. image:: https://img.shields.io/pypi/v/python-magnumclient.svg - :target: https://pypi.python.org/pypi/python-magnumclient/ + :target: https://pypi.org/project/python-magnumclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-magnumclient.svg - :target: https://pypi.python.org/pypi/python-magnumclient/ + :target: https://pypi.org/project/python-magnumclient/ :alt: Downloads This is a client library for Magnum built on the Magnum API. It @@ -33,8 +33,8 @@ * `Bugs`_ - issue tracking * `Source`_ -.. _PyPi: https://pypi.python.org/pypi/python-magnumclient +.. _PyPi: https://pypi.org/project/python-magnumclient .. _Online Documentation: https://docs.openstack.org/python-magnumclient/latest/ .. _Launchpad project: https://launchpad.net/python-magnumclient .. _Bugs: https://bugs.launchpad.net/python-magnumclient -.. _Source: https://git.openstack.org/cgit/openstack/python-magnumclient +.. _Source: https://opendev.org/openstack/python-magnumclient diff -Nru python-magnumclient-2.11.0/releasenotes/source/index.rst python-magnumclient-3.0.1/releasenotes/source/index.rst --- python-magnumclient-2.11.0/releasenotes/source/index.rst 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/releasenotes/source/index.rst 2020-05-14 11:51:49.000000000 +0000 @@ -6,6 +6,8 @@ :maxdepth: 1 unreleased + train + stein rocky OpenStack Releases diff -Nru python-magnumclient-2.11.0/releasenotes/source/stein.rst python-magnumclient-3.0.1/releasenotes/source/stein.rst --- python-magnumclient-2.11.0/releasenotes/source/stein.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/releasenotes/source/stein.rst 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein diff -Nru python-magnumclient-2.11.0/releasenotes/source/train.rst python-magnumclient-3.0.1/releasenotes/source/train.rst --- python-magnumclient-2.11.0/releasenotes/source/train.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-magnumclient-3.0.1/releasenotes/source/train.rst 2020-05-14 11:51:49.000000000 +0000 @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train diff -Nru python-magnumclient-2.11.0/setup.cfg python-magnumclient-3.0.1/setup.cfg --- python-magnumclient-2.11.0/setup.cfg 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/setup.cfg 2020-05-14 11:51:49.000000000 +0000 @@ -4,7 +4,7 @@ description-file = README.rst author = OpenStack -author-email = openstack-dev@lists.openstack.org +author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-magnumclient/latest/ classifier = Environment :: OpenStack @@ -14,9 +14,9 @@ Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 [files] packages = @@ -42,6 +42,8 @@ coe_cluster_show = magnumclient.osc.v1.clusters:ShowCluster coe_cluster_update = magnumclient.osc.v1.clusters:UpdateCluster coe_cluster_config = magnumclient.osc.v1.clusters:ConfigCluster + coe_cluster_resize = magnumclient.osc.v1.clusters:ResizeCluster + coe_cluster_upgrade = magnumclient.osc.v1.clusters:UpgradeCluster coe_ca_rotate = magnumclient.osc.v1.certificates:RotateCa coe_ca_show = magnumclient.osc.v1.certificates:ShowCa coe_ca_sign = magnumclient.osc.v1.certificates:SignCa @@ -54,6 +56,11 @@ coe_service_list = magnumclient.osc.v1.mservices:ListService + coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup + coe_nodegroup_show = magnumclient.osc.v1.nodegroups:ShowNodeGroup + coe_nodegroup_create = magnumclient.osc.v1.nodegroups:CreateNodeGroup + coe_nodegroup_delete = magnumclient.osc.v1.nodegroups:DeleteNodeGroup + coe_nodegroup_update = magnumclient.osc.v1.nodegroups:UpdateNodeGroup [compile_catalog] directory = magnumclient/locale diff -Nru python-magnumclient-2.11.0/test-requirements.txt python-magnumclient-3.0.1/test-requirements.txt --- python-magnumclient-2.11.0/test-requirements.txt 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/test-requirements.txt 2020-05-14 11:51:49.000000000 +0000 @@ -1,8 +1,8 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -bandit>=1.1.0 # Apache-2.0 +hacking>=2.0,<2.1 # Apache-2.0 +bandit!=1.6.0,>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD python-openstackclient>=3.12.0 # Apache-2.0 diff -Nru python-magnumclient-2.11.0/tools/cover.sh python-magnumclient-3.0.1/tools/cover.sh --- python-magnumclient-2.11.0/tools/cover.sh 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/tools/cover.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -#!/bin/bash -# -# 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. - -ALLOWED_EXTRA_MISSING=0 - -show_diff () { - head -1 $1 - diff -U 0 $1 $2 | sed 1,2d -} - -if ! git diff --exit-code || ! git diff --cached --exit-code -then - echo "There are uncommitted changes!" - echo "Please clean git working directory and try again" - exit 1 -fi - -# Checkout master and save coverage report -git checkout HEAD^ - -baseline_report=$(mktemp -t magnumclient_coverageXXXXXXX) -find . -type f -name "*.pyc" -delete -stestr run "$*" -coverage combine -coverage report --fail-under=80 --skip-covered -coverage html -d cover -coverage xml -o cover/coverage.xml -coverage report > $baseline_report -mv cover cover-master -cat $baseline_report -baseline_missing=$(awk 'END { print $3 }' $baseline_report) - -# Checkout back and save coverage report -git checkout - - -current_report=$(mktemp -t magnumclient_coverageXXXXXXX) -find . -type f -name "*.pyc" -delete -stestr run "$*" -coverage combine -coverage report --fail-under=80 --skip-covered -coverage html -d cover -coverage xml -o cover/coverage.xml -coverage report > $current_report -current_missing=$(awk 'END { print $3 }' $current_report) - -# Show coverage details -allowed_missing=$((baseline_missing+ALLOWED_EXTRA_MISSING)) - -echo "Allowed to introduce missing lines : ${ALLOWED_EXTRA_MISSING}" -echo "Missing lines in master : ${baseline_missing}" -echo "Missing lines in proposed change : ${current_missing}" - -if [ $allowed_missing -ge $current_missing ]; then - if [ $baseline_missing -lt $current_missing ]; then - show_diff $baseline_report $current_report - echo "We believe you can test your code with 100% coverage!" - else - echo "Thank you! You are awesome! Keep writing unit tests! :)" - fi - exit_code=0 -else - show_diff $baseline_report $current_report - echo "Please write more unit tests, we must maintain our test coverage :( " - exit_code=1 -fi - -rm $baseline_report $current_report -exit $exit_code diff -Nru python-magnumclient-2.11.0/tox.ini python-magnumclient-3.0.1/tox.ini --- python-magnumclient-2.11.0/tox.ini 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/tox.ini 2020-05-14 11:51:49.000000000 +0000 @@ -1,6 +1,6 @@ [tox] -minversion = 1.6 -envlist = py35,py27,pypy,pep8 +minversion = 2.0 +envlist = py37,pypy,pep8 skipsdist = True [testenv] @@ -11,7 +11,7 @@ VIRTUAL_ENV={envdir} PYTHONWARNINGS=default::DeprecationWarning deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = @@ -21,14 +21,14 @@ [testenv:bandit] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} -r{toxinidir}/test-requirements.txt commands = bandit -r magnumclient -x tests -n5 -ll [testenv:pypy] basepython = python3 deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} + -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} setuptools<3.2 -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt @@ -37,14 +37,6 @@ basepython = python3 commands = oslo_debug_helper -t magnumclient/tests {posargs} -[testenv:debug-py27] -basepython = python2.7 -commands = oslo_debug_helper -t magnumclient/tests {posargs} - -[testenv:debug-py35] -basepython = python3.5 -commands = oslo_debug_helper -t magnumclient/tests {posargs} - [testenv:pep8] basepython = python3 commands = @@ -64,13 +56,20 @@ [testenv:cover] basepython = python3 -commands = {toxinidir}/tools/cover.sh {posargs} +setenv = + PYTHON=coverage run --source magnumclient --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report [flake8] # E123, E125 skipped as they are invalid PEP-8. show-source = True -ignore = E123,E125 +ignore = E123,E125,W503,W504 builtins = _ exclude=.venv,.git,.tox,dist,doc,,*lib/python*,*egg,build @@ -89,4 +88,4 @@ deps = -r{toxinidir}/doc/requirements.txt commands = rm -rf releasenotes/build - sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html \ No newline at end of file + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html diff -Nru python-magnumclient-2.11.0/.zuul.yaml python-magnumclient-3.0.1/.zuul.yaml --- python-magnumclient-2.11.0/.zuul.yaml 2018-09-14 10:13:57.000000000 +0000 +++ python-magnumclient-3.0.1/.zuul.yaml 2020-05-14 11:51:49.000000000 +0000 @@ -1,18 +1,15 @@ - project: templates: - - openstack-python-jobs - - openstack-python35-jobs - - openstack-python36-jobs - check-requirements + - openstack-cover-jobs + - openstack-lower-constraints-jobs + - openstack-python3-ussuri-jobs - publish-openstack-docs-pti check: jobs: - - openstack-tox-lower-constraints - build-openstack-releasenotes - openstack-tox-cover: voting: false - gate: jobs: - - openstack-tox-lower-constraints - build-openstack-releasenotes