valet/plugins/heat/group.py
Chris fb512cd3ab Refactor valet_plugins
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
2017-08-28 11:42:32 -05:00

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,
}