Implement CLI v1 for openstack configuration

Add openstack-config commands for fuel
Examples for fuel:

fuel openstack-config --env 1 --list
fuel openstack-config --env 1 [--node 1 | --role controller] --upload
    --file config.yaml
fuel openstack-config --config 1 --download --file config.yaml
fuel openstack-config --env 1 [--node 1 | --role controller] --execute

Change-Id: Id701d4b8408bd275ac6e7bc53ca23b9f3deb4f6d
Implements: blueprint openstack-config-change
This commit is contained in:
Alexander Saprykin 2015-11-18 16:58:37 +01:00 committed by sslypushenko
parent 4f9a873b1a
commit 88274ba134
9 changed files with 395 additions and 4 deletions

View File

@ -32,6 +32,7 @@ from fuelclient.cli.actions.node import NodeAction
from fuelclient.cli.actions.nodegroup import NodeGroupAction
from fuelclient.cli.actions.notifications import NotificationsAction
from fuelclient.cli.actions.notifications import NotifyAction
from fuelclient.cli.actions.openstack_config import OpenstackConfigAction
from fuelclient.cli.actions.release import ReleaseAction
from fuelclient.cli.actions.role import RoleAction
from fuelclient.cli.actions.settings import SettingsAction
@ -68,6 +69,7 @@ actions_tuple = (
GraphAction,
FuelVersionAction,
NetworkGroupAction,
OpenstackConfigAction,
)
actions = dict(

View File

@ -0,0 +1,148 @@
# Copyright 2015 Mirantis, Inc.
#
# 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 fuelclient.cli.actions.base import Action
from fuelclient.cli.actions.base import check_all
import fuelclient.cli.arguments as Args
from fuelclient.cli.arguments import group
from fuelclient.cli.formatting import format_table
from fuelclient.objects.openstack_config import OpenstackConfig
class OpenstackConfigAction(Action):
"""Manage openstack configuration"""
action_name = 'openstack-config'
acceptable_keys = ('id', 'is_active', 'config_type',
'cluster_id', 'node_id', 'node_role')
def __init__(self):
super(OpenstackConfigAction, self).__init__()
self.args = (
Args.get_env_arg(),
Args.get_file_arg("Openstack configuration file"),
Args.get_single_node_arg("Node ID"),
Args.get_single_role_arg("Node role"),
Args.get_config_id_arg("Openstack config ID"),
Args.get_deleted_arg("Get deleted configurations"),
group(
Args.get_list_arg("List openstack configurations"),
Args.get_download_arg(
"Download current openstack configuration"),
Args.get_upload_arg("Upload new openstack configuration"),
Args.get_delete_arg("Delete openstack configuration"),
Args.get_execute_arg("Apply openstack configuration"),
required=True,
)
)
self.flag_func_map = (
('list', self.list),
('download', self.download),
('upload', self.upload),
('delete', self.delete),
('execute', self.execute)
)
def list(self, params):
"""List all available configurations:
fuel openstack-config --list --env 1
fuel openstack-config --list --env 1 --node 1
fuel openstack-config --list --env 1 --deleted
"""
filters = {}
if 'env' in params:
filters['cluster_id'] = params.env
if 'deleted' in params:
filters['is_active'] = int(not params.deleted)
if 'node' in params:
filters['node_id'] = params.node
if 'role' in params:
filters['node_role'] = params.role
configs = OpenstackConfig.get_filtered_data(**filters)
self.serializer.print_to_output(
configs,
format_table(
configs,
acceptable_keys=self.acceptable_keys
)
)
@check_all('config-id')
def download(self, params):
"""Download an existing configuration to file:
fuel openstack-config --download --config-id 1 --file config.yaml
"""
config_id = getattr(params, 'config-id')
config = OpenstackConfig(config_id)
data = config.data
OpenstackConfig.write_file(params.file, {
'configuration': data['configuration']})
@check_all('env')
def upload(self, params):
"""Upload new configuration from file:
fuel openstack-config --upload --env 1 --file config.yaml
fuel openstack-config --upload --env 1 --node 1 --file config.yaml
fuel openstack-config --upload --env 1
--role controller --file config.yaml
"""
node_id = getattr(params, 'node', None)
node_role = getattr(params, 'role', None)
data = OpenstackConfig.read_file(params.file)
config = OpenstackConfig.create(
cluster_id=params.env,
configuration=data['configuration'],
node_id=node_id, node_role=node_role)
print("Openstack configuration with id {0} "
"has been uploaded from file '{1}'"
"".format(config.id, params.file))
@check_all('config-id')
def delete(self, params):
"""Delete an existing configuration:
fuel openstack-config --delete --config 1
"""
config_id = getattr(params, 'config-id')
config = OpenstackConfig(config_id)
config.delete()
print("Openstack configuration '{0}' "
"has been deleted.".format(config_id))
@check_all('env')
def execute(self, params):
"""Deploy configuration:
fuel openstack-config --execute --env 1
fuel openstack-config --execute --env 1 --node 1
fuel openstack-config --execute --env 1 --role controller
"""
node_id = getattr(params, 'node', None)
node_role = getattr(params, 'role', None)
task_result = OpenstackConfig.execute(
cluster_id=params.env, node_id=node_id,
node_role=node_role)
if task_result['status'] == 'error':
print(
'Error applying openstack configuration: {0}.'.format(
task_result['message'])
)
else:
print('Openstack configuration update is started.')

View File

@ -284,6 +284,10 @@ def get_role_arg(help_msg):
return get_set_type_arg("role", flags=("-r",), help=help_msg)
def get_single_role_arg(help_msg):
return get_str_arg("role", flags=('--role', ), help=help_msg)
def get_check_arg(help_msg):
return get_set_type_arg("check", help=help_msg)
@ -415,6 +419,10 @@ def get_delete_arg(help_msg):
return get_boolean_arg("delete", help=help_msg)
def get_execute_arg(help_msg):
return get_boolean_arg("execute", help=help_msg)
def get_assign_arg(help_msg):
return get_boolean_arg("assign", help=help_msg)
@ -486,6 +494,10 @@ def get_node_arg(help_msg):
return get_arg("node", **default_kwargs)
def get_single_node_arg(help_msg):
return get_int_arg('node', flags=('--node-id',), help=help_msg)
def get_task_arg(help_msg):
return get_array_arg(
'task',
@ -494,6 +506,17 @@ def get_task_arg(help_msg):
)
def get_config_id_arg(help_msg):
return get_int_arg(
'config-id',
help=help_msg)
def get_deleted_arg(help_msg):
return get_boolean_arg(
'deleted', help=help_msg)
def get_plugin_install_arg(help_msg):
return get_str_arg(
"install",

View File

@ -88,14 +88,17 @@ class Serializer(object):
def write_to_path(self, path, data):
full_path = self.prepare_path(path)
return self.write_to_full_path(full_path, data)
def write_to_full_path(self, path, data):
try:
with open(full_path, "w") as file_to_write:
with open(path, "w") as file_to_write:
self.write_to_file(file_to_write, data)
except IOError as e:
raise error.InvalidFileException(
"Can't write to file '{0}': {1}.".format(
full_path, e.strerror))
return full_path
path, e.strerror))
return path
def read_from_file(self, path):
return self.read_from_full_path(self.prepare_path(path))

