Node labels support

This patch implement support for node custom attributes

Filter nodes by specific labels:
fuel2 node list --labels "label_key_1=label_val_1" [..]

List all labels in system or only for specific nodes:
fuel2 node label list --nodes 1 2

Set label(s) for all or only specific nodes:
fuel2 node label set "label_key_1=label_val_1" [..] --nodes 1 2

Delete label(s) by key for all or only specific nodes:
fuel2 node label delete "label_key_1" [..] --nodes 1 2

Implements: blueprint node-custom-attributes
Change-Id: Iaa00219a8e4a9c5fa62df2ce4d59057ef17e89a0
This commit is contained in:
Andriy Popovych
2015-07-20 21:57:26 +03:00
committed by Sebastian Kalinowski
parent f85a6eecbb
commit fe289321ac
8 changed files with 488 additions and 35 deletions

View File

@@ -116,9 +116,10 @@ class BaseDeleteCommand(BaseCommand):
def get_parser(self, prog_name):
parser = super(BaseDeleteCommand, self).get_parser(prog_name)
parser.add_argument('id', type=int,
help='Id of the {0} to '
'delete.'.format(self.entity_name))
parser.add_argument(
'id',
type=int,
help='Id of the {0} to delete.'.format(self.entity_name))
return parser
@@ -127,5 +128,7 @@ class BaseDeleteCommand(BaseCommand):
msg = '{ent} with id {ent_id} was deleted\n'
self.app.stdout.write(msg.format(ent=self.entity_name.capitalize(),
ent_id=parsed_args.id))
self.app.stdout.write(
msg.format(
ent=self.entity_name.capitalize(),
ent_id=parsed_args.id))

View File

@@ -43,14 +43,19 @@ class NodeList(NodeMixIn, base.BaseListCommand):
'-e',
'--env',
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(
'-l',
'--labels',
nargs='+',
help='Show only nodes that have specific labels')
return parser
def take_action(self, parsed_args):
data = self.client.get_all(environment_id=parsed_args.env)
data = self.client.get_all(
environment_id=parsed_args.env, labels=parsed_args.labels)
data = data_utils.get_display_data_multi(self.columns, data)
return (self.columns, data)
@@ -82,6 +87,37 @@ class NodeShow(NodeMixIn, base.BaseShowCommand):
'manufacturer')
class NodeUpdate(NodeMixIn, base.BaseShowCommand):
"""Change given attributes for a node."""
columns = NodeShow.columns
def get_parser(self, prog_name):
parser = super(NodeUpdate, self).get_parser(prog_name)
parser.add_argument(
'-H',
'--hostname',
type=str,
default=None,
help='New hostname for node')
return parser
def take_action(self, parsed_args):
updates = {}
for attr in self.client._updatable_attributes:
if getattr(parsed_args, attr, None):
updates[attr] = getattr(parsed_args, attr)
updated_node = self.client.update(
parsed_args.id, **updates)
updated_node = data_utils.get_display_data_single(
self.columns, updated_node)
return (self.columns, updated_node)
class NodeVmsList(NodeMixIn, base.BaseShowCommand):
"""Show list vms for node."""
@@ -118,31 +154,82 @@ class NodeCreateVMsConf(NodeMixIn, base.BaseCommand):
self.app.stdout.write(msg)
class NodeUpdate(NodeMixIn, base.BaseShowCommand):
"""Change given attributes for a node."""
class NodeLabelList(NodeMixIn, base.BaseListCommand):
"""Show list of all labels."""
columns = NodeShow.columns
columns = (
'node_id',
'label_name',
'label_value')
def get_parser(self, prog_name):
parser = super(NodeUpdate, self).get_parser(prog_name)
parser = super(NodeLabelList, self).get_parser(prog_name)
parser.add_argument('-H',
'--hostname',
type=str,
default=None,
help='New hostname for node')
parser.add_argument(
'-n',
'--nodes',
nargs='+',
help='Show labels for specific nodes')
return parser
def take_action(self, parsed_args):
updates = {}
for attr in self.client._updatable_attributes:
if getattr(parsed_args, attr, None):
updates[attr] = getattr(parsed_args, attr)
data = self.client.get_all_labels_for_nodes(
node_ids=parsed_args.nodes)
data = data_utils.get_display_data_multi(self.columns, data)
updated_node = self.client.update(parsed_args.id,
updates)
updated_node = data_utils.get_display_data_single(self.columns,
updated_node)
return (self.columns, data)
return (self.columns, updated_node)
class NodeLabelSet(NodeMixIn, base.BaseCommand):
"""Create or update specifc labels on nodes."""
def get_parser(self, prog_name):
parser = super(NodeLabelSet, self).get_parser(prog_name)
parser.add_argument(
'labels',
nargs='+',
help='List of labels for create or update')
parser.add_argument(
'-n',
'--nodes',
nargs='+',
help='Create or update labels only for specific nodes')
return parser
def take_action(self, parsed_args):
data = self.client.set_labels_for_nodes(
labels=parsed_args.labels, node_ids=parsed_args.nodes)
msg = "Labels have been updated on nodes: {0} \n".format(
','.join(data))
self.app.stdout.write(msg)
class NodeLabelDelete(NodeMixIn, base.BaseCommand):
"""Delete specific labels on nodes."""
def get_parser(self, prog_name):
parser = super(NodeLabelDelete, self).get_parser(prog_name)
parser.add_argument(
'labels_keys',
nargs='+',
help='List of labels keys for delete')
parser.add_argument(
'-n',
'--nodes',
nargs='+',
help='Delete labels only for specific nodes')
return parser
def take_action(self, parsed_args):
data = self.client.delete_labels_for_nodes(
labels_keys=parsed_args.labels_keys, node_ids=parsed_args.nodes)
msg = "Labels have been deleted on nodes: {0} \n".format(
','.join(data))
self.app.stdout.write(msg)

