diff --git a/fuelclient/__init__.py b/fuelclient/__init__.py index 07d1bc0c..d9c92b88 100644 --- a/fuelclient/__init__.py +++ b/fuelclient/__init__.py @@ -78,6 +78,7 @@ def get_client(resource, version='v1', connection=None): 'sequence': v1.sequence, 'snapshot': v1.snapshot, 'task': v1.task, + 'tag': v1.tag, 'vip': v1.vip } } diff --git a/fuelclient/commands/node.py b/fuelclient/commands/node.py index f878e40e..cc552e48 100644 --- a/fuelclient/commands/node.py +++ b/fuelclient/commands/node.py @@ -168,16 +168,22 @@ class BaseDownloadCommand(NodeMixIn, base.BaseCommand): class NodeList(NodeMixIn, base.BaseListCommand): """Show list of all available nodes.""" - columns = ('id', - 'name', - 'status', - 'os_platform', - 'roles', - 'ip', - 'mac', - 'cluster', - 'platform_name', - 'online') + _columns = ( + 'id', + 'name', + 'status', + 'os_platform', + 'ip', + 'mac', + 'cluster', + 'platform_name', + 'online') + + @property + def columns(self): + if self.app.is_advanced_mode: + return self._columns + ('tags',) + return self._columns + ('pending_roles', 'roles') def get_parser(self, prog_name): parser = super(NodeList, self).get_parser(prog_name) @@ -187,7 +193,6 @@ class NodeList(NodeMixIn, base.BaseListCommand): '--env', type=int, help='Show only nodes that are in the specified environment') - parser.add_argument( '-l', '--labels', @@ -202,35 +207,40 @@ class NodeList(NodeMixIn, base.BaseListCommand): environment_id=parsed_args.env, labels=parsed_args.labels) data = data_utils.get_display_data_multi(self.columns, data) - return (self.columns, data) + return self.columns, data class NodeShow(NodeMixIn, base.BaseShowCommand): """Show info about node with given id.""" - columns = ('id', - 'name', - 'status', - 'os_platform', - 'roles', - 'kernel_params', - 'pending_roles', - 'ip', - 'mac', - 'error_type', - 'pending_addition', - 'hostname', - 'fqdn', - 'platform_name', - 'cluster', - 'online', - 'progress', - 'pending_deletion', - 'group_id', - # TODO(romcheg): network_data mostly never fits the screen - # 'network_data', - 'manufacturer') - columns += NodeMixIn.numa_fields + _columns = ( + 'id', + 'name', + 'status', + 'os_platform', + 'kernel_params', + 'ip', + 'mac', + 'error_type', + 'pending_addition', + 'hostname', + 'fqdn', + 'platform_name', + 'cluster', + 'online', + 'progress', + 'pending_deletion', + 'group_id', + # TODO(romcheg): network_data mostly never fits the screen + # 'network_data', + 'manufacturer') + _columns += NodeMixIn.numa_fields + + @property + def columns(self): + if self.app.is_advanced_mode: + return self._columns + ('tags',) + return self._columns + ('pending_roles', 'roles') def take_action(self, parsed_args): data = self.client.get_by_id(parsed_args.id) @@ -240,9 +250,8 @@ class NodeShow(NodeMixIn, base.BaseShowCommand): return self.columns, data -class NodeUpdate(NodeMixIn, base.BaseShowCommand): +class NodeUpdate(NodeShow): """Change given attributes for a node.""" - columns = NodeShow.columns def get_parser(self, prog_name): diff --git a/fuelclient/commands/tag.py b/fuelclient/commands/tag.py new file mode 100644 index 00000000..fd9d47e4 --- /dev/null +++ b/fuelclient/commands/tag.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 abc +import argparse +from cliff import show +import os + +from oslo_utils import fileutils +import six + +from fuelclient.cli import error +from fuelclient.commands import base +from fuelclient.common import data_utils +from fuelclient import utils + + +class TagMixIn(object): + + entity_name = 'tag' + supported_file_formats = ('json', 'yaml') + fields_mapper = ( + ('env', 'clusters'), + ('release', 'releases'), + ('plugin', 'plugins'), + ) + + def parse_model(self, args): + for param, tag_class in self.fields_mapper: + model_id = getattr(args, param) + if model_id: + return tag_class, model_id + + @staticmethod + def check_file_path(file_path): + if not utils.file_exists(file_path): + raise argparse.ArgumentTypeError( + 'File "{0}" does not exist.'.format(file_path)) + return file_path + + @staticmethod + def get_file_path(directory, tag_id, file_format): + return os.path.join(os.path.abspath(directory), + 'tag_{}.{}'.format(tag_id, file_format)) + + @staticmethod + def read_tag_data(file_format, file_path): + try: + with open(file_path, 'r') as stream: + data = data_utils.safe_load(file_format, stream) + except (OSError, IOError): + msg = "Could not read tag's description at {}.".format(file_path) + raise error.InvalidFileException(msg) + return data + + +class TagShow(TagMixIn, base.BaseShowCommand): + """Show single tag by id.""" + columns = ("id", "tag", "has_primary") + + +class TagList(TagMixIn, base.BaseListCommand): + """Show list of available tags for release, cluster or plugin.""" + columns = TagShow.columns + + def get_parser(self, prog_name): + parser = super(TagList, self).get_parser(prog_name) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + '-r', + '--release', + type=int, + help='release id') + group.add_argument( + '-e', + '--env', + type=int, + help='environment id') + group.add_argument( + '-p', + '--plugin', + type=int, + help='plugin id') + + return parser + + def take_action(self, parsed_args): + model, model_id = self.parse_model(parsed_args) + data = self.client.get_all(model, model_id) + display_data = data_utils.get_display_data_multi(self.columns, data) + return self.columns, display_data + + +class TagDownload(TagMixIn, base.BaseCommand): + """Download full tag description to file.""" + + def get_parser(self, prog_name): + parser = super(TagDownload, self).get_parser(prog_name) + parser.add_argument('-t', + '--tag_id', + type=int, + required=True, + help='Id of the tag.') + parser.add_argument('-f', + '--format', + required=True, + choices=self.supported_file_formats, + help='Format of serialized tag description.') + parser.add_argument('-d', + '--directory', + required=False, + default=os.path.curdir, + help='Destination. Defaults to ' + 'the current directory.') + return parser + + def take_action(self, parsed_args): + file_path = self.get_file_path(parsed_args.directory, + parsed_args.tag_id, + parsed_args.format) + data = self.client.get_by_id(parsed_args.tag_id) + 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, data) + except (OSError, IOError): + msg = ("Could not store description data " + "for tag {} at {}".format(parsed_args.tag_id, file_path)) + raise error.InvalidFileException(msg) + + msg = ("Description data of tag with id '{}'" + " was stored in {}\n".format(parsed_args.tag_id, + file_path)) + self.app.stdout.write(msg) + + +@six.add_metaclass(abc.ABCMeta) +class BaseTagUploader(TagMixIn, base.BaseShowCommand): + """Upload a tag data from file.""" + columns = TagShow.columns + + @abc.abstractmethod + def upload(self, parsed_args, data): + """String with the name of the action.""" + pass + + def get_parser(self, prog_name): + parser = show.ShowOne.get_parser(self, prog_name) + parser.add_argument( + '--file_path', + required=True, + type=self.check_file_path, + help="Full path to the file in {} format that contains tag's " + "data.".format("/".join(self.supported_file_formats))) + + return parser + + def take_action(self, parsed_args): + file_path = parsed_args.file_path + file_format = os.path.splitext(file_path)[1].lstrip('.') + + data = self.read_tag_data(file_format, file_path) + display_data = data_utils.get_display_data_single( + self.columns, + self.upload(parsed_args, data)) + + return self.columns, display_data + + +class TagUpdate(BaseTagUploader): + """Update a tag from file.""" + + def upload(self, parsed_args, data): + return self.client.update(parsed_args.tag_id, data) + + def get_parser(self, prog_name): + parser = super(TagUpdate, self).get_parser(prog_name) + parser.add_argument( + '-t', + '--tag_id', + type=int, + required=True, + help='Id of the tag.') + + return parser + + +class TagCreate(BaseTagUploader): + """Create a tag from file.""" + + def upload(self, parsed_args, data): + model, model_id = self.parse_model(parsed_args) + return self.client.create(model, model_id, data) + + def get_parser(self, prog_name): + parser = super(TagCreate, self).get_parser(prog_name) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + '-r', + '--release', + type=int, + help='release id') + group.add_argument( + '-e', + '--env', + type=int, + help='environment id') + group.add_argument( + '-p', + '--plugin', + type=int, + help='plugin id') + + return parser + + +class TagDelete(base.BaseDeleteCommand): + """Delete a tag by id.""" + + entity_name = 'tag' + + +@six.add_metaclass(abc.ABCMeta) +class BaseTagAssignee(TagMixIn, base.BaseCommand): + """Base class for tags assignment.""" + + @abc.abstractproperty + def action(self): + """String with the name of the action.""" + pass + + @abc.abstractproperty + def assignment_method(self): + """Assignment method.""" + pass + + def get_parser(self, prog_name): + parser = super(BaseTagAssignee, self).get_parser(prog_name) + parser.add_argument('-t', + '--tags', + type=str, + nargs='+', + required=True, + help='List of tags to be {} ' + 'node.'.format(self.action)) + parser.add_argument('-n', + '--node', + type=int, + required=True, + help='Id of the node.') + + return parser + + def take_action(self, parsed_args): + self.assignment_method(node=parsed_args.node, + tag_ids=parsed_args.tags) + + self.app.stdout.write('Tags {t} were {a} the node {n}.' + '\n'.format(t=parsed_args.tags, + a=self.action, + n=parsed_args.node)) + + +class TagAssign(BaseTagAssignee): + """Assign tags to the node.""" + + action = 'assigned to' + + @property + def assignment_method(self): + return self.client.assign + + +class TagUnassign(BaseTagAssignee): + """Unassign tags from the node.""" + + action = 'unassigned from' + + @property + def assignment_method(self): + return self.client.unassign diff --git a/fuelclient/consts.py b/fuelclient/consts.py index f76aebc0..5e179b46 100644 --- a/fuelclient/consts.py +++ b/fuelclient/consts.py @@ -32,3 +32,8 @@ TASK_STATUSES = Enum( 'ready', 'running' ) + +CLIENT_MODES = Enum( + 'advanced', + 'simple' +) diff --git a/fuelclient/fuel_client.yaml b/fuelclient/fuel_client.yaml index c89b3f3d..473174ad 100644 --- a/fuelclient/fuel_client.yaml +++ b/fuelclient/fuel_client.yaml @@ -9,6 +9,9 @@ OS_PASSWORD: OS_TENANT_NAME: "admin" HTTP_PROXY: null HTTP_TIMEOUT: 10 +# application mode +# possible options: 'advanced', 'simple' +MODE: 'simple' # Performance tests settings PERFORMANCE_PROFILING_TESTS: 0 diff --git a/fuelclient/main.py b/fuelclient/main.py index 9843552f..a39906e2 100644 --- a/fuelclient/main.py +++ b/fuelclient/main.py @@ -18,6 +18,8 @@ import sys from cliff import app from cliff.commandmanager import CommandManager +from fuelclient.cli import error +from fuelclient import consts from fuelclient import fuelclient_settings from fuelclient import utils @@ -32,6 +34,19 @@ class FuelClient(app.App): configuration of basic engines. """ + _is_advanced_mode = None + + @property + def is_advanced_mode(self): + if self._is_advanced_mode is None: + self._is_advanced_mode = False + settings = fuelclient_settings.get_settings() + try: + if settings.MODE == consts.CLIENT_MODES.advanced: + self._is_advanced_mode = True + except error.SettingsException: + pass + return self._is_advanced_mode def build_option_parser(self, description, version, argparse_kwargs=None): """Overrides default options for backwards compatibility.""" diff --git a/fuelclient/objects/__init__.py b/fuelclient/objects/__init__.py index be03f9d3..157cccc9 100644 --- a/fuelclient/objects/__init__.py +++ b/fuelclient/objects/__init__.py @@ -28,6 +28,7 @@ from fuelclient.objects.role import Role from fuelclient.objects.task import DeployTask from fuelclient.objects.task import SnapshotTask from fuelclient.objects.task import Task +from fuelclient.objects.tag import Tag from fuelclient.objects.fuelversion import FuelVersion from fuelclient.objects.network_group import NetworkGroup from fuelclient.objects.plugins import Plugins diff --git a/fuelclient/objects/tag.py b/fuelclient/objects/tag.py new file mode 100644 index 00000000..fcf941d9 --- /dev/null +++ b/fuelclient/objects/tag.py @@ -0,0 +1,59 @@ +# 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.objects.base import BaseObject + + +class Tag(BaseObject): + + class_api_path = "{}/{}/tags/" + instance_api_path = "tags/{}/" + assign_api_path = "nodes/{}/tags/" + + def get_tag(self): + return self.connection.get_request( + self.instance_api_path.format(self.id)) + + def update_tag(self, data): + return self.connection.put_request( + self.instance_api_path.format(self.id), data) + + def delete_tag(self): + return self.connection.delete_request( + self.instance_api_path.format(self.id)) + + @classmethod + def get_all(cls, owner_type, owner_id): + return cls.connection.get_request( + cls.class_api_path.format(owner_type, owner_id)) + + @classmethod + def create_tag(cls, owner_type, owner_id, data): + return cls.connection.post_request( + cls.class_api_path.format(owner_type, owner_id), + data) + + @classmethod + def assign_tags(cls, tag_ids, node_id): + return cls.connection.post_request( + cls.assign_api_path.format(node_id), tag_ids) + + @classmethod + def unassign_tags(cls, tag_ids, node_id): + url = '{0}?tags={1}'.format( + cls.assign_api_path.format(node_id), + ','.join(map(str, tag_ids)) + ) + return cls.connection.delete_request(url) diff --git a/fuelclient/tests/unit/v2/cli/test_node.py b/fuelclient/tests/unit/v2/cli/test_node.py index dc3d87ab..01627e93 100644 --- a/fuelclient/tests/unit/v2/cli/test_node.py +++ b/fuelclient/tests/unit/v2/cli/test_node.py @@ -194,7 +194,9 @@ node-4 ansible_host=10.20.0.5 node.NodeClient._updatable_attributes node_id = 42 hostname = 'test-name' - expected_field_data = cmd_node.NodeShow.columns + + expected_field_data = cmd_node.NodeShow( + mock.Mock(is_advanced_mode=False), []).columns self.m_client.update.return_value = \ fake_node.get_fake_node(node_id=node_id, @@ -209,7 +211,7 @@ node-4 ansible_host=10.20.0.5 mock.ANY, mock.ANY) - self.m_get_client.assert_called_once_with('node', mock.ANY) + self.m_get_client.assert_called_with('node', mock.ANY) self.m_client.update.assert_called_once_with( node_id, hostname=hostname) @@ -218,7 +220,9 @@ node-4 ansible_host=10.20.0.5 self.m_client._updatable_attributes = \ node.NodeClient._updatable_attributes node_id = 37 - expected_field_data = cmd_node.NodeShow.columns + + expected_field_data = cmd_node.NodeShow( + mock.Mock(is_advanced_mode=False), []).columns test_cases = ('new-name', 'New Name', 'śćż∑ Pó', '你一定是无聊') for name in test_cases: @@ -238,7 +242,7 @@ node-4 ansible_host=10.20.0.5 mock.ANY, mock.ANY, mock.ANY) - self.m_get_client.assert_called_once_with('node', mock.ANY) + self.m_get_client.assert_called_with('node', mock.ANY) self.m_client.update.assert_called_once_with( node_id, name=name) self.m_get_client.reset_mock() diff --git a/fuelclient/tests/unit/v2/cli/test_tag.py b/fuelclient/tests/unit/v2/cli/test_tag.py new file mode 100644 index 00000000..5481c254 --- /dev/null +++ b/fuelclient/tests/unit/v2/cli/test_tag.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 json +import mock +import yaml + +from fuelclient.tests.unit.v2.cli import test_engine +from fuelclient.tests.utils import fake_tag + + +class TestTagCommand(test_engine.BaseCLITest): + """Tests for fuel2 tag * commands.""" + + def test_tag_list(self): + self.m_client.get_all.return_value = fake_tag.get_fake_tags(10) + args = 'tag list -r 2' + self.exec_command(args) + self.m_client.get_all.assert_called_once_with('releases', 2) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + + @mock.patch('json.dump') + def test_tag_download_json(self, m_dump): + tag_data = fake_tag.get_fake_tag() + tag_id = tag_data['id'] + args = 'tag download -t {} -f json -d /tmp'.format(tag_id) + expected_path = '/tmp/tag_{}.json'.format(tag_id) + + self.m_client.get_by_id.return_value = tag_data + + m_open = mock.mock_open() + with mock.patch('fuelclient.commands.tag.open', m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(expected_path, 'w') + m_dump.assert_called_once_with(tag_data, mock.ANY, indent=4) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.get_by_id.assert_called_once_with(tag_id) + + @mock.patch('yaml.safe_dump') + def test_tag_download_yaml(self, m_safe_dump): + tag_data = fake_tag.get_fake_tag() + tag_id = tag_data['id'] + args = 'tag download -t {} -f yaml -d /tmp'.format(tag_id) + expected_path = '/tmp/tag_{}.yaml'.format(tag_id) + + self.m_client.get_by_id.return_value = tag_data + + m_open = mock.mock_open() + with mock.patch('fuelclient.commands.tag.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(tag_data, mock.ANY, + default_flow_style=False) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.get_by_id.assert_called_once_with(tag_id) + + @mock.patch('fuelclient.utils.file_exists', return_value=True) + def test_tag_update_json(self, m_file_exists): + tag_data = fake_tag.get_fake_tag() + tag_id = tag_data['id'] + file_path = '/tmp/tag_{}.yaml'.format(tag_id) + args = 'tag update -t {} --file_path {}'.format(tag_id, file_path) + + m_open = mock.mock_open(read_data=json.dumps(tag_data)) + with mock.patch('fuelclient.commands.tag.open', m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(file_path, 'r') + m_file_exists.assert_called_once_with(file_path) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.update.assert_called_once_with(tag_id, tag_data) + + @mock.patch('fuelclient.utils.file_exists', return_value=True) + def test_tag_update_yaml(self, m_file_exists): + tag_data = fake_tag.get_fake_tag() + tag_id = tag_data['id'] + file_path = '/tmp/tag_{}.yaml'.format(tag_id) + args = 'tag update -t {} --file_path {}'.format(tag_id, file_path) + + m_open = mock.mock_open(read_data=yaml.safe_dump(tag_data)) + with mock.patch('fuelclient.commands.tag.open', m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(file_path, 'r') + m_file_exists.assert_called_once_with(file_path) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.update.assert_called_once_with(tag_id, tag_data) + + @mock.patch('fuelclient.utils.file_exists', return_value=True) + def test_tag_create_json(self, m_file_exists): + tag_data = fake_tag.get_fake_tag() + tag_path = "/tmp/tag22.json" + args = 'tag create -r 2 --file_path {}'.format(tag_path) + + m_open = mock.mock_open(read_data=json.dumps(tag_data)) + with mock.patch('fuelclient.commands.tag.open', m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(tag_path, 'r') + m_file_exists.assert_called_once_with(tag_path) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.create.assert_called_once_with('releases', 2, tag_data) + + @mock.patch('fuelclient.utils.file_exists', return_value=True) + def test_tag_create_yaml(self, m_file_exists): + tag_data = fake_tag.get_fake_tag() + tag_path = "/tmp/tag22.yaml" + args = 'tag create -r 2 --file_path {}'.format(tag_path) + + m_open = mock.mock_open(read_data=yaml.safe_dump(tag_data)) + with mock.patch('fuelclient.commands.tag.open', m_open, create=True): + self.exec_command(args) + + m_open.assert_called_once_with(tag_path, 'r') + m_file_exists.assert_called_once_with(tag_path) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.create.assert_called_once_with('releases', 2, tag_data) + + def test_tag_delete(self): + tag_id = 1 + args = 'tag delete {}'.format(tag_id) + + self.exec_command(args) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.delete_by_id.assert_called_once_with(tag_id) + + def test_tag_assign(self): + node_id = 1 + tag_ids = ['4', '5', '6'] + args = 'tag assign -n {} -t {}'.format(node_id, " ".join(tag_ids)) + + self.exec_command(args) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.assign.assert_called_once_with(node=node_id, + tag_ids=tag_ids) + + def test_tag_unassign(self): + node_id = 1 + tag_ids = ['4', '5', '6'] + args = 'tag unassign -n {} -t {}'.format(node_id, " ".join(tag_ids)) + + self.exec_command(args) + self.m_get_client.assert_called_once_with('tag', mock.ANY) + self.m_client.unassign.assert_called_once_with(node=node_id, + tag_ids=tag_ids) diff --git a/fuelclient/tests/unit/v2/lib/test_tag.py b/fuelclient/tests/unit/v2/lib/test_tag.py new file mode 100644 index 00000000..a0423efe --- /dev/null +++ b/fuelclient/tests/unit/v2/lib/test_tag.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Vitalii Kulanov +# +# 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 fuelclient +from fuelclient.tests.unit.v2.lib import test_api +from fuelclient.tests import utils + + +class TestTagFacade(test_api.BaseLibTest): + + def setUp(self): + super(TestTagFacade, self).setUp() + self.owner_map = { + 'release': 'releases', + 'cluster': 'clusters', + 'plugin': 'plugins' + } + self.version = 'v1' + self.res_uri = '/api/{version}/'.format( + version=self.version) + self.fake_tag = utils.get_fake_tag('fake_tag') + self.fake_tags = utils.get_fake_tags(10) + self.instance_uri = (self.res_uri + + 'tags/{}/'.format(self.fake_tag['id'])) + + self.client = fuelclient.get_client('tag', self.version) + + def _get_class_uri_path(self, tag): + owner_type = self.owner_map[tag['owner_type']] + return self.res_uri + "{}/{}/tags/".format(owner_type, + tag['owner_id']) + + def test_tag_list(self): + expected_uri = self._get_class_uri_path(self.fake_tag) + matcher = self.m_request.get(expected_uri, json=self.fake_tags) + self.client.get_all(self.owner_map[self.fake_tag['owner_type']], + self.fake_tag['owner_id']) + self.assertTrue(matcher.called) + + def test_tag_download(self): + expected_uri = self.instance_uri + tag_matcher = self.m_request.get(expected_uri, json=self.fake_tag) + tag = self.client.get_by_id(self.fake_tag['id']) + + self.assertTrue(expected_uri, tag_matcher.called) + self.assertEqual(tag, self.fake_tag) + + def test_tag_upload(self): + expected_uri = self.instance_uri + upd_matcher = self.m_request.put(expected_uri, json=self.fake_tag) + + self.client.update(self.fake_tag['id'], self.fake_tag) + + self.assertTrue(upd_matcher.called) + self.assertEqual(self.fake_tag, upd_matcher.last_request.json()) + + def test_tag_create(self): + expected_uri = self._get_class_uri_path(self.fake_tag) + post_matcher = self.m_request.post(expected_uri, json=self.fake_tag) + + self.client.create(self.owner_map[self.fake_tag['owner_type']], + self.fake_tag['owner_id'], + self.fake_tag) + + self.assertTrue(post_matcher.called) + self.assertEqual(self.fake_tag, post_matcher.last_request.json()) + + def test_tag_delete(self): + expected_uri = self.instance_uri + delete_matcher = self.m_request.delete(expected_uri, json={}) + self.client.delete_by_id(self.fake_tag['id']) + + self.assertTrue(delete_matcher.called) + + def test_tag_assign(self): + node_id = 1 + expected_uri = (self.res_uri + + 'nodes/{}/tags/'.format(node_id)) + tag_ids = [self.fake_tag['id']] + + post_matcher = self.m_request.post(expected_uri, json=tag_ids) + self.client.assign(tag_ids, node_id) + + self.assertTrue(post_matcher.called) + self.assertEqual(tag_ids, post_matcher.last_request.json()) + + def test_tag_unassign(self): + node_id = 1 + tag_ids = [self.fake_tag['id']] + expected_uri = '{0}?tags={1}'.format( + self.res_uri + 'nodes/{}/tags/'.format(node_id), + ','.join(map(str, tag_ids)) + ) + + delete_matcher = self.m_request.delete(expected_uri, json={}) + self.client.unassign(tag_ids, node_id) + + self.assertTrue(delete_matcher.called) diff --git a/fuelclient/tests/utils/__init__.py b/fuelclient/tests/utils/__init__.py index ff9f138c..94bb1d9a 100644 --- a/fuelclient/tests/utils/__init__.py +++ b/fuelclient/tests/utils/__init__.py @@ -54,6 +54,8 @@ from fuelclient.tests.utils.fake_release import get_fake_release_component from fuelclient.tests.utils.fake_release import get_fake_release_components from fuelclient.tests.utils.fake_role import get_fake_role from fuelclient.tests.utils.fake_role import get_fake_roles +from fuelclient.tests.utils.fake_tag import get_fake_tag +from fuelclient.tests.utils.fake_tag import get_fake_tags __all__ = (get_fake_deployment_history, @@ -85,4 +87,6 @@ __all__ = (get_fake_deployment_history, get_fake_node_groups, get_fake_openstack_config, get_fake_plugin, - get_fake_plugins) + get_fake_plugins, + get_fake_tag, + get_fake_tags) diff --git a/fuelclient/tests/utils/fake_tag.py b/fuelclient/tests/utils/fake_tag.py new file mode 100644 index 00000000..d516b890 --- /dev/null +++ b/fuelclient/tests/utils/fake_tag.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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_tag(tag_name=None, has_primary=False, owner_id=None, + owner_type=None): + """Create a random fake tag + + Returns the serialized and parametrized representation of a dumped Fuel + tag. Represents the average amount of data. + + """ + return { + "id": 1, + "tag": tag_name or "controller", + "has_primary": has_primary, + "owner_id": owner_id or 1, + "owner_type": owner_type or 'release' + } + + +def get_fake_tags(tag_count, **kwargs): + """Create a random fake list of tags.""" + return [get_fake_tag(**kwargs) for _ in range(tag_count)] diff --git a/fuelclient/utils.py b/fuelclient/utils.py index 856f7ac9..942dd094 100644 --- a/fuelclient/utils.py +++ b/fuelclient/utils.py @@ -27,6 +27,7 @@ from distutils.version import StrictVersion from fnmatch import fnmatch from fuelclient.cli import error +from fuelclient import consts def _wait_and_check_exit_code(cmd, child): @@ -212,3 +213,8 @@ def add_os_cli_parameters(parser): parser.add_argument( '--os-password', metavar='', help='Authentication password, defaults to env[OS_PASSWORD].') + + parser.add_argument( + '--mode', + choices=list(consts.CLIENT_MODES), + help="Application mode, defaults to 'simple'.") diff --git a/fuelclient/v1/__init__.py b/fuelclient/v1/__init__.py index 1fbc2239..d1039522 100644 --- a/fuelclient/v1/__init__.py +++ b/fuelclient/v1/__init__.py @@ -30,6 +30,7 @@ from fuelclient.v1 import plugins from fuelclient.v1 import sequence from fuelclient.v1 import snapshot from fuelclient.v1 import task +from fuelclient.v1 import tag from fuelclient.v1 import vip # Please keeps the list in alphabetical order @@ -51,4 +52,5 @@ __all__ = ('cluster_settings', 'sequence', 'snapshot', 'task', + 'tag', 'vip') diff --git a/fuelclient/v1/tag.py b/fuelclient/v1/tag.py new file mode 100644 index 00000000..2343d306 --- /dev/null +++ b/fuelclient/v1/tag.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 import objects +from fuelclient.v1 import base_v1 + + +class TagClient(base_v1.BaseV1Client): + + _entity_wrapper = objects.Tag + + def get_all(self, tag_owner, owner_id): + return self._entity_wrapper.get_all(tag_owner, owner_id) + + def update(self, tag_id, data): + tag = self._entity_wrapper(obj_id=tag_id) + return tag.update_tag(data) + + def create(self, tag_owner, owner_id, data): + return self._entity_wrapper.create_tag(tag_owner, owner_id, data) + + def delete_by_id(self, tag_id): + tag = self._entity_wrapper(obj_id=tag_id) + return tag.delete_tag() + + def assign(self, tag_ids, node): + return self._entity_wrapper.assign_tags(tag_ids, node) + + def unassign(self, tag_ids, node): + return self._entity_wrapper.unassign_tags(tag_ids, node) + + +def get_client(connection): + return TagClient(connection) diff --git a/setup.cfg b/setup.cfg index 3e966b71..90dbf08f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -114,6 +114,14 @@ fuelclient = role_download=fuelclient.commands.role:RoleDownload role_list=fuelclient.commands.role:RoleList role_update=fuelclient.commands.role:RoleUpdate + tag_assign=fuelclient.commands.tag:TagAssign + tag_create=fuelclient.commands.tag:TagCreate + tag_delete=fuelclient.commands.tag:TagDelete + tag_download=fuelclient.commands.tag:TagDownload + tag_list=fuelclient.commands.tag:TagList + tag_show=fuelclient.commands.tag:TagShow + tag_unassign=fuelclient.commands.tag:TagUnassign + tag_update=fuelclient.commands.tag:TagUpdate task_delete=fuelclient.commands.task:TaskDelete task_deployment-info_download=fuelclient.commands.task:TaskDeploymentInfoDownload task_history_show=fuelclient.commands.task:TaskHistoryShow