View File

@ -20,6 +20,7 @@ from fuelclient.objects.base import BaseObject
from fuelclient.objects.environment import Environment
from fuelclient.objects.node import Node
from fuelclient.objects.node import NodeCollection
from fuelclient.objects.openstack_config import OpenstackConfig
from fuelclient.objects.release import Release
from fuelclient.objects.task import DeployTask
from fuelclient.objects.task import SnapshotTask

View File

@ -0,0 +1,67 @@
# Copyright 2015 Mirantis, Inc.
#
# 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
import six
from fuelclient.cli import error
from fuelclient.cli.serializers import Serializer
from fuelclient.objects.base import BaseObject
class OpenstackConfig(BaseObject):
class_api_path = 'openstack-config/'
instance_api_path = 'openstack-config/{0}/'
execute_api_path = 'openstack-config/execute/'
@classmethod
def _prepare_params(cls, filters):
return dict((k, v) for k, v in six.iteritems(filters) if v is not None)
@classmethod
def create(cls, **kwargs):
params = cls._prepare_params(kwargs)
data = cls.connection.post_request(cls.class_api_path, params)
return cls.init_with_data(data)
def delete(self):
return self.connection.delete_request(
self.instance_api_path.format(self.id))
@classmethod
def execute(cls, **kwargs):
params = cls._prepare_params(kwargs)
return cls.connection.put_request(cls.execute_api_path, params)
@classmethod
def get_filtered_data(cls, **kwargs):
url = cls.class_api_path
params = cls._prepare_params(kwargs)
return cls.connection.get_request(url, params=params)
@classmethod
def read_file(cls, path):
if not os.path.exists(path):
raise error.InvalidFileException(
"File '{0}' doesn't exist.".format(path))
serializer = Serializer()
return serializer.read_from_full_path(path)
@classmethod
def write_file(cls, path, data):
serializer = Serializer()
return serializer.write_to_full_path(path, data)

View File

