Add new CLI commands for managing cluster networks and settings

This patch introduces the following commands to the new fuel2
CLI:

  - fuel2 env network download
  - fuel2 env network upload
  - fuel2 env network verify
  - fuel2 env settings download
  - fuel2 env settings upload

DocImpact
Change-Id: Iab92de0f0dbc453d5ea36f83d10252911e37d163
This commit is contained in:
Roman Prykhodchenko 2016-07-19 16:51:32 +02:00
parent 2b1ebf7016
commit d1ce60ba62
11 changed files with 579 additions and 68 deletions

View File

@ -13,6 +13,7 @@
# under the License.
import abc
import os
from cliff import command
from cliff import lister
@ -30,6 +31,29 @@ VERSION = 'v1'
class BaseCommand(command.Command):
"""Base Fuel Client command."""
def get_attributes_path(self, attr_type, file_format, ent_id, directory):
"""Returnes a path for attributes of an entity
:param attr_type: Type of the attribute, e. g., disks, networks.
:param file_format: The format of the file that contains or will
contain the attributes, e. g., json or yaml.
:param ent_id: Id of an entity
:param directory: Directory that is used to store attributes.
"""
if attr_type not in self.allowed_attr_types:
raise ValueError('attr_type must be '
'one of {}'.format(self.allowed_attr_types))
if file_format not in self.supported_file_formats:
raise ValueError('file_format must be '
'one of {}'.format(self.supported_file_formats))
return os.path.join(os.path.abspath(directory),
'{ent}_{id}'.format(ent=self.entity_name,
id=ent_id),
'{}.{}'.format(attr_type, file_format))
def __init__(self, *args, **kwargs):
super(BaseCommand, self).__init__(*args, **kwargs)
self.client = fuelclient.get_client(self.entity_name, VERSION)
@ -39,6 +63,14 @@ class BaseCommand(command.Command):
"""Name of the Fuel entity."""
pass
@property
def supported_file_formats(self):
raise NotImplemented()
@property
def allowed_attr_types(self):
raise NotImplemented()
@six.add_metaclass(abc.ABCMeta)
class BaseListCommand(lister.Lister, BaseCommand):

View File

