Add tags support in CLI

Releases new fuel2 commands to operate on tags:
    fuel2 tag assign
    fuel2 tag create
    fuel2 tag delete
    fuel2 tag download
    fuel2 tag list
    fuel2 tag unassign
    fuel2 tag update

Advanced mode has been introduced.

DocImpact

Change-Id: Ib0f48598552f6b066fd375966594cb3a934574e2
Implements: blueprint role-decomposition
This commit is contained in:
slava 2016-09-16 02:34:50 +03:00 committed by Viacheslav Valyavskiy
parent 14e8bee51b
commit adf8d677b4
17 changed files with 810 additions and 42 deletions

View File

@ -78,6 +78,7 @@ def get_client(resource, version='v1', connection=None):
'sequence': v1.sequence, 'sequence': v1.sequence,
'snapshot': v1.snapshot, 'snapshot': v1.snapshot,
'task': v1.task, 'task': v1.task,
'tag': v1.tag,
'vip': v1.vip 'vip': v1.vip
} }
} }

View File

@ -168,16 +168,22 @@ class BaseDownloadCommand(NodeMixIn, base.BaseCommand):
class NodeList(NodeMixIn, base.BaseListCommand): class NodeList(NodeMixIn, base.BaseListCommand):
"""Show list of all available nodes.""" """Show list of all available nodes."""
columns = ('id', _columns = (
'name', 'id',
'status', 'name',
'os_platform', 'status',
'roles', 'os_platform',
'ip', 'ip',
'mac', 'mac',
'cluster', 'cluster',
'platform_name', 'platform_name',
'online') '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): def get_parser(self, prog_name):
parser = super(NodeList, self).get_parser(prog_name) parser = super(NodeList, self).get_parser(prog_name)
@ -187,7 +193,6 @@ class NodeList(NodeMixIn, base.BaseListCommand):
'--env', '--env',
type=int, type=int,
help='Show only nodes that are in the specified environment') help='Show only nodes that are in the specified environment')
parser.add_argument( parser.add_argument(
'-l', '-l',
'--labels', '--labels',
@ -202,35 +207,40 @@ class NodeList(NodeMixIn, base.BaseListCommand):
environment_id=parsed_args.env, labels=parsed_args.labels) environment_id=parsed_args.env, labels=parsed_args.labels)
data = data_utils.get_display_data_multi(self.columns, data) data = data_utils.get_display_data_multi(self.columns, data)
return (self.columns, data) return self.columns, data
class NodeShow(NodeMixIn, base.BaseShowCommand): class NodeShow(NodeMixIn, base.BaseShowCommand):
"""Show info about node with given id.""" """Show info about node with given id."""
columns = ('id', _columns = (
'name', 'id',
'status', 'name',
'os_platform', 'status',
'roles', 'os_platform',
'kernel_params', 'kernel_params',
'pending_roles', 'ip',
'ip', 'mac',
'mac', 'error_type',
'error_type', 'pending_addition',
'pending_addition', 'hostname',
'hostname', 'fqdn',
'fqdn', 'platform_name',
'platform_name', 'cluster',
'cluster', 'online',
'online', 'progress',
'progress', 'pending_deletion',
'pending_deletion', 'group_id',
'group_id', # TODO(romcheg): network_data mostly never fits the screen
# TODO(romcheg): network_data mostly never fits the screen # 'network_data',
# 'network_data', 'manufacturer')
'manufacturer') _columns += NodeMixIn.numa_fields
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): def take_action(self, parsed_args):
data = self.client.get_by_id(parsed_args.id) data = self.client.get_by_id(parsed_args.id)
@ -240,9 +250,8 @@ class NodeShow(NodeMixIn, base.BaseShowCommand):
return self.columns, data return self.columns, data
class NodeUpdate(NodeMixIn, base.BaseShowCommand): class NodeUpdate(NodeShow):
"""Change given attributes for a node.""" """Change given attributes for a node."""
columns = NodeShow.columns columns = NodeShow.columns
def get_parser(self, prog_name): def get_parser(self, prog_name):

296
fuelclient/commands/tag.py Normal file
View File

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

View File

@ -32,3 +32,8 @@ TASK_STATUSES = Enum(
'ready', 'ready',
'running' 'running'
) )
CLIENT_MODES = Enum(
'advanced',
'simple'
)

View File

@ -9,6 +9,9 @@ OS_PASSWORD:
OS_TENANT_NAME: "admin" OS_TENANT_NAME: "admin"
HTTP_PROXY: null HTTP_PROXY: null
HTTP_TIMEOUT: 10 HTTP_TIMEOUT: 10
# application mode
# possible options: 'advanced', 'simple'
MODE: 'simple'
# Performance tests settings # Performance tests settings
PERFORMANCE_PROFILING_TESTS: 0 PERFORMANCE_PROFILING_TESTS: 0

View File