@ -0,0 +1,105 @@
# Copyright 2015 Mirantis, Inc.
#
# 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
import yaml
from fuelclient.tests.unit.v1 import base
from fuelclient.tests import utils
class TestOpenstackConfigActions(base.UnitTestCase):
def setUp(self):
super(TestOpenstackConfigActions, self).setUp()
self.config = utils.get_fake_openstack_config()
def test_config_download(self):
m_get = self.m_request.get(
'/api/v1/openstack-config/42/', json=self.config)
m_open = mock.mock_open()
with mock.patch('fuelclient.cli.serializers.open',
m_open, create=True):
self.execute(['fuel', 'openstack-config',
'--config-id', '42', '--download',
'--file', 'config.yaml'])
self.assertTrue(m_get.called)
content = m_open().write.mock_calls[0][1][0]
content = yaml.safe_load(content)
self.assertEqual(self.config['configuration'],
content['configuration'])
def test_config_upload(self):
m_post = self.m_request.post(
'/api/v1/openstack-config/', json=self.config)
m_open = mock.mock_open(read_data=yaml.safe_dump(
{'configuration': self.config['configuration']}))
with mock.patch('fuelclient.cli.serializers.open',
m_open, create=True):
with mock.patch('fuelclient.objects.openstack_config.os'):
self.execute(['fuel', 'openstack-config', '--env', '1',
'--upload', '--file', 'config.yaml'])
self.assertTrue(m_post.called)
def test_config_list(self):
m_get = self.m_request.get(
'/api/v1/openstack-config/?cluster_id=84', json=[
utils.get_fake_openstack_config(id=1, cluster_id=32),
utils.get_fake_openstack_config(id=2, cluster_id=64)
])
self.execute(['fuel', 'openstack-config', '--env', '84', '--list'])
self.assertTrue(m_get.called)
def test_config_list_w_filters(self):
m_get = self.m_request.get(
'/api/v1/openstack-config/?cluster_id=84&node_role=controller',
json=[utils.get_fake_openstack_config(id=1, cluster_id=32)])
self.execute(['fuel', 'openstack-config', '--env', '84',
'--role', 'controller', '--list'])
self.assertTrue(m_get.called)
m_get = self.m_request.get(
'/api/v1/openstack-config/?cluster_id=84&node_id=42', json=[
utils.get_fake_openstack_config(id=1, cluster_id=32),
])
self.execute(['fuel', 'openstack-config', '--env', '84',
'--node', '42', '--list'])
self.assertTrue(m_get.called)
def test_config_delete(self):
m_del = self.m_request.delete(
'/api/v1/openstack-config/42/', json={})
self.execute(['fuel', 'openstack-config',
'--config-id', '42', '--delete'])
self.assertTrue(m_del.called)
def test_config_execute(self):
m_put = self.m_request.put('/api/v1/openstack-config/execute/',
json={'status': 'ready'})
self.execute(['fuel', 'openstack-config', '--env', '42', '--execute'])
self.assertTrue(m_put.called)
def test_config_execute_fail(self):
message = 'Some error'
m_put = self.m_request.put(
'/api/v1/openstack-config/execute/',
json={'status': 'error', 'message': message})
with mock.patch("sys.stdout") as m_stdout:
self.execute(['fuel', 'openstack-config',
'--env', '42', '--execute'])
self.assertTrue(m_put.called)
self.assertIn(message, m_stdout.write.call_args_list[0][0][0])

View File

@ -24,6 +24,8 @@ from fuelclient.tests.utils.fake_fuel_version import get_fake_fuel_version
from fuelclient.tests.utils.fake_task import get_fake_task
from fuelclient.tests.utils.fake_node_group import get_fake_node_group
from fuelclient.tests.utils.fake_node_group import get_fake_node_groups
from fuelclient.tests.utils.fake_openstack_config \
import get_fake_openstack_config
__all__ = (get_fake_env,
@ -35,4 +37,5 @@ __all__ = (get_fake_env,
get_fake_task,
random_string,
get_fake_node_group,
get_fake_node_groups)
get_fake_node_groups,
get_fake_openstack_config)

View File

@ -0,0 +1,39 @@
# Copyright 2015 Mirantis, Inc.
#
# 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.
def get_fake_openstack_config(
id=None, config_type=None, cluster_id=None, node_id=None,
node_role=None, configuration=None):
config = {
'id': id or 42,
'is_active': True,
'config_type': config_type or 'cluster',
'cluster_id': cluster_id or 84,
'node_id': node_id or None,
'node_role': node_role or None,
'configuration': configuration or {
'nova_config': {
'DEFAULT/debug': {
'value': True,
},
},
'keystone_config': {
'DEFAULT/debug': {
'value': True,
},
},
},
}
return config