fb512cd3ab
Part of support for nested stacks and updates story To add nested stack support to Valet, make up for missing Heat resource Orchestration IDs in nested resources by generating a subset of Heat stack lifecycle scheduler hints for each resource in advance, store them as opaque metadata in Valet, then leverage the metadata at Nova scheduling time. Make additional accommodations in anticipation of complexities brought about by adding support for stack updates. To add a minimally viable amount of Heat `stack-update` support to Valet, significantly restrict the number of update use cases using a set of acceptance criteria. Skip holistic placement at `stack-update` time in favor of Valet's existing re-plan mechanism, placing or replacing resources one at a time, albeit still in consideration of other resources in the same stack hierarchy. Change-Id: I4654bcb4eacd5d64f76e262fe4c29553796e3f06 Story: #2001139 Task: #4858
330 lines
11 KiB
Python
330 lines
11 KiB
Python
#
|
|
# Copyright (c) 2014-2017 AT&T Intellectual Property
|
|
#
|
|
# 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.
|
|
|
|
"""Group Heat Resource Plugin"""
|
|
|
|
from heat.common import exception as heat_exception
|
|
from heat.common.i18n import _
|
|
from heat.engine import attributes
|
|
from heat.engine import constraints
|
|
from heat.engine import properties
|
|
from heat.engine import resource
|
|
from heat.engine.resources import scheduler_hints as sh
|
|
from heat.engine import support
|
|
from oslo_log import log as logging
|
|
|
|
from plugins.common import valet_api
|
|
from plugins import exceptions
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Group(resource.Resource, sh.SchedulerHintsMixin):
|
|
"""Valet Group Resource
|
|
|
|
A Group is used to define a particular association amongst
|
|
resources. Groups may be used only by their assigned members,
|
|
currently identified by project (tenant) IDs. If no members are
|
|
assigned, any project (tenant) may assign resources to the group.
|
|
|
|
There are three types of groups: affinity, diversity, and exclusivity.
|
|
There are two levels: host and rack.
|
|
|
|
All groups must have a unique name, regardless of how they were created
|
|
and regardless of membership.
|
|
|
|
There is no lone group owner. Any user with an admin role, regardless
|
|
of project/tenant, can edit or delete the group.
|
|
"""
|
|
|
|
support_status = support.SupportStatus(version='2015.1')
|
|
|
|
_LEVEL_TYPES = (
|
|
HOST, RACK,
|
|
) = (
|
|
'host', 'rack',
|
|
)
|
|
|
|
_RELATIONSHIP_TYPES = (
|
|
AFFINITY, DIVERSITY, EXCLUSIVITY,
|
|
) = (
|
|
'affinity', 'diversity', 'exclusivity',
|
|
)
|
|
|
|
PROPERTIES = (
|
|
DESCRIPTION, LEVEL, MEMBERS, NAME, TYPE,
|
|
) = (
|
|
'description', 'level', 'members', 'name', 'type',
|
|
)
|
|
|
|
ATTRIBUTES = (
|
|
DESCRIPTION_ATTR, LEVEL_ATTR, MEMBERS_ATTR, NAME_ATTR, TYPE_ATTR,
|
|
) = (
|
|
'description', 'level', 'members', 'name', 'type',
|
|
)
|
|
|
|
properties_schema = {
|
|
DESCRIPTION: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Description of group.'),
|
|
required=False,
|
|
update_allowed=True
|
|
),
|
|
LEVEL: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Level of relationship between resources.'),
|
|
constraints=[
|
|
constraints.AllowedValues([HOST, RACK])
|
|
],
|
|
required=True,
|
|
update_allowed=False
|
|
),
|
|
MEMBERS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('List of one or more member IDs allowed to use this group.'),
|
|
required=False,
|
|
update_allowed=True
|
|
),
|
|
NAME: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Name of group.'),
|
|
constraints=[
|
|
constraints.CustomConstraint('valet.group_name'),
|
|
],
|
|
required=True,
|
|
update_allowed=False
|
|
),
|
|
TYPE: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Type of group.'),
|
|
constraints=[
|
|
constraints.AllowedValues([AFFINITY, DIVERSITY, EXCLUSIVITY])
|
|
],
|
|
required=True,
|
|
update_allowed=False
|
|
),
|
|
}
|
|
|
|
# To maintain Kilo compatibility, do not use "type" here.
|
|
attributes_schema = {
|
|
DESCRIPTION_ATTR: attributes.Schema(
|
|
_('Description of group.')
|
|
),
|
|
LEVEL_ATTR: attributes.Schema(
|
|
_('Level of relationship between resources.')
|
|
),
|
|
MEMBERS_ATTR: attributes.Schema(
|
|
_('List of one or more member IDs allowed to use this group.')
|
|
),
|
|
NAME_ATTR: attributes.Schema(
|
|
_('Name of group.')
|
|
),
|
|
TYPE_ATTR: attributes.Schema(
|
|
_('Type of group.')
|
|
),
|
|
}
|
|
|
|
def __init__(self, name, json_snippet, stack):
|
|
"""Initialization"""
|
|
super(Group, self).__init__(name, json_snippet, stack)
|
|
self.api = valet_api.ValetAPI()
|
|
self.api.auth_token = self.context.auth_token
|
|
self._group = None
|
|
|
|
def _get_resource(self):
|
|
if self._group is None and self.resource_id is not None:
|
|
try:
|
|
groups = self.api.groups_get(
|
|
self.resource_id)
|
|
if groups:
|
|
self._group = groups.get('group', {})
|
|
except exceptions.NotFoundError:
|
|
# Ignore Not Found and fall through
|
|
pass
|
|
|
|
return self._group
|
|
|
|
def _group_name(self):
|
|
"""Group Name"""
|
|
name = self.properties.get(self.NAME)
|
|
if name:
|
|
return name
|
|
|
|
return self.physical_resource_name()
|
|
|
|
def FnGetRefId(self):
|
|
"""Get Reference ID"""
|
|
return self.physical_resource_name_or_FnGetRefId()
|
|
|
|
def handle_create(self):
|
|
"""Create resource"""
|
|
if self.resource_id is not None:
|
|
# TODO(jdandrea): Delete the resource and re-create?
|
|
# I've seen this called if a stack update fails.
|
|
# For now, just leave it be.
|
|
return
|
|
|
|
group_type = self.properties.get(self.TYPE)
|
|
level = self.properties.get(self.LEVEL)
|
|
description = self.properties.get(self.DESCRIPTION)
|
|
members = self.properties.get(self.MEMBERS)
|
|
group_args = {
|
|
'name': self._group_name(),
|
|
'type': group_type,
|
|
'level': level,
|
|
'description': description,
|
|
}
|
|
kwargs = {
|
|
'group': group_args,
|
|
}
|
|
|
|
# Create the group first. If an exception is
|
|
# thrown by groups_create, let Heat catch it.
|
|
group = self.api.groups_create(**kwargs)
|
|
if group is not None and 'id' in group:
|
|
self.resource_id_set(group.get('id'))
|
|
else:
|
|
raise heat_exception.ResourceNotAvailable(
|
|
resource_name=self._group_name())
|
|
|
|
# Now add members to the group
|
|
if members:
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
'members': members,
|
|
}
|
|
err = None
|
|
group = None
|
|
try:
|
|
group = self.api.groups_members_update(**kwargs)
|
|
except exceptions.PythonAPIError as err:
|
|
# Hold on to err. We'll use it in a moment.
|
|
pass
|
|
finally:
|
|
if group is None:
|
|
# Members couldn't be added.
|
|
# Delete the group we just created.
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
}
|
|
try:
|
|
self.api.groups_delete(**kwargs)
|
|
except exceptions.PythonAPIError:
|
|
# Ignore group deletion errors.
|
|
pass
|
|
if err:
|
|
raise err
|
|
else:
|
|
raise heat_exception.ResourceNotAvailable(
|
|
resource_name=self._group_name())
|
|
|
|
def handle_update(self, json_snippet, templ_diff, prop_diff):
|
|
"""Update resource"""
|
|
if prop_diff:
|
|
if self.DESCRIPTION in prop_diff:
|
|
description = prop_diff.get(
|
|
self.DESCRIPTION, self.properties.get(self.DESCRIPTION))
|
|
|
|
# If an exception is thrown by groups_update,
|
|
# let Heat catch it. Let the state remain as-is.
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
'group': {
|
|
self.DESCRIPTION: description,
|
|
},
|
|
}
|
|
self.api.groups_update(**kwargs)
|
|
|
|
if self.MEMBERS in prop_diff:
|
|
members_update = prop_diff.get(self.MEMBERS, [])
|
|
members = self.properties.get(self.MEMBERS, [])
|
|
|
|
# Delete original members not in updated list.
|
|
# If an exception is thrown by groups_member_delete,
|
|
# let Heat catch it. Let the state remain as-is.
|
|
member_deletions = set(members) - set(members_update)
|
|
for member_id in member_deletions:
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
'member_id': member_id,
|
|
}
|
|
self.api.groups_member_delete(**kwargs)
|
|
|
|
# Add members_update members not in original list.
|
|
# If an exception is thrown by groups_members_update,
|
|
# let Heat catch it. Let the state remain as-is.
|
|
member_additions = set(members_update) - set(members)
|
|
if member_additions:
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
'members': list(member_additions),
|
|
}
|
|
self.api.groups_members_update(**kwargs)
|
|
|
|
# Clear cached group info
|
|
self._group = None
|
|
|
|
def handle_delete(self):
|
|
"""Delete resource"""
|
|
if self.resource_id is None:
|
|
return
|
|
|
|
kwargs = {
|
|
'group_id': self.resource_id,
|
|
}
|
|
|
|
group = self._get_resource()
|
|
if group:
|
|
# First, delete all the members
|
|
members = group.get('members', [])
|
|
if members:
|
|
try:
|
|
self.api.groups_members_delete(**kwargs)
|
|
except exceptions.NotFoundError:
|
|
# Ignore Not Found and fall through
|
|
pass
|
|
|
|
# Now delete the group.
|
|
try:
|
|
response = self.api.groups_delete(**kwargs)
|
|
if type(response) is dict and len(response) == 0:
|
|
self.resource_id_set(None)
|
|
self._group = None
|
|
except exceptions.NotFoundError:
|
|
# Ignore Not Found and fall through
|
|
pass
|
|
|
|
def _resolve_attribute(self, key):
|
|
"""Resolve Attributes"""
|
|
if self.resource_id is None:
|
|
return
|
|
group = self._get_resource()
|
|
if group:
|
|
attributes = {
|
|
self.NAME_ATTR: group.get(self.NAME),
|
|
self.TYPE_ATTR: group.get(self.TYPE),
|
|
self.LEVEL_ATTR: group.get(self.LEVEL),
|
|
self.DESCRIPTION_ATTR: group.get(self.DESCRIPTION),
|
|
self.MEMBERS_ATTR: group.get(self.MEMBERS, []),
|
|
}
|
|
return attributes.get(key)
|
|
|
|
|
|
def resource_mapping():
|
|
"""Map names to resources."""
|
|
return {
|
|
'OS::Valet::Group': Group,
|
|
}
|