View File

@@ -57,6 +57,10 @@ class Node(BaseObject):
data = self.get_fresh_data()
return data["progress"]
@property
def labels(self):
return self.get_fresh_data().get('labels', {})
def get_attribute_default_url(self, attributes_type):
url_path, default_url_path = self.attributes_urls[attributes_type]
return "nodes/{0}/{1}/{2}".format(self.id, url_path, default_url_path)

View File

@@ -17,7 +17,7 @@
def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None,
roles=None, mac=None, memory_b=None, os_platform=None,
status=None, node_name=None, group_id=None):
status=None, node_name=None, group_id=None, labels=None):
"""Creates a fake node
Returns the serialized and parametrized representation of a dumped Fuel
@@ -25,6 +25,11 @@ def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None,
"""
host_name = hostname or 'fake-node-42'
labels = labels or {
'key_1': 'val_1',
'key_2': None,
'key_3': 'val_3'
}
return {'name': node_name or host_name,
'error_type': None,
@@ -125,4 +130,5 @@ def get_fake_node(cluster=None, hostname=None, node_id=None, cpu_model=None,
'vlan': None},
{'dev': 'eth0',
'name': 'admin'}]}
'name': 'admin'}],
'labels': labels}

View File

@@ -36,7 +36,8 @@ class TestNodeCommand(test_engine.BaseCLITest):
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all.assert_called_once_with(environment_id=None)
self.m_client.get_all.assert_called_once_with(
environment_id=None, labels=None)
def test_node_list_with_env(self):
env_id = 42
@@ -45,7 +46,31 @@ class TestNodeCommand(test_engine.BaseCLITest):
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all.assert_called_once_with(environment_id=env_id)
self.m_client.get_all.assert_called_once_with(
environment_id=env_id, labels=None)
def test_node_list_with_labels(self):
labels = ['key_1=val_1', 'key_2=val_2']
args = 'node list --labels {labels}'.format(
labels=' '.join(labels))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all.assert_called_once_with(
environment_id=None, labels=labels)
def test_node_list_with_env_and_labels(self):
env_id = 42
labels = ["key_1=val_1", "key_2=val_2"]
args = 'node list --env {env} --labels {labels}'.format(
env=env_id, labels=' '.join(labels))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all.assert_called_once_with(
environment_id=env_id, labels=labels)
def test_node_show(self):
node_id = 42
@@ -97,4 +122,70 @@ class TestNodeCommand(test_engine.BaseCLITest):
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.update.assert_called_once_with(
node_id, {"hostname": hostname})
node_id, **{"hostname": hostname})
def test_node_label_list_for_all_nodes(self):
args = 'node label list'
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all_labels_for_nodes.assert_called_once_with(
node_ids=None)
def test_node_label_list_for_specific_nodes(self):
node_ids = ['42', '43']
args = 'node label list --nodes {node_ids}'.format(
node_ids=' '.join(node_ids))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.get_all_labels_for_nodes.assert_called_once_with(
node_ids=node_ids)
def test_node_label_set_for_all_nodes(self):
labels = ['key_1=val_1', 'key_2=val_2']
args = 'node label set {labels}'.format(
labels=' '.join(labels))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.set_labels_for_nodes.assert_called_once_with(
labels=labels, node_ids=None)
def test_node_label_set_for_specific_nodes(self):
labels = ['key_1=val_1', 'key_2=val_2']
node_ids = ['42', '43']
args = 'node label set {labels} --nodes {node_ids}'.format(
labels=' '.join(labels), node_ids=' '.join(node_ids))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.set_labels_for_nodes.assert_called_once_with(
labels=labels, node_ids=node_ids)
def test_node_label_delete_for_all_nodes(self):
labels_keys = ['key_1', 'key_2']
args = 'node label delete {labels_keys}'.format(
labels_keys=' '.join(labels_keys))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.delete_labels_for_nodes.assert_called_once_with(
labels_keys=labels_keys, node_ids=None)
def test_node_label_delete_for_specific_nodes(self):
labels_keys = ['key_1', 'key_2']
node_ids = ['42', '43']
args = 'node label delete {labels_keys} --nodes {node_ids}'.format(
labels_keys=' '.join(labels_keys), node_ids=' '.join(node_ids))
self.exec_command(args)
self.m_get_client.assert_called_once_with('node', mock.ANY)
self.m_client.delete_labels_for_nodes.assert_called_once_with(
labels_keys=labels_keys, node_ids=node_ids)