@ -12,8 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
from cliff import show
import abc
import functools
import os
from cliff import show
from oslo_utils import fileutils
import six
from fuelclient.cli import error
from fuelclient.commands import base
from fuelclient.common import data_utils
@ -21,6 +28,122 @@ from fuelclient.common import data_utils
class EnvMixIn(object):
entity_name = 'environment'
supported_file_formats = ('json', 'yaml')
allowed_attr_types = ('network', 'settings')
@six.add_metaclass(abc.ABCMeta)
class BaseUploadCommand(EnvMixIn, base.BaseCommand):
@abc.abstractproperty
def uploader(self):
pass
@abc.abstractproperty
def attribute(self):
pass
def get_parser(self, prog_name):
parser = super(BaseUploadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of environment.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{}.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Source directory. Defaults to the '
'current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
with open(file_path, 'r') as stream:
attribute = data_utils.safe_load(parsed_args.format, stream)
except (IOError, OSError):
msg = 'Could not read configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
self.uploader(parsed_args.id, attribute)
msg = ('Configuration of {t} for the environment with id '
'{env} was loaded from {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
env=parsed_args.id,
path=file_path))
@six.add_metaclass(abc.ABCMeta)
class BaseDownloadCommand(EnvMixIn, base.BaseCommand):
@abc.abstractproperty
def downloader(self):
pass
@abc.abstractproperty
def attribute(self):
pass
def get_parser(self, prog_name):
parser = super(BaseDownloadCommand, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of an environment.')
parser.add_argument('-f',
'--format',
required=True,
choices=self.supported_file_formats,
help='Format of serialized '
'{}.'.format(self.attribute))
parser.add_argument('-d',
'--directory',
required=False,
default=os.curdir,
help='Destination directory. Defaults to the '
'current directory.')
return parser
def take_action(self, parsed_args):
directory = parsed_args.directory or os.curdir
attributes = self.downloader(parsed_args.id)
file_path = self.get_attributes_path(self.attribute,
parsed_args.format,
parsed_args.id,
directory)
try:
fileutils.ensure_tree(os.path.dirname(file_path))
fileutils.delete_if_exists(file_path)
with open(file_path, 'w') as stream:
data_utils.safe_dump(parsed_args.format, stream, attributes)
except (IOError, OSError):
msg = 'Could not store configuration of {} at {}.'
raise error.InvalidFileException(msg.format(self.attribute,
file_path))
msg = ('Configuration of {t} for the environment with id '
'{env} was stored in {path}\n')
self.app.stdout.write(msg.format(t=self.attribute,
env=parsed_args.id,
path=file_path))
class EnvList(EnvMixIn, base.BaseListCommand):
"""Show list of all available environments."""
@ -355,3 +478,77 @@ class EnvSpawnVms(EnvMixIn, base.BaseCommand):
def take_action(self, parsed_args):
return self.client.spawn_vms(parsed_args.id)
class EnvNetworkVerify(EnvMixIn, base.BaseCommand):
"""Run network verification for specified environment."""
def get_parser(self, prog_name):
parser = super(EnvNetworkVerify, self).get_parser(prog_name)
parser.add_argument('id',
type=int,
help='Id of the environment to verify network.')
return parser
def take_action(self, parsed_args):
task = self.client.verify_network(parsed_args.id)
msg = 'Network verification task with id {t} for the environment {e} '\
'has been started.\n'.format(t=task['id'], e=parsed_args.id)
self.app.stdout.write(msg)
class EnvNetworkUpload(BaseUploadCommand):
"""Upload network configuration and apply it to an environment."""
attribute = 'network'
@property
def uploader(self):
return self.client.set_network_configuration
class EnvNetworkDownload(BaseDownloadCommand):
"""Download and store network configuration of an environment."""
attribute = 'network'
@property
def downloader(self):
return self.client.get_network_configuration
class EnvSettingsUpload(BaseUploadCommand):
"""Upload and apply environment settings."""
attribute = 'settings'
@property
def uploader(self):
return functools.partial(self.client.set_settings,
force=self.force_flag)
def get_parser(self, prog_name):
parser = super(EnvSettingsUpload, self).get_parser(prog_name)
parser.add_argument('--force',
action='store_true',
help='Force applying the settings.')
return parser
def take_action(self, parsed_args):
self.force_flag = parsed_args.force
super(EnvSettingsUpload, self).take_action(parsed_args)
class EnvSettingsDownload(BaseDownloadCommand):
"""Download and store environment settings."""
attribute = 'settings'
@property
def downloader(self):
return self.client.get_settings

View File

@ -44,29 +44,6 @@ class NodeMixIn(object):
numa_topology_info[key] = numa_topology.get(key)
return numa_topology_info
def get_attributes_path(self, attr_type, file_format, node_id, directory):
"""Returnes a path for attributes of a node
:param attr_type: Attribute type.
Should be one of {attributes, disks, interfaces}
:param file_format: The format of the file that contains or will
contain the attributes. Must be json or yaml.
:param node_id: Id of a node
:param directory: Directory that is used to store attributes.
"""
if attr_type not in self.allowed_attr_types:
raise ValueError('attr_type must be '
'one of {}'.format(self.allowed_attr_types))
if file_format not in self.supported_file_formats:
raise ValueError('file_format must be '
'one of {}'.format(self.supported_file_formats))
return os.path.join(os.path.abspath(directory),
'node_{0}'.format(node_id),
'{}.{}'.format(attr_type, file_format))
class NodeList(NodeMixIn, base.BaseListCommand):
"""Show list of all available nodes."""

View File

@ -22,6 +22,7 @@ from oslotest import base as oslo_base
import fuelclient
from fuelclient.commands import environment as env
from fuelclient.commands import node as node
from fuelclient import main as main_mod
from fuelclient.tests import utils
@ -129,3 +130,42 @@ class BaseCLITest(oslo_base.BaseTestCase):
self.assertEqual('tname', passed_settings.os_username)
self.assertEqual('tpass', passed_settings.os_password)
self.assertEqual('tten', passed_settings.os_tenant_name)
def test_get_attribute_path(self):
cmd = node.NodeShow(None, None)
attr_types = ('attributes', 'interfaces', 'disks')
file_format = 'json'
node_id = 42
directory = '/test'
for attr_type in attr_types:
expected_path = '/test/node_42/{t}.json'.format(t=attr_type)
real_path = cmd.get_attributes_path(attr_type, file_format,
node_id, directory)
self.assertEqual(expected_path, real_path)
def test_get_attribute_path_wrong_attr_type(self):
cmd = node.NodeShow(None, None)
attr_type = 'wrong'
file_format = 'json'
node_id = 42
directory = '/test'
self.assertRaises(ValueError,
cmd.get_attributes_path,
attr_type, file_format, node_id, directory)
def test_get_attribute_path_wrong_file_format(self):
cmd = node.NodeShow(None, None)
attr_type = 'interfaces'
file_format = 'wrong'
node_id = 42
directory = '/test'
self.assertRaises(ValueError,
cmd.get_attributes_path,
attr_type, file_format, node_id, directory)

View File

@ -15,8 +15,10 @@
# under the License.
import json
import mock
from six import moves
import yaml
from fuelclient.tests.unit.v2.cli import test_engine
from fuelclient.tests.utils import fake_env
@ -200,11 +202,9 @@ class TestEnvCommand(test_engine.BaseCLITest):
args = ('env nodes deploy '
'--nodes {n[0]} {n[1]} --env {e}').format(e=env_id, n=node_ids)
self.m_client.deploy_nodes.return_value = fake_task.get_fake_task()
self.exec_command(args)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.deploy_nodes.return_value = fake_task.get_fake_task()
self.m_client.deploy_nodes.assert_called_once_with(env_id, node_ids)
def test_env_nodes_provision(self):
@ -219,3 +219,166 @@ class TestEnvCommand(test_engine.BaseCLITest):
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.provision_nodes.assert_called_once_with(env_id, node_ids)
def test_env_network_verify(self):
env_id = 42
args = 'env network verify {}'.format(env_id)
self.exec_command(args)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.verify_network.assert_called_once_with(env_id)
@mock.patch('json.dump')
def test_env_network_download_json(self, m_dump):
args = 'env network download --format json -d /tmp 42'
test_data = {'foo': 'bar'}
expected_path = '/tmp/environment_42/network.json'
self.m_client.get_network_configuration.return_value = test_data
m_open = mock.mock_open()
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'w')
m_dump.assert_called_once_with(test_data, mock.ANY, indent=4)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.get_network_configuration.assert_called_once_with(42)
def test_env_network_upload_json(self):
args = 'env network upload --format json -d /tmp 42'
config = {'foo': 'bar'}
expected_path = '/tmp/environment_42/network.json'
m_open = mock.mock_open(read_data=json.dumps(config))
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'r')
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.set_network_configuration.assert_called_once_with(42,
config)
@mock.patch('yaml.safe_dump')
def test_env_network_download_yaml(self, m_safe_dump):
args = 'env network download --format yaml -d /tmp 42'
test_data = {'foo': 'bar'}
expected_path = '/tmp/environment_42/network.yaml'
self.m_client.get_network_configuration.return_value = test_data
m_open = mock.mock_open()
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'w')
m_safe_dump.assert_called_once_with(test_data, mock.ANY,
default_flow_style=False)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.get_network_configuration.assert_called_once_with(42)
def test_env_network_upload_yaml(self):
args = 'env network upload --format yaml -d /tmp 42'
config = {'foo': 'bar'}
expected_path = '/tmp/environment_42/network.yaml'
m_open = mock.mock_open(read_data=yaml.dump(config))
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'r')
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.set_network_configuration.assert_called_once_with(42,
config)
@mock.patch('json.dump')
def test_env_settings_download_json(self, m_dump):
args = 'env settings download --format json -d /tmp 42'
test_data = {'foo': 'bar'}
expected_path = '/tmp/environment_42/settings.json'
self.m_client.get_settings.return_value = test_data
m_open = mock.mock_open()
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'w')
m_dump.assert_called_once_with(test_data, mock.ANY, indent=4)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.get_settings.assert_called_once_with(42)
def test_env_settings_upload_json(self):
args = 'env settings upload --format json -d /tmp 42'
config = {'foo': 'bar'}
expected_path = '/tmp/environment_42/settings.json'
m_open = mock.mock_open(read_data=json.dumps(config))
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'r')
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.set_settings.assert_called_once_with(42,
config,
force=False)
@mock.patch('yaml.safe_dump')
def test_env_settings_download_yaml(self, m_safe_dump):
args = 'env settings download --format yaml -d /tmp 42'
test_data = {'foo': 'bar'}
expected_path = '/tmp/environment_42/settings.yaml'
self.m_client.get_settings.return_value = test_data
m_open = mock.mock_open()
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'w')
m_safe_dump.assert_called_once_with(test_data, mock.ANY,
default_flow_style=False)
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.get_settings.assert_called_once_with(42)
def test_env_settings_upload_yaml(self):
args = 'env settings upload --format yaml -d /tmp 42'
config = {'foo': 'bar'}
expected_path = '/tmp/environment_42/settings.yaml'
m_open = mock.mock_open(read_data=yaml.dump(config))
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'r')
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.set_settings.assert_called_once_with(42,
config,
force=False)
def test_env_settings_upload_force(self):
args = 'env settings upload --format yaml -d /tmp --force 42'
config = {'foo': 'bar'}
expected_path = '/tmp/environment_42/settings.yaml'
m_open = mock.mock_open(read_data=yaml.dump(config))
with mock.patch('fuelclient.commands.environment.open',
m_open, create=True):
self.exec_command(args)
m_open.assert_called_once_with(expected_path, 'r')
self.m_get_client.assert_called_once_with('environment', mock.ANY)
self.m_client.set_settings.assert_called_once_with(42,
config,
force=True)

