Adds 'cluster' and 'cluster template'

Add new base classes for Bay and Cluster to inherit from.
Add new tests for cluster and cluster_template code.
Leaves all commands in-place and adds new cluster-related
commands for magnumclient.
Add deprecation decorator and messages for bay/baymodel.

Implements: blueprint rename-bay-to-cluster
Change-Id: I64e1aa6a71b109687568005655c6de55f86fad29
This commit is contained in:
Stephen Watson 2016-08-10 13:43:09 -07:00
parent 9a10538f40
commit d9ae385588
18 changed files with 2324 additions and 170 deletions

View File

@ -24,6 +24,7 @@ import os
import sys import sys
import textwrap import textwrap
import decorator
from magnumclient.common.apiclient import exceptions from magnumclient.common.apiclient import exceptions
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import strutils from oslo_utils import strutils
@ -75,6 +76,22 @@ def validate_args(fn, *args, **kwargs):
raise MissingArgs(missing) 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): def arg(*args, **kwargs):
"""Decorator for CLI args. """Decorator for CLI args.

View File

@ -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)

View File

@ -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 <op>: 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

108
magnumclient/v1/baseunit.py Normal file
View File

@ -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)

View File

@ -10,94 +10,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from magnumclient.common import base from magnumclient.v1 import basemodels
from magnumclient.common import utils
from magnumclient import exceptions
CREATION_ATTRIBUTES = ['name', 'image_id', 'flavor_id', 'master_flavor_id', CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES
'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 BayModel(base.Resource): class BayModel(basemodels.BaseModel):
def __repr__(self): model_name = "BayModel"
return "<BayModel %s>" % self._info
class BayModelManager(base.Manager): class BayModelManager(basemodels.BaseModelManager):
api_name = "baymodels"
resource_class = BayModel 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)

View File

@ -19,6 +19,11 @@ from magnumclient.common import utils as magnum_utils
from magnumclient.i18n import _ 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): def _show_baymodel(baymodel):
del baymodel._info['links'] del baymodel._info['links']
utils.print_dict(baymodel._info) utils.print_dict(baymodel._info)
@ -115,6 +120,7 @@ def _show_baymodel(baymodel):
action='store_true', default=False, action='store_true', default=False,
help='Indicates whether created bays should have a load balancer ' help='Indicates whether created bays should have a load balancer '
'for master nodes or not.') 'for master nodes or not.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_baymodel_create(cs, args): def do_baymodel_create(cs, args):
"""Create a baymodel.""" """Create a baymodel."""
opts = {} opts = {}
@ -150,6 +156,7 @@ def do_baymodel_create(cs, args):
metavar='<baymodels>', metavar='<baymodels>',
nargs='+', nargs='+',
help='ID or name of the (baymodel)s to delete.') help='ID or name of the (baymodel)s to delete.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_baymodel_delete(cs, args): def do_baymodel_delete(cs, args):
"""Delete specified baymodel.""" """Delete specified baymodel."""
for baymodel in args.baymodels: for baymodel in args.baymodels:
@ -165,6 +172,7 @@ def do_baymodel_delete(cs, args):
@utils.arg('baymodel', @utils.arg('baymodel',
metavar='<baymodel>', metavar='<baymodel>',
help='ID or name of the baymodel to show.') help='ID or name of the baymodel to show.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_baymodel_show(cs, args): def do_baymodel_show(cs, args):
"""Show details about the given baymodel.""" """Show details about the given baymodel."""
baymodel = cs.baymodels.get(args.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' 'apiserver_port, server_type, tls_disabled, registry_enabled'
) )
) )
@utils.deprecated(DEPRECATION_MESSAGE)
def do_baymodel_list(cs, args): def do_baymodel_list(cs, args):
"""Print a list of baymodels.""" """Print a list of baymodels."""
nodes = cs.baymodels.list(limit=args.limit, nodes = cs.baymodels.list(limit=args.limit,
@ -218,6 +227,7 @@ def do_baymodel_list(cs, args):
default=[], default=[],
help="Attributes to add/replace or remove " help="Attributes to add/replace or remove "
"(only PATH is necessary on remove)") "(only PATH is necessary on remove)")
@utils.deprecated(DEPRECATION_MESSAGE)
def do_baymodel_update(cs, args): def do_baymodel_update(cs, args):
"""Updates one or more baymodel attributes.""" """Updates one or more baymodel attributes."""
patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0])