View File

@@ -87,3 +87,121 @@ class TestNodeFacade(test_api.BaseLibTest):
self.assertTrue(matcher.called)
self.assertEqual(data, matcher.last_request.json())
def test_get_all_labels_for_all_nodes(self):
matcher = self.m_request.get(self.res_uri, json=self.fake_nodes)
self.client.get_all_labels_for_nodes()
self.assertTrue(matcher.called)
def test_set_labels_for_all_nodes(self):
labels = ['key_1=val_1', 'key_2=val_2', 'key_3 = val_4']
data = {'labels': {
'key_1': 'val_1',
'key_2': 'val_2',
'key_3': 'val_4'
}}
expected_uri = self.get_object_uri(self.res_uri, 42)
matcher_get = self.m_request.get(self.res_uri, json=self.fake_nodes)
matcher_put = self.m_request.put(expected_uri, json=data)
self.client.set_labels_for_nodes(labels=labels)
self.assertTrue(matcher_get.called)
self.assertTrue(matcher_put.called)
self.assertEqual(data, matcher_put.last_request.json())
def test_set_labels_for_specific_nodes(self):
labels = ['key_1=val_1', 'key_2=val_2', 'key_3 = val_4']
node_ids = ['42']
data = {'labels': {
'key_1': 'val_1',
'key_2': 'val_2',
'key_3': 'val_4'
}}
expected_uri = self.get_object_uri(self.res_uri, 42)
matcher_get = self.m_request.get(expected_uri, json=self.fake_node)
matcher_put = self.m_request.put(expected_uri, json=data)
self.client.set_labels_for_nodes(
labels=labels, node_ids=node_ids)
self.assertTrue(matcher_get.called)
self.assertTrue(matcher_put.called)
self.assertEqual(data, matcher_put.last_request.json())
def test_delete_labels_for_all_nodes(self):
labels_keys = ['key_1', ' key_3 ']
data = {'labels': {'key_2': None}}
expected_uri = self.get_object_uri(self.res_uri, 42)
matcher_get = self.m_request.get(self.res_uri, json=self.fake_nodes)
matcher_put = self.m_request.put(expected_uri, json=data)
self.client.delete_labels_for_nodes(labels_keys=labels_keys)
self.assertTrue(matcher_get.called)
self.assertTrue(matcher_put.called)
self.assertEqual(data, matcher_put.last_request.json())
def test_delete_labels_for_specific_nodes(self):
labels_keys = ['key_2']
node_ids = ['42']
data = {'labels': {'key_1': 'val_1', 'key_3': 'val_3'}}
expected_uri = self.get_object_uri(self.res_uri, 42)
matcher_get = self.m_request.get(expected_uri, json=self.fake_node)
matcher_put = self.m_request.put(expected_uri, json=data)
self.client.delete_labels_for_nodes(
labels_keys=labels_keys, node_ids=node_ids)
self.assertTrue(matcher_get.called)
self.assertTrue(matcher_put.called)
self.assertEqual(data, matcher_put.last_request.json())
def test_get_name_and_value_from_labels(self):
for label in (
'key=value',
' key =value',
'key= value ',
' key = value ',
):
name, value = self.client._split_label(label)
self.assertEqual(name, 'key')
self.assertEqual(value, 'value')
def test_get_name_and_empty_value_from_lables(self):
for label in (
'key=',
' key =',
'key= ',
' key = ',
):
name, value = self.client._split_label(label)
self.assertEqual(name, 'key')
self.assertIsNone(value)
def test_labels_after_delete(self):
all_labels = {
'label_A': 'A',
'label_B': None,
'label_C': 'C',
'label_D': None,
}
expected_labels = {
'label_A': 'A',
'label_D': None,
}
for labels_to_delete in (
('label_B', 'label_C'),
(' label_B', 'label_C '),
('label_B', 'label_C ', 'unknown_label'),
):
result = self.client._labels_after_delete(
all_labels, labels_to_delete)
self.assertEqual(result, expected_labels)