View File

@ -374,44 +374,3 @@ node-4 ansible_host=10.20.0.5
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.upload_attributes.assert_called_once_with(42, None)
class TestNodeMixIn(test_engine.BaseCLITest):
def test_get_attribute_path(self):
mixin = cmd_node.NodeMixIn()
attr_types = ('attributes', 'interfaces', 'disks')
file_format = 'json'
node_id = 42
directory = '/test'
for attr_type in attr_types:
expected_path = '/test/node_42/{t}.json'.format(t=attr_type)
real_path = mixin.get_attributes_path(attr_type, file_format,
node_id, directory)
self.assertEqual(expected_path, real_path)
def test_get_attribute_path_wrong_attr_type(self):
mixin = cmd_node.NodeMixIn()
attr_type = 'wrong'
file_format = 'json'
node_id = 42
directory = '/test'
self.assertRaises(ValueError,
mixin.get_attributes_path,
attr_type, file_format, node_id, directory)
def test_get_attribute_path_wrong_file_format(self):
mixin = cmd_node.NodeMixIn()
attr_type = 'interfaces'
file_format = 'wrong'
node_id = 42
directory = '/test'
self.assertRaises(ValueError,
mixin.get_attributes_path,
attr_type, file_format, node_id, directory)

View File

@ -32,6 +32,9 @@ class TestEnvFacade(test_api.BaseLibTest):
self.version = 'v1'
self.res_uri = '/api/{version}/clusters/'.format(version=self.version)
self.net_conf_uri = '/network_configuration/neutron'
self.settings_uri = '/attributes'
self.net_verify_uri = '/network_configuration/neutron/verify'
self.fake_env = utils.get_fake_env()
self.fake_envs = [utils.get_fake_env() for i in range(10)]
@ -286,3 +289,106 @@ class TestEnvFacade(test_api.BaseLibTest):
self.assertTrue(matcher.called)
self.assertEqual([','.join(str(i) for i in node_ids)],
matcher.last_request.qs['nodes'])
def test_env_network_verify(self):
env_id = 42
fake_env = utils.get_fake_env(env_id=env_id)
test_conf = utils.get_fake_env_network_conf()
env_uri = self.get_object_uri(self.res_uri, env_id)
download_uri = self.get_object_uri(self.res_uri,
env_id,
self.net_conf_uri)
verify_uri = self.get_object_uri(self.res_uri,
env_id,
self.net_verify_uri)
m_get = self.m_request.get(env_uri, json=fake_env)
m_download = self.m_request.get(download_uri, json=test_conf)
m_verify = self.m_request.put(verify_uri, json=utils.get_fake_task())
self.client.verify_network(env_id)
self.assertTrue(m_get.called)
self.assertTrue(m_download.called)
self.assertTrue(m_verify.called)
self.assertEqual(test_conf, m_verify.last_request.json())
def test_env_network_download(self):
env_id = 42
fake_env = utils.get_fake_env(env_id=env_id)
env_uri = self.get_object_uri(self.res_uri, env_id)
download_uri = self.get_object_uri(self.res_uri,
env_id,
self.net_conf_uri)
test_conf = utils.get_fake_env_network_conf()
m_get = self.m_request.get(env_uri, json=fake_env)
m_download = self.m_request.get(download_uri, json=test_conf)
net_conf = self.client.get_network_configuration(env_id)
self.assertEqual(test_conf, net_conf)
self.assertTrue(m_get.called)
self.assertTrue(m_download.called)
def test_env_network_upload(self):
env_id = 42
fake_env = utils.get_fake_env(env_id=env_id)
env_uri = self.get_object_uri(self.res_uri, env_id)
upload_uri = self.get_object_uri(self.res_uri,
env_id,
self.net_conf_uri)
test_conf = utils.get_fake_env_network_conf()
m_get = self.m_request.get(env_uri, json=fake_env)
m_upload = self.m_request.put(upload_uri, json={})
self.client.set_network_configuration(env_id, test_conf)
self.assertTrue(m_get.called)
self.assertTrue(m_upload.called)
self.assertEqual(test_conf, m_upload.last_request.json())
def test_env_settings_download(self):
env_id = 42
download_uri = self.get_object_uri(self.res_uri,
env_id,
self.settings_uri)
test_settings = {'test-data': 42}
m_download = self.m_request.get(download_uri, json=test_settings)
settings = self.client.get_settings(env_id)
self.assertEqual(test_settings, settings)
self.assertTrue(m_download.called)
def test_env_settings_upload(self):
env_id = 42
upload_uri = self.get_object_uri(self.res_uri,
env_id,
self.settings_uri)
test_settings = {'test-data': 42}
m_upload = self.m_request.put(upload_uri, json={})
self.client.set_settings(env_id, test_settings)
self.assertTrue(m_upload.called)
self.assertEqual(test_settings, m_upload.last_request.json())
def test_env_settings_upload_force(self):
env_id = 42
upload_uri = self.get_object_uri(self.res_uri,
env_id,
self.settings_uri)
test_settings = {'test-data': 42}
m_upload = self.m_request.put(upload_uri, json={})
self.client.set_settings(env_id, test_settings, force=True)
self.assertTrue(m_upload.called)
self.assertEqual(test_settings, m_upload.last_request.json())
self.assertEqual(['1'], m_upload.last_request.qs.get('force'))

