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

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
# 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 "<BayModel %s>" % 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)

View File

@ -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='<baymodels>',
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='<baymodel>',
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])

View File

@ -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 "<Bay %s>" % 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'

View File

@ -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='<name>',
help='Name of the bay to create.')
@ -118,6 +125,7 @@ def do_bay_create(cs, args):
metavar='<bay>',
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"

View File

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

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 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,
]

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
PrettyTable<0.8,>=0.7 # BSD
cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0
decorator>=3.4.0 # BSD