View File

@@ -11,6 +11,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from functools import partial
import six
from fuelclient.cli import error
@@ -21,14 +25,26 @@ from fuelclient.v1 import base_v1
class NodeClient(base_v1.BaseV1Client):
_entity_wrapper = objects.Node
_updatable_attributes = ('hostname',)
_updatable_attributes = ('hostname', 'labels')
def get_all(self, environment_id=None):
def get_all(self, environment_id=None, labels=None):
"""Get nodes by specific environment or labels
:param environment_id: Id of specific environment(cluster)
:type environment_id: int
:param labels: List of string labels for filtering nodes
:type labels: list
:returns: list -- filtered list of nodes
"""
result = self._entity_wrapper.get_all_data()
if environment_id is not None:
result = filter(lambda n: n['cluster'] == environment_id, result)
if labels:
result = filter(
partial(self._check_label, labels), result)
return result
def get_node_vms_conf(self, node_id):
@@ -40,14 +56,139 @@ class NodeClient(base_v1.BaseV1Client):
return node.node_vms_create(config)
def update(self, node_id, **updated_attributes):
node = self._entity_wrapper(obj_id=node_id)
for attr in six.iterkeys(updated_attributes):
if attr not in self._updatable_attributes:
msg = 'Only {0} are updatable'.format(
self._updatable_attributes)
raise error.ArgumentException(msg)
node = self._entity_wrapper(obj_id=node_id)
return node.set(updated_attributes)
def get_all_labels_for_nodes(self, node_ids=None):
"""Get list of labels for specific nodes. If no node_ids then all
labels should be returned
:param node_ids: List of node ids for filtering labels
:type node_ids: list
:returns: list -- filtered list of labels
"""
labels = []
result = self._entity_wrapper.get_all_data()
if node_ids:
result = filter(lambda node: str(node['id']) in node_ids, result)
for node in result:
for key, value in six.iteritems(node.get('labels', [])):
labels.append({
'node_id': node.get('id'),
'label_name': key,
'label_value': value
})
labels = sorted(labels, key=lambda label: label.get('node_id'))
return labels
def set_labels_for_nodes(self, labels=None, node_ids=None):
"""Update nodes labels attribute with new data. If node_ids
are empty list then labels will be updated on all nodes
:param labels: List of string pairs `key=val` for labels
:type labels: list
:param node_ids: List of node ids where labels should be updated
:type node_ids: list
:return: list -- ids of nodes where labels were updated
"""
data_to_return = []
labels_to_update = {}
for label in labels:
key, val = self._split_label(label)
labels_to_update[key] = val
if node_ids:
for node_id in node_ids:
node = self._entity_wrapper(obj_id=node_id)
db_labels = copy.deepcopy(node.labels)
db_labels.update(labels_to_update)
result = self.update(node_id, **{'labels': db_labels})
data_to_return.append(str(result.get('id')))
else:
nodes = self._entity_wrapper.get_all_data()
for node in nodes:
db_labels = copy.deepcopy(node['labels'])
db_labels.update(labels_to_update)
result = self.update(node['id'], **{'labels': db_labels})
data_to_return.append(str(result.get('id')))
return data_to_return
def delete_labels_for_nodes(self, labels_keys=None, node_ids=None):
"""Delete labels data from nodes labels. If node_ids are
empty list then labels will be deleted on all nodes
:param labels: List of string label keys
:type labels: list
:param node_ids: List of node ids where labels should be deleted
:type node_ids: list
:returns: list -- ids of nodes where labels were deleted
"""
data_to_return = []
if node_ids:
for node_id in node_ids:
node = self._entity_wrapper(obj_id=node_id)
updated_labels = self._labels_after_delete(
node.labels, labels_keys)
result = self.update(node_id, **{'labels': updated_labels})
data_to_return.append(str(result.get('id')))
else:
nodes = self._entity_wrapper.get_all_data()
for node in nodes:
updated_labels = self._labels_after_delete(
node['labels'], labels_keys)
result = self.update(node['id'], **{'labels': updated_labels})
data_to_return.append(str(result.get('id')))
return data_to_return
def _check_label(self, labels, item):
checking_list = []
for label in labels:
key, val = self._split_label(label)
if key in item.get('labels'):
checking_val = item['labels'][key] == val
checking_list.append(checking_val)
return True in checking_list
@staticmethod
def _labels_after_delete(labels, labels_keys):
db_labels = copy.deepcopy(labels)
for label_key in labels_keys:
label_key = label_key.strip()
db_labels.pop(label_key, None)
return db_labels
@staticmethod
def _split_label(label):
name, value = label.split('=')
name = name.strip()
value = value.strip()
value = None if value == '' else value
return name, value
def get_client():
return NodeClient()

View File

@@ -42,6 +42,9 @@ fuelclient =
node_list-vms-conf=fuelclient.commands.node:NodeVmsList
node_create-vms-conf=fuelclient.commands.node:NodeCreateVMsConf
node_update=fuelclient.commands.node:NodeUpdate
node_label_list=fuelclient.commands.node:NodeLabelList
node_label_set=fuelclient.commands.node:NodeLabelSet
node_label_delete=fuelclient.commands.node:NodeLabelDelete
task_list=fuelclient.commands.task:TaskList
task_show=fuelclient.commands.task:TaskShow
fuel-version=fuelclient.commands.fuelversion:FuelVersion