View File

@ -12,89 +12,18 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from magnumclient.common import base from magnumclient.v1 import baseunit
from magnumclient.common import utils
from magnumclient import exceptions
CREATION_ATTRIBUTES = ['name', 'baymodel_id', 'node_count', 'discovery_url', CREATION_ATTRIBUTES = baseunit.CREATION_ATTRIBUTES
'bay_create_timeout', 'master_count'] CREATION_ATTRIBUTES.append('baymodel_id')
CREATION_ATTRIBUTES.append('bay_create_timeout')
class Bay(base.Resource): class Bay(baseunit.BaseTemplate):
def __repr__(self): template_name = "Bays"
return "<Bay %s>" % self._info
class BayManager(base.Manager): class BayManager(baseunit.BaseTemplateManager):
resource_class = Bay resource_class = Bay
template_name = 'bays'
@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)

View File

@ -26,6 +26,11 @@ from cryptography.x509.oid import NameOID
import os 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): def _show_bay(bay):
del bay._info['links'] del bay._info['links']
utils.print_dict(bay._info) utils.print_dict(bay._info)
@ -55,6 +60,7 @@ def _show_bay(bay):
'status, master_count, node_count, links, bay_create_timeout' 'status, master_count, node_count, links, bay_create_timeout'
) )
) )
@utils.deprecated(DEPRECATION_MESSAGE)
def do_bay_list(cs, args): def do_bay_list(cs, args):
"""Print a list of available bays.""" """Print a list of available bays."""
bays = cs.bays.list(marker=args.marker, limit=args.limit, bays = cs.bays.list(marker=args.marker, limit=args.limit,
@ -69,6 +75,7 @@ def do_bay_list(cs, args):
sortby_index=None) sortby_index=None)
@utils.deprecated(DEPRECATION_MESSAGE)
@utils.arg('--name', @utils.arg('--name',
metavar='<name>', metavar='<name>',
help='Name of the bay to create.') help='Name of the bay to create.')
@ -118,6 +125,7 @@ def do_bay_create(cs, args):
metavar='<bay>', metavar='<bay>',
nargs='+', nargs='+',
help='ID or name of the (bay)s to delete.') help='ID or name of the (bay)s to delete.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_bay_delete(cs, args): def do_bay_delete(cs, args):
"""Delete specified bay.""" """Delete specified bay."""
for id in args.bay: for id in args.bay:
@ -136,6 +144,7 @@ def do_bay_delete(cs, args):
@utils.arg('--long', @utils.arg('--long',
action='store_true', default=False, action='store_true', default=False,
help='Display extra associated Baymodel info.') help='Display extra associated Baymodel info.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_bay_show(cs, args): def do_bay_show(cs, args):
"""Show details about the given bay.""" """Show details about the given bay."""
bay = cs.bays.get(args.bay) bay = cs.bays.get(args.bay)
@ -163,6 +172,7 @@ def do_bay_show(cs, args):
default=[], default=[],
help="Attributes to add/replace or remove " help="Attributes to add/replace or remove "
"(only PATH is necessary on remove)") "(only PATH is necessary on remove)")
@utils.deprecated(DEPRECATION_MESSAGE)
def do_bay_update(cs, args): def do_bay_update(cs, args):
"""Update information about the given bay.""" """Update information about the given bay."""
patch = magnum_utils.args_array_to_patch(args.op, args.attributes[0]) 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', @utils.arg('--force',
action='store_true', default=False, action='store_true', default=False,
help='Overwrite files if existing.') help='Overwrite files if existing.')
@utils.deprecated(DEPRECATION_MESSAGE)
def do_bay_config(cs, args): def do_bay_config(cs, args):
"""Configure native client to access bay. """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 cfg_file = "%s/config" % cfg_dir
if baymodel.tls_disabled: if baymodel.tls_disabled:
cfg = ("apiVersion: v1\n" cfg = ("apiVersion: v1\n"
"clusters:\n" "bays:\n"
"- cluster:\n" "- bay:\n"
" server: %(api_address)s\n" " server: %(api_address)s\n"
" name: %(name)s\n" " name: %(name)s\n"
"contexts:\n" "contexts:\n"
"- context:\n" "- context:\n"
" cluster: %(name)s\n" " bay: %(name)s\n"
" user: %(name)s\n" " user: %(name)s\n"
" name: default/%(name)s\n" " name: default/%(name)s\n"
"current-context: 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}) % {'name': bay.name, 'api_address': bay.api_address})
else: else:
cfg = ("apiVersion: v1\n" cfg = ("apiVersion: v1\n"
"clusters:\n" "bays:\n"
"- cluster:\n" "- bay:\n"
" certificate-authority: ca.pem\n" " certificate-authority: ca.pem\n"
" server: %(api_address)s\n" " server: %(api_address)s\n"
" name: %(name)s\n" " name: %(name)s\n"
"contexts:\n" "contexts:\n"
"- context:\n" "- context:\n"
" cluster: %(name)s\n" " bay: %(name)s\n"
" user: %(name)s\n" " user: %(name)s\n"
" name: default/%(name)s\n" " name: default/%(name)s\n"
"current-context: default/%(name)s\n" "current-context: default/%(name)s\n"

View File

@ -21,6 +21,8 @@ from magnumclient.common import httpclient
from magnumclient.v1 import baymodels from magnumclient.v1 import baymodels
from magnumclient.v1 import bays from magnumclient.v1 import bays
from magnumclient.v1 import certificates from magnumclient.v1 import certificates
from magnumclient.v1 import cluster_templates
from magnumclient.v1 import clusters
from magnumclient.v1 import mservices from magnumclient.v1 import mservices
@ -145,6 +147,9 @@ class Client(object):
endpoint_override=endpoint_override, endpoint_override=endpoint_override,
) )
self.bays = bays.BayManager(self.http_client) self.bays = bays.BayManager(self.http_client)
self.clusters = clusters.ClusterManager(self.http_client)
self.certificates = certificates.CertificateManager(self.http_client) self.certificates = certificates.CertificateManager(self.http_client)
self.baymodels = baymodels.BayModelManager(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) self.mservices = mservices.MServiceManager(self.http_client)

View File

@ -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

View File

@ -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='<name>',
help='Name of the cluster template to create.')
@utils.arg('--image-id',
required=True,
metavar='<image-id>',
help='The name or UUID of the base image to customize for the bay.')
@utils.arg('--keypair-id',
required=True,
metavar='<keypair-id>',
help='The name or UUID of the SSH keypair to load into the'
' Bay nodes.')
@utils.arg('--external-network-id',
required=True,
metavar='<external-network-id>',
help='The external Neutron network ID to connect to this bay'
' model.')
@utils.arg('--coe',
required=True,
metavar='<coe>',
help='Specify the Container Orchestration Engine to use.')
@utils.arg('--fixed-network',
metavar='<fixed-network>',
help='The private Neutron network name to connect to this bay'
' model.')
@utils.arg('--fixed-subnet',
metavar='<fixed-subnet>',
help='The private Neutron subnet name to connect to bay.')
@utils.arg('--network-driver',
metavar='<network-driver>',
help='The network driver name for instantiating container'
' networks.')
@utils.arg('--volume-driver',
metavar='<volume-driver>',
help='The volume driver name for instantiating container'
' volume.')
@utils.arg('--dns-nameserver',
metavar='<dns-nameserver>',
default='8.8.8.8',
help='The DNS nameserver to use for this cluster template.')
@utils.arg('--flavor-id',
metavar='<flavor-id>',
default='m1.medium',
help='The nova flavor id to use when launching the bay.')
@utils.arg('--master-flavor-id',
metavar='<master-flavor-id>',
help='The nova flavor id to use when launching the master node '
'of the bay.')
@utils.arg('--docker-volume-size',
metavar='<docker-volume-size>',
type=int,
help='Specify the number of size in GB '
'for the docker volume to use.')
@utils.arg('--docker-storage-driver',
metavar='<docker-storage-driver>',
default='devicemapper',
help='Select a docker storage driver. Supported: devicemapper, '
'overlay. Default: devicemapper')
@utils.arg('--http-proxy',
metavar='<http-proxy>',
help='The http_proxy address to use for nodes in bay.')
@utils.arg('--https-proxy',
metavar='<https-proxy>',
help='The https_proxy address to use for nodes in bay.')
@utils.arg('--no-proxy',
metavar='<no-proxy>',
help='The no_proxy address to use for nodes in bay.')
@utils.arg('--labels', metavar='<KEY1=VALUE1,KEY2=VALUE2;KEY3=VALUE3...>',
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='<server-type>',
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='<cluster_templates>',
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='<cluster_template>',
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='<limit>',
type=int,
help='Maximum number of cluster templates to return')
@utils.arg('--sort-key',
metavar='<sort-key>',
help='Column to sort results by')
@utils.arg('--sort-dir',
metavar='<sort-dir>',
choices=['desc', 'asc'],
help='Direction to sort. "asc" or "desc".')
@utils.arg('--fields',
default=None,
metavar='<fields>',
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='<cluster_template>',
help="UUID or name of cluster template")
@utils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operations: 'add', 'replace' or 'remove'")
@utils.arg(
'attributes',
metavar='<path=value>',
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)

View File

@ -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'

View File

@ -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='<marker>',
default=None,
help='The last cluster UUID of the previous page; '
'displays list of clusters after "marker".')
@utils.arg('--limit',
metavar='<limit>',
type=int,
help='Maximum number of clusters to return.')
@utils.arg('--sort-key',
metavar='<sort-key>',
help='Column to sort results by.')
@utils.arg('--sort-dir',
metavar='<sort-dir>',
choices=['desc', 'asc'],
help='Direction to sort. "asc" or "desc".')
@utils.arg('--fields',
default=None,
metavar='<fields>',
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='<name>',
help='Name of the cluster to create.')
@utils.arg('--cluster-template',
required=True,
metavar='<cluster_template>',
help='ID or name of the cluster template.')
@utils.arg('--node-count',
metavar='<node-count>',
type=int,
default=1,
help='The cluster node count.')
@utils.arg('--master-count',
metavar='<master-count>',
type=int,
default=1,
help='The number of master nodes for the cluster.')
@utils.arg('--discovery-url',
metavar='<discovery-url>',
help='Specifies custom discovery url for node discovery.')
@utils.arg('--timeout',
metavar='<timeout>',
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='<cluster>',
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='<cluster>',
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='<cluster>', help="UUID or name of cluster")
@utils.arg(
'op',
metavar='<op>',
choices=['add', 'replace', 'remove'],
help="Operations: 'add', 'replace' or 'remove'")
@utils.arg(
'attributes',
metavar='<path=value>',
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='<cluster>',
help='ID or name of the cluster to retrieve config.')
@utils.arg('--dir',
metavar='<dir>',
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-name>).
"""
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

View File

@ -16,11 +16,15 @@
from magnumclient.v1 import baymodels_shell from magnumclient.v1 import baymodels_shell
from magnumclient.v1 import bays_shell from magnumclient.v1 import bays_shell
from magnumclient.v1 import certificates_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 from magnumclient.v1 import mservices_shell
COMMAND_MODULES = [ COMMAND_MODULES = [
baymodels_shell, baymodels_shell,
bays_shell, bays_shell,
certificates_shell, certificates_shell,
clusters_shell,
cluster_templates_shell,
mservices_shell, mservices_shell,
] ]

View File

@ -14,4 +14,4 @@ oslo.utils>=3.16.0 # Apache-2.0
os-client-config!=1.19.0,>=1.13.1 # Apache-2.0 os-client-config!=1.19.0,>=1.13.1 # Apache-2.0
PrettyTable<0.8,>=0.7 # BSD PrettyTable<0.8,>=0.7 # BSD
cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0 cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0
decorator>=3.4.0 # BSD