@ -18,6 +18,8 @@ import sys
from cliff import app from cliff import app
from cliff.commandmanager import CommandManager from cliff.commandmanager import CommandManager
from fuelclient.cli import error
from fuelclient import consts
from fuelclient import fuelclient_settings from fuelclient import fuelclient_settings
from fuelclient import utils from fuelclient import utils
@ -32,6 +34,19 @@ class FuelClient(app.App):
configuration of basic engines. 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): def build_option_parser(self, description, version, argparse_kwargs=None):
"""Overrides default options for backwards compatibility.""" """Overrides default options for backwards compatibility."""

View File

@ -28,6 +28,7 @@ from fuelclient.objects.role import Role
from fuelclient.objects.task import DeployTask from fuelclient.objects.task import DeployTask
from fuelclient.objects.task import SnapshotTask from fuelclient.objects.task import SnapshotTask
from fuelclient.objects.task import Task from fuelclient.objects.task import Task
from fuelclient.objects.tag import Tag
from fuelclient.objects.fuelversion import FuelVersion from fuelclient.objects.fuelversion import FuelVersion
from fuelclient.objects.network_group import NetworkGroup from fuelclient.objects.network_group import NetworkGroup
from fuelclient.objects.plugins import Plugins from fuelclient.objects.plugins import Plugins

59
fuelclient/objects/tag.py Normal file
View File

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

View File

@ -194,7 +194,9 @@ node-4 ansible_host=10.20.0.5
node.NodeClient._updatable_attributes node.NodeClient._updatable_attributes
node_id = 42 node_id = 42
hostname = 'test-name' 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 = \ self.m_client.update.return_value = \
fake_node.get_fake_node(node_id=node_id, 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,
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( self.m_client.update.assert_called_once_with(
node_id, hostname=hostname) node_id, hostname=hostname)
@ -218,7 +220,9 @@ node-4 ansible_host=10.20.0.5
self.m_client._updatable_attributes = \ self.m_client._updatable_attributes = \
node.NodeClient._updatable_attributes node.NodeClient._updatable_attributes
node_id = 37 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ó', '你一定是无聊') test_cases = ('new-name', 'New Name', 'śćż∑ Pó', '你一定是无聊')
for name in test_cases: for name in test_cases:
@ -238,7 +242,7 @@ node-4 ansible_host=10.20.0.5
mock.ANY, mock.ANY,
mock.ANY, 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( self.m_client.update.assert_called_once_with(
node_id, name=name) node_id, name=name)
self.m_get_client.reset_mock() self.m_get_client.reset_mock()

View File

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

View File

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

View File

@ -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_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_role
from fuelclient.tests.utils.fake_role import get_fake_roles 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, __all__ = (get_fake_deployment_history,
@ -85,4 +87,6 @@ __all__ = (get_fake_deployment_history,
get_fake_node_groups, get_fake_node_groups,
get_fake_openstack_config, get_fake_openstack_config,
get_fake_plugin, get_fake_plugin,
get_fake_plugins) get_fake_plugins,
get_fake_tag,
get_fake_tags)

View File

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

View File

@ -27,6 +27,7 @@ from distutils.version import StrictVersion
from fnmatch import fnmatch from fnmatch import fnmatch
from fuelclient.cli import error from fuelclient.cli import error
from fuelclient import consts
def _wait_and_check_exit_code(cmd, child): def _wait_and_check_exit_code(cmd, child):
@ -212,3 +213,8 @@ def add_os_cli_parameters(parser):
parser.add_argument( parser.add_argument(
'--os-password', metavar='<auth-password>', '--os-password', metavar='<auth-password>',
help='Authentication password, defaults to env[OS_PASSWORD].') help='Authentication password, defaults to env[OS_PASSWORD].')
parser.add_argument(
'--mode',
choices=list(consts.CLIENT_MODES),
help="Application mode, defaults to 'simple'.")

View File

@ -30,6 +30,7 @@ from fuelclient.v1 import plugins
from fuelclient.v1 import sequence from fuelclient.v1 import sequence
from fuelclient.v1 import snapshot from fuelclient.v1 import snapshot
from fuelclient.v1 import task from fuelclient.v1 import task
from fuelclient.v1 import tag
from fuelclient.v1 import vip from fuelclient.v1 import vip
# Please keeps the list in alphabetical order # Please keeps the list in alphabetical order
@ -51,4 +52,5 @@ __all__ = ('cluster_settings',
'sequence', 'sequence',
'snapshot', 'snapshot',
'task', 'task',
'tag',
'vip') 'vip')

47
fuelclient/v1/tag.py Normal file
View File

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

View File

@ -114,6 +114,14 @@ fuelclient =
role_download=fuelclient.commands.role:RoleDownload role_download=fuelclient.commands.role:RoleDownload
role_list=fuelclient.commands.role:RoleList role_list=fuelclient.commands.role:RoleList
role_update=fuelclient.commands.role:RoleUpdate 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_delete=fuelclient.commands.task:TaskDelete
task_deployment-info_download=fuelclient.commands.task:TaskDeploymentInfoDownload task_deployment-info_download=fuelclient.commands.task:TaskDeploymentInfoDownload
task_history_show=fuelclient.commands.task:TaskHistoryShow task_history_show=fuelclient.commands.task:TaskHistoryShow