View File

@ -21,6 +21,8 @@ from fuelclient.tests.utils.fake_additional_info \
import get_fake_yaml_deployment_info
from fuelclient.tests.utils.fake_additional_info \
import get_fake_yaml_network_conf
from fuelclient.tests.utils.fake_additional_info \
import get_fake_env_network_conf
from fuelclient.tests.utils.fake_deployment_history \
import get_fake_deployment_history
from fuelclient.tests.utils.fake_deployment_history \
@ -51,6 +53,7 @@ __all__ = (get_fake_deployment_history,
get_fake_yaml_deployment_info,
get_fake_yaml_network_conf,
get_fake_env,
get_fake_env_network_conf,
get_fake_release,
get_fake_releases,
get_fake_attributes_metadata,

View File

@ -14,6 +14,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import yaml
CLUSTER_SETTINGS = '''---
editable:
service_user:
@ -88,3 +91,7 @@ def get_fake_yaml_network_conf():
"""
return NETWORK_CONF
def get_fake_env_network_conf():
return yaml.load(get_fake_yaml_network_conf())

View File

@ -138,6 +138,28 @@ class EnvironmentClient(base_v1.BaseV1Client):
env = self._entity_wrapper(environment_id)
env.delete_network_template_data()
def get_network_configuration(self, environment_id):
env = self._entity_wrapper(environment_id)
return env.get_network_data()
def set_network_configuration(self, environment_id, new_configuration):
env = self._entity_wrapper(environment_id)
env.set_network_data(new_configuration)
def verify_network(self, environment_id):
"""Start network verification for an environment."""
env = self._entity_wrapper(environment_id)
return env.verify_network()
def get_settings(self, environment_id):
env = self._entity_wrapper(environment_id)
return env.get_settings_data()
def set_settings(self, environment_id, new_configuration, force=False):
env = self._entity_wrapper(environment_id)
env.set_settings_data(new_configuration, force=force)
def get_client(connection):
return EnvironmentClient(connection)

View File

@ -36,8 +36,13 @@ fuelclient =
env_nodes_deploy=fuelclient.commands.environment:EnvDeployNodes
env_list=fuelclient.commands.environment:EnvList
env_nodes_provision=fuelclient.commands.environment:EnvProvisionNodes
env_network_download=fuelclient.commands.environment:EnvNetworkDownload
env_network_upload=fuelclient.commands.environment:EnvNetworkUpload
env_network_verify=fuelclient.commands.environment:EnvNetworkVerify
env_redeploy=fuelclient.commands.environment:EnvRedeploy
env_remove_nodes=fuelclient.commands.environment:EnvRemoveNodes
env_settings_download=fuelclient.commands.environment:EnvSettingsDownload
env_settings_upload=fuelclient.commands.environment:EnvSettingsUpload
env_show=fuelclient.commands.environment:EnvShow
env_spawn-vms=fuelclient.commands.environment:EnvSpawnVms
env_update=fuelclient.commands.environment:EnvUpdate