diff --git a/magnumclient/common/cliutils.py b/magnumclient/common/cliutils.py index 2065b3b9..bde8bffc 100644 --- a/magnumclient/common/cliutils.py +++ b/magnumclient/common/cliutils.py @@ -24,6 +24,7 @@ import os import sys import textwrap +import decorator from magnumclient.common.apiclient import exceptions from oslo_utils import encodeutils from oslo_utils import strutils @@ -75,6 +76,22 @@ def validate_args(fn, *args, **kwargs): raise MissingArgs(missing) +def deprecated(message): + '''Decorator for marking a call as deprecated by printing a given message. + + Example: + >>> @deprecated("Bay functions are deprecated and should be replaced by " + ... "calls to cluster") + ... def bay_create(args): + ... pass + ''' + @decorator.decorator + def wrapper(func, *args, **kwargs): + print(message) + return func(*args, **kwargs) + return wrapper + + def arg(*args, **kwargs): """Decorator for CLI args. diff --git a/magnumclient/tests/v1/test_clusters.py b/magnumclient/tests/v1/test_clusters.py new file mode 100644 index 00000000..2286e062 --- /dev/null +++ b/magnumclient/tests/v1/test_clusters.py @@ -0,0 +1,304 @@ +# Copyright 2015 IBM Corp. +# +# 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 clusters + + +CLUSTER1 = {'id': 123, + 'uuid': '66666666-7777-8888-9999-000000000001', + 'name': 'cluster1', + 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a61', + 'stack_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a51', + 'api_address': '172.17.2.1', + 'node_addresses': ['172.17.2.3'], + 'node_count': 2, + 'master_count': 1, + } +CLUSTER2 = {'id': 124, + 'uuid': '66666666-7777-8888-9999-000000000002', + 'name': 'cluster2', + 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a62', + 'stack_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52', + 'api_address': '172.17.2.2', + 'node_addresses': ['172.17.2.4'], + 'node_count': 2, + 'master_count': 1, + } + +CREATE_CLUSTER = copy.deepcopy(CLUSTER1) +del CREATE_CLUSTER['id'] +del CREATE_CLUSTER['uuid'] +del CREATE_CLUSTER['stack_id'] +del CREATE_CLUSTER['api_address'] +del CREATE_CLUSTER['node_addresses'] + +UPDATED_CLUSTER = copy.deepcopy(CLUSTER1) +NEW_NAME = 'newcluster' +UPDATED_CLUSTER['name'] = NEW_NAME + +fake_responses = { + '/v1/clusters': + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + 'POST': ( + {}, + CREATE_CLUSTER, + ), + }, + '/v1/clusters/%s' % CLUSTER1['id']: + { + 'GET': ( + {}, + CLUSTER1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_CLUSTER, + ), + }, + '/v1/clusters/%s' % CLUSTER1['name']: + { + 'GET': ( + {}, + CLUSTER1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_CLUSTER, + ), + }, + '/v1/clusters/?limit=2': + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + }, + '/v1/clusters/?marker=%s' % CLUSTER2['uuid']: + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + }, + '/v1/clusters/?limit=2&marker=%s' % CLUSTER2['uuid']: + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + }, + '/v1/clusters/?sort_dir=asc': + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + }, + '/v1/clusters/?sort_key=uuid': + { + 'GET': ( + {}, + {'clusters': [CLUSTER1, CLUSTER2]}, + ), + }, + '/v1/clusters/?sort_key=uuid&sort_dir=desc': + { + 'GET': ( + {}, + {'clusters': [CLUSTER2, CLUSTER1]}, + ), + }, +} + + +class ClusterManagerTest(testtools.TestCase): + + def setUp(self): + super(ClusterManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = clusters.ClusterManager(self.api) + + def test_cluster_list(self): + clusters = self.mgr.list() + expect = [ + ('GET', '/v1/clusters', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(clusters, matchers.HasLength(2)) + + def _test_cluster_list_with_filters(self, limit=None, marker=None, + sort_key=None, sort_dir=None, + detail=False, expect=[]): + clusters_filter = self.mgr.list(limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + detail=detail) + self.assertEqual(expect, self.api.calls) + self.assertThat(clusters_filter, matchers.HasLength(2)) + + def test_cluster_list_with_limit(self): + expect = [ + ('GET', '/v1/clusters/?limit=2', {}, None), + ] + self._test_cluster_list_with_filters( + limit=2, + expect=expect) + + def test_cluster_list_with_marker(self): + expect = [ + ('GET', '/v1/clusters/?marker=%s' % CLUSTER2['uuid'], {}, None), + ] + self._test_cluster_list_with_filters( + marker=CLUSTER2['uuid'], + expect=expect) + + def test_cluster_list_with_marker_limit(self): + expect = [ + ('GET', '/v1/clusters/?limit=2&marker=%s' % CLUSTER2['uuid'], + {}, + None), + ] + self._test_cluster_list_with_filters( + limit=2, marker=CLUSTER2['uuid'], + expect=expect) + + def test_cluster_list_with_sort_dir(self): + expect = [ + ('GET', '/v1/clusters/?sort_dir=asc', {}, None), + ] + self._test_cluster_list_with_filters( + sort_dir='asc', + expect=expect) + + def test_cluster_list_with_sort_key(self): + expect = [ + ('GET', '/v1/clusters/?sort_key=uuid', {}, None), + ] + self._test_cluster_list_with_filters( + sort_key='uuid', + expect=expect) + + def test_cluster_list_with_sort_key_dir(self): + expect = [ + ('GET', '/v1/clusters/?sort_key=uuid&sort_dir=desc', {}, None), + ] + self._test_cluster_list_with_filters( + sort_key='uuid', sort_dir='desc', + expect=expect) + + def test_cluster_show_by_id(self): + cluster = self.mgr.get(CLUSTER1['id']) + expect = [ + ('GET', '/v1/clusters/%s' % CLUSTER1['id'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CLUSTER1['name'], cluster.name) + self.assertEqual(CLUSTER1['cluster_template_id'], + cluster.cluster_template_id) + + def test_cluster_show_by_name(self): + cluster = self.mgr.get(CLUSTER1['name']) + expect = [ + ('GET', '/v1/clusters/%s' % CLUSTER1['name'], {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CLUSTER1['name'], cluster.name) + self.assertEqual(CLUSTER1['cluster_template_id'], + cluster.cluster_template_id) + + def test_cluster_create(self): + cluster = self.mgr.create(**CREATE_CLUSTER) + expect = [ + ('POST', '/v1/clusters', {}, CREATE_CLUSTER), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(cluster) + + def test_cluster_create_with_discovery_url(self): + cluster_with_discovery = dict() + cluster_with_discovery.update(CREATE_CLUSTER) + cluster_with_discovery['discovery_url'] = 'discovery_url' + cluster = self.mgr.create(**cluster_with_discovery) + expect = [ + ('POST', '/v1/clusters', {}, cluster_with_discovery), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(cluster) + + def test_cluster_create_with_cluster_create_timeout(self): + cluster_with_timeout = dict() + cluster_with_timeout.update(CREATE_CLUSTER) + cluster_with_timeout['create_timeout'] = '15' + cluster = self.mgr.create(**cluster_with_timeout) + expect = [ + ('POST', '/v1/clusters', {}, cluster_with_timeout), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(cluster) + + def test_cluster_create_fail(self): + CREATE_CLUSTER_FAIL = copy.deepcopy(CREATE_CLUSTER) + CREATE_CLUSTER_FAIL["wrong_key"] = "wrong" + self.assertRaisesRegexp(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(clusters.CREATION_ATTRIBUTES)), + self.mgr.create, **CREATE_CLUSTER_FAIL) + self.assertEqual([], self.api.calls) + + def test_cluster_delete_by_id(self): + cluster = self.mgr.delete(CLUSTER1['id']) + expect = [ + ('DELETE', '/v1/clusters/%s' % CLUSTER1['id'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(cluster) + + def test_cluster_delete_by_name(self): + cluster = self.mgr.delete(CLUSTER1['name']) + expect = [ + ('DELETE', '/v1/clusters/%s' % CLUSTER1['name'], {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(cluster) + + def test_cluster_update(self): + patch = {'op': 'replace', + 'value': NEW_NAME, + 'path': '/name'} + cluster = self.mgr.update(id=CLUSTER1['id'], patch=patch) + expect = [ + ('PATCH', '/v1/clusters/%s' % CLUSTER1['id'], {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_NAME, cluster.name) diff --git a/magnumclient/tests/v1/test_clusters_shell.py b/magnumclient/tests/v1/test_clusters_shell.py new file mode 100644 index 00000000..72b4df92 --- /dev/null +++ b/magnumclient/tests/v1/test_clusters_shell.py @@ -0,0 +1,298 @@ +# Copyright 2015 NEC Corporation. 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 mock + +from magnumclient import exceptions +from magnumclient.tests.v1 import shell_test_base +from magnumclient.tests.v1 import test_clustertemplates_shell +from magnumclient.v1.clusters import Cluster + + +class FakeCluster(Cluster): + def __init__(self, manager=None, info={}, **kwargs): + Cluster.__init__(self, manager=manager, info=info) + self.uuid = kwargs.get('uuid', 'x') + self.name = kwargs.get('name', 'x') + self.cluster_template_id = kwargs.get('cluster_template_id', 'x') + self.stack_id = kwargs.get('stack_id', 'x') + self.status = kwargs.get('status', 'x') + self.master_count = kwargs.get('master_count', 1) + self.node_count = kwargs.get('node_count', 1) + self.links = kwargs.get('links', []) + self.create_timeout = kwargs.get('create_timeout', 60) + + +class ShellTest(shell_test_base.TestCommandLineArgument): + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_success(self, mock_list): + self._test_arg_success('cluster-list') + self.assertTrue(mock_list.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_success_with_arg(self, mock_list): + self._test_arg_success('cluster-list ' + '--marker some_uuid ' + '--limit 1 ' + '--sort-dir asc ' + '--sort-key uuid') + self.assertTrue(mock_list.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_ignored_duplicated_field(self, mock_list): + mock_list.return_value = [FakeCluster()] + self._test_arg_success( + 'cluster-list --fields status,status,status,name', + keyword=('\n| uuid | name | node_count | ' + 'master_count | status |\n')) + # Output should be + # +------+------+------------+--------------+--------+ + # | uuid | name | node_count | master_count | status | + # +------+------+------------+--------------+--------+ + # | x | x | x | x | x | + # +------+------+------------+--------------+--------+ + self.assertTrue(mock_list.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_failure_with_invalid_field(self, mock_list): + mock_list.return_value = [FakeCluster()] + _error_msg = [".*?^Non-existent fields are specified: ['xxx','zzz']"] + self.assertRaises(exceptions.CommandError, + self._test_arg_failure, + 'cluster-list --fields xxx,stack_id,zzz,status', + _error_msg) + self.assertTrue(mock_list.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_failure_invalid_arg(self, mock_list): + _error_msg = [ + '.*?^usage: magnum cluster-list ', + '.*?^error: argument --sort-dir: invalid choice: ', + ".*?^Try 'magnum help cluster-list' for more information." + ] + self._test_arg_failure('cluster-list --sort-dir aaa', _error_msg) + self.assertFalse(mock_list.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.list') + def test_cluster_list_failure(self, mock_list): + self._test_arg_failure('cluster-list --wrong', + self._unrecognized_arg_error) + self.assertFalse(mock_list.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_success(self, mock_create, mock_get): + self._test_arg_success('cluster-create --name test ' + '--cluster-template xxx ' + '--node-count 123 --timeout 15') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-create --cluster-template xxx') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-create --name test ' + '--cluster-template xxx') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-create --cluster-template xxx ' + '--node-count 123') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-create --cluster-template xxx ' + '--node-count 123 --master-count 123') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-create --cluster-template xxx ' + '--timeout 15') + self.assertTrue(mock_create.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + @mock.patch('magnumclient.v1.clusters.ClusterManager.get') + def test_cluster_show_clustertemplate_metadata(self, + mock_cluster, + mock_clustertemplate): + mock_cluster.return_value = FakeCluster(info={'links': 0, + 'baymodel_id': 0}) + mock_clustertemplate.return_value = \ + test_clustertemplates_shell.FakeClusterTemplate(info={'links': 0, + 'uuid': 0, + 'id': 0, + 'name': ''}) + + self._test_arg_success('cluster-show --long x', 'clustertemplate_name') + self.assertTrue(mock_cluster.called) + self.assertTrue(mock_clustertemplate.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_success_only_clustertemplate_arg(self, + mock_create, + mock_get): + self._test_arg_success('cluster-create --cluster-template xxx') + self.assertTrue(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_only_name(self, mock_create): + self._test_arg_failure('cluster-create --name test', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_only_node_count(self, mock_create): + self._test_arg_failure('cluster-create --node-count 1', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_invalid_node_count(self, mock_create): + self._test_arg_failure('cluster-create --cluster-template xxx ' + '--node-count test', + self._invalid_value_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_only_cluster_create_timeout(self, + mock_create): + self._test_arg_failure('cluster-create --timeout 15', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_no_arg(self, mock_create): + self._test_arg_failure('cluster-create', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.create') + def test_cluster_create_failure_invalid_master_count(self, mock_create): + self._test_arg_failure('cluster-create --cluster-template xxx ' + '--master-count test', + self._invalid_value_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.delete') + def test_cluster_delete_success(self, mock_delete): + self._test_arg_success('cluster-delete xxx') + self.assertTrue(mock_delete.called) + self.assertEqual(1, mock_delete.call_count) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.delete') + def test_cluster_delete_multiple_id_success(self, mock_delete): + self._test_arg_success('cluster-delete xxx xyz') + self.assertTrue(mock_delete.called) + self.assertEqual(2, mock_delete.call_count) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.delete') + def test_cluster_delete_failure_no_arg(self, mock_delete): + self._test_arg_failure('cluster-delete', self._few_argument_error) + self.assertFalse(mock_delete.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.get') + def test_cluster_show_success(self, mock_show): + self._test_arg_success('cluster-show xxx') + self.assertTrue(mock_show.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.get') + def test_cluster_show_failure_no_arg(self, mock_show): + self._test_arg_failure('cluster-show', self._few_argument_error) + self.assertFalse(mock_show.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.update') + def test_cluster_update_success(self, mock_update): + self._test_arg_success('cluster-update test add test=test') + self.assertTrue(mock_update.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.update') + def test_cluster_update_success_many_attribute(self, mock_update): + self._test_arg_success('cluster-update test add test=test test1=test1') + self.assertTrue(mock_update.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.update') + def test_cluster_update_failure_wrong_op(self, mock_update): + _error_msg = [ + '.*?^usage: magnum cluster-update ', + '.*?^error: argument : invalid choice: ', + ".*?^Try 'magnum help cluster-update' for more information." + ] + self._test_arg_failure('cluster-update test wrong test=test', + _error_msg) + self.assertFalse(mock_update.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.update') + def test_cluster_update_failure_wrong_attribute(self, mock_update): + _error_msg = [ + '.*?^ERROR: Attributes must be a list of PATH=VALUE' + ] + self.assertRaises(exceptions.CommandError, self._test_arg_failure, + 'cluster-update test add test', _error_msg) + self.assertFalse(mock_update.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.update') + def test_cluster_update_failure_few_args(self, mock_update): + _error_msg = [ + '.*?^usage: magnum cluster-update ', + '.*?^error: (the following arguments|too few arguments)', + ".*?^Try 'magnum help cluster-update' for more information." + ] + self._test_arg_failure('cluster-update', _error_msg) + self.assertFalse(mock_update.called) + + self._test_arg_failure('cluster-update test', _error_msg) + self.assertFalse(mock_update.called) + + self._test_arg_failure('cluster-update test add', _error_msg) + self.assertFalse(mock_update.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + @mock.patch('magnumclient.v1.clusters.ClusterManager.get') + def test_cluster_config_success(self, mock_cluster, mock_clustertemplate): + mock_cluster.return_value = FakeCluster(status='UPDATE_COMPLETE') + self._test_arg_success('cluster-config xxx') + self.assertTrue(mock_cluster.called) + + mock_cluster.return_value = FakeCluster(status='CREATE_COMPLETE') + self._test_arg_success('cluster-config xxx') + self.assertTrue(mock_cluster.called) + + self._test_arg_success('cluster-config --dir /tmp xxx') + self.assertTrue(mock_cluster.called) + + self._test_arg_success('cluster-config --force xxx') + self.assertTrue(mock_cluster.called) + + self._test_arg_success('cluster-config --dir /tmp --force xxx') + self.assertTrue(mock_cluster.called) + + @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) + self.assertFalse(mock_cluster.called) + + @mock.patch('magnumclient.v1.clusters.ClusterManager.get') + def test_cluster_config_failure_wrong_arg(self, mock_cluster): + self._test_arg_failure('cluster-config xxx yyy', + self._unrecognized_arg_error) + self.assertFalse(mock_cluster.called) diff --git a/magnumclient/tests/v1/test_clustertemplates.py b/magnumclient/tests/v1/test_clustertemplates.py new file mode 100644 index 00000000..99980e08 --- /dev/null +++ b/magnumclient/tests/v1/test_clustertemplates.py @@ -0,0 +1,412 @@ +# Copyright 2015 IBM Corp. +# +# 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 cluster_templates + + +CLUSTERTEMPLATE1 = { + 'id': 123, + 'uuid': '66666666-7777-8888-9999-000000000001', + 'name': 'clustertemplate1', + 'image_id': 'clustertemplate1-image', + 'master_flavor_id': 'm1.tiny', + 'flavor_id': 'm1.small', + 'keypair_id': 'keypair1', + 'external_network_id': 'd1f02cfb-d27f-4068-9332-84d907cb0e21', + 'fixed_network': 'private', + 'fixed_subnet': 'private-subnet', + 'network_driver': 'libnetwork', + 'volume_driver': 'rexray', + 'dns_nameserver': '8.8.1.1', + 'docker_volume_size': '71', + 'docker_storage_driver': 'devicemapper', + 'coe': 'swarm', + 'http_proxy': 'http_proxy', + 'https_proxy': 'https_proxy', + 'no_proxy': 'no_proxy', + 'labels': 'key1=val1,key11=val11', + 'tls_disabled': False, + 'public': False, + 'registry_enabled': False, + 'master_lb_enabled': True} + +CLUSTERTEMPLATE2 = { + 'id': 124, + 'uuid': '66666666-7777-8888-9999-000000000002', + 'name': 'clustertemplate2', + 'image_id': 'clustertemplate2-image', + 'flavor_id': 'm2.small', + 'master_flavor_id': 'm2.tiny', + 'keypair_id': 'keypair2', + 'external_network_id': 'd1f02cfb-d27f-4068-9332-84d907cb0e22', + 'fixed_network': 'private2', + 'network_driver': 'flannel', + 'volume_driver': 'cinder', + 'dns_nameserver': '8.8.1.2', + 'docker_volume_size': '50', + 'docker_storage_driver': 'overlay', + 'coe': 'kubernetes', + 'labels': 'key2=val2,key22=val22', + 'tls_disabled': True, + 'public': True, + 'registry_enabled': True} + +CREATE_CLUSTERTEMPLATE = copy.deepcopy(CLUSTERTEMPLATE1) +del CREATE_CLUSTERTEMPLATE['id'] +del CREATE_CLUSTERTEMPLATE['uuid'] + +UPDATED_CLUSTERTEMPLATE = copy.deepcopy(CLUSTERTEMPLATE1) +NEW_NAME = 'newcluster' +UPDATED_CLUSTERTEMPLATE['name'] = NEW_NAME + +fake_responses = { + '/v1/clustertemplates': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + 'POST': ( + {}, + CREATE_CLUSTERTEMPLATE, + ), + }, + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id']: + { + 'GET': ( + {}, + CLUSTERTEMPLATE1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_CLUSTERTEMPLATE, + ), + }, + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name']: + { + 'GET': ( + {}, + CLUSTERTEMPLATE1 + ), + 'DELETE': ( + {}, + None, + ), + 'PATCH': ( + {}, + UPDATED_CLUSTERTEMPLATE, + ), + }, + '/v1/clustertemplates/detail': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?limit=2': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?marker=%s' % CLUSTERTEMPLATE2['uuid']: + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?limit=2&marker=%s' % CLUSTERTEMPLATE2['uuid']: + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?sort_dir=asc': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?sort_key=uuid': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, + ), + }, + '/v1/clustertemplates/?sort_key=uuid&sort_dir=desc': + { + 'GET': ( + {}, + {'clustertemplates': [CLUSTERTEMPLATE2, CLUSTERTEMPLATE1]}, + ), + }, +} + + +class ClusterTemplateManagerTest(testtools.TestCase): + + def setUp(self): + super(ClusterTemplateManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = cluster_templates.ClusterTemplateManager(self.api) + + def test_clustertemplate_list(self): + clustertemplates = self.mgr.list() + expect = [ + ('GET', '/v1/clustertemplates', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(clustertemplates, matchers.HasLength(2)) + + def _test_clustertemplate_list_with_filters( + self, limit=None, marker=None, + sort_key=None, sort_dir=None, + detail=False, expect=[]): + clustertemplates_filter = self.mgr.list(limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + detail=detail) + self.assertEqual(expect, self.api.calls) + self.assertThat(clustertemplates_filter, matchers.HasLength(2)) + + def test_clustertemplate_list_with_detail(self): + expect = [ + ('GET', '/v1/clustertemplates/detail', {}, None), + ] + self._test_clustertemplate_list_with_filters( + detail=True, + expect=expect) + + def test_clustertemplate_list_with_limit(self): + expect = [ + ('GET', '/v1/clustertemplates/?limit=2', {}, None), + ] + self._test_clustertemplate_list_with_filters( + limit=2, + expect=expect) + + def test_clustertemplate_list_with_marker(self): + expect = [ + ('GET', + '/v1/clustertemplates/?marker=%s' % CLUSTERTEMPLATE2['uuid'], + {}, + None), + ] + self._test_clustertemplate_list_with_filters( + marker=CLUSTERTEMPLATE2['uuid'], + expect=expect) + + def test_clustertemplate_list_with_marker_limit(self): + expect = [ + ('GET', + '/v1/clustertemplates/?limit=2&marker=%s' % + CLUSTERTEMPLATE2['uuid'], + {}, + None), + ] + self._test_clustertemplate_list_with_filters( + limit=2, marker=CLUSTERTEMPLATE2['uuid'], + expect=expect) + + def test_clustertemplate_list_with_sort_dir(self): + expect = [ + ('GET', '/v1/clustertemplates/?sort_dir=asc', {}, None), + ] + self._test_clustertemplate_list_with_filters( + sort_dir='asc', + expect=expect) + + def test_clustertemplate_list_with_sort_key(self): + expect = [ + ('GET', '/v1/clustertemplates/?sort_key=uuid', {}, None), + ] + self._test_clustertemplate_list_with_filters( + sort_key='uuid', + expect=expect) + + def test_clustertemplate_list_with_sort_key_dir(self): + expect = [ + ('GET', + '/v1/clustertemplates/?sort_key=uuid&sort_dir=desc', + {}, + None), + ] + self._test_clustertemplate_list_with_filters( + sort_key='uuid', sort_dir='desc', + expect=expect) + + def test_clustertemplate_show_by_id(self): + cluster_template = self.mgr.get(CLUSTERTEMPLATE1['id']) + expect = [ + ('GET', + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], + {}, + None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CLUSTERTEMPLATE1['name'], + cluster_template.name) + self.assertEqual(CLUSTERTEMPLATE1['image_id'], + cluster_template.image_id) + self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], + cluster_template.docker_volume_size) + self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], + cluster_template.docker_storage_driver) + self.assertEqual(CLUSTERTEMPLATE1['fixed_network'], + cluster_template.fixed_network) + self.assertEqual(CLUSTERTEMPLATE1['fixed_subnet'], + cluster_template.fixed_subnet) + self.assertEqual(CLUSTERTEMPLATE1['coe'], + cluster_template.coe) + self.assertEqual(CLUSTERTEMPLATE1['http_proxy'], + cluster_template.http_proxy) + self.assertEqual(CLUSTERTEMPLATE1['https_proxy'], + cluster_template.https_proxy) + self.assertEqual(CLUSTERTEMPLATE1['no_proxy'], + cluster_template.no_proxy) + self.assertEqual(CLUSTERTEMPLATE1['network_driver'], + cluster_template.network_driver) + self.assertEqual(CLUSTERTEMPLATE1['volume_driver'], + cluster_template.volume_driver) + self.assertEqual(CLUSTERTEMPLATE1['labels'], + cluster_template.labels) + self.assertEqual(CLUSTERTEMPLATE1['tls_disabled'], + cluster_template.tls_disabled) + self.assertEqual(CLUSTERTEMPLATE1['public'], + cluster_template.public) + self.assertEqual(CLUSTERTEMPLATE1['registry_enabled'], + cluster_template.registry_enabled) + self.assertEqual(CLUSTERTEMPLATE1['master_lb_enabled'], + cluster_template.master_lb_enabled) + + def test_clustertemplate_show_by_name(self): + cluster_template = self.mgr.get(CLUSTERTEMPLATE1['name']) + expect = [ + ('GET', + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name'], + {}, + None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(CLUSTERTEMPLATE1['name'], + cluster_template.name) + self.assertEqual(CLUSTERTEMPLATE1['image_id'], + cluster_template.image_id) + self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], + cluster_template.docker_volume_size) + self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], + cluster_template.docker_storage_driver) + self.assertEqual(CLUSTERTEMPLATE1['fixed_network'], + cluster_template.fixed_network) + self.assertEqual(CLUSTERTEMPLATE1['fixed_subnet'], + cluster_template.fixed_subnet) + self.assertEqual(CLUSTERTEMPLATE1['coe'], + cluster_template.coe) + self.assertEqual(CLUSTERTEMPLATE1['http_proxy'], + cluster_template.http_proxy) + self.assertEqual(CLUSTERTEMPLATE1['https_proxy'], + cluster_template.https_proxy) + self.assertEqual(CLUSTERTEMPLATE1['no_proxy'], + cluster_template.no_proxy) + self.assertEqual(CLUSTERTEMPLATE1['network_driver'], + cluster_template.network_driver) + self.assertEqual(CLUSTERTEMPLATE1['volume_driver'], + cluster_template.volume_driver) + self.assertEqual(CLUSTERTEMPLATE1['labels'], + cluster_template.labels) + self.assertEqual(CLUSTERTEMPLATE1['tls_disabled'], + cluster_template.tls_disabled) + self.assertEqual(CLUSTERTEMPLATE1['public'], + cluster_template.public) + self.assertEqual(CLUSTERTEMPLATE1['registry_enabled'], + cluster_template.registry_enabled) + self.assertEqual(CLUSTERTEMPLATE1['master_lb_enabled'], + cluster_template.master_lb_enabled) + + def test_clustertemplate_create(self): + cluster_template = self.mgr.create(**CREATE_CLUSTERTEMPLATE) + expect = [ + ('POST', '/v1/clustertemplates', {}, CREATE_CLUSTERTEMPLATE), + ] + self.assertEqual(expect, self.api.calls) + self.assertTrue(cluster_template) + self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], + cluster_template.docker_volume_size) + self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], + cluster_template.docker_storage_driver) + + def test_clustertemplate_create_fail(self): + CREATE_CLUSTERTEMPLATE_FAIL = copy.deepcopy(CREATE_CLUSTERTEMPLATE) + CREATE_CLUSTERTEMPLATE_FAIL["wrong_key"] = "wrong" + self.assertRaisesRegexp( + exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(cluster_templates.CREATION_ATTRIBUTES)), + self.mgr.create, **CREATE_CLUSTERTEMPLATE_FAIL) + self.assertEqual([], self.api.calls) + + def test_clustertemplate_delete_by_id(self): + cluster_template = self.mgr.delete(CLUSTERTEMPLATE1['id']) + expect = [ + ('DELETE', + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], + {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(cluster_template) + + def test_clustertemplate_delete_by_name(self): + cluster_template = self.mgr.delete(CLUSTERTEMPLATE1['name']) + expect = [ + ('DELETE', + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name'], + {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(cluster_template) + + def test_clustertemplate_update(self): + patch = {'op': 'replace', + 'value': NEW_NAME, + 'path': '/name'} + cluster_template = self.mgr.update(id=CLUSTERTEMPLATE1['id'], + patch=patch) + expect = [ + ('PATCH', + '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], + {}, + patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_NAME, cluster_template.name) diff --git a/magnumclient/tests/v1/test_clustertemplates_shell.py b/magnumclient/tests/v1/test_clustertemplates_shell.py new file mode 100644 index 00000000..c62b611d --- /dev/null +++ b/magnumclient/tests/v1/test_clustertemplates_shell.py @@ -0,0 +1,408 @@ +# Copyright 2015 NEC Corporation. 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 mock + +from magnumclient.common.apiclient import exceptions +from magnumclient.tests.v1 import shell_test_base +from magnumclient.v1.cluster_templates import ClusterTemplate + + +class FakeClusterTemplate(ClusterTemplate): + def __init__(self, manager=None, info={}, **kwargs): + ClusterTemplate.__init__(self, manager=manager, info=info) + self.apiserver_port = kwargs.get('apiserver_port', None) + self.uuid = kwargs.get('uuid', 'x') + self.links = kwargs.get('links', []) + self.server_type = kwargs.get('server_type', 'vm') + self.image_id = kwargs.get('image_id', 'x') + self.tls_disabled = kwargs.get('tls_disabled', False) + self.registry_enabled = kwargs.get('registry_enabled', False) + self.coe = kwargs.get('coe', 'x') + self.public = kwargs.get('public', False) + self.name = kwargs.get('name', 'x') + + +class ShellTest(shell_test_base.TestCommandLineArgument): + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--image-id test_image ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--coe swarm ' + '--dns-nameserver test_dns ' + '--flavor-id test_flavor ' + '--fixed-network private ' + '--fixed-network private-subnet ' + '--volume-driver test_volume ' + '--network-driver test_driver ' + '--labels key=val ' + '--master-flavor-id test_flavor ' + '--docker-volume-size 10 ' + '--docker-storage-driver devicemapper ' + '--public ' + '--server-type vm' + '--master-lb-enabled') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-template-create ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe kubernetes ' + '--name test ' + '--server-type vm') + + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_success_no_servertype(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--image-id test_image ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--coe swarm ' + '--dns-nameserver test_dns ' + '--flavor-id test_flavor ' + '--fixed-network public ' + '--network-driver test_driver ' + '--labels key=val ' + '--master-flavor-id test_flavor ' + '--docker-volume-size 10 ' + '--docker-storage-driver devicemapper ' + '--public ') + self.assertTrue(mock_create.called) + + self._test_arg_success('cluster-template-create ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe kubernetes ' + '--name test ') + + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_success_with_registry_enabled( + self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--network-driver test_driver ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--registry-enabled') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_public_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --network-driver test_driver ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm' + '--public ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_success_with_master_flavor(self, + mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--image-id test_image ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--coe swarm ' + '--dns-nameserver test_dns ' + '--master-flavor-id test_flavor') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_docker_vol_size_success(self, + mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --docker-volume-size 4514 ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_docker_storage_driver_success( + self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--docker-storage-driver devicemapper ' + '--coe swarm' + ) + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_fixed_network_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --fixed-network private ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_network_driver_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --network-driver test_driver ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_volume_driver_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --volume-driver test_volume ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_http_proxy_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --fixed-network private ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--http-proxy http_proxy ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_https_proxy_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --fixed-network private ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--https-proxy https_proxy ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_no_proxy_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test --fixed-network private ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--no-proxy no_proxy ' + '--server-type vm') + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_labels_success(self, mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--labels key=val ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_separate_labels_success(self, + mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--labels key1=val1 ' + '--labels key2=val2 ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_combined_labels_success(self, + mock_create): + self._test_arg_success('cluster-template-create ' + '--name test ' + '--labels key1=val1,key2=val2 ' + '--keypair-id test_keypair ' + '--external-network-id test_net ' + '--image-id test_image ' + '--coe swarm ' + '--server-type vm') + self.assertTrue(mock_create.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.create') + def test_cluster_template_create_failure_few_arg(self, mock_create): + self._test_arg_failure('cluster-template-create ' + '--name test', self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create ' + '--image-id test', self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create ' + '--keypair-id test', self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create ' + '--external-network-id test', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create ' + '--coe test', self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create ' + '--server-type test', self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + self._test_arg_failure('cluster-template-create', + self._mandatory_arg_error) + self.assertFalse(mock_create.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + def test_cluster_template_show_success(self, mock_show): + self._test_arg_success('cluster-template-show xxx') + self.assertTrue(mock_show.called) + + @mock.patch('magnumclient.v1.cluster_templates.ClusterTemplateManager.get') + def test_cluster_template_show_failure_no_arg(self, mock_show): + self._test_arg_failure('cluster-template-show', + self._few_argument_error) + self.assertFalse(mock_show.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.delete') + def test_cluster_template_delete_success(self, mock_delete): + self._test_arg_success('cluster-template-delete xxx') + self.assertTrue(mock_delete.called) + self.assertEqual(1, mock_delete.call_count) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.delete') + def test_cluster_template_delete_multiple_id_success(self, mock_delete): + self._test_arg_success('cluster-template-delete xxx xyz') + self.assertTrue(mock_delete.called) + self.assertEqual(2, mock_delete.call_count) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.delete') + def test_cluster_template_delete_failure_no_arg(self, mock_delete): + self._test_arg_failure('cluster-template-delete', + self._few_argument_error) + self.assertFalse(mock_delete.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_success(self, mock_list): + self._test_arg_success('cluster-template-list') + self.assertTrue(mock_list.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_success_with_arg(self, mock_list): + self._test_arg_success('cluster-template-list ' + '--limit 1 ' + '--sort-dir asc ' + '--sort-key uuid') + self.assertTrue(mock_list.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_ignored_duplicated_field(self, mock_list): + mock_list.return_value = [FakeClusterTemplate()] + self._test_arg_success( + 'cluster-template-list --fields coe,coe,coe,name,name', + keyword='\n| uuid | name | Coe |\n') + # Output should be + # +------+------+-----+ + # | uuid | name | Coe | + # +------+------+-----+ + # | x | x | x | + # +------+------+-----+ + self.assertTrue(mock_list.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_failure_with_invalid_field(self, mock_list): + mock_list.return_value = [FakeClusterTemplate()] + _error_msg = [".*?^Non-existent fields are specified: ['xxx','zzz']"] + self.assertRaises(exceptions.CommandError, + self._test_arg_failure, + 'cluster-template-list --fields xxx,coe,zzz', + _error_msg) + self.assertTrue(mock_list.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_failure_invalid_arg(self, mock_list): + _error_msg = [ + '.*?^usage: magnum cluster-template-list ', + '.*?^error: argument --sort-dir: invalid choice: ', + ".*?^Try 'magnum help cluster-template-list' for more information." + ] + self._test_arg_failure('cluster-template-list --sort-dir aaa', + _error_msg) + self.assertFalse(mock_list.called) + + @mock.patch( + 'magnumclient.v1.cluster_templates.ClusterTemplateManager.list') + def test_cluster_template_list_failure(self, mock_list): + self._test_arg_failure('cluster-template-list --wrong', + self._unrecognized_arg_error) + self.assertFalse(mock_list.called) diff --git a/magnumclient/v1/basemodels.py b/magnumclient/v1/basemodels.py new file mode 100644 index 00000000..98af1719 --- /dev/null +++ b/magnumclient/v1/basemodels.py @@ -0,0 +1,112 @@ +# 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 base +from magnumclient.common import utils +from magnumclient import exceptions + + +CREATION_ATTRIBUTES = ['name', 'image_id', 'flavor_id', 'master_flavor_id', + 'keypair_id', 'external_network_id', 'fixed_network', + 'fixed_subnet', 'dns_nameserver', 'docker_volume_size', + 'labels', 'coe', 'http_proxy', 'https_proxy', + 'no_proxy', 'network_driver', 'tls_disabled', 'public', + 'registry_enabled', 'volume_driver', 'server_type', + 'docker_storage_driver', 'master_lb_enabled'] + + +class BaseModel(base.Resource): + # model_name needs to be overridden by any derived class. + # model_name should be capitalized and singular, e.g. "Cluster" + model_name = '' + + def __repr__(self): + return "<" + self.__class__.model_name + "%s>" % self._info + + +class BaseModelManager(base.Manager): + # api_name needs to be overridden by any derived class. + # api_name should be pluralized and lowercase, e.g. "clustertemplates", as + # it shows up in the URL path: "/v1/{api_name}" + api_name = '' + + @classmethod + def _path(cls, id=None): + return '/v1/' + cls.api_name + \ + '/%s' % id if id else '/v1/' + cls.api_name + + def list(self, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of baymodels. + + :param marker: Optional, the UUID of a baymodel, eg the last + baymodel from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of baymodels to return. + 2) limit == 0, return the entire list of baymodels. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about baymodels. + + :returns: A list of baymodels. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), self.__class__.api_name) + else: + return self._list_pagination(self._path(path), + self.__class__.api_name, + limit=limit) + + def get(self, id): + try: + return self._list(self._path(id))[0] + except IndexError: + return None + + def create(self, **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(), new) + + def delete(self, id): + return self._delete(self._path(id)) + + def update(self, id, patch): + return self._update(self._path(id), patch) diff --git a/magnumclient/v1/baseunit.py b/magnumclient/v1/baseunit.py new file mode 100644 index 00000000..4017c6f0 --- /dev/null +++ b/magnumclient/v1/baseunit.py @@ -0,0 +1,108 @@ +# Copyright 2014 NEC Corporation. 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 base +from magnumclient.common import utils +from magnumclient import exceptions + + +# Derived classes may append their own custom attributes to this default list +CREATION_ATTRIBUTES = ['name', 'node_count', 'discovery_url', 'master_count'] + + +class BaseTemplate(base.Resource): + # template_name must be overridden by any derived class. + # template_name should be an uppercase plural, e.g. "Clusters" + template_name = '' + + def __repr__(self): + return "<" + self.__class__.template_name + " %s>" % self._info + + +class BaseTemplateManager(base.Manager): + # template_name must be overridden by any derived class. + # template_name should be a lowercase plural, e.g. "clusters" + template_name = '' + + @classmethod + def _path(cls, id=None): + return '/v1/' + cls.template_name + \ + '/%s' % id if id else '/v1/' + cls.template_name + + def list(self, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False): + """Retrieve a list of bays. + + :param marker: Optional, the UUID of a bay, eg the last + bay from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of bays to return. + 2) limit == 0, return the entire list of bays. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the Magnum API + (see Magnum's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about bays. + + :returns: A list of bays. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(self._path(path), self.__class__.template_name) + else: + return self._list_pagination(self._path(path), + self.__class__.template_name, + limit=limit) + + def get(self, id): + try: + return self._list(self._path(id))[0] + except IndexError: + return None + + def create(self, **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(), new) + + def delete(self, id): + return self._delete(self._path(id)) + + def update(self, id, patch): + return self._update(self._path(id), patch) diff --git a/magnumclient/v1/baymodels.py b/magnumclient/v1/baymodels.py index fd6e7b9e..175679fe 100644 --- a/magnumclient/v1/baymodels.py +++ b/magnumclient/v1/baymodels.py @@ -10,94 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. -from magnumclient.common import base -from magnumclient.common import utils -from magnumclient import exceptions +from magnumclient.v1 import basemodels -CREATION_ATTRIBUTES = ['name', 'image_id', 'flavor_id', 'master_flavor_id', - 'keypair_id', 'external_network_id', 'fixed_network', - 'fixed_subnet', 'dns_nameserver', 'docker_volume_size', - 'labels', 'coe', 'http_proxy', 'https_proxy', - 'no_proxy', 'network_driver', 'tls_disabled', 'public', - 'registry_enabled', 'volume_driver', 'server_type', - 'docker_storage_driver', 'master_lb_enabled'] +CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES -class BayModel(base.Resource): - def __repr__(self): - return "" % self._info +class BayModel(basemodels.BaseModel): + model_name = "BayModel" -class BayModelManager(base.Manager): +class BayModelManager(basemodels.BaseModelManager): + api_name = "baymodels" resource_class = BayModel - - @staticmethod - def _path(id=None): - return '/v1/baymodels/%s' % id if id else '/v1/baymodels' - - def list(self, limit=None, marker=None, sort_key=None, - sort_dir=None, detail=False): - """Retrieve a list of baymodels. - - :param marker: Optional, the UUID of a baymodel, eg the last - baymodel from a previous result set. Return - the next result set. - :param limit: The maximum number of results to return per - request, if: - - 1) limit > 0, the maximum number of baymodels to return. - 2) limit == 0, return the entire list of baymodels. - 3) limit param is NOT specified (None), the number of items - returned respect the maximum imposed by the Magnum API - (see Magnum's api.max_limit option). - - :param sort_key: Optional, field used for sorting. - - :param sort_dir: Optional, direction of sorting, either 'asc' (the - default) or 'desc'. - - :param detail: Optional, boolean whether to return detailed information - about baymodels. - - :returns: A list of baymodels. - - """ - if limit is not None: - limit = int(limit) - - filters = utils.common_filters(marker, limit, sort_key, sort_dir) - - path = '' - if detail: - path += 'detail' - if filters: - path += '?' + '&'.join(filters) - - if limit is None: - return self._list(self._path(path), "baymodels") - else: - return self._list_pagination(self._path(path), "baymodels", - limit=limit) - - def get(self, id): - try: - return self._list(self._path(id))[0] - except IndexError: - return None - - def create(self, **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(), new) - - def delete(self, id): - return self._delete(self._path(id)) - - def update(self, id, patch): - return self._update(self._path(id), patch) diff --git a/magnumclient/v1/baymodels_shell.py b/magnumclient/v1/baymodels_shell.py index 4424674f..93b85eb0 100644 --- a/magnumclient/v1/baymodels_shell.py +++ b/magnumclient/v1/baymodels_shell.py @@ -19,6 +19,11 @@ from magnumclient.common import utils as magnum_utils from magnumclient.i18n import _ +DEPRECATION_MESSAGE = ( + 'WARNING: Baymodel commands are deprecated and will be removed in a future' + ' release.\nUse cluster commands to avoid seeing this message.') + + def _show_baymodel(baymodel): del baymodel._info['links'] utils.print_dict(baymodel._info) @@ -115,6 +120,7 @@ def _show_baymodel(baymodel): action='store_true', default=False, help='Indicates whether created bays should have a load balancer ' 'for master nodes or not.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_baymodel_create(cs, args): """Create a baymodel.""" opts = {} @@ -150,6 +156,7 @@ def do_baymodel_create(cs, args): metavar='', nargs='+', help='ID or name of the (baymodel)s to delete.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_baymodel_delete(cs, args): """Delete specified baymodel.""" for baymodel in args.baymodels: @@ -165,6 +172,7 @@ def do_baymodel_delete(cs, args): @utils.arg('baymodel', metavar='', help='ID or name of the baymodel to show.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_baymodel_show(cs, args): """Show details about the given baymodel.""" baymodel = cs.baymodels.get(args.baymodel) @@ -190,6 +198,7 @@ def do_baymodel_show(cs, args): 'apiserver_port, server_type, tls_disabled, registry_enabled' ) ) +@utils.deprecated(DEPRECATION_MESSAGE) def do_baymodel_list(cs, args): """Print a list of baymodels.""" nodes = cs.baymodels.list(limit=args.limit, @@ -218,6 +227,7 @@ def do_baymodel_list(cs, args): default=[], help="Attributes to add/replace or remove " "(only PATH is necessary on remove)") +@utils.deprecated(DEPRECATION_MESSAGE) def do_baymodel_update(cs, args): """Updates one or more baymodel attributes.""" patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) diff --git a/magnumclient/v1/bays.py b/magnumclient/v1/bays.py index 6bfd877f..3d532d1d 100644 --- a/magnumclient/v1/bays.py +++ b/magnumclient/v1/bays.py @@ -12,89 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. -from magnumclient.common import base -from magnumclient.common import utils -from magnumclient import exceptions +from magnumclient.v1 import baseunit -CREATION_ATTRIBUTES = ['name', 'baymodel_id', 'node_count', 'discovery_url', - 'bay_create_timeout', 'master_count'] +CREATION_ATTRIBUTES = baseunit.CREATION_ATTRIBUTES +CREATION_ATTRIBUTES.append('baymodel_id') +CREATION_ATTRIBUTES.append('bay_create_timeout') -class Bay(base.Resource): - def __repr__(self): - return "" % self._info +class Bay(baseunit.BaseTemplate): + template_name = "Bays" -class BayManager(base.Manager): +class BayManager(baseunit.BaseTemplateManager): resource_class = Bay - - @staticmethod - def _path(id=None): - return '/v1/bays/%s' % id if id else '/v1/bays' - - def list(self, limit=None, marker=None, sort_key=None, - sort_dir=None, detail=False): - """Retrieve a list of bays. - - :param marker: Optional, the UUID of a bay, eg the last - bay from a previous result set. Return - the next result set. - :param limit: The maximum number of results to return per - request, if: - - 1) limit > 0, the maximum number of bays to return. - 2) limit == 0, return the entire list of bays. - 3) limit param is NOT specified (None), the number of items - returned respect the maximum imposed by the Magnum API - (see Magnum's api.max_limit option). - - :param sort_key: Optional, field used for sorting. - - :param sort_dir: Optional, direction of sorting, either 'asc' (the - default) or 'desc'. - - :param detail: Optional, boolean whether to return detailed information - about bays. - - :returns: A list of bays. - - """ - if limit is not None: - limit = int(limit) - - filters = utils.common_filters(marker, limit, sort_key, sort_dir) - - path = '' - if detail: - path += 'detail' - if filters: - path += '?' + '&'.join(filters) - - if limit is None: - return self._list(self._path(path), "bays") - else: - return self._list_pagination(self._path(path), "bays", - limit=limit) - - def get(self, id): - try: - return self._list(self._path(id))[0] - except IndexError: - return None - - def create(self, **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(), new) - - def delete(self, id): - return self._delete(self._path(id)) - - def update(self, id, patch): - return self._update(self._path(id), patch) + template_name = 'bays' diff --git a/magnumclient/v1/bays_shell.py b/magnumclient/v1/bays_shell.py index 058a633f..b3a1ff72 100644 --- a/magnumclient/v1/bays_shell.py +++ b/magnumclient/v1/bays_shell.py @@ -26,6 +26,11 @@ from cryptography.x509.oid import NameOID import os +DEPRECATION_MESSAGE = ( + 'WARNING: Bay commands are deprecated and will be removed in a future ' + 'release.\nUse cluster commands to avoid seeing this message.') + + def _show_bay(bay): del bay._info['links'] utils.print_dict(bay._info) @@ -55,6 +60,7 @@ def _show_bay(bay): 'status, master_count, node_count, links, bay_create_timeout' ) ) +@utils.deprecated(DEPRECATION_MESSAGE) def do_bay_list(cs, args): """Print a list of available bays.""" bays = cs.bays.list(marker=args.marker, limit=args.limit, @@ -69,6 +75,7 @@ def do_bay_list(cs, args): sortby_index=None) +@utils.deprecated(DEPRECATION_MESSAGE) @utils.arg('--name', metavar='', help='Name of the bay to create.') @@ -118,6 +125,7 @@ def do_bay_create(cs, args): metavar='', nargs='+', help='ID or name of the (bay)s to delete.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_bay_delete(cs, args): """Delete specified bay.""" for id in args.bay: @@ -136,6 +144,7 @@ def do_bay_delete(cs, args): @utils.arg('--long', action='store_true', default=False, help='Display extra associated Baymodel info.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_bay_show(cs, args): """Show details about the given bay.""" bay = cs.bays.get(args.bay) @@ -163,6 +172,7 @@ def do_bay_show(cs, args): default=[], help="Attributes to add/replace or remove " "(only PATH is necessary on remove)") +@utils.deprecated(DEPRECATION_MESSAGE) def do_bay_update(cs, args): """Update information about the given bay.""" patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) @@ -180,6 +190,7 @@ def do_bay_update(cs, args): @utils.arg('--force', action='store_true', default=False, help='Overwrite files if existing.') +@utils.deprecated(DEPRECATION_MESSAGE) def do_bay_config(cs, args): """Configure native client to access bay. @@ -226,13 +237,13 @@ def _config_bay_kubernetes(bay, baymodel, cfg_dir='.', force=False): cfg_file = "%s/config" % cfg_dir if baymodel.tls_disabled: cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" + "bays:\n" + "- bay:\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" - " cluster: %(name)s\n" + " bay: %(name)s\n" " user: %(name)s\n" " name: default/%(name)s\n" "current-context: default/%(name)s\n" @@ -243,14 +254,14 @@ def _config_bay_kubernetes(bay, baymodel, cfg_dir='.', force=False): % {'name': bay.name, 'api_address': bay.api_address}) else: cfg = ("apiVersion: v1\n" - "clusters:\n" - "- cluster:\n" + "bays:\n" + "- bay:\n" " certificate-authority: ca.pem\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" - " cluster: %(name)s\n" + " bay: %(name)s\n" " user: %(name)s\n" " name: default/%(name)s\n" "current-context: default/%(name)s\n" diff --git a/magnumclient/v1/client.py b/magnumclient/v1/client.py index b4585e3b..0c106294 100644 --- a/magnumclient/v1/client.py +++ b/magnumclient/v1/client.py @@ -21,6 +21,8 @@ from magnumclient.common import httpclient from magnumclient.v1 import baymodels from magnumclient.v1 import bays from magnumclient.v1 import certificates +from magnumclient.v1 import cluster_templates +from magnumclient.v1 import clusters from magnumclient.v1 import mservices @@ -145,6 +147,9 @@ class Client(object): endpoint_override=endpoint_override, ) self.bays = bays.BayManager(self.http_client) + self.clusters = clusters.ClusterManager(self.http_client) self.certificates = certificates.CertificateManager(self.http_client) self.baymodels = baymodels.BayModelManager(self.http_client) + self.cluster_templates = \ + cluster_templates.ClusterTemplateManager(self.http_client) self.mservices = mservices.MServiceManager(self.http_client) diff --git a/magnumclient/v1/cluster_templates.py b/magnumclient/v1/cluster_templates.py new file mode 100644 index 00000000..4b2ab6e9 --- /dev/null +++ b/magnumclient/v1/cluster_templates.py @@ -0,0 +1,25 @@ +# 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.v1 import basemodels + + +CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES + + +class ClusterTemplate(basemodels.BaseModel): + model_name = "ClusterTemplate" + + +class ClusterTemplateManager(basemodels.BaseModelManager): + api_name = "clustertemplates" + resource_class = ClusterTemplate diff --git a/magnumclient/v1/cluster_templates_shell.py b/magnumclient/v1/cluster_templates_shell.py new file mode 100644 index 00000000..1057db6a --- /dev/null +++ b/magnumclient/v1/cluster_templates_shell.py @@ -0,0 +1,234 @@ +# Copyright 2015 NEC Corporation. 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 os.path + +from magnumclient.common import cliutils as utils +from magnumclient.common import utils as magnum_utils +from magnumclient.i18n import _ + + +def _show_cluster_template(cluster_template): + del cluster_template._info['links'] + utils.print_dict(cluster_template._info) + + +@utils.arg('--name', + metavar='', + help='Name of the cluster template to create.') +@utils.arg('--image-id', + required=True, + metavar='', + help='The name or UUID of the base image to customize for the bay.') +@utils.arg('--keypair-id', + required=True, + metavar='', + help='The name or UUID of the SSH keypair to load into the' + ' Bay nodes.') +@utils.arg('--external-network-id', + required=True, + metavar='', + help='The external Neutron network ID to connect to this bay' + ' model.') +@utils.arg('--coe', + required=True, + metavar='', + help='Specify the Container Orchestration Engine to use.') +@utils.arg('--fixed-network', + metavar='', + help='The private Neutron network name to connect to this bay' + ' model.') +@utils.arg('--fixed-subnet', + metavar='', + help='The private Neutron subnet name to connect to bay.') +@utils.arg('--network-driver', + metavar='', + help='The network driver name for instantiating container' + ' networks.') +@utils.arg('--volume-driver', + metavar='', + help='The volume driver name for instantiating container' + ' volume.') +@utils.arg('--dns-nameserver', + metavar='', + default='8.8.8.8', + help='The DNS nameserver to use for this cluster template.') +@utils.arg('--flavor-id', + metavar='', + default='m1.medium', + help='The nova flavor id to use when launching the bay.') +@utils.arg('--master-flavor-id', + metavar='', + help='The nova flavor id to use when launching the master node ' + 'of the bay.') +@utils.arg('--docker-volume-size', + metavar='', + type=int, + help='Specify the number of size in GB ' + 'for the docker volume to use.') +@utils.arg('--docker-storage-driver', + metavar='', + default='devicemapper', + help='Select a docker storage driver. Supported: devicemapper, ' + 'overlay. Default: devicemapper') +@utils.arg('--http-proxy', + metavar='', + help='The http_proxy address to use for nodes in bay.') +@utils.arg('--https-proxy', + metavar='', + help='The https_proxy address to use for nodes in bay.') +@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 cluster template. ' + 'May be used multiple times.') +@utils.arg('--tls-disabled', + action='store_true', default=False, + help='Disable TLS in the Bay.') +@utils.arg('--public', + action='store_true', default=False, + help='Make cluster template public.') +@utils.arg('--registry-enabled', + action='store_true', default=False, + help='Enable docker registry in the Bay') +@utils.arg('--server-type', + metavar='', + default='vm', + help='Specify the server type to be used ' + 'for example vm. For this release ' + 'default server type will be vm.') +@utils.arg('--master-lb-enabled', + action='store_true', default=False, + help='Indicates whether created bays should have a load balancer ' + 'for master nodes or not.') +def do_cluster_template_create(cs, args): + """Create a cluster template.""" + opts = {} + opts['name'] = args.name + opts['flavor_id'] = args.flavor_id + opts['master_flavor_id'] = args.master_flavor_id + opts['image_id'] = args.image_id + opts['keypair_id'] = args.keypair_id + opts['external_network_id'] = args.external_network_id + opts['fixed_network'] = args.fixed_network + opts['fixed_subnet'] = args.fixed_subnet + opts['network_driver'] = args.network_driver + opts['volume_driver'] = args.volume_driver + opts['dns_nameserver'] = args.dns_nameserver + opts['docker_volume_size'] = args.docker_volume_size + opts['docker_storage_driver'] = args.docker_storage_driver + opts['coe'] = args.coe + opts['http_proxy'] = args.http_proxy + opts['https_proxy'] = args.https_proxy + opts['no_proxy'] = args.no_proxy + opts['labels'] = magnum_utils.handle_labels(args.labels) + opts['tls_disabled'] = args.tls_disabled + opts['public'] = args.public + opts['registry_enabled'] = args.registry_enabled + opts['server_type'] = args.server_type + opts['master_lb_enabled'] = args.master_lb_enabled + + cluster_template = cs.cluster_templates.create(**opts) + _show_cluster_template(cluster_template) + + +@utils.arg('cluster_templates', + metavar='', + nargs='+', + help='ID or name of the (cluster template)s to delete.') +def do_cluster_template_delete(cs, args): + """Delete specified cluster template.""" + for cluster_template in args.cluster_templates: + try: + cs.cluster_templates.delete(cluster_template) + print("Request to delete cluster template %s has been accepted." % + cluster_template) + except Exception as e: + print("Delete for cluster template " + "%(cluster_template)s failed: %(e)s" % + {'cluster_template': cluster_template, 'e': e}) + + +@utils.arg('cluster_template', + metavar='', + help='ID or name of the cluster template to show.') +def do_cluster_template_show(cs, args): + """Show details about the given cluster template.""" + cluster_template = cs.cluster_templates.get(args.cluster_template) + _show_cluster_template(cluster_template) + + +@utils.arg('--limit', + metavar='', + type=int, + help='Maximum number of cluster templates to return') +@utils.arg('--sort-key', + metavar='', + help='Column to sort results by') +@utils.arg('--sort-dir', + metavar='', + choices=['desc', 'asc'], + help='Direction to sort. "asc" or "desc".') +@utils.arg('--fields', + default=None, + metavar='', + help=_('Comma-separated list of fields to display. ' + 'Available fields: uuid, name, coe, image_id, public, link, ' + 'apiserver_port, server_type, tls_disabled, registry_enabled' + ) + ) +def do_cluster_template_list(cs, args): + """Print a list of cluster templates.""" + nodes = cs.cluster_templates.list(limit=args.limit, + sort_key=args.sort_key, + sort_dir=args.sort_dir) + columns = ['uuid', 'name'] + columns += utils._get_list_table_columns_and_formatters( + args.fields, nodes, + exclude_fields=(c.lower() for c in columns))[0] + utils.print_list(nodes, columns, + {'versions': magnum_utils.print_list_field('versions')}, + sortby_index=None) + + +@utils.arg('cluster_template', + metavar='', + help="UUID or name of cluster template") +@utils.arg( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help="Operations: 'add', 'replace' or 'remove'") +@utils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help="Attributes to add/replace or remove " + "(only PATH is necessary on remove)") +def do_cluster_template_update(cs, args): + """Updates one or more cluster template attributes.""" + patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) + p = patch[0] + if p['path'] == '/manifest' and os.path.isfile(p['value']): + with open(p['value'], 'r') as f: + p['value'] = f.read() + + cluster_template = cs.cluster_templates.update(args.cluster_template, + patch) + _show_cluster_template(cluster_template) diff --git a/magnumclient/v1/clusters.py b/magnumclient/v1/clusters.py new file mode 100644 index 00000000..b95973fc --- /dev/null +++ b/magnumclient/v1/clusters.py @@ -0,0 +1,29 @@ +# Copyright 2014 NEC Corporation. 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.v1 import baseunit + + +CREATION_ATTRIBUTES = baseunit.CREATION_ATTRIBUTES +CREATION_ATTRIBUTES.append('cluster_template_id') +CREATION_ATTRIBUTES.append('create_timeout') + + +class Cluster(baseunit.BaseTemplate): + template_name = "Clusters" + + +class ClusterManager(baseunit.BaseTemplateManager): + resource_class = Cluster + template_name = 'clusters' diff --git a/magnumclient/v1/clusters_shell.py b/magnumclient/v1/clusters_shell.py new file mode 100644 index 00000000..971c40d8 --- /dev/null +++ b/magnumclient/v1/clusters_shell.py @@ -0,0 +1,326 @@ +# Copyright 2015 NEC Corporation. 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 os + +from magnumclient.common import cliutils as utils +from magnumclient.common import utils as magnum_utils +from magnumclient import exceptions +from magnumclient.i18n import _ + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography import x509 +from cryptography.x509.oid import NameOID + + +def _show_cluster(cluster): + del cluster._info['links'] + utils.print_dict(cluster._info) + + +@utils.arg('--marker', + metavar='', + default=None, + help='The last cluster UUID of the previous page; ' + 'displays list of clusters after "marker".') +@utils.arg('--limit', + metavar='', + type=int, + help='Maximum number of clusters to return.') +@utils.arg('--sort-key', + metavar='', + help='Column to sort results by.') +@utils.arg('--sort-dir', + metavar='', + choices=['desc', 'asc'], + help='Direction to sort. "asc" or "desc".') +@utils.arg('--fields', + default=None, + metavar='', + help=_('Comma-separated list of fields to display. ' + 'Available fields: uuid, name, baymodel_id, stack_id, ' + 'status, master_count, node_count, links, ' + 'cluster_create_timeout' + ) + ) +def do_cluster_list(cs, args): + """Print a list of available clusters.""" + clusters = cs.clusters.list(marker=args.marker, limit=args.limit, + sort_key=args.sort_key, + sort_dir=args.sort_dir) + columns = ['uuid', 'name', 'node_count', 'master_count', 'status'] + columns += utils._get_list_table_columns_and_formatters( + args.fields, clusters, + exclude_fields=(c.lower() for c in columns))[0] + utils.print_list(clusters, columns, + {'versions': magnum_utils.print_list_field('versions')}, + sortby_index=None) + + +@utils.arg('--name', + metavar='', + help='Name of the cluster to create.') +@utils.arg('--cluster-template', + required=True, + metavar='', + help='ID or name of the cluster template.') +@utils.arg('--node-count', + metavar='', + type=int, + default=1, + help='The cluster node count.') +@utils.arg('--master-count', + metavar='', + type=int, + default=1, + help='The number of master nodes for the cluster.') +@utils.arg('--discovery-url', + metavar='', + help='Specifies custom discovery url for node discovery.') +@utils.arg('--timeout', + metavar='', + type=int, + default=60, + help='The timeout for cluster creation in minutes. The default ' + 'is 60 minutes.') +def do_cluster_create(cs, args): + """Create a cluster.""" + cluster_template = cs.cluster_templates.get(args.cluster_template) + + opts = {} + opts['name'] = args.name + opts['cluster_template_id'] = cluster_template.uuid + opts['node_count'] = args.node_count + opts['master_count'] = args.master_count + opts['discovery_url'] = args.discovery_url + opts['create_timeout'] = args.timeout + try: + cluster = cs.clusters.create(**opts) + _show_cluster(cluster) + except Exception as e: + print("Create for cluster %s failed: %s" % + (opts['name'], e)) + + +@utils.arg('cluster', + metavar='', + nargs='+', + help='ID or name of the (cluster)s to delete.') +def do_cluster_delete(cs, args): + """Delete specified cluster.""" + for id in args.cluster: + try: + cs.clusters.delete(id) + print("Request to delete cluster %s has been accepted." % + id) + except Exception as e: + print("Delete for cluster %(cluster)s failed: %(e)s" % + {'cluster': id, 'e': e}) + + +@utils.arg('cluster', + metavar='', + help='ID or name of the cluster to show.') +@utils.arg('--long', + action='store_true', default=False, + help='Display extra associated cluster template info.') +def do_cluster_show(cs, args): + """Show details about the given cluster.""" + cluster = cs.clusters.get(args.cluster) + if args.long: + cluster_template = \ + cs.cluster_templates.get(cluster.cluster_template_id) + del cluster_template._info['links'], cluster_template._info['uuid'] + + for key in cluster_template._info: + if 'clustertemplate_' + key not in cluster._info: + cluster._info['clustertemplate_' + key] = \ + cluster_template._info[key] + _show_cluster(cluster) + + +@utils.arg('cluster', metavar='', help="UUID or name of cluster") +@utils.arg( + 'op', + metavar='', + choices=['add', 'replace', 'remove'], + help="Operations: 'add', 'replace' or 'remove'") +@utils.arg( + 'attributes', + metavar='', + nargs='+', + action='append', + default=[], + help="Attributes to add/replace or remove " + "(only PATH is necessary on remove)") +def do_cluster_update(cs, args): + """Update information about the given cluster.""" + patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) + cluster = cs.clusters.update(args.cluster, patch) + _show_cluster(cluster) + + +@utils.arg('cluster', + metavar='', + help='ID or name of the cluster to retrieve config.') +@utils.arg('--dir', + metavar='', + default='.', + help='Directory to save the certificate and config files.') +@utils.arg('--force', + action='store_true', default=False, + help='Overwrite files if existing.') +def do_cluster_config(cs, args): + """Configure native client to access cluster. + + You can source the output of this command to get the native client of the + corresponding COE configured to access the cluster. + + Example: eval $(magnum cluster-config ). + """ + cluster = cs.clusters.get(args.cluster) + if cluster.status not in ('CREATE_COMPLETE', 'UPDATE_COMPLETE'): + raise exceptions.CommandError("cluster in status %s" % cluster.status) + cluster_template = cs.cluster_templates.get(cluster.cluster_template_id) + opts = { + 'cluster_uuid': cluster.uuid, + } + + if not cluster_template.tls_disabled: + tls = _generate_csr_and_key() + tls['ca'] = cs.certificates.get(**opts).pem + opts['csr'] = tls['csr'] + tls['cert'] = cs.certificates.create(**opts).pem + for k in ('key', 'cert', 'ca'): + fname = "%s/%s.pem" % (args.dir, k) + if os.path.exists(fname) and not args.force: + raise Exception("File %s exists, aborting." % fname) + else: + f = open(fname, "w") + f.write(tls[k]) + f.close() + + print(_config_cluster(cluster, cluster_template, + cfg_dir=args.dir, force=args.force)) + + +def _config_cluster(cluster, cluster_template, cfg_dir='.', force=False): + """Return and write configuration for the given cluster.""" + if cluster_template.coe == 'kubernetes': + return _config_cluster_kubernetes(cluster, cluster_template, + cfg_dir, force) + elif cluster_template.coe == 'swarm': + return _config_cluster_swarm(cluster, cluster_template, cfg_dir, force) + + +def _config_cluster_kubernetes(cluster, cluster_template, + cfg_dir='.', force=False): + """Return and write configuration for the given kubernetes cluster.""" + cfg_file = "%s/config" % cfg_dir + if cluster_template.tls_disabled: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: %(name)s\n" + " name: default/%(name)s\n" + "current-context: default/%(name)s\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: %(name)s'\n" + % {'name': cluster.name, 'api_address': cluster.api_address}) + else: + cfg = ("apiVersion: v1\n" + "clusters:\n" + "- cluster:\n" + " certificate-authority: ca.pem\n" + " server: %(api_address)s\n" + " name: %(name)s\n" + "contexts:\n" + "- context:\n" + " cluster: %(name)s\n" + " user: %(name)s\n" + " name: default/%(name)s\n" + "current-context: default/%(name)s\n" + "kind: Config\n" + "preferences: {}\n" + "users:\n" + "- name: %(name)s\n" + " user:\n" + " client-certificate: cert.pem\n" + " client-key: key.pem\n" + % {'name': cluster.name, 'api_address': cluster.api_address}) + + if os.path.exists(cfg_file) and not force: + raise exceptions.CommandError("File %s exists, aborting." % cfg_file) + else: + f = open(cfg_file, "w") + f.write(cfg) + f.close() + if 'csh' in os.environ['SHELL']: + return "setenv KUBECONFIG %s\n" % cfg_file + else: + return "export KUBECONFIG=%s\n" % cfg_file + + +def _config_cluster_swarm(cluster, cluster_template, cfg_dir='.', force=False): + """Return and write configuration for the given swarm cluster.""" + if 'csh' in os.environ['SHELL']: + result = ("setenv DOCKER_HOST %(docker_host)s\n" + "setenv DOCKER_CERT_PATH %(cfg_dir)s\n" + "setenv DOCKER_TLS_VERIFY %(tls)s\n" + % {'docker_host': cluster.api_address, + 'cfg_dir': cfg_dir, + 'tls': not cluster_template.tls_disabled} + ) + else: + result = ("export DOCKER_HOST=%(docker_host)s\n" + "export DOCKER_CERT_PATH=%(cfg_dir)s\n" + "export DOCKER_TLS_VERIFY=%(tls)s\n" + % {'docker_host': cluster.api_address, + 'cfg_dir': cfg_dir, + 'tls': not cluster_template.tls_disabled} + ) + + return result + + +def _generate_csr_and_key(): + """Return a dict with a new csr and key.""" + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend()) + + csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"Magnum User"), + ])).sign(key, hashes.SHA256(), default_backend()) + + result = { + 'csr': csr.public_bytes(encoding=serialization.Encoding.PEM), + 'key': key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()), + } + + return result diff --git a/magnumclient/v1/shell.py b/magnumclient/v1/shell.py index 408bb4d9..2380f768 100644 --- a/magnumclient/v1/shell.py +++ b/magnumclient/v1/shell.py @@ -16,11 +16,15 @@ from magnumclient.v1 import baymodels_shell from magnumclient.v1 import bays_shell from magnumclient.v1 import certificates_shell +from magnumclient.v1 import cluster_templates_shell +from magnumclient.v1 import clusters_shell from magnumclient.v1 import mservices_shell COMMAND_MODULES = [ baymodels_shell, bays_shell, certificates_shell, + clusters_shell, + cluster_templates_shell, mservices_shell, ] diff --git a/requirements.txt b/requirements.txt index 4a17f954..9881a50d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,4 @@ oslo.utils>=3.16.0 # Apache-2.0 os-client-config!=1.19.0,>=1.13.1 # Apache-2.0 PrettyTable<0.8,>=0.7 # BSD cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0 - +decorator>=3.4.0 # BSD