Add support for host aggregates
This adds support to manage host aggregates and host aggregate membership. Change-Id: Iec84164c535171116dd3f97f30c5dc249bf09f0d
This commit is contained in:
parent
31ac451e12
commit
e6891082b3
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add support for host aggregates and host aggregate
|
||||||
|
membership.
|
@ -177,6 +177,41 @@ class HypervisorList(task_manager.Task):
|
|||||||
return client.nova_client.hypervisors.list(**self.args)
|
return client.nova_client.hypervisors.list(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateList(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.list(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateCreate(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.create(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateUpdate(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.update(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateDelete(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.delete(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateAddHost(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.add_host(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateRemoveHost(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.remove_host(**self.args)
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateSetMetadata(task_manager.Task):
|
||||||
|
def main(self, client):
|
||||||
|
return client.nova_client.aggregates.set_metadata(**self.args)
|
||||||
|
|
||||||
|
|
||||||
class KeypairList(task_manager.Task):
|
class KeypairList(task_manager.Task):
|
||||||
def main(self, client):
|
def main(self, client):
|
||||||
return client.nova_client.keypairs.list()
|
return client.nova_client.keypairs.list()
|
||||||
|
@ -1764,3 +1764,169 @@ class OperatorCloud(openstackcloud.OpenStackCloud):
|
|||||||
|
|
||||||
with _utils.shade_exceptions("Error fetching hypervisor list"):
|
with _utils.shade_exceptions("Error fetching hypervisor list"):
|
||||||
return self.manager.submitTask(_tasks.HypervisorList())
|
return self.manager.submitTask(_tasks.HypervisorList())
|
||||||
|
|
||||||
|
def search_aggregates(self, name_or_id=None, filters=None):
|
||||||
|
"""Seach host aggregates.
|
||||||
|
|
||||||
|
:param name: aggregate name or id.
|
||||||
|
:param filters: a dict containing additional filters to use.
|
||||||
|
|
||||||
|
:returns: a list of dicts containing the aggregates
|
||||||
|
|
||||||
|
:raises: ``OpenStackCloudException``: if something goes wrong during
|
||||||
|
the openstack API call.
|
||||||
|
"""
|
||||||
|
aggregates = self.list_aggregates()
|
||||||
|
return _utils._filter_list(aggregates, name_or_id, filters)
|
||||||
|
|
||||||
|
def list_aggregates(self):
|
||||||
|
"""List all available host aggregates.
|
||||||
|
|
||||||
|
:returns: A list of aggregate dicts.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with _utils.shade_exceptions("Error fetching aggregate list"):
|
||||||
|
return self.manager.submitTask(_tasks.AggregateList())
|
||||||
|
|
||||||
|
def get_aggregate(self, name_or_id, filters=None):
|
||||||
|
"""Get an aggregate by name or ID.
|
||||||
|
|
||||||
|
:param name_or_id: Name or ID of the aggregate.
|
||||||
|
:param dict filters:
|
||||||
|
A dictionary of meta data to use for further filtering. Elements
|
||||||
|
of this dictionary may, themselves, be dictionaries. Example::
|
||||||
|
|
||||||
|
{
|
||||||
|
'availability_zone': 'nova',
|
||||||
|
'metadata': {
|
||||||
|
'cpu_allocation_ratio': '1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:returns: An aggregate dict or None if no matching aggregate is
|
||||||
|
found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _utils._get_entity(self.search_aggregates, name_or_id, filters)
|
||||||
|
|
||||||
|
def create_aggregate(self, name, availability_zone=None):
|
||||||
|
"""Create a new host aggregate.
|
||||||
|
|
||||||
|
:param name: Name of the host aggregate being created
|
||||||
|
:param availability_zone: Availability zone to assign hosts
|
||||||
|
|
||||||
|
:returns: a dict representing the new host aggregate.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Unable to create host aggregate {name}".format(
|
||||||
|
name=name)):
|
||||||
|
return self.manager.submitTask(_tasks.AggregateCreate(
|
||||||
|
name=name, availability_zone=availability_zone))
|
||||||
|
|
||||||
|
@_utils.valid_kwargs('name', 'availability_zone')
|
||||||
|
def update_aggregate(self, name_or_id, **kwargs):
|
||||||
|
"""Update a host aggregate.
|
||||||
|
|
||||||
|
:param name_or_id: Name or ID of the aggregate being updated.
|
||||||
|
:param name: New aggregate name
|
||||||
|
:param availability_zone: Availability zone to assign to hosts
|
||||||
|
|
||||||
|
:returns: a dict representing the updated host aggregate.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
aggregate = self.get_aggregate(name_or_id)
|
||||||
|
if not aggregate:
|
||||||
|
raise OpenStackCloudException(
|
||||||
|
"Host aggregate %s not found." % name_or_id)
|
||||||
|
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Error updating aggregate {name}".format(name=name_or_id)):
|
||||||
|
new_aggregate = self.manager.submitTask(
|
||||||
|
_tasks.AggregateUpdate(
|
||||||
|
aggregate=aggregate['id'], values=kwargs))
|
||||||
|
|
||||||
|
return new_aggregate
|
||||||
|
|
||||||
|
def delete_aggregate(self, name_or_id):
|
||||||
|
"""Delete a host aggregate.
|
||||||
|
|
||||||
|
:param name_or_id: Name or ID of the host aggregate to delete.
|
||||||
|
|
||||||
|
:returns: True if delete succeeded, False otherwise.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
aggregate = self.get_aggregate(name_or_id)
|
||||||
|
if not aggregate:
|
||||||
|
self.log.debug("Aggregate %s not found for deleting" % name_or_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Error deleting aggregate {name}".format(name=name_or_id)):
|
||||||
|
self.manager.submitTask(
|
||||||
|
_tasks.AggregateDelete(aggregate=aggregate['id']))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_aggregate_metadata(self, name_or_id, metadata):
|
||||||
|
"""Set aggregate metadata, replacing the existing metadata.
|
||||||
|
|
||||||
|
:param name_or_id: Name of the host aggregate to update
|
||||||
|
:param metadata: Dict containing metadata to replace (Use
|
||||||
|
{'key': None} to remove a key)
|
||||||
|
|
||||||
|
:returns: a dict representing the new host aggregate.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
aggregate = self.get_aggregate(name_or_id)
|
||||||
|
if not aggregate:
|
||||||
|
raise OpenStackCloudException(
|
||||||
|
"Host aggregate %s not found." % name_or_id)
|
||||||
|
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Unable to set metadata for host aggregate {name}".format(
|
||||||
|
name=name_or_id)):
|
||||||
|
return self.manager.submitTask(_tasks.AggregateSetMetadata(
|
||||||
|
aggregate=aggregate['id'], metadata=metadata))
|
||||||
|
|
||||||
|
def add_host_to_aggregate(self, name_or_id, host_name):
|
||||||
|
"""Add a host to an aggregate.
|
||||||
|
|
||||||
|
:param name_or_id: Name or ID of the host aggregate.
|
||||||
|
:param host_name: Host to add.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
aggregate = self.get_aggregate(name_or_id)
|
||||||
|
if not aggregate:
|
||||||
|
raise OpenStackCloudException(
|
||||||
|
"Host aggregate %s not found." % name_or_id)
|
||||||
|
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Unable to add host {host} to aggregate {name}".format(
|
||||||
|
name=name_or_id, host=host_name)):
|
||||||
|
return self.manager.submitTask(_tasks.AggregateAddHost(
|
||||||
|
aggregate=aggregate['id'], host=host_name))
|
||||||
|
|
||||||
|
def remove_host_from_aggregate(self, name_or_id, host_name):
|
||||||
|
"""Remove a host from an aggregate.
|
||||||
|
|
||||||
|
:param name_or_id: Name or ID of the host aggregate.
|
||||||
|
:param host_name: Host to remove.
|
||||||
|
|
||||||
|
:raises: OpenStackCloudException on operation error.
|
||||||
|
"""
|
||||||
|
aggregate = self.get_aggregate(name_or_id)
|
||||||
|
if not aggregate:
|
||||||
|
raise OpenStackCloudException(
|
||||||
|
"Host aggregate %s not found." % name_or_id)
|
||||||
|
|
||||||
|
with _utils.shade_exceptions(
|
||||||
|
"Unable to remove host {host} from aggregate {name}".format(
|
||||||
|
name=name_or_id, host=host_name)):
|
||||||
|
return self.manager.submitTask(_tasks.AggregateRemoveHost(
|
||||||
|
aggregate=aggregate['id'], host=host_name))
|
||||||
|
@ -272,3 +272,17 @@ class FakeRecordset(object):
|
|||||||
self.description = description
|
self.description = description
|
||||||
self.ttl = ttl
|
self.ttl = ttl
|
||||||
self.records = records
|
self.records = records
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAggregate(object):
|
||||||
|
def __init__(self, id, name, availability_zone=None, metadata=None,
|
||||||
|
hosts=None):
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.availability_zone = availability_zone
|
||||||
|
if not metadata:
|
||||||
|
metadata = {}
|
||||||
|
self.metadata = metadata
|
||||||
|
if not hosts:
|
||||||
|
hosts = []
|
||||||
|
self.hosts = hosts
|
||||||
|
63
shade/tests/functional/test_aggregate.py
Normal file
63
shade/tests/functional/test_aggregate.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_aggregate
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Functional tests for `shade` aggregate resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from shade.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestAggregate(base.BaseFunctionalTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestAggregate, self).setUp()
|
||||||
|
|
||||||
|
def test_aggregates(self):
|
||||||
|
aggregate_name = self.getUniqueString()
|
||||||
|
availability_zone = self.getUniqueString()
|
||||||
|
self.addCleanup(self.cleanup, aggregate_name)
|
||||||
|
aggregate = self.operator_cloud.create_aggregate(aggregate_name)
|
||||||
|
|
||||||
|
aggregate_ids = [v['id']
|
||||||
|
for v in self.operator_cloud.list_aggregates()]
|
||||||
|
self.assertIn(aggregate['id'], aggregate_ids)
|
||||||
|
|
||||||
|
aggregate = self.operator_cloud.update_aggregate(
|
||||||
|
aggregate_name,
|
||||||
|
availability_zone=availability_zone
|
||||||
|
)
|
||||||
|
self.assertEqual(availability_zone, aggregate['availability_zone'])
|
||||||
|
|
||||||
|
aggregate = self.operator_cloud.set_aggregate_metadata(
|
||||||
|
aggregate_name,
|
||||||
|
{'key': 'value'}
|
||||||
|
)
|
||||||
|
self.assertIn('key', aggregate['metadata'])
|
||||||
|
|
||||||
|
aggregate = self.operator_cloud.set_aggregate_metadata(
|
||||||
|
aggregate_name,
|
||||||
|
{'key': None}
|
||||||
|
)
|
||||||
|
self.assertNotIn('key', aggregate['metadata'])
|
||||||
|
|
||||||
|
self.operator_cloud.delete_aggregate(aggregate_name)
|
||||||
|
|
||||||
|
def cleanup(self, aggregate_name):
|
||||||
|
aggregate = self.operator_cloud.get_aggregate(aggregate_name)
|
||||||
|
if aggregate:
|
||||||
|
self.operator_cloud.delete_aggregate(aggregate_name)
|
116
shade/tests/unit/test_aggregate.py
Normal file
116
shade/tests/unit/test_aggregate.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
import shade
|
||||||
|
from shade.tests.unit import base
|
||||||
|
from shade.tests import fakes
|
||||||
|
|
||||||
|
|
||||||
|
class TestAggregate(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestAggregate, self).setUp()
|
||||||
|
self.cloud = shade.operator_cloud(validate=False)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_create_aggregate(self, mock_nova):
|
||||||
|
aggregate_name = 'aggr1'
|
||||||
|
self.cloud.create_aggregate(name=aggregate_name)
|
||||||
|
|
||||||
|
mock_nova.aggregates.create.assert_called_once_with(
|
||||||
|
name=aggregate_name, availability_zone=None
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_create_aggregate_with_az(self, mock_nova):
|
||||||
|
aggregate_name = 'aggr1'
|
||||||
|
availability_zone = 'az1'
|
||||||
|
self.cloud.create_aggregate(name=aggregate_name,
|
||||||
|
availability_zone=availability_zone)
|
||||||
|
|
||||||
|
mock_nova.aggregates.create.assert_called_once_with(
|
||||||
|
name=aggregate_name, availability_zone=availability_zone
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_delete_aggregate(self, mock_nova):
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name')
|
||||||
|
]
|
||||||
|
self.assertTrue(self.cloud.delete_aggregate('1234'))
|
||||||
|
mock_nova.aggregates.list.assert_called_once_with()
|
||||||
|
mock_nova.aggregates.delete.assert_called_once_with(
|
||||||
|
aggregate='1234'
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_update_aggregate_set_az(self, mock_nova):
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name')
|
||||||
|
]
|
||||||
|
self.cloud.update_aggregate('1234', availability_zone='az')
|
||||||
|
mock_nova.aggregates.update.assert_called_once_with(
|
||||||
|
aggregate='1234',
|
||||||
|
values={'availability_zone': 'az'},
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_update_aggregate_unset_az(self, mock_nova):
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name', availability_zone='az')
|
||||||
|
]
|
||||||
|
self.cloud.update_aggregate('1234', availability_zone=None)
|
||||||
|
mock_nova.aggregates.update.assert_called_once_with(
|
||||||
|
aggregate='1234',
|
||||||
|
values={'availability_zone': None},
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_set_aggregate_metadata(self, mock_nova):
|
||||||
|
metadata = {'key', 'value'}
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name')
|
||||||
|
]
|
||||||
|
self.cloud.set_aggregate_metadata('1234', metadata)
|
||||||
|
mock_nova.aggregates.set_metadata.assert_called_once_with(
|
||||||
|
aggregate='1234',
|
||||||
|
metadata=metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_add_host_to_aggregate(self, mock_nova):
|
||||||
|
hostname = 'host1'
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name')
|
||||||
|
]
|
||||||
|
self.cloud.add_host_to_aggregate('1234', hostname)
|
||||||
|
mock_nova.aggregates.add_host.assert_called_once_with(
|
||||||
|
aggregate='1234',
|
||||||
|
host=hostname
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(shade.OpenStackCloud, 'nova_client')
|
||||||
|
def test_remove_host_from_aggregate(self, mock_nova):
|
||||||
|
hostname = 'host1'
|
||||||
|
mock_nova.aggregates.list.return_value = [
|
||||||
|
fakes.FakeAggregate('1234', 'name', hosts=[hostname])
|
||||||
|
]
|
||||||
|
self.cloud.remove_host_from_aggregate('1234', hostname)
|
||||||
|
mock_nova.aggregates.remove_host.assert_called_once_with(
|
||||||
|
aggregate='1234',
|
||||||
|
host=hostname
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user