From ce8924bb435a14057ca29f3337358161d4425789 Mon Sep 17 00:00:00 2001 From: Daneyon Hansen Date: Thu, 20 Aug 2015 17:22:49 +0000 Subject: [PATCH] Adds Labels Support Adds labels attribute and associated tools/tests to the magnum client. Implements: blueprint extend-client-network-attributes Change-Id: Ie8465275646b82aa9bd7a9de756c519bdd6ec0e2 --- magnumclient/common/utils.py | 29 ++++++++++ magnumclient/tests/test_shell_args.py | 34 +++++++++++ magnumclient/tests/test_utils.py | 77 +++++++++++++++++++++++++ magnumclient/tests/v1/test_baymodels.py | 4 ++ magnumclient/tests/v1/test_shell.py | 10 +++- magnumclient/v1/baymodels.py | 2 +- magnumclient/v1/shell.py | 6 ++ 7 files changed, 158 insertions(+), 4 deletions(-) diff --git a/magnumclient/common/utils.py b/magnumclient/common/utils.py index df20ee69..34171315 100644 --- a/magnumclient/common/utils.py +++ b/magnumclient/common/utils.py @@ -78,3 +78,32 @@ def args_array_to_patch(op, attributes): else: raise exc.CommandError(_('Unknown PATCH operation: %s') % op) return patch + + +def format_labels(lbls, parse_comma=True): + '''Reformat labels into dict of format expected by the API.''' + + if not lbls: + return {} + + if parse_comma: + # expect multiple invocations of --labels but fall back + # to , delimited if only one --labels is specified + if len(lbls) == 1: + lbls = lbls[0].split(',') + + labels = {} + for l in lbls: + try: + (k, v) = l.split(('='), 1) + except ValueError: + raise exc.CommandError(_('labels must be a list of KEY=VALUE ' + 'not %s') % l) + if k not in labels: + labels[k] = v + else: + if not isinstance(labels[k], list): + labels[k] = [labels[k]] + labels[k].append(v) + + return labels diff --git a/magnumclient/tests/test_shell_args.py b/magnumclient/tests/test_shell_args.py index 242f7d36..d642373b 100644 --- a/magnumclient/tests/test_shell_args.py +++ b/magnumclient/tests/test_shell_args.py @@ -215,6 +215,7 @@ class TestCommandLineArgument(utils.TestCase): '--flavor-id test_flavor ' '--fixed-network public ' '--network-driver test_driver ' + '--labels key=val ' '--master-flavor-id test_flavor ' '--docker-volume-size 10') self.assertTrue(mock_create.called) @@ -314,6 +315,39 @@ class TestCommandLineArgument(utils.TestCase): '--image-id test_image ' '--coe swarm ' '--no-proxy no_proxy ') + + @mock.patch('magnumclient.v1.baymodels.BayModelManager.create') + def test_baymodel_create_labels_success(self, mock_create): + self._test_arg_success('baymodel-create ' + '--name test ' + '--labels key=val ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm') + self.assertTrue(mock_create.called) + + @mock.patch('magnumclient.v1.baymodels.BayModelManager.create') + def test_baymodel_create_separate_labels_success(self, mock_create): + self._test_arg_success('baymodel-create ' + '--name test ' + '--labels key1=val1 ' + '--labels key2=val2 ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm') + self.assertTrue(mock_create.called) + + @mock.patch('magnumclient.v1.baymodels.BayModelManager.create') + def test_baymodel_create_combined_labels_success(self, mock_create): + self._test_arg_success('baymodel-create ' + '--name test ' + '--labels key1=val1,key2=val2 ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm') self.assertTrue(mock_create.called) @mock.patch('magnumclient.v1.baymodels.BayModelManager.create') diff --git a/magnumclient/tests/test_utils.py b/magnumclient/tests/test_utils.py index e67cf38a..dc2feb89 100644 --- a/magnumclient/tests/test_utils.py +++ b/magnumclient/tests/test_utils.py @@ -95,3 +95,80 @@ class ArgsArrayToPatchTest(test_utils.BaseTestCase): my_args['attributes']) self.assertEqual([{'op': 'remove', 'path': '/foo'}, {'op': 'remove', 'path': '/extra/bar'}], patch) + + +class FormatLabelsTest(test_utils.BaseTestCase): + + def test_format_label_none(self): + self.assertEqual({}, utils.format_labels(None)) + + def test_format_labels(self): + l = utils.format_labels([ + 'K1=V1,K2=V2,' + 'K3=V3,K4=V4,' + 'K5=V5']) + self.assertEqual({'K1': 'V1', + 'K2': 'V2', + 'K3': 'V3', + 'K4': 'V4', + 'K5': 'V5' + }, l) + + def test_format_labels_split(self): + l = utils.format_labels([ + 'K1=V1,' + 'K2=V22222222222222222222222222222' + '222222222222222222222222222,' + 'K3=3.3.3.3']) + self.assertEqual({'K1': 'V1', + 'K2': 'V22222222222222222222222222222' + '222222222222222222222222222', + 'K3': '3.3.3.3'}, l) + + def test_format_labels_multiple(self): + l = utils.format_labels([ + 'K1=V1', + 'K2=V22222222222222222222222222222' + '222222222222222222222222222', + 'K3=3.3.3.3']) + self.assertEqual({'K1': 'V1', + 'K2': 'V22222222222222222222222222222' + '222222222222222222222222222', + 'K3': '3.3.3.3'}, l) + + def test_format_labels_multiple_colon_values(self): + l = 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) + + def test_format_labels_parse_comma_false(self): + l = 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) + + def test_format_labels_multiple_values_per_labels(self): + l = utils.format_labels([ + 'K1=V1', + 'K1=V2']) + self.assertIn('K1', l) + self.assertIn('V1', l['K1']) + self.assertIn('V2', l['K1']) + + def test_format_label_bad_label(self): + labels = ['K1=V1,K22.2.2.2'] + ex = self.assertRaises(exc.CommandError, + utils.format_labels, labels) + self.assertEqual('labels must be a list of KEY=VALUE ' + 'not K22.2.2.2', str(ex)) + + def test_format_multiple_bad_label(self): + labels = ['K1=V1', 'K22.2.2.2'] + ex = self.assertRaises(exc.CommandError, + utils.format_labels, labels) + self.assertEqual('labels must be a list of KEY=VALUE ' + 'not K22.2.2.2', str(ex)) diff --git a/magnumclient/tests/v1/test_baymodels.py b/magnumclient/tests/v1/test_baymodels.py index 55767eb4..a3de3386 100644 --- a/magnumclient/tests/v1/test_baymodels.py +++ b/magnumclient/tests/v1/test_baymodels.py @@ -44,6 +44,7 @@ BAYMODEL1 = {'id': 123, 'http_proxy': 'http_proxy', 'https_proxy': 'https_proxy', 'no_proxy': 'no_proxy', + 'labels': 'key1=val1,key11=val11', } BAYMODEL2 = {'id': 124, 'uuid': '66666666-7777-8888-9999-000000000002', @@ -65,6 +66,7 @@ BAYMODEL2 = {'id': 124, 'X8vjlQUnTK0HijrbSTLxp/9kazWWraBS0AyXe6' 'Jv0Zio4VeFrfpytB8RtAAA test1234@magnum', 'coe': 'kubernetes', + 'labels': 'key2=val2,key22=val22', } CREATE_BAYMODEL = copy.deepcopy(BAYMODEL1) @@ -153,6 +155,7 @@ class BayModelManagerTest(testtools.TestCase): self.assertEqual(BAYMODEL1['https_proxy'], baymodel.https_proxy) self.assertEqual(BAYMODEL1['no_proxy'], baymodel.no_proxy) self.assertEqual(BAYMODEL1['network_driver'], baymodel.network_driver) + self.assertEqual(BAYMODEL1['labels'], baymodel.labels) def test_baymodel_show_by_name(self): baymodel = self.mgr.get(BAYMODEL1['name']) @@ -172,6 +175,7 @@ class BayModelManagerTest(testtools.TestCase): self.assertEqual(BAYMODEL1['https_proxy'], baymodel.https_proxy) self.assertEqual(BAYMODEL1['no_proxy'], baymodel.no_proxy) self.assertEqual(BAYMODEL1['network_driver'], baymodel.network_driver) + self.assertEqual(BAYMODEL1['labels'], baymodel.labels) def test_baymodel_create(self): baymodel = self.mgr.create(**CREATE_BAYMODEL) diff --git a/magnumclient/tests/v1/test_shell.py b/magnumclient/tests/v1/test_shell.py index 26b92374..18a922ab 100644 --- a/magnumclient/tests/v1/test_shell.py +++ b/magnumclient/tests/v1/test_shell.py @@ -14,6 +14,7 @@ import mock +from magnumclient.common import utils as magnum_utils from magnumclient.tests import base from magnumclient.v1 import shell @@ -190,6 +191,8 @@ class ShellTest(base.TestCase): args.https_proxy = 'https_proxy' no_proxy = 'no_proxy' args.no_proxy = no_proxy + labels = ['key1=val1'] + args.labels = labels shell.do_baymodel_create(client_mock, args) client_mock.baymodels.create.assert_called_once_with( @@ -200,7 +203,8 @@ class ShellTest(base.TestCase): fixed_network=fixed_network, dns_nameserver=dns_nameserver, ssh_authorized_key=ssh_authorized_key, coe=coe, http_proxy=http_proxy, https_proxy=https_proxy, - no_proxy=no_proxy, network_driver=network_driver) + no_proxy=no_proxy, network_driver=network_driver, + labels=magnum_utils.format_labels(labels)) def test_do_baymodel_delete(self): client_mock = mock.MagicMock() @@ -297,10 +301,10 @@ class ShellTest(base.TestCase): args.pod = pod_id op = 'add' args.op = op - attributes = "label={'name': 'value'}" + attributes = "labels={'name': 'value'}" args.attributes = attributes shell.magnum_utils.args_array_to_patch = mock.MagicMock() - patch = [{'path': '/label', 'value': {'name': 'value'}, 'op': 'add'}] + patch = [{'path': '/labels', 'value': {'name': 'value'}, 'op': 'add'}] shell.magnum_utils.args_array_to_patch.return_value = patch shell.do_pod_update(client_mock, args) diff --git a/magnumclient/v1/baymodels.py b/magnumclient/v1/baymodels.py index bdbed2bd..64165deb 100644 --- a/magnumclient/v1/baymodels.py +++ b/magnumclient/v1/baymodels.py @@ -17,7 +17,7 @@ from magnumclient import exceptions CREATION_ATTRIBUTES = ['name', 'image_id', 'flavor_id', 'master_flavor_id', 'keypair_id', 'external_network_id', 'fixed_network', - 'dns_nameserver', 'docker_volume_size', + 'dns_nameserver', 'docker_volume_size', 'labels', 'ssh_authorized_key', 'coe', 'http_proxy', 'https_proxy', 'no_proxy', 'network_driver'] diff --git a/magnumclient/v1/shell.py b/magnumclient/v1/shell.py index 6dfe7679..5a4fb3f5 100644 --- a/magnumclient/v1/shell.py +++ b/magnumclient/v1/shell.py @@ -200,6 +200,11 @@ def do_bay_update(cs, args): @utils.arg('--no-proxy', metavar='', help='The no_proxy address to use for nodes in bay.') +@utils.arg('--labels', metavar='', + action='append', default=[], + help='Arbitrary labels in the form of key=value pairs ' + 'to associate with a baymodel. ' + 'May be used multiple times.') def do_baymodel_create(cs, args): """Create a baymodel.""" opts = {} @@ -218,6 +223,7 @@ def do_baymodel_create(cs, args): opts['http_proxy'] = args.http_proxy opts['https_proxy'] = args.https_proxy opts['no_proxy'] = args.no_proxy + opts['labels'] = magnum_utils.format_labels(args.labels) baymodel = cs.baymodels.create(**opts) _show_baymodel(baymodel)