Merge "Update the API and database models"
This commit is contained in:
commit
65d93c4ed5
@ -17,4 +17,5 @@
|
|||||||
|
|
||||||
import gettext
|
import gettext
|
||||||
|
|
||||||
|
# TODO(jdandrea): Use oslo_i18n.TranslatorFactory
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
|
@ -13,34 +13,39 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Ostro helper library."""
|
"""Ostro helper library"""
|
||||||
|
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
from pecan import conf
|
from pecan import conf
|
||||||
import time
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
from valet.api.common.i18n import _
|
from valet.api.common.i18n import _
|
||||||
|
from valet.api.common import validation
|
||||||
from valet.api.db.models.music.groups import Group
|
from valet.api.db.models.music.groups import Group
|
||||||
from valet.api.db.models.music.ostro import PlacementRequest
|
from valet.api.db.models.music.ostro import PlacementRequest
|
||||||
from valet.api.db.models.music.ostro import PlacementResult
|
from valet.api.db.models.music.ostro import PlacementResult
|
||||||
from valet.api.db.models import Query
|
from valet.api.db.models import Query
|
||||||
from valet.api import LOG
|
from valet.api import LOG
|
||||||
|
|
||||||
|
SERVER = 'OS::Nova::Server'
|
||||||
SERVICEABLE_RESOURCES = [
|
SERVICEABLE_RESOURCES = [
|
||||||
'OS::Nova::Server'
|
SERVER,
|
||||||
]
|
]
|
||||||
GROUP_ASSIGNMENT = 'ATT::Valet::GroupAssignment'
|
METADATA = 'metadata'
|
||||||
GROUP_TYPE = 'group_type'
|
GROUP_ASSIGNMENT = 'OS::Valet::GroupAssignment'
|
||||||
GROUP_NAME = 'group_name'
|
GROUP_ID = 'group'
|
||||||
AFFINITY = 'affinity'
|
_GROUP_TYPES = (
|
||||||
DIVERSITY = 'diversity'
|
AFFINITY, DIVERSITY, EXCLUSIVITY,
|
||||||
EXCLUSIVITY = 'exclusivity'
|
) = (
|
||||||
|
'affinity', 'diversity', 'exclusivity',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _log(text, title="Ostro"):
|
def _log(text, title="Ostro"):
|
||||||
"""Log helper."""
|
"""Log helper"""
|
||||||
log_text = "%s: %s" % (title, text)
|
log_text = "%s: %s" % (title, text)
|
||||||
LOG.debug(log_text)
|
LOG.debug(log_text)
|
||||||
|
|
||||||
@ -49,6 +54,7 @@ class Ostro(object):
|
|||||||
"""Ostro optimization engine helper class."""
|
"""Ostro optimization engine helper class."""
|
||||||
|
|
||||||
args = None
|
args = None
|
||||||
|
asynchronous = False
|
||||||
request = None
|
request = None
|
||||||
response = None
|
response = None
|
||||||
error_uri = None
|
error_uri = None
|
||||||
@ -60,71 +66,47 @@ class Ostro(object):
|
|||||||
# Interval in seconds to poll for placement.
|
# Interval in seconds to poll for placement.
|
||||||
interval = None
|
interval = None
|
||||||
|
|
||||||
|
# valet-engine response types
|
||||||
|
_STATUS = (
|
||||||
|
STATUS_OK, STATUS_ERROR,
|
||||||
|
) = (
|
||||||
|
'ok', 'error',
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_error(cls, message):
|
def _build_error(cls, message=None):
|
||||||
"""Build an Ostro-style error message."""
|
"""Build an Ostro-style error response"""
|
||||||
if not message:
|
if not message:
|
||||||
message = _("Unknown error")
|
message = _("Unknown error")
|
||||||
error = {
|
return cls._build_response(cls.STATUS_ERROR, message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_ok(cls, message):
|
||||||
|
"""Build an Ostro-style ok response"""
|
||||||
|
if not message:
|
||||||
|
message = _("Unknown message")
|
||||||
|
return cls._build_response(cls.STATUS_OK, message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_response(cls, status=None, message=None):
|
||||||
|
"""Build an Ostro-style response"""
|
||||||
|
if status not in (cls._STATUS):
|
||||||
|
status = cls.STATUS_ERROR
|
||||||
|
if not message:
|
||||||
|
message = _("Unknown")
|
||||||
|
response = {
|
||||||
'status': {
|
'status': {
|
||||||
'type': 'error',
|
'type': status,
|
||||||
'message': message,
|
'message': message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return error
|
return response
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _build_uuid_map(cls, resources):
|
|
||||||
"""Build a dict mapping names to UUIDs."""
|
|
||||||
mapping = {}
|
|
||||||
for key in resources.iterkeys():
|
|
||||||
if 'name' in resources[key]:
|
|
||||||
name = resources[key]['name']
|
|
||||||
mapping[name] = key
|
|
||||||
return mapping
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _sanitize_resources(cls, resources):
|
|
||||||
"""Ensure lowercase keys at the top level of each resource."""
|
|
||||||
for res in resources.itervalues():
|
|
||||||
for key in list(res.keys()):
|
|
||||||
if not key.islower():
|
|
||||||
res[key.lower()] = res.pop(key)
|
|
||||||
return resources
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initializer."""
|
"""Initializer"""
|
||||||
self.tries = conf.music.get('tries', 1000)
|
self.tries = conf.music.get('tries', 1000)
|
||||||
self.interval = conf.music.get('interval', 0.1)
|
self.interval = conf.music.get('interval', 0.1)
|
||||||
|
|
||||||
def _map_names_to_uuids(self, mapping, data):
|
|
||||||
"""Map resource names to their UUID equivalents."""
|
|
||||||
if isinstance(data, dict):
|
|
||||||
for key in data.iterkeys():
|
|
||||||
if key != 'name':
|
|
||||||
data[key] = self._map_names_to_uuids(mapping, data[key])
|
|
||||||
elif isinstance(data, list):
|
|
||||||
for key, value in enumerate(data):
|
|
||||||
data[key] = self._map_names_to_uuids(mapping, value)
|
|
||||||
elif isinstance(data, basestring) and data in mapping:
|
|
||||||
return mapping[data]
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _prepare_resources(self, resources):
|
|
||||||
"""Pre-digest resource data for use by Ostro.
|
|
||||||
|
|
||||||
Maps Heat resource names to Orchestration UUIDs.
|
|
||||||
Ensures exclusivity groups exist and have tenant_id as a member.
|
|
||||||
"""
|
|
||||||
mapping = self._build_uuid_map(resources)
|
|
||||||
ostro_resources = self._map_names_to_uuids(mapping, resources)
|
|
||||||
self._sanitize_resources(ostro_resources)
|
|
||||||
|
|
||||||
verify_error = self._verify_groups(ostro_resources, self.tenant_id)
|
|
||||||
if isinstance(verify_error, dict):
|
|
||||||
return verify_error
|
|
||||||
return {'resources': ostro_resources}
|
|
||||||
|
|
||||||
# TODO(JD): This really belongs in valet-engine once it exists.
|
# TODO(JD): This really belongs in valet-engine once it exists.
|
||||||
def _send(self, stack_id, request):
|
def _send(self, stack_id, request):
|
||||||
"""Send request."""
|
"""Send request."""
|
||||||
@ -132,9 +114,16 @@ class Ostro(object):
|
|||||||
PlacementRequest(stack_id=stack_id, request=request)
|
PlacementRequest(stack_id=stack_id, request=request)
|
||||||
result_query = Query(PlacementResult)
|
result_query = Query(PlacementResult)
|
||||||
|
|
||||||
for __ in range(self.tries, 0, -1): # pylint: disable=W0612
|
if self.asynchronous:
|
||||||
|
message = _("Asynchronous request sent")
|
||||||
|
LOG.info(_("{} for stack_id = {}").format(message, stack_id))
|
||||||
|
response = self._build_ok(message)
|
||||||
|
return json.dumps(response)
|
||||||
|
|
||||||
|
for __ in range(self.tries, 0, -1):
|
||||||
# Take a breather in between checks.
|
# Take a breather in between checks.
|
||||||
# TODO(JD): This is a blocking operation at the moment.
|
# FIXME(jdandrea): This is blocking. Use futurist...
|
||||||
|
# or oslo.message. Hint hint. :)
|
||||||
time.sleep(self.interval)
|
time.sleep(self.interval)
|
||||||
|
|
||||||
result = result_query.filter_by(stack_id=stack_id).first()
|
result = result_query.filter_by(stack_id=stack_id).first()
|
||||||
@ -144,117 +133,126 @@ class Ostro(object):
|
|||||||
return placement
|
return placement
|
||||||
|
|
||||||
self.error_uri = '/errors/server_error'
|
self.error_uri = '/errors/server_error'
|
||||||
message = "Timed out waiting for a response."
|
message = _("Timed out waiting for a response")
|
||||||
|
LOG.error(_("{} for stack_id = {}").format(message, stack_id))
|
||||||
LOG.error(message + " for stack_id = " + stack_id)
|
|
||||||
|
|
||||||
response = self._build_error(message)
|
response = self._build_error(message)
|
||||||
return json.dumps(response)
|
return json.dumps(response)
|
||||||
|
|
||||||
def _verify_groups(self, resources, tenant_id):
|
def _resolve_group(self, group_id):
|
||||||
"""Verify group settings.
|
"""Resolve a group by ID or name"""
|
||||||
|
if validation.is_valid_uuid4(group_id):
|
||||||
Returns an error status dict if the group type is invalid, if a
|
group = Group.query.filter_by(id=group_id).first()
|
||||||
group name is used when the type is affinity or diversity, if a
|
|
||||||
nonexistant exclusivity group is found, or if the tenant
|
|
||||||
is not a group member. Returns None if ok.
|
|
||||||
"""
|
|
||||||
message = None
|
|
||||||
for res in resources.itervalues():
|
|
||||||
res_type = res.get('type')
|
|
||||||
if res_type == GROUP_ASSIGNMENT:
|
|
||||||
properties = res.get('properties')
|
|
||||||
group_type = properties.get(GROUP_TYPE, '').lower()
|
|
||||||
group_name = properties.get(GROUP_NAME, '').lower()
|
|
||||||
if group_type == AFFINITY or \
|
|
||||||
group_type == DIVERSITY:
|
|
||||||
if group_name:
|
|
||||||
self.error_uri = '/errors/conflict'
|
|
||||||
message = _("%s must not be used when"
|
|
||||||
" {0} is '{1}'.").format(GROUP_NAME,
|
|
||||||
GROUP_TYPE,
|
|
||||||
group_type)
|
|
||||||
break
|
|
||||||
elif group_type == EXCLUSIVITY:
|
|
||||||
message = self._verify_exclusivity(group_name, tenant_id)
|
|
||||||
else:
|
else:
|
||||||
|
group = Group.query.filter_by(name=group_id).first()
|
||||||
|
if not group:
|
||||||
|
self.error_uri = '/errors/not_found'
|
||||||
|
message = _("Group '{}' not found").format(group_id)
|
||||||
|
return (None, message)
|
||||||
|
|
||||||
|
if not group.name or not group.type or not group.level:
|
||||||
self.error_uri = '/errors/invalid'
|
self.error_uri = '/errors/invalid'
|
||||||
message = _("{0} '{1}' is invalid.").format(GROUP_TYPE,
|
message = _("Group name, type, and level "
|
||||||
group_type)
|
"must all be specified.")
|
||||||
break
|
return (None, message)
|
||||||
|
|
||||||
|
if group.type not in _GROUP_TYPES:
|
||||||
|
self.error_uri = '/errors/invalid'
|
||||||
|
message = _("Unknown group type '{}'.").format(
|
||||||
|
group.type)
|
||||||
|
return (None, message)
|
||||||
|
elif (len(group.members) > 0 and
|
||||||
|
self.tenant_id not in group.members):
|
||||||
|
self.error_uri = '/errors/conflict'
|
||||||
|
message = _("ID {} not a member of "
|
||||||
|
"group {} ({})").format(
|
||||||
|
self.tenant_id, group.name, group.id)
|
||||||
|
return (None, message)
|
||||||
|
|
||||||
|
return (group, None)
|
||||||
|
|
||||||
|
def _prepare_resources(self, resources):
|
||||||
|
"""Pre-digests resource data for use by Ostro.
|
||||||
|
|
||||||
|
Maps Heat resource names to Orchestration UUIDs.
|
||||||
|
Removes opaque metadata from resources.
|
||||||
|
Ensures group assignments refer to valid groups.
|
||||||
|
Ensures groups have tenant_id as a member.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We're going to mess with the resources, so make a copy.
|
||||||
|
res_copy = copy.deepcopy(resources)
|
||||||
|
groups = {}
|
||||||
|
message = None
|
||||||
|
|
||||||
|
for res in res_copy.itervalues():
|
||||||
|
if METADATA in res:
|
||||||
|
# Discard valet-api-specific metadata.
|
||||||
|
res.pop(METADATA)
|
||||||
|
res_type = res.get('type')
|
||||||
|
|
||||||
|
# If OS::Nova::Server has valet metadata, use it
|
||||||
|
# to propagate group assignments to the engine.
|
||||||
|
if res_type == SERVER:
|
||||||
|
properties = res.get('properties')
|
||||||
|
metadata = properties.get(METADATA, {})
|
||||||
|
valet_metadata = metadata.get('valet', {})
|
||||||
|
group_assignments = valet_metadata.get('groups', [])
|
||||||
|
|
||||||
|
# Resolve all the groups and normalize the IDs.
|
||||||
|
normalized_ids = []
|
||||||
|
for group_id in group_assignments:
|
||||||
|
(group, message) = self._resolve_group(group_id)
|
||||||
if message:
|
if message:
|
||||||
return self._build_error(message)
|
return self._build_error(message)
|
||||||
|
|
||||||
def _verify_exclusivity(self, group_name, tenant_id):
|
# Normalize each group id
|
||||||
return_message = None
|
normalized_ids.append(group.id)
|
||||||
if not group_name:
|
|
||||||
self.error_uri = '/errors/invalid'
|
|
||||||
return _("%s must be used when {0} is '{1}'.").format(GROUP_NAME,
|
|
||||||
GROUP_TYPE,
|
|
||||||
EXCLUSIVITY)
|
|
||||||
|
|
||||||
group = Group.query.filter_by(name=group_name).first()
|
groups[group.id] = {
|
||||||
|
"name": group.name,
|
||||||
if not group:
|
"type": group.type,
|
||||||
self.error_uri = '/errors/not_found'
|
"level": group.level,
|
||||||
return_message = "%s '%s' not found" % (GROUP_NAME, group_name)
|
|
||||||
elif group and tenant_id not in group.members:
|
|
||||||
self.error_uri = '/errors/conflict'
|
|
||||||
return_message = _("Tenant ID %s not a member of "
|
|
||||||
"{0} '{1}' ({2})").format(self.tenant_id,
|
|
||||||
GROUP_NAME,
|
|
||||||
group.name,
|
|
||||||
group.id)
|
|
||||||
return return_message
|
|
||||||
|
|
||||||
def build_request(self, **kwargs):
|
|
||||||
"""Build an Ostro request.
|
|
||||||
|
|
||||||
If False is returned then the response attribute contains
|
|
||||||
status as to the error.
|
|
||||||
"""
|
|
||||||
# TODO(JD): Refactor this into create and update methods?
|
|
||||||
self.args = kwargs.get('args')
|
|
||||||
self.tenant_id = kwargs.get('tenant_id')
|
|
||||||
self.response = None
|
|
||||||
self.error_uri = None
|
|
||||||
|
|
||||||
resources = self.args['resources']
|
|
||||||
if 'resources_update' in self.args:
|
|
||||||
action = 'update'
|
|
||||||
resources_update = self.args['resources_update']
|
|
||||||
else:
|
|
||||||
action = 'create'
|
|
||||||
resources_update = None
|
|
||||||
|
|
||||||
# If we get any status in the response, it's an error. Bail.
|
|
||||||
self.response = self._prepare_resources(resources)
|
|
||||||
if 'status' in self.response:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.request = {
|
|
||||||
"action": action,
|
|
||||||
"resources": self.response['resources'],
|
|
||||||
"stack_id": self.args['stack_id'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only add locations if we have it (no need for an empty object)
|
# Update all the IDs with normalized values if we have 'em.
|
||||||
locations = self.args.get('locations')
|
if normalized_ids and valet_metadata:
|
||||||
if locations:
|
valet_metadata['groups'] = normalized_ids
|
||||||
self.request['locations'] = locations
|
|
||||||
|
|
||||||
if resources_update:
|
# OS::Valet::GroupAssignment has been pre-empted.
|
||||||
# If we get any status in the response, it's an error. Bail.
|
# We're opting to leave the existing/working logic as-is.
|
||||||
self.response = self._prepare_resources(resources_update)
|
# Propagate group assignment resources to the engine.
|
||||||
if 'status' in self.response:
|
if res_type == GROUP_ASSIGNMENT:
|
||||||
return False
|
properties = res.get('properties')
|
||||||
self.request['resources_update'] = self.response['resources']
|
group_id = properties.get(GROUP_ID)
|
||||||
|
if not group_id:
|
||||||
|
self.error_uri = '/errors/invalid'
|
||||||
|
message = _("Property 'group' must be specified.")
|
||||||
|
break
|
||||||
|
|
||||||
return True
|
(group, message) = self._resolve_group(group_id)
|
||||||
|
if message:
|
||||||
|
return self._build_error(message)
|
||||||
|
|
||||||
|
# Normalize the group id
|
||||||
|
properties[GROUP_ID] = group.id
|
||||||
|
|
||||||
|
groups[group.id] = {
|
||||||
|
"name": group.name,
|
||||||
|
"type": group.type,
|
||||||
|
"level": group.level,
|
||||||
|
}
|
||||||
|
|
||||||
|
if message:
|
||||||
|
return self._build_error(message)
|
||||||
|
prepared_resources = {
|
||||||
|
"resources": res_copy,
|
||||||
|
"groups": groups,
|
||||||
|
}
|
||||||
|
return prepared_resources
|
||||||
|
|
||||||
def is_request_serviceable(self):
|
def is_request_serviceable(self):
|
||||||
"""Return true if request has at least one serviceable resource."""
|
"""Returns true if request has at least one serviceable resources."""
|
||||||
# TODO(JD): Ostro should return no placements vs throw an error.
|
# TODO(jdandrea): Ostro should return no placements vs throw an error.
|
||||||
resources = self.request.get('resources', {})
|
resources = self.request.get('resources', {})
|
||||||
for res in resources.itervalues():
|
for res in resources.itervalues():
|
||||||
res_type = res.get('type')
|
res_type = res.get('type')
|
||||||
@ -262,6 +260,53 @@ class Ostro(object):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# FIXME(jdandrea): Change name to create_or_update
|
||||||
|
def build_request(self, **kwargs):
|
||||||
|
"""Create or update a set of placements.
|
||||||
|
|
||||||
|
If False is returned, response attribute contains error info.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.args = kwargs.get('args')
|
||||||
|
self.tenant_id = kwargs.get('tenant_id')
|
||||||
|
self.response = None
|
||||||
|
self.error_uri = None
|
||||||
|
|
||||||
|
request = {
|
||||||
|
"action": kwargs.get('action', 'create'),
|
||||||
|
"stack_id": self.args.get('stack_id'),
|
||||||
|
"tenant_id": self.tenant_id,
|
||||||
|
"groups": {}, # Start with an empty dict to aid updates
|
||||||
|
}
|
||||||
|
|
||||||
|
# If we're updating, original_resources arg will have original info.
|
||||||
|
# Get this info first.
|
||||||
|
original_resources = self.args.get('original_resources')
|
||||||
|
if original_resources:
|
||||||
|
self.response = self._prepare_resources(original_resources)
|
||||||
|
if 'status' in self.response:
|
||||||
|
return False
|
||||||
|
request['original_resources'] = self.response['resources']
|
||||||
|
if 'groups' in self.response:
|
||||||
|
request['groups'] = self.response['groups']
|
||||||
|
|
||||||
|
# resources arg must always have new/updated info.
|
||||||
|
resources = self.args.get('resources')
|
||||||
|
self.response = self._prepare_resources(resources)
|
||||||
|
if 'status' in self.response:
|
||||||
|
return False
|
||||||
|
request['resources'] = self.response['resources']
|
||||||
|
if 'groups' in self.response:
|
||||||
|
# Update groups dict with new/updated group info.
|
||||||
|
request['groups'].update(self.response['groups'])
|
||||||
|
|
||||||
|
locations = self.args.get('locations')
|
||||||
|
if locations:
|
||||||
|
request['locations'] = locations
|
||||||
|
|
||||||
|
self.request = request
|
||||||
|
return True
|
||||||
|
|
||||||
def ping(self):
|
def ping(self):
|
||||||
"""Send a ping request and obtain a response."""
|
"""Send a ping request and obtain a response."""
|
||||||
stack_id = str(uuid.uuid4())
|
stack_id = str(uuid.uuid4())
|
||||||
@ -282,10 +327,24 @@ class Ostro(object):
|
|||||||
"action": "replan",
|
"action": "replan",
|
||||||
"stack_id": self.args['stack_id'],
|
"stack_id": self.args['stack_id'],
|
||||||
"locations": self.args['locations'],
|
"locations": self.args['locations'],
|
||||||
|
"resource_id": self.args['resource_id'],
|
||||||
"orchestration_id": self.args['orchestration_id'],
|
"orchestration_id": self.args['orchestration_id'],
|
||||||
"exclusions": self.args['exclusions'],
|
"exclusions": self.args['exclusions'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def identify(self, **kwargs):
|
||||||
|
"""Identify a placement for an existing resource."""
|
||||||
|
self.args = kwargs.get('args')
|
||||||
|
self.response = None
|
||||||
|
self.error_uri = None
|
||||||
|
self.asynchronous = True
|
||||||
|
self.request = {
|
||||||
|
"action": "identify",
|
||||||
|
"stack_id": self.args['stack_id'],
|
||||||
|
"orchestration_id": self.args['orchestration_id'],
|
||||||
|
"resource_id": self.args['uuid'],
|
||||||
|
}
|
||||||
|
|
||||||
def migrate(self, **kwargs):
|
def migrate(self, **kwargs):
|
||||||
"""Replan the placement for an existing resource."""
|
"""Replan the placement for an existing resource."""
|
||||||
self.args = kwargs.get('args')
|
self.args = kwargs.get('args')
|
||||||
@ -294,6 +353,7 @@ class Ostro(object):
|
|||||||
self.request = {
|
self.request = {
|
||||||
"action": "migrate",
|
"action": "migrate",
|
||||||
"stack_id": self.args['stack_id'],
|
"stack_id": self.args['stack_id'],
|
||||||
|
"tenant_id": self.args['tenant_id'],
|
||||||
"excluded_hosts": self.args['excluded_hosts'],
|
"excluded_hosts": self.args['excluded_hosts'],
|
||||||
"orchestration_id": self.args['orchestration_id'],
|
"orchestration_id": self.args['orchestration_id'],
|
||||||
}
|
}
|
||||||
|
28
valet/api/common/validation.py
Normal file
28
valet/api/common/validation.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Common Validation Helpers"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_uuid4(uuid_string):
|
||||||
|
"""Ensure uuid_string is v4 compliant."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
val = uuid.UUID(uuid_string, version=4)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return str(val) == uuid_string or val.hex == uuid_string
|
@ -12,20 +12,23 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Group Model"""
|
||||||
|
|
||||||
import simplejson
|
import simplejson
|
||||||
|
|
||||||
from valet.api.db.models.music import Base
|
from valet.api.db.models.music import Base
|
||||||
|
|
||||||
|
|
||||||
class Group(Base):
|
class Group(Base):
|
||||||
"""Group model."""
|
"""Group model"""
|
||||||
|
|
||||||
__tablename__ = 'groups'
|
__tablename__ = 'groups'
|
||||||
|
|
||||||
id = None # pylint: disable=C0103
|
id = None
|
||||||
name = None
|
name = None
|
||||||
description = None
|
description = None
|
||||||
type = None # pylint: disable=W0622
|
type = None
|
||||||
|
level = None
|
||||||
members = None
|
members = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -36,6 +39,7 @@ class Group(Base):
|
|||||||
'name': 'text',
|
'name': 'text',
|
||||||
'description': 'text',
|
'description': 'text',
|
||||||
'type': 'text',
|
'type': 'text',
|
||||||
|
'level': 'text',
|
||||||
'members': 'text',
|
'members': 'text',
|
||||||
'PRIMARY KEY': '(id)',
|
'PRIMARY KEY': '(id)',
|
||||||
}
|
}
|
||||||
@ -43,48 +47,52 @@ class Group(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pk_name(cls):
|
def pk_name(cls):
|
||||||
"""Primary key name."""
|
"""Primary key name"""
|
||||||
return 'id'
|
return 'id'
|
||||||
|
|
||||||
def pk_value(self):
|
def pk_value(self):
|
||||||
"""Primary key value."""
|
"""Primary key value"""
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
"""Values."""
|
"""Values"""
|
||||||
# TODO(UNKNOWN): Support lists in Music
|
# TODO(JD): Support lists in Music
|
||||||
# Lists aren't directly supported in Music, so we have to
|
# Lists aren't directly supported in Music, so we have to
|
||||||
# convert to/from json on the way out/in.
|
# convert to/from json on the way out/in.
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
|
'level': self.level,
|
||||||
'members': simplejson.dumps(self.members),
|
'members': simplejson.dumps(self.members),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, description, type, members, _insert=True):
|
def __init__(self, name, description, type, level, members, _insert=True):
|
||||||
"""Initializer."""
|
"""Initializer"""
|
||||||
super(Group, self).__init__()
|
super(Group, self).__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description or ""
|
self.description = description or ""
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.level = level
|
||||||
if _insert:
|
if _insert:
|
||||||
self.members = [] # members ignored at init time
|
self.members = members
|
||||||
self.insert()
|
self.insert()
|
||||||
else:
|
else:
|
||||||
# TODO(UNKNOWN): Support lists in Music
|
# TODO(UNKNOWN): Support lists in Music
|
||||||
self.members = simplejson.loads(members)
|
self.members = simplejson.loads(members)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Object representation."""
|
"""Object representation"""
|
||||||
return '<Group %r>' % self.name
|
return '<Group {} (type={}, level={})>'.format(
|
||||||
|
self.name, self.type, self.level)
|
||||||
|
|
||||||
def __json__(self):
|
def __json__(self):
|
||||||
"""JSON representation."""
|
"""JSON representation"""
|
||||||
json_ = {}
|
json_ = {}
|
||||||
json_['id'] = self.id
|
json_['id'] = self.id
|
||||||
json_['name'] = self.name
|
json_['name'] = self.name
|
||||||
json_['description'] = self.description
|
json_['description'] = self.description
|
||||||
json_['type'] = self.type
|
json_['type'] = self.type
|
||||||
|
json_['level'] = self.level
|
||||||
json_['members'] = self.members
|
json_['members'] = self.members
|
||||||
return json_
|
return json_
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
"""Placement Model."""
|
"""Placement Model."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from valet.api.db.models.music import Base
|
from valet.api.db.models.music import Base
|
||||||
from valet.api.db.models.music import Query
|
from valet.api.db.models.music import Query
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ class Placement(Base):
|
|||||||
orchestration_id = None
|
orchestration_id = None
|
||||||
resource_id = None
|
resource_id = None
|
||||||
location = None
|
location = None
|
||||||
|
metadata = None
|
||||||
plan_id = None
|
plan_id = None
|
||||||
plan = None
|
plan = None
|
||||||
|
|
||||||
@ -43,6 +46,7 @@ class Placement(Base):
|
|||||||
'location': 'text',
|
'location': 'text',
|
||||||
'reserved': 'boolean',
|
'reserved': 'boolean',
|
||||||
'plan_id': 'text',
|
'plan_id': 'text',
|
||||||
|
'metadata': 'text',
|
||||||
'PRIMARY KEY': '(id)',
|
'PRIMARY KEY': '(id)',
|
||||||
}
|
}
|
||||||
return schema
|
return schema
|
||||||
@ -64,12 +68,14 @@ class Placement(Base):
|
|||||||
'resource_id': self.resource_id,
|
'resource_id': self.resource_id,
|
||||||
'location': self.location,
|
'location': self.location,
|
||||||
'reserved': self.reserved,
|
'reserved': self.reserved,
|
||||||
|
'metadata': json.dumps(self.metadata),
|
||||||
'plan_id': self.plan_id,
|
'plan_id': self.plan_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name, orchestration_id, resource_id=None, plan=None,
|
def __init__(self, name, orchestration_id, resource_id=None, plan=None,
|
||||||
plan_id=None, location=None, reserved=False, _insert=True):
|
plan_id=None, location=None, reserved=False, metadata=None,
|
||||||
"""Initializer."""
|
_insert=True):
|
||||||
|
"""Initializer"""
|
||||||
super(Placement, self).__init__()
|
super(Placement, self).__init__()
|
||||||
self.name = name
|
self.name = name
|
||||||
self.orchestration_id = orchestration_id
|
self.orchestration_id = orchestration_id
|
||||||
@ -81,7 +87,10 @@ class Placement(Base):
|
|||||||
self.location = location
|
self.location = location
|
||||||
self.reserved = reserved
|
self.reserved = reserved
|
||||||
if _insert:
|
if _insert:
|
||||||
|
self.metadata = metadata
|
||||||
self.insert()
|
self.insert()
|
||||||
|
else:
|
||||||
|
self.metadata = json.loads(metadata or "{}")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Object representation."""
|
"""Object representation."""
|
||||||
@ -96,5 +105,6 @@ class Placement(Base):
|
|||||||
json_['resource_id'] = self.resource_id
|
json_['resource_id'] = self.resource_id
|
||||||
json_['location'] = self.location
|
json_['location'] = self.location
|
||||||
json_['reserved'] = self.reserved
|
json_['reserved'] = self.reserved
|
||||||
|
json_['metadata'] = self.metadata
|
||||||
json_['plan_id'] = self.plan.id
|
json_['plan_id'] = self.plan.id
|
||||||
return json_
|
return json_
|
||||||
|
@ -13,34 +13,61 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Controllers Package."""
|
"""Controllers Package"""
|
||||||
|
|
||||||
from notario.decorators import instance_of
|
|
||||||
from notario import ensure
|
|
||||||
from os import path
|
from os import path
|
||||||
|
import string
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from notario.exceptions import Invalid
|
||||||
|
from notario.utils import forced_leaf_validator
|
||||||
from pecan import redirect
|
from pecan import redirect
|
||||||
from pecan import request
|
from pecan import request
|
||||||
import string
|
|
||||||
|
|
||||||
from valet import api
|
from valet import api
|
||||||
from valet.api.common.i18n import _
|
from valet.api.common.i18n import _
|
||||||
from valet.api.db.models.music.placements import Placement
|
from valet.api.db.models.music.placements import Placement
|
||||||
|
|
||||||
|
# Supported valet-engine query types
|
||||||
|
QUERY_TYPES = (
|
||||||
|
'group_vms',
|
||||||
|
'invalid_placements'
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Notario Helpers
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
def valid_group_name(value):
|
def valid_group_name(value):
|
||||||
"""Validator for group name type."""
|
"""Validator for group name type."""
|
||||||
if (not value or
|
valid_chars = set(string.letters + string.digits + "-._~")
|
||||||
not set(value) <= set(string.letters + string.digits + "-._~")):
|
if not value or not set(value) <= valid_chars:
|
||||||
api.LOG.error("group name is not valid")
|
api.LOG.error("group name is not valid")
|
||||||
api.LOG.error("group name must contain only uppercase and lowercase "
|
api.LOG.error("group name must contain only uppercase and lowercase "
|
||||||
"letters, decimal digits, hyphens, periods, "
|
"letters, decimal digits, hyphens, periods, "
|
||||||
"underscores, "" and tildes [RFC 3986, Section 2.3]")
|
"underscores, and tildes [RFC 3986, Section 2.3]")
|
||||||
|
|
||||||
|
|
||||||
@instance_of((list, dict))
|
# There is a bug in Notario that prevents basic checks for a list/dict
|
||||||
def valid_plan_resources(value):
|
# (without recursion/depth). Instead, we borrow a hack used in the Ceph
|
||||||
"""Validator for plan resources."""
|
# installer, which it turns out also isn't quite correct. Some of the
|
||||||
ensure(len(value) > 0)
|
# code has been removed. Source: https://github.com/ceph/ceph-installer ...
|
||||||
|
# /blob/master/ceph_installer/schemas.py#L15-L31 (devices_object())
|
||||||
|
@forced_leaf_validator
|
||||||
|
def list_or_dict(value, *args):
|
||||||
|
"""Validator - Value must be of type list or dict"""
|
||||||
|
error_msg = 'not of type list or dict'
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
assert isinstance(value, list)
|
||||||
|
except AssertionError:
|
||||||
|
if args:
|
||||||
|
# What does 'dict type' and 'value' mean in this context?
|
||||||
|
raise Invalid(
|
||||||
|
'dict type', pair='value', msg=None, reason=error_msg, *args)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def valid_plan_update_action(value):
|
def valid_plan_update_action(value):
|
||||||
@ -53,13 +80,14 @@ def valid_plan_update_action(value):
|
|||||||
|
|
||||||
|
|
||||||
def set_placements(plan, resources, placements):
|
def set_placements(plan, resources, placements):
|
||||||
"""Set placements."""
|
"""Set placements"""
|
||||||
for uuid in placements.iterkeys():
|
for uuid_key in placements.iterkeys():
|
||||||
name = resources[uuid]['name']
|
name = resources[uuid_key]['name']
|
||||||
properties = placements[uuid]['properties']
|
properties = placements[uuid_key]['properties']
|
||||||
location = properties['host']
|
location = properties['host']
|
||||||
Placement(name, uuid, plan=plan, location=location)
|
metadata = resources[uuid_key].get('metadata', {})
|
||||||
|
Placement(name, uuid_key, plan=plan,
|
||||||
|
location=location, metadata=metadata)
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
|
|
||||||
@ -70,41 +98,75 @@ def reserve_placement(placement, resource_id=None, reserve=True, update=True):
|
|||||||
the data store (if the update will be made later).
|
the data store (if the update will be made later).
|
||||||
"""
|
"""
|
||||||
if placement:
|
if placement:
|
||||||
api.LOG.info(_('%(rsrv)s placement of %(orch_id)s in %(loc)s.'),
|
msg = _('%(rsrv)s placement of %(orch_id)s in %(loc)s.')
|
||||||
{'rsrv': _("Reserving") if reserve else _("Unreserving"),
|
args = {
|
||||||
|
'rsrv': _("Reserving") if reserve else _("Unreserving"),
|
||||||
'orch_id': placement.orchestration_id,
|
'orch_id': placement.orchestration_id,
|
||||||
'loc': placement.location})
|
'loc': placement.location,
|
||||||
|
}
|
||||||
|
api.LOG.info(msg, args)
|
||||||
placement.reserved = reserve
|
placement.reserved = reserve
|
||||||
if resource_id:
|
if resource_id:
|
||||||
msg = _('Associating resource id %(res_id)s with orchestration '
|
msg = _('Associating resource id %(res_id)s with '
|
||||||
'id %(orch_id)s.')
|
'orchestration id %(orch_id)s.')
|
||||||
api.LOG.info(msg, {'res_id': resource_id,
|
args = {
|
||||||
'orch_id': placement.orchestration_id})
|
'res_id': resource_id,
|
||||||
|
'orch_id': placement.orchestration_id,
|
||||||
|
}
|
||||||
|
api.LOG.info(msg, args)
|
||||||
placement.resource_id = resource_id
|
placement.resource_id = resource_id
|
||||||
if update:
|
if update:
|
||||||
placement.update()
|
placement.update()
|
||||||
|
|
||||||
|
|
||||||
def update_placements(placements, reserve_id=None, unlock_all=False):
|
def engine_query_args(query_type=None, parameters={}):
|
||||||
|
"""Make a general query of valet-engine."""
|
||||||
|
if query_type not in QUERY_TYPES:
|
||||||
|
return {}
|
||||||
|
transaction_id = str(uuid.uuid4())
|
||||||
|
args = {
|
||||||
|
"stack_id": transaction_id,
|
||||||
|
}
|
||||||
|
if query_type:
|
||||||
|
args['type'] = query_type
|
||||||
|
args['parameters'] = parameters
|
||||||
|
ostro_kwargs = {
|
||||||
|
"args": args,
|
||||||
|
}
|
||||||
|
return ostro_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def update_placements(placements, plan=None, resources=None,
|
||||||
|
reserve_id=None, unlock_all=False):
|
||||||
"""Update placements. Optionally reserve one placement."""
|
"""Update placements. Optionally reserve one placement."""
|
||||||
for uuid in placements.iterkeys():
|
new_placements = {}
|
||||||
placement = Placement.query.filter_by( # pylint: disable=E1101
|
for uuid_key in placements.iterkeys():
|
||||||
orchestration_id=uuid).first()
|
placement = Placement.query.filter_by(
|
||||||
|
orchestration_id=uuid_key).first()
|
||||||
if placement:
|
if placement:
|
||||||
properties = placements[uuid]['properties']
|
# Don't use plan or resources for upates (metadata stays as-is).
|
||||||
|
properties = placements[uuid_key]['properties']
|
||||||
location = properties['host']
|
location = properties['host']
|
||||||
if placement.location != location:
|
if placement.location != location:
|
||||||
msg = _('Changing placement of %(orch_id)s from %(old_loc)s '
|
msg = _('Changing placement of %(orch_id)s from '
|
||||||
'to %(new_loc)s.')
|
'%(old_loc)s to %(new_loc)s.')
|
||||||
api.LOG.info(msg, {'orch_id': placement.orchestration_id,
|
args = {
|
||||||
|
'orch_id': placement.orchestration_id,
|
||||||
'old_loc': placement.location,
|
'old_loc': placement.location,
|
||||||
'new_loc': location})
|
'new_loc': location,
|
||||||
|
}
|
||||||
|
api.LOG.info(msg, args)
|
||||||
placement.location = location
|
placement.location = location
|
||||||
if unlock_all:
|
if unlock_all:
|
||||||
reserve_placement(placement, reserve=False, update=False)
|
reserve_placement(placement, reserve=False, update=False)
|
||||||
elif reserve_id and placement.orchestration_id == reserve_id:
|
elif reserve_id and placement.orchestration_id == reserve_id:
|
||||||
reserve_placement(placement, reserve=True, update=False)
|
reserve_placement(placement, reserve=True, update=False)
|
||||||
placement.update()
|
placement.update()
|
||||||
|
else:
|
||||||
|
new_placements[uuid_key] = placements[uuid_key]
|
||||||
|
|
||||||
|
if new_placements and plan and resources:
|
||||||
|
set_placements(plan, resources, new_placements)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@ -113,7 +175,7 @@ def update_placements(placements, reserve_id=None, unlock_all=False):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def error(url, msg=None, **kwargs):
|
def error(url, msg=None, **kwargs):
|
||||||
"""Error handler."""
|
"""Error handler"""
|
||||||
if msg:
|
if msg:
|
||||||
request.context['error_message'] = msg
|
request.context['error_message'] = msg
|
||||||
if kwargs:
|
if kwargs:
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Groups."""
|
"""Groups"""
|
||||||
|
|
||||||
from notario import decorators
|
from notario import decorators
|
||||||
from notario.validators import types
|
from notario.validators import types
|
||||||
@ -28,38 +28,31 @@ from valet.api.common.compute import nova_client
|
|||||||
from valet.api.common.i18n import _
|
from valet.api.common.i18n import _
|
||||||
from valet.api.common.ostro_helper import Ostro
|
from valet.api.common.ostro_helper import Ostro
|
||||||
from valet.api.db.models.music.groups import Group
|
from valet.api.db.models.music.groups import Group
|
||||||
|
from valet.api.v1.controllers import engine_query_args
|
||||||
from valet.api.v1.controllers import error
|
from valet.api.v1.controllers import error
|
||||||
from valet.api.v1.controllers import valid_group_name
|
from valet.api.v1.controllers import valid_group_name
|
||||||
|
|
||||||
|
|
||||||
GROUPS_SCHEMA = (
|
GROUPS_SCHEMA = (
|
||||||
(decorators.optional('description'), types.string),
|
(decorators.optional('description'), types.string),
|
||||||
|
('level', types.string),
|
||||||
('name', valid_group_name),
|
('name', valid_group_name),
|
||||||
('type', types.string)
|
('type', types.string),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Schemas with one field MUST NOT get trailing commas, kthx.
|
||||||
UPDATE_GROUPS_SCHEMA = (
|
UPDATE_GROUPS_SCHEMA = (
|
||||||
(decorators.optional('description'), types.string)
|
(decorators.optional('description'), types.string))
|
||||||
)
|
|
||||||
|
|
||||||
MEMBERS_SCHEMA = (
|
MEMBERS_SCHEMA = (
|
||||||
('members', types.array)
|
('members', types.array))
|
||||||
)
|
|
||||||
|
|
||||||
# pylint: disable=R0201
|
|
||||||
|
|
||||||
|
|
||||||
def server_list_for_group(group):
|
def server_list_for_group(group):
|
||||||
"""Return a list of VMs associated with a member/group."""
|
"""Returns a list of VMs associated with a member/group."""
|
||||||
args = {
|
parameters = {
|
||||||
"type": "group_vms",
|
|
||||||
"parameters": {
|
|
||||||
"group_name": group.name,
|
"group_name": group.name,
|
||||||
},
|
|
||||||
}
|
|
||||||
ostro_kwargs = {
|
|
||||||
"args": args,
|
|
||||||
}
|
}
|
||||||
|
ostro_kwargs = engine_query_args(query_type="group_vms",
|
||||||
|
parameters=parameters)
|
||||||
ostro = Ostro()
|
ostro = Ostro()
|
||||||
ostro.query(**ostro_kwargs)
|
ostro.query(**ostro_kwargs)
|
||||||
ostro.send()
|
ostro.send()
|
||||||
@ -74,7 +67,7 @@ def server_list_for_group(group):
|
|||||||
|
|
||||||
|
|
||||||
def tenant_servers_in_group(tenant_id, group):
|
def tenant_servers_in_group(tenant_id, group):
|
||||||
"""Return a list of servers the current tenant has in group_name."""
|
"""Returns a list of servers the current tenant has in group_name"""
|
||||||
servers = []
|
servers = []
|
||||||
server_list = server_list_for_group(group)
|
server_list = server_list_for_group(group)
|
||||||
nova = nova_client()
|
nova = nova_client()
|
||||||
@ -97,17 +90,16 @@ def no_tenant_servers_in_group(tenant_id, group):
|
|||||||
"""
|
"""
|
||||||
server_list = tenant_servers_in_group(tenant_id, group)
|
server_list = tenant_servers_in_group(tenant_id, group)
|
||||||
if server_list:
|
if server_list:
|
||||||
error('/errors/conflict', _('Tenant Member {0} has servers in group '
|
msg = _('Tenant Member {0} has servers in group "{1}": {2}')
|
||||||
'"{1}": {2}').format(tenant_id,
|
error('/errors/conflict',
|
||||||
group.name,
|
msg.format(tenant_id, group.name, server_list))
|
||||||
server_list))
|
|
||||||
|
|
||||||
|
|
||||||
class MembersItemController(object):
|
class MembersItemController(object):
|
||||||
"""Member Item Controller /v1/groups/{group_id}/members/{member_id}."""
|
"""Members Item Controller /v1/groups/{group_id}/members/{member_id}"""
|
||||||
|
|
||||||
def __init__(self, member_id):
|
def __init__(self, member_id):
|
||||||
"""Initialize group member."""
|
"""Initialize group member"""
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
if member_id not in group.members:
|
if member_id not in group.members:
|
||||||
error('/errors/not_found', _('Member not found in group'))
|
error('/errors/not_found', _('Member not found in group'))
|
||||||
@ -115,30 +107,30 @@ class MembersItemController(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,DELETE'
|
return 'GET,DELETE'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catch all for unallowed methods."""
|
"""Catch all for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self):
|
||||||
"""Verify group member."""
|
"""Verify group member"""
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='DELETE', template='json')
|
@index.when(method='DELETE', template='json')
|
||||||
def index_delete(self):
|
def index_delete(self):
|
||||||
"""Delete group member."""
|
"""Delete group member"""
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
member_id = request.context['member_id']
|
member_id = request.context['member_id']
|
||||||
|
|
||||||
@ -151,38 +143,39 @@ class MembersItemController(object):
|
|||||||
|
|
||||||
|
|
||||||
class MembersController(object):
|
class MembersController(object):
|
||||||
"""Members Controller /v1/groups/{group_id}/members."""
|
"""Members Controller /v1/groups/{group_id}/members"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'PUT,DELETE'
|
return 'PUT,DELETE'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='PUT', template='json')
|
@index.when(method='PUT', template='json')
|
||||||
@validate(MEMBERS_SCHEMA, '/errors/schema')
|
@validate(MEMBERS_SCHEMA, '/errors/schema')
|
||||||
def index_put(self, **kwargs):
|
def index_put(self, **kwargs):
|
||||||
"""Add one or more members to a group."""
|
"""Add one or more members to a group"""
|
||||||
new_members = kwargs.get('members', None)
|
new_members = kwargs.get('members', [])
|
||||||
|
|
||||||
if not conf.identity.engine.is_tenant_list_valid(new_members):
|
if not conf.identity.engine.is_tenant_list_valid(new_members):
|
||||||
error('/errors/conflict', _('Member list contains '
|
error('/errors/conflict',
|
||||||
'invalid tenant IDs'))
|
_('Member list contains invalid tenant IDs'))
|
||||||
|
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
group.members = list(set(group.members + new_members))
|
member_list = group.members or []
|
||||||
|
group.members = list(set(member_list + new_members))
|
||||||
group.update()
|
group.update()
|
||||||
response.status = 201
|
response.status = 201
|
||||||
|
|
||||||
@ -192,7 +185,7 @@ class MembersController(object):
|
|||||||
|
|
||||||
@index.when(method='DELETE', template='json')
|
@index.when(method='DELETE', template='json')
|
||||||
def index_delete(self):
|
def index_delete(self):
|
||||||
"""Delete all group members."""
|
"""Delete all group members"""
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
|
|
||||||
# Can't delete a member if it has associated VMs.
|
# Can't delete a member if it has associated VMs.
|
||||||
@ -205,51 +198,52 @@ class MembersController(object):
|
|||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _lookup(self, member_id, *remainder):
|
def _lookup(self, member_id, *remainder):
|
||||||
"""Pecan subcontroller routing callback."""
|
"""Pecan subcontroller routing callback"""
|
||||||
return MembersItemController(member_id), remainder
|
return MembersItemController(member_id), remainder
|
||||||
|
|
||||||
|
|
||||||
class GroupsItemController(object):
|
class GroupsItemController(object):
|
||||||
"""Group Item Controller /v1/groups/{group_id}."""
|
"""Groups Item Controller /v1/groups/{group_id}"""
|
||||||
|
|
||||||
members = MembersController()
|
members = MembersController()
|
||||||
|
|
||||||
def __init__(self, group_id):
|
def __init__(self, group_id):
|
||||||
"""Initialize group."""
|
"""Initialize group"""
|
||||||
# pylint:disable=E1101
|
|
||||||
group = Group.query.filter_by(id=group_id).first()
|
group = Group.query.filter_by(id=group_id).first()
|
||||||
|
if not group:
|
||||||
|
group = Group.query.filter_by(name=group_id).first()
|
||||||
if not group:
|
if not group:
|
||||||
error('/errors/not_found', _('Group not found'))
|
error('/errors/not_found', _('Group not found'))
|
||||||
request.context['group'] = group
|
request.context['group'] = group
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,PUT,DELETE'
|
return 'GET,PUT,DELETE'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self):
|
||||||
"""Display a group."""
|
"""Display a group"""
|
||||||
return {"group": request.context['group']}
|
return {"group": request.context['group']}
|
||||||
|
|
||||||
@index.when(method='PUT', template='json')
|
@index.when(method='PUT', template='json')
|
||||||
@validate(UPDATE_GROUPS_SCHEMA, '/errors/schema')
|
@validate(UPDATE_GROUPS_SCHEMA, '/errors/schema')
|
||||||
def index_put(self, **kwargs):
|
def index_put(self, **kwargs):
|
||||||
"""Update a group."""
|
"""Update a group"""
|
||||||
# Name and type are immutable.
|
# Name, type, and level are immutable.
|
||||||
# Group Members are updated in MembersController.
|
# Group Members are updated in MembersController.
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
group.description = kwargs.get('description', group.description)
|
group.description = kwargs.get('description', group.description)
|
||||||
@ -262,42 +256,44 @@ class GroupsItemController(object):
|
|||||||
|
|
||||||
@index.when(method='DELETE', template='json')
|
@index.when(method='DELETE', template='json')
|
||||||
def index_delete(self):
|
def index_delete(self):
|
||||||
"""Delete a group."""
|
"""Delete a group"""
|
||||||
group = request.context['group']
|
group = request.context['group']
|
||||||
|
# tenant_id = request.context['tenant_id']
|
||||||
if isinstance(group.members, list) and len(group.members) > 0:
|
if isinstance(group.members, list) and len(group.members) > 0:
|
||||||
error('/errors/conflict',
|
message = _('Unable to delete a Group with members.')
|
||||||
_('Unable to delete a Group with members.'))
|
error('/errors/conflict', message)
|
||||||
|
|
||||||
group.delete()
|
group.delete()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
|
|
||||||
class GroupsController(object):
|
class GroupsController(object):
|
||||||
"""Group Controller /v1/groups."""
|
"""Groups Controller /v1/groups"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,POST'
|
return 'GET,POST'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catch all for unallowed methods."""
|
"""Catch all for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self):
|
||||||
"""List groups."""
|
"""List groups"""
|
||||||
try:
|
try:
|
||||||
groups_array = []
|
groups_array = []
|
||||||
for group in Group.query.all(): # pylint: disable=E1101
|
for group in Group.query.all():
|
||||||
groups_array.append(group)
|
groups_array.append(group)
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
@ -308,14 +304,21 @@ class GroupsController(object):
|
|||||||
@index.when(method='POST', template='json')
|
@index.when(method='POST', template='json')
|
||||||
@validate(GROUPS_SCHEMA, '/errors/schema')
|
@validate(GROUPS_SCHEMA, '/errors/schema')
|
||||||
def index_post(self, **kwargs):
|
def index_post(self, **kwargs):
|
||||||
"""Create a group."""
|
"""Create a group"""
|
||||||
group_name = kwargs.get('name', None)
|
group_name = kwargs.get('name', None)
|
||||||
description = kwargs.get('description', None)
|
description = kwargs.get('description', None)
|
||||||
group_type = kwargs.get('type', None)
|
group_type = kwargs.get('type', None)
|
||||||
|
group_level = kwargs.get('level', None)
|
||||||
members = [] # Use /v1/groups/members endpoint to add members
|
members = [] # Use /v1/groups/members endpoint to add members
|
||||||
|
|
||||||
|
group = Group.query.filter_by(name=group_name).first()
|
||||||
|
if group:
|
||||||
|
message = _("A group named {} already exists")
|
||||||
|
error('/errors/invalid', message.format(group_name))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
group = Group(group_name, description, group_type, members)
|
group = Group(group_name, description, group_type,
|
||||||
|
group_level, members)
|
||||||
if group:
|
if group:
|
||||||
response.status = 201
|
response.status = 201
|
||||||
|
|
||||||
@ -327,5 +330,5 @@ class GroupsController(object):
|
|||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _lookup(self, group_id, *remainder):
|
def _lookup(self, group_id, *remainder):
|
||||||
"""Pecan subcontroller routing callback."""
|
"""Pecan subcontroller routing callback"""
|
||||||
return GroupsItemController(group_id), remainder
|
return GroupsItemController(group_id), remainder
|
||||||
|
@ -12,6 +12,11 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Placements"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from pecan import expose
|
from pecan import expose
|
||||||
from pecan import request
|
from pecan import request
|
||||||
from pecan import response
|
from pecan import response
|
||||||
@ -26,40 +31,35 @@ from valet.api.v1.controllers import reserve_placement
|
|||||||
from valet.api.v1.controllers import update_placements
|
from valet.api.v1.controllers import update_placements
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=R0201
|
|
||||||
|
|
||||||
|
|
||||||
class PlacementsItemController(object):
|
class PlacementsItemController(object):
|
||||||
"""Placements Item Controller /v1/placements/{placement_id}."""
|
"""Placements Item Controller /v1/placements/{placement_id}"""
|
||||||
|
|
||||||
def __init__(self, uuid4):
|
def __init__(self, uuid4):
|
||||||
"""Initializer."""
|
"""Initializer."""
|
||||||
self.uuid = uuid4
|
self.uuid = uuid4
|
||||||
self.placement = Placement.query.filter_by(id=self.uuid).first()
|
self.placement = Placement.query.filter_by(id=self.uuid).first()
|
||||||
# pylint: disable=E1101
|
|
||||||
if not self.placement:
|
if not self.placement:
|
||||||
self.placement = Placement.query.filter_by(
|
self.placement = Placement.query.filter_by(
|
||||||
orchestration_id=self.uuid).first()
|
orchestration_id=self.uuid).first()
|
||||||
# disable=E1101
|
|
||||||
if not self.placement:
|
if not self.placement:
|
||||||
error('/errors/not_found', _('Placement not found'))
|
error('/errors/not_found', _('Placement not found'))
|
||||||
request.context['placement_id'] = self.placement.id
|
request.context['placement_id'] = self.placement.id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,POST,DELETE'
|
return 'GET,POST,DELETE'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@ -75,37 +75,72 @@ class PlacementsItemController(object):
|
|||||||
def index_post(self, **kwargs):
|
def index_post(self, **kwargs):
|
||||||
"""Reserve a placement. This and other placements may be replanned.
|
"""Reserve a placement. This and other placements may be replanned.
|
||||||
|
|
||||||
Once reserved, the location effectively becomes immutable.
|
Once reserved, the location effectively becomes immutable unless
|
||||||
|
a replan is forced (due to a resource replacement, for example).
|
||||||
"""
|
"""
|
||||||
res_id = kwargs.get('resource_id')
|
res_id = kwargs.get('resource_id')
|
||||||
msg = _('Placement reservation request for resource id %(res_id)s, '
|
api.LOG.info(_('Placement reservation request for resource \
|
||||||
'orchestration id %(orch_id)s.')
|
id %(res_id)s, orchestration id %(orch_id)s.'),
|
||||||
api.LOG.info(msg, {'res_id': res_id,
|
{'res_id': res_id,
|
||||||
'orch_id': self.placement.orchestration_id})
|
'orch_id': self.placement.orchestration_id})
|
||||||
|
|
||||||
|
actions = ('reserve', 'replan')
|
||||||
|
action = kwargs.get('action', 'reserve')
|
||||||
|
if action not in actions:
|
||||||
|
message = _('Invalid action: {}. Must be one of {}')
|
||||||
|
error('/errors/invalid', message.format(action, actions))
|
||||||
|
|
||||||
locations = kwargs.get('locations', [])
|
locations = kwargs.get('locations', [])
|
||||||
locations_str = ', '.join(locations)
|
locations_str = ', '.join(locations)
|
||||||
api.LOG.info(_('Candidate locations: %s'), locations_str)
|
api.LOG.info(_('Candidate locations: %s'), locations_str)
|
||||||
if self.placement.location in locations:
|
if action == 'reserve' and self.placement.location in locations:
|
||||||
# Ostro's placement is in the list of candidates. Good!
|
# Ostro's placement is in the list of candidates. Good!
|
||||||
# Reserve it. Remember the resource id too.
|
# But first, we have to pass the engine's identify test.
|
||||||
|
|
||||||
|
plan = Plan.query.filter_by(id=self.placement.plan_id).first()
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"stack_id": plan.stack_id,
|
||||||
|
"orchestration_id": self.placement.orchestration_id,
|
||||||
|
"uuid": res_id,
|
||||||
|
}
|
||||||
|
ostro_kwargs = {"args": args, }
|
||||||
|
ostro = Ostro()
|
||||||
|
ostro.identify(**ostro_kwargs)
|
||||||
|
ostro.send()
|
||||||
|
|
||||||
|
status_type = ostro.response['status']['type']
|
||||||
|
if status_type != 'ok':
|
||||||
|
message = ostro.response['status']['message']
|
||||||
|
error(ostro.error_uri, _('Ostro error: %s') % message)
|
||||||
|
|
||||||
|
# We're in the clear. Reserve it. Remember the resource id too.
|
||||||
kwargs = {'resource_id': res_id}
|
kwargs = {'resource_id': res_id}
|
||||||
reserve_placement(self.placement, **kwargs)
|
reserve_placement(self.placement, **kwargs)
|
||||||
response.status = 201
|
response.status = 201
|
||||||
else:
|
else:
|
||||||
|
if action == 'reserve':
|
||||||
# Ostro's placement is NOT in the list of candidates.
|
# Ostro's placement is NOT in the list of candidates.
|
||||||
# Time for Plan B.
|
# Time for Plan B.
|
||||||
msg = _('Placement of resource id %(res_id)s, orchestration id '
|
api.LOG.info(_('Placement of resource id %(res_id)s, \
|
||||||
'%(orch_id)s in %(loc)s not allowed. Replanning.')
|
orchestration id %(orch_id)s in %(loc)s \
|
||||||
api.LOG.info(msg, {'res_id': res_id,
|
not allowed. Replanning.'),
|
||||||
|
{'res_id': res_id,
|
||||||
'orch_id': self.placement.orchestration_id,
|
'orch_id': self.placement.orchestration_id,
|
||||||
'loc': self.placement.location})
|
'loc': self.placement.location})
|
||||||
|
else:
|
||||||
|
# A replan was expressly requested (action == 'replan')
|
||||||
|
api.LOG.info(_('Replanning resource id %(res_id)s, \
|
||||||
|
orchestration id %(orch_id)s.'),
|
||||||
|
{'res_id': res_id,
|
||||||
|
'orch_id': self.placement.orchestration_id})
|
||||||
|
|
||||||
# Unreserve the placement. Remember the resource id too.
|
# Unreserve the placement. Remember the resource id too.
|
||||||
kwargs = {'resource_id': res_id, 'reserve': False}
|
kwargs = {'resource_id': res_id, 'reserve': False}
|
||||||
reserve_placement(self.placement, **kwargs)
|
reserve_placement(self.placement, **kwargs)
|
||||||
|
|
||||||
# Find all the reserved placements for the related plan.
|
# Find all the reserved placements for the related plan.
|
||||||
reserved = Placement.query.filter_by( # pylint: disable=E1101
|
reserved = Placement.query.filter_by(
|
||||||
plan_id=self.placement.plan_id, reserved=True)
|
plan_id=self.placement.plan_id, reserved=True)
|
||||||
|
|
||||||
# Keep this placement's orchestration ID handy.
|
# Keep this placement's orchestration ID handy.
|
||||||
@ -125,11 +160,11 @@ class PlacementsItemController(object):
|
|||||||
# One of those will be the original placement
|
# One of those will be the original placement
|
||||||
# we are trying to reserve.
|
# we are trying to reserve.
|
||||||
plan = Plan.query.filter_by(id=self.placement.plan_id).first()
|
plan = Plan.query.filter_by(id=self.placement.plan_id).first()
|
||||||
# pylint: disable=E1101
|
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"stack_id": plan.stack_id,
|
"stack_id": plan.stack_id,
|
||||||
"locations": locations,
|
"locations": locations,
|
||||||
|
"resource_id": res_id,
|
||||||
"orchestration_id": orchestration_id,
|
"orchestration_id": orchestration_id,
|
||||||
"exclusions": exclusions,
|
"exclusions": exclusions,
|
||||||
}
|
}
|
||||||
@ -148,49 +183,97 @@ class PlacementsItemController(object):
|
|||||||
update_placements(placements, reserve_id=orchestration_id)
|
update_placements(placements, reserve_id=orchestration_id)
|
||||||
response.status = 201
|
response.status = 201
|
||||||
|
|
||||||
placement = Placement.query.filter_by( # pylint: disable=E1101
|
placement = Placement.query.filter_by(
|
||||||
orchestration_id=self.placement.orchestration_id).first()
|
orchestration_id=self.placement.orchestration_id).first()
|
||||||
return {"placement": placement}
|
return {"placement": placement}
|
||||||
|
|
||||||
@index.when(method='DELETE', template='json')
|
@index.when(method='DELETE', template='json')
|
||||||
def index_delete(self):
|
def index_delete(self):
|
||||||
"""Delete a Placement."""
|
"""Delete a Placement"""
|
||||||
orch_id = self.placement.orchestration_id
|
orch_id = self.placement.orchestration_id
|
||||||
self.placement.delete()
|
self.placement.delete()
|
||||||
api.LOG.info(_('Placement with orchestration id %s deleted.'), orch_id)
|
api.LOG.info(_('Placement with orchestration id %s deleted.'),
|
||||||
|
orch_id)
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
|
|
||||||
class PlacementsController(object):
|
class PlacementsController(object):
|
||||||
"""Placements Controller /v1/placements."""
|
"""Placements Controller /v1/placements"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET'
|
return 'GET'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self, **kwargs):
|
||||||
"""Get placements."""
|
"""Get placements."""
|
||||||
placements_array = []
|
placements_array = []
|
||||||
for placement in Placement.query.all(): # pylint: disable=E1101
|
for placement in Placement.query.all():
|
||||||
|
# If there are query string args, look for them in two places,
|
||||||
|
# and in this order:
|
||||||
|
#
|
||||||
|
# 1. The main placement object, only for these reserved
|
||||||
|
# keys: id, orchestration_id, plan_id, resource_id,
|
||||||
|
# location, name, reserved.
|
||||||
|
# 2. The metadata.
|
||||||
|
#
|
||||||
|
# Support only exact matches for now. AND, not OR.
|
||||||
|
#
|
||||||
|
# Start by presuming we have a match, and look for fail cases.
|
||||||
|
# If search fails, no error, just don't append that placement.
|
||||||
|
# This also ends up appending if there are no kwargs (good).
|
||||||
|
append = True
|
||||||
|
for key, value in kwargs.iteritems():
|
||||||
|
# We don't allow the same key multiple times, so no lists,
|
||||||
|
# only strings. Don't even allow NoneType.
|
||||||
|
if not isinstance(value, basestring):
|
||||||
|
append = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Try loading as if it were json. If we can't, that's ok.
|
||||||
|
try:
|
||||||
|
# Using json_value to prevent side-effects.
|
||||||
|
json_value = json.loads(value)
|
||||||
|
value = json_value
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 1. If the key is one of our reserved keys ...
|
||||||
|
if key in ('id', 'orchestration_id', 'plan_id',
|
||||||
|
'resource_id', 'location', 'name',
|
||||||
|
'reserved') and hasattr(placement, key):
|
||||||
|
# ... and the value does not match in the main object,
|
||||||
|
# don't append it, and don't go on to check metadata.
|
||||||
|
if value != getattr(placement, key):
|
||||||
|
append = False
|
||||||
|
break
|
||||||
|
# 2. Otherwise, if the key is not in the metadata or
|
||||||
|
# the value does not match, don't append it.
|
||||||
|
elif (key not in placement.metadata or
|
||||||
|
value != placement.metadata.get(key)):
|
||||||
|
append = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if append:
|
||||||
placements_array.append(placement)
|
placements_array.append(placement)
|
||||||
|
|
||||||
return {"placements": placements_array}
|
return {"placements": placements_array}
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _lookup(self, uuid4, *remainder):
|
def _lookup(self, uuid4, *remainder):
|
||||||
"""Pecan subcontroller routing callback."""
|
"""Pecan subcontroller routing callback"""
|
||||||
return PlacementsItemController(uuid4), remainder
|
return PlacementsItemController(uuid4), remainder
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Plans."""
|
"""Plans"""
|
||||||
|
|
||||||
from notario import decorators
|
from notario import decorators
|
||||||
from notario.validators import types
|
from notario.validators import types
|
||||||
@ -28,41 +28,37 @@ from valet.api.db.models.music.placements import Placement
|
|||||||
from valet.api.db.models.music.plans import Plan
|
from valet.api.db.models.music.plans import Plan
|
||||||
from valet.api import LOG
|
from valet.api import LOG
|
||||||
from valet.api.v1.controllers import error
|
from valet.api.v1.controllers import error
|
||||||
|
from valet.api.v1.controllers import list_or_dict
|
||||||
from valet.api.v1.controllers import set_placements
|
from valet.api.v1.controllers import set_placements
|
||||||
from valet.api.v1.controllers import update_placements
|
from valet.api.v1.controllers import update_placements
|
||||||
from valet.api.v1.controllers import valid_plan_update_action
|
from valet.api.v1.controllers import valid_plan_update_action
|
||||||
|
|
||||||
|
|
||||||
CREATE_SCHEMA = (
|
CREATE_SCHEMA = (
|
||||||
(decorators.optional('locations'), types.array),
|
(decorators.optional('locations'), types.array),
|
||||||
('plan_name', types.string),
|
('plan_name', types.string),
|
||||||
('resources', types.dictionary),
|
('resources', types.dictionary),
|
||||||
('stack_id', types.string),
|
('stack_id', types.string),
|
||||||
(decorators.optional('timeout'), types.string)
|
(decorators.optional('timeout'), types.string))
|
||||||
)
|
|
||||||
|
|
||||||
UPDATE_SCHEMA = (
|
UPDATE_SCHEMA = (
|
||||||
('action', valid_plan_update_action),
|
('action', valid_plan_update_action),
|
||||||
(decorators.optional('excluded_hosts'), types.array),
|
(decorators.optional('excluded_hosts'), types.array),
|
||||||
|
(decorators.optional('original_resources'), types.dictionary),
|
||||||
(decorators.optional('plan_name'), types.string),
|
(decorators.optional('plan_name'), types.string),
|
||||||
# FIXME: resources needs to work against valid_plan_resources
|
('resources', list_or_dict), # list: migrate, dict: update
|
||||||
('resources', types.array),
|
(decorators.optional('timeout'), types.string))
|
||||||
(decorators.optional('timeout'), types.string)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlansItemController(object):
|
class PlansItemController(object):
|
||||||
"""Plan Item Controller /v1/plans/{plan_id}."""
|
"""Plans Item Controller /v1/plans/{plan_id}"""
|
||||||
|
|
||||||
def __init__(self, uuid4):
|
def __init__(self, uuid4):
|
||||||
"""Initializer."""
|
"""Initializer."""
|
||||||
self.uuid = uuid4
|
self.uuid = uuid4
|
||||||
self.plan = Plan.query.filter_by(id=self.uuid).first()
|
self.plan = Plan.query.filter_by(id=self.uuid).first()
|
||||||
# pylint: disable=E1101
|
|
||||||
|
|
||||||
if not self.plan:
|
if not self.plan:
|
||||||
self.plan = Plan.query.filter_by(stack_id=self.uuid).first()
|
self.plan = Plan.query.filter_by(stack_id=self.uuid).first()
|
||||||
# pylint: disable=E1101
|
|
||||||
|
|
||||||
if not self.plan:
|
if not self.plan:
|
||||||
error('/errors/not_found', _('Plan not found'))
|
error('/errors/not_found', _('Plan not found'))
|
||||||
@ -70,32 +66,33 @@ class PlansItemController(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,PUT,DELETE'
|
return 'GET,PUT,DELETE'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self):
|
||||||
"""Get plan."""
|
"""Get plan"""
|
||||||
return {"plan": self.plan}
|
return {"plan": self.plan}
|
||||||
|
|
||||||
@index.when(method='PUT', template='json')
|
@index.when(method='PUT', template='json')
|
||||||
@validate(UPDATE_SCHEMA, '/errors/schema')
|
@validate(UPDATE_SCHEMA, '/errors/schema')
|
||||||
def index_put(self, **kwargs):
|
def index_put(self, **kwargs):
|
||||||
"""Update a Plan."""
|
"""Update a Plan"""
|
||||||
action = kwargs.get('action')
|
ostro = Ostro()
|
||||||
|
action = kwargs.get('action', 'update')
|
||||||
if action == 'migrate':
|
if action == 'migrate':
|
||||||
# Replan the placement of an existing resource.
|
# Replan the placement of an existing resource.
|
||||||
excluded_hosts = kwargs.get('excluded_hosts', [])
|
excluded_hosts = kwargs.get('excluded_hosts', [])
|
||||||
@ -109,28 +106,26 @@ class PlansItemController(object):
|
|||||||
# We either got a resource or orchestration id.
|
# We either got a resource or orchestration id.
|
||||||
the_id = resources[0]
|
the_id = resources[0]
|
||||||
placement = Placement.query.filter_by(resource_id=the_id).first()
|
placement = Placement.query.filter_by(resource_id=the_id).first()
|
||||||
# pylint: disable=E1101
|
|
||||||
if not placement:
|
if not placement:
|
||||||
placement = Placement.query.filter_by(
|
placement = Placement.query.filter_by(
|
||||||
orchestration_id=the_id).first() # pylint: disable=E1101
|
orchestration_id=the_id).first()
|
||||||
if not placement:
|
if not placement:
|
||||||
error('/errors/invalid',
|
msg = _('Unknown resource or orchestration id: %s')
|
||||||
_('Unknown resource or '
|
error('/errors/invalid', msg.format(the_id))
|
||||||
'orchestration id: %s') % the_id)
|
|
||||||
|
|
||||||
LOG.info(_('Migration request for resource id {0}, '
|
|
||||||
'orchestration id {1}.').format(
|
|
||||||
placement.resource_id, placement.orchestration_id))
|
|
||||||
|
|
||||||
|
msg = _('Migration request for resource id {0}, '
|
||||||
|
'orchestration id {1}.')
|
||||||
|
LOG.info(msg.format(placement.resource_id,
|
||||||
|
placement.orchestration_id))
|
||||||
args = {
|
args = {
|
||||||
"stack_id": self.plan.stack_id,
|
"stack_id": self.plan.stack_id,
|
||||||
|
"tenant_id": request.context['tenant_id'],
|
||||||
"excluded_hosts": excluded_hosts,
|
"excluded_hosts": excluded_hosts,
|
||||||
"orchestration_id": placement.orchestration_id,
|
"orchestration_id": placement.orchestration_id,
|
||||||
}
|
}
|
||||||
ostro_kwargs = {
|
ostro_kwargs = {
|
||||||
"args": args,
|
"args": args,
|
||||||
}
|
}
|
||||||
ostro = Ostro()
|
|
||||||
ostro.migrate(**ostro_kwargs)
|
ostro.migrate(**ostro_kwargs)
|
||||||
ostro.send()
|
ostro.send()
|
||||||
|
|
||||||
@ -146,57 +141,61 @@ class PlansItemController(object):
|
|||||||
# Flush so that the DB is current.
|
# Flush so that the DB is current.
|
||||||
self.plan.flush()
|
self.plan.flush()
|
||||||
self.plan = Plan.query.filter_by(
|
self.plan = Plan.query.filter_by(
|
||||||
stack_id=self.plan.stack_id).first() # pylint: disable=E1101
|
stack_id=self.plan.stack_id).first()
|
||||||
LOG.info(_('Plan with stack id %s updated.'), self.plan.stack_id)
|
LOG.info(_('Plan with stack id %s updated.'), self.plan.stack_id)
|
||||||
return {"plan": self.plan}
|
return {"plan": self.plan}
|
||||||
|
elif action == 'update':
|
||||||
|
# Update an existing plan.
|
||||||
|
resources = kwargs.get('resources', [])
|
||||||
|
|
||||||
# TODO(JD): Throw unimplemented error?
|
if not isinstance(resources, dict):
|
||||||
|
error('/errors/invalid', _('resources must be a dictionary.'))
|
||||||
|
|
||||||
# pylint: disable=W0612
|
ostro_kwargs = {
|
||||||
'''
|
'action': 'update',
|
||||||
# FIXME: This is broken. Save for Valet 1.1
|
|
||||||
# New placements are not being seen in the response, so
|
|
||||||
# set_placements is currently failing as a result.
|
|
||||||
ostro = Ostro()
|
|
||||||
args = request.json
|
|
||||||
|
|
||||||
kwargs = {
|
|
||||||
'tenant_id': request.context['tenant_id'],
|
'tenant_id': request.context['tenant_id'],
|
||||||
'args': args
|
'args': kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# stack_id comes from the plan at update-time
|
||||||
|
ostro_kwargs['args']['stack_id'] = self.plan.stack_id
|
||||||
|
|
||||||
# Prepare the request. If request prep fails,
|
# Prepare the request. If request prep fails,
|
||||||
# an error message will be in the response.
|
# an error message will be in the response.
|
||||||
# Though the Ostro helper reports the error,
|
# Though the Ostro helper reports the error,
|
||||||
# we cite it as a Valet error.
|
# we cite it as a Valet error.
|
||||||
if not ostro.build_request(**kwargs):
|
if not ostro.build_request(**ostro_kwargs):
|
||||||
message = ostro.response['status']['message']
|
message = ostro.response['status']['message']
|
||||||
error(ostro.error_uri, _('Valet error: %s') % message)
|
error(ostro.error_uri, _('Valet error: %s') % message)
|
||||||
|
|
||||||
|
# If there are no serviceable resources, bail. Not an error.
|
||||||
|
# Treat it as if an "empty plan" was created.
|
||||||
|
# FIXME: Ostro should likely handle this and not error out.
|
||||||
|
if not ostro.is_request_serviceable():
|
||||||
|
LOG.info(_('Plan has no serviceable resources. Skipping.'))
|
||||||
|
response.status = 201
|
||||||
|
return {"plan": self.plan}
|
||||||
|
|
||||||
ostro.send()
|
ostro.send()
|
||||||
status_type = ostro.response['status']['type']
|
status_type = ostro.response['status']['type']
|
||||||
if status_type != 'ok':
|
if status_type != 'ok':
|
||||||
message = ostro.response['status']['message']
|
message = ostro.response['status']['message']
|
||||||
error(ostro.error_uri, _('Ostro error: %s') % message)
|
error(ostro.error_uri, _('Ostro error: %s') % message)
|
||||||
|
|
||||||
# TODO(JD): Keep. See if we will eventually need these for Ostro.
|
resources = ostro.request['resources']
|
||||||
#plan_name = args['plan_name']
|
|
||||||
#stack_id = args['stack_id']
|
|
||||||
resources = ostro.request['resources_update']
|
|
||||||
placements = ostro.response['resources']
|
placements = ostro.response['resources']
|
||||||
|
|
||||||
set_placements(self.plan, resources, placements)
|
update_placements(placements, plan=self.plan, resources=resources)
|
||||||
response.status = 201
|
response.status = 201
|
||||||
|
|
||||||
# Flush so that the DB is current.
|
# Flush so that the DB is current.
|
||||||
self.plan.flush()
|
self.plan.flush()
|
||||||
return self.plan
|
LOG.info(_('Plan with stack id %s updated.'), self.plan.stack_id)
|
||||||
'''
|
return {"plan": self.plan}
|
||||||
# pylint: enable=W0612
|
|
||||||
|
|
||||||
@index.when(method='DELETE', template='json')
|
@index.when(method='DELETE', template='json')
|
||||||
def index_delete(self):
|
def index_delete(self):
|
||||||
"""Delete a Plan."""
|
"""Delete a Plan"""
|
||||||
for placement in self.plan.placements():
|
for placement in self.plan.placements():
|
||||||
placement.delete()
|
placement.delete()
|
||||||
stack_id = self.plan.stack_id
|
stack_id = self.plan.stack_id
|
||||||
@ -206,42 +205,43 @@ class PlansItemController(object):
|
|||||||
|
|
||||||
|
|
||||||
class PlansController(object):
|
class PlansController(object):
|
||||||
"""Plans Controller /v1/plans."""
|
"""Plans Controller /v1/plans"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allow(cls):
|
def allow(cls):
|
||||||
"""Allowed methods."""
|
"""Allowed methods"""
|
||||||
return 'GET,POST'
|
return 'GET,POST'
|
||||||
|
|
||||||
@expose(generic=True, template='json')
|
@expose(generic=True, template='json')
|
||||||
def index(self):
|
def index(self):
|
||||||
"""Catchall for unallowed methods."""
|
"""Catchall for unallowed methods"""
|
||||||
message = _('The %s method is not allowed.') % request.method
|
message = _('The %s method is not allowed.') % request.method
|
||||||
kwargs = {'allow': self.allow()}
|
kwargs = {'allow': self.allow()}
|
||||||
error('/errors/not_allowed', message, **kwargs)
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
@index.when(method='OPTIONS', template='json')
|
@index.when(method='OPTIONS', template='json')
|
||||||
def index_options(self):
|
def index_options(self):
|
||||||
"""Index Options."""
|
"""Options"""
|
||||||
response.headers['Allow'] = self.allow()
|
response.headers['Allow'] = self.allow()
|
||||||
response.status = 204
|
response.status = 204
|
||||||
|
|
||||||
@index.when(method='GET', template='json')
|
@index.when(method='GET', template='json')
|
||||||
def index_get(self):
|
def index_get(self):
|
||||||
"""Get all the plans."""
|
"""Get all the plans"""
|
||||||
plans_array = []
|
plans_array = []
|
||||||
for plan in Plan.query.all(): # pylint: disable=E1101
|
for plan in Plan.query.all():
|
||||||
plans_array.append(plan)
|
plans_array.append(plan)
|
||||||
return {"plans": plans_array}
|
return {"plans": plans_array}
|
||||||
|
|
||||||
@index.when(method='POST', template='json')
|
@index.when(method='POST', template='json')
|
||||||
@validate(CREATE_SCHEMA, '/errors/schema')
|
@validate(CREATE_SCHEMA, '/errors/schema')
|
||||||
def index_post(self):
|
def index_post(self):
|
||||||
"""Create a Plan."""
|
"""Create a Plan"""
|
||||||
ostro = Ostro()
|
ostro = Ostro()
|
||||||
args = request.json
|
args = request.json
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
|
'action': 'create',
|
||||||
'tenant_id': request.context['tenant_id'],
|
'tenant_id': request.context['tenant_id'],
|
||||||
'args': args,
|
'args': args,
|
||||||
}
|
}
|
||||||
@ -287,5 +287,5 @@ class PlansController(object):
|
|||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
def _lookup(self, uuid4, *remainder):
|
def _lookup(self, uuid4, *remainder):
|
||||||
"""Pecan subcontroller routing callback."""
|
"""Pecan subcontroller routing callback"""
|
||||||
return PlansItemController(uuid4), remainder
|
return PlansItemController(uuid4), remainder
|
||||||
|
89
valet/api/v1/controllers/resources.py
Normal file
89
valet/api/v1/controllers/resources.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#
|
||||||
|
# Copyright 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.
|
||||||
|
|
||||||
|
"""Resources"""
|
||||||
|
|
||||||
|
from pecan import expose
|
||||||
|
from pecan import request
|
||||||
|
from pecan import response
|
||||||
|
from valet.api.common.i18n import _
|
||||||
|
from valet.api.common.ostro_helper import Ostro
|
||||||
|
from valet.api.v1.controllers import engine_query_args
|
||||||
|
from valet.api.v1.controllers import error
|
||||||
|
|
||||||
|
|
||||||
|
class ResourcesController(object):
|
||||||
|
"""Status Controller /v1/resources"""
|
||||||
|
|
||||||
|
def _invalid_placements(self):
|
||||||
|
"""Returns a dict of VMs with invalid placements."""
|
||||||
|
|
||||||
|
# TODO(gjung): Support checks on individual placements as well
|
||||||
|
ostro_kwargs = engine_query_args(query_type="invalid_placements")
|
||||||
|
ostro = Ostro()
|
||||||
|
ostro.query(**ostro_kwargs)
|
||||||
|
ostro.send()
|
||||||
|
|
||||||
|
status_type = ostro.response['status']['type']
|
||||||
|
if status_type != 'ok':
|
||||||
|
message = ostro.response['status']['message']
|
||||||
|
error(ostro.error_uri, _('Ostro error: %s') % message)
|
||||||
|
|
||||||
|
resources = ostro.response['resources']
|
||||||
|
return resources or {}
|
||||||
|
|
||||||
|
def _resource_status(self):
|
||||||
|
"""Get resource status."""
|
||||||
|
|
||||||
|
# All we do at the moment is check for invalid placements.
|
||||||
|
# This structure will evolve in the future. The only kind of
|
||||||
|
# resource type we'll see at the moment are servers.
|
||||||
|
invalid = self._invalid_placements()
|
||||||
|
resources = {}
|
||||||
|
for resource_id, info in invalid.items():
|
||||||
|
resources[resource_id] = {
|
||||||
|
"type": "OS::Nova::Server",
|
||||||
|
"status": "error",
|
||||||
|
"message": info.get('status'),
|
||||||
|
}
|
||||||
|
response = {
|
||||||
|
"resources": resources,
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def allow(cls):
|
||||||
|
"""Allowed methods"""
|
||||||
|
return 'GET'
|
||||||
|
|
||||||
|
@expose(generic=True, template='json')
|
||||||
|
def index(self):
|
||||||
|
"""Catchall for unallowed methods"""
|
||||||
|
message = _('The %s method is not allowed.') % request.method
|
||||||
|
kwargs = {'allow': self.allow()}
|
||||||
|
error('/errors/not_allowed', message, **kwargs)
|
||||||
|
|
||||||
|
@index.when(method='OPTIONS', template='json')
|
||||||
|
def index_options(self):
|
||||||
|
"""Options"""
|
||||||
|
response.headers['Allow'] = self.allow()
|
||||||
|
response.status = 204
|
||||||
|
|
||||||
|
@index.when(method='GET', template='json')
|
||||||
|
def index_get(self):
|
||||||
|
"""Get Valet resource status"""
|
||||||
|
_response = self._resource_status()
|
||||||
|
response.status = 200
|
||||||
|
return _response
|
@ -24,6 +24,7 @@ from valet.api.v1.controllers import error
|
|||||||
from valet.api.v1.controllers.groups import GroupsController
|
from valet.api.v1.controllers.groups import GroupsController
|
||||||
from valet.api.v1.controllers.placements import PlacementsController
|
from valet.api.v1.controllers.placements import PlacementsController
|
||||||
from valet.api.v1.controllers.plans import PlansController
|
from valet.api.v1.controllers.plans import PlansController
|
||||||
|
from valet.api.v1.controllers.resources import ResourcesController
|
||||||
from valet.api.v1.controllers.status import StatusController
|
from valet.api.v1.controllers.status import StatusController
|
||||||
|
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class V1Controller(SecureController):
|
|||||||
groups = GroupsController()
|
groups = GroupsController()
|
||||||
placements = PlacementsController()
|
placements = PlacementsController()
|
||||||
plans = PlansController()
|
plans = PlansController()
|
||||||
|
resources = ResourcesController()
|
||||||
status = StatusController()
|
status = StatusController()
|
||||||
|
|
||||||
# Update this whenever a new endpoint is made.
|
# Update this whenever a new endpoint is made.
|
||||||
|
@ -16,170 +16,291 @@
|
|||||||
"""Test Ostro Helper."""
|
"""Test Ostro Helper."""
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import valet.api.common.ostro_helper as helper
|
|
||||||
from valet.api.common.ostro_helper import Ostro
|
from valet.api.common import ostro_helper
|
||||||
from valet.tests.unit.api.v1.api_base import ApiBase
|
from valet.api.db.models import music as models
|
||||||
|
from valet.tests.unit.api.v1 import api_base
|
||||||
|
from valet.tests.unit import fakes
|
||||||
|
|
||||||
|
|
||||||
class TestOstroHelper(ApiBase):
|
class TestOstroHelper(api_base.ApiBase):
|
||||||
"""Test Ostro (Engine) Helper Class."""
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Setup Test Ostro and call init Ostro."""
|
"""Setup Test Ostro and call init Ostro."""
|
||||||
super(TestOstroHelper, self).setUp()
|
super(TestOstroHelper, self).setUp()
|
||||||
|
|
||||||
self.ostro = self.init_Ostro()
|
self.engine = self.init_engine()
|
||||||
|
self.groups = []
|
||||||
|
|
||||||
@mock.patch.object(helper, 'conf')
|
kwargs = {
|
||||||
def init_Ostro(self, mock_conf):
|
'description': 'test',
|
||||||
"""Init Engine(Ostro) and return."""
|
'members': ['test_tenant_id'],
|
||||||
mock_conf.ostro = {}
|
}
|
||||||
mock_conf.ostro["tries"] = 10
|
for group_type in ('affinity', 'diversity', 'exclusivity'):
|
||||||
mock_conf.ostro["interval"] = 1
|
kwargs['type'] = group_type
|
||||||
|
for group_level in ('host', 'rack'):
|
||||||
|
# Build names like host_affinity, rack_diversity, etc.
|
||||||
|
kwargs['name'] = "{}_{}".format(group_level, group_type)
|
||||||
|
kwargs['level'] = group_level
|
||||||
|
group = models.groups.Group(**kwargs)
|
||||||
|
self.groups.append(group)
|
||||||
|
|
||||||
return Ostro()
|
@mock.patch.object(ostro_helper, 'conf')
|
||||||
|
def init_engine(self, mock_conf):
|
||||||
|
mock_conf.music = {}
|
||||||
|
mock_conf.music["tries"] = 10
|
||||||
|
mock_conf.music["interval"] = 1
|
||||||
|
|
||||||
def test_build_request(self):
|
return ostro_helper.Ostro()
|
||||||
"""Test Build Request in Engine API using many different kwargs."""
|
|
||||||
kwargs = {'tenant_id': 'test_tenant_id',
|
|
||||||
'args': {'stack_id': 'test_stack_id',
|
|
||||||
'plan_name': 'test_plan_name',
|
|
||||||
'resources': {'test_resource': {
|
|
||||||
'Type': 'ATT::Valet::GroupAssignment',
|
|
||||||
'Properties': {
|
|
||||||
'resources': ['my-instance-1',
|
|
||||||
'my-instance-2'],
|
|
||||||
'group_type': 'affinity',
|
|
||||||
'level': 'host'},
|
|
||||||
'name': 'test-affinity-group3'}}}}
|
|
||||||
self.validate_test(self.ostro.build_request(**kwargs))
|
|
||||||
|
|
||||||
kwargs = {'tenant_id': 'test_tenant_id',
|
def build_request_kwargs(self):
|
||||||
'args': {'stack_id': 'test_stack_id',
|
"""Boilerplate for the build_request tests"""
|
||||||
'plan_name': 'test_plan_name',
|
# TODO(jdandrea): Sample Data should be co-located elsewhere
|
||||||
'resources': {'test_resource': {
|
base_kwargs = {
|
||||||
'Type': 'ATT::Valet::GroupAssignment',
|
'tenant_id': 'test_tenant_id',
|
||||||
'Properties': {
|
'args': {
|
||||||
'resources': ['my-instance-1',
|
'stack_id': 'test_stack_id',
|
||||||
'my-instance-2'],
|
|
||||||
'group_type': 'affinity',
|
|
||||||
'group_name': "test_group_name",
|
|
||||||
'level': 'host'},
|
|
||||||
'name': 'test-affinity-group3'}}}}
|
|
||||||
self.validate_test(not self.ostro.build_request(**kwargs))
|
|
||||||
self.validate_test("conflict" in self.ostro.error_uri)
|
|
||||||
|
|
||||||
kwargs = {'tenant_id': 'test_tenant_id',
|
|
||||||
'args': {'stack_id': 'test_stack_id',
|
|
||||||
'plan_name': 'test_plan_name',
|
|
||||||
'resources': {'test_resource': {
|
|
||||||
'Type': 'ATT::Valet::GroupAssignment',
|
|
||||||
'Properties': {
|
|
||||||
'resources': ['my-instance-1',
|
|
||||||
'my-instance-2'],
|
|
||||||
'group_type': 'exclusivity',
|
|
||||||
'level': 'host'},
|
|
||||||
'name': 'test-affinity-group3'}}}}
|
|
||||||
self.validate_test(not self.ostro.build_request(**kwargs))
|
|
||||||
self.validate_test("invalid" in self.ostro.error_uri)
|
|
||||||
|
|
||||||
kwargs = {'tenant_id': 'test_tenant_id',
|
|
||||||
'args': {'stack_id': 'test_stack_id',
|
|
||||||
'plan_name': 'test_plan_name',
|
|
||||||
'resources': {'test_resource': {
|
|
||||||
'Type': 'ATT::Valet::GroupAssignment',
|
|
||||||
'Properties': {
|
|
||||||
'resources': ['my-instance-1',
|
|
||||||
'my-instance-2'],
|
|
||||||
'group_type': 'exclusivity',
|
|
||||||
'group_name': "test_group_name",
|
|
||||||
'level': 'host'},
|
|
||||||
'name': 'test-affinity-group3'}}}}
|
|
||||||
self.validate_test(not self.ostro.build_request(**kwargs))
|
|
||||||
self.validate_test("not_found" in self.ostro.error_uri)
|
|
||||||
|
|
||||||
kwargs = {'tenant_id': 'test_tenant_id',
|
|
||||||
'args': {'stack_id': 'test_stack_id',
|
|
||||||
'plan_name': 'test_plan_name',
|
'plan_name': 'test_plan_name',
|
||||||
'timeout': '60 sec',
|
'timeout': '60 sec',
|
||||||
'resources': {
|
'resources': {
|
||||||
'ca039d18-1976-4e13-b083-edb12b806e25': {
|
"test_server": {
|
||||||
'Type': 'ATT::Valet::GroupAssignment',
|
'type': 'OS::Nova::Server',
|
||||||
'Properties': {
|
'properties': {
|
||||||
'resources': ['my-instance-1',
|
'key_name': 'ssh_key',
|
||||||
'my-instance-2'],
|
'image': 'ubuntu_server',
|
||||||
'group_type': 'non_type',
|
'name': 'my_server',
|
||||||
'group_name': "test_group_name",
|
'flavor': 'm1.small',
|
||||||
'level': 'host'},
|
'metadata': {
|
||||||
'name': 'test-affinity-group3'}}}}
|
'valet': {
|
||||||
self.validate_test(not self.ostro.build_request(**kwargs))
|
'groups': [
|
||||||
self.validate_test("invalid" in self.ostro.error_uri)
|
'host_affinity'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'networks': [
|
||||||
|
{
|
||||||
|
'network': 'private'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'name': 'my_instance',
|
||||||
|
},
|
||||||
|
'test_group_assignment': {
|
||||||
|
'type': 'OS::Valet::GroupAssignment',
|
||||||
|
'properties': {
|
||||||
|
'group': 'host_affinity',
|
||||||
|
'resources': ['my-instance-1', 'my-instance-2'],
|
||||||
|
},
|
||||||
|
'name': 'test_name',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base_kwargs
|
||||||
|
|
||||||
@mock.patch.object(helper, 'uuid')
|
# TODO(jdandrea): Turn these build_request tests into scenarios?
|
||||||
|
|
||||||
|
# The next five build_request methods exercise OS::Nova::Server metadata
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_affinity_using_metadata(self, mock_results):
|
||||||
|
mock_results.return_value = fakes.group(type="affinity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_server'][
|
||||||
|
'properties']['metadata']['valet']['groups'][0] = "host_affinity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_diversity_using_metadata(self, mock_results):
|
||||||
|
mock_results.return_value = fakes.group(type="diversity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_server'][
|
||||||
|
'properties']['metadata']['valet']['groups'][0] = \
|
||||||
|
"host_diversity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_exclusivity_using_metadata(self, mock_results):
|
||||||
|
mock_results.return_value = \
|
||||||
|
fakes.group(name="host_exclusivity", type="exclusivity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_server'][
|
||||||
|
'properties']['metadata']['valet']['groups'][0] = \
|
||||||
|
"host_exclusivity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_exclusivity_wrong_tenant_using_metadata(
|
||||||
|
self, mock_results):
|
||||||
|
mock_results.return_value = \
|
||||||
|
fakes.group(name="host_exclusivity", type="exclusivity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_server'][
|
||||||
|
'properties']['metadata']['valet']['groups'][0] = \
|
||||||
|
"host_exclusivity"
|
||||||
|
kwargs['tenant_id'] = "bogus_tenant"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertFalse(request)
|
||||||
|
self.assertIn('conflict', self.engine.error_uri)
|
||||||
|
|
||||||
|
def test_build_request_nonexistant_group_using_metadata(self):
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_server'][
|
||||||
|
'properties']['metadata']['valet']['groups'][0] = "bogus_name"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertFalse(request)
|
||||||
|
self.assertIn('not_found', self.engine.error_uri)
|
||||||
|
|
||||||
|
# The next five build_request methods exercise OS::Valet::GroupAssignment
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_affinity(self, mock_results):
|
||||||
|
mock_results.return_value = fakes.group(type="affinity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_group_assignment'][
|
||||||
|
'properties']['group'] = "host_affinity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_diversity(self, mock_results):
|
||||||
|
mock_results.return_value = fakes.group(type="diversity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_group_assignment'][
|
||||||
|
'properties']['group'] = "host_diversity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_exclusivity(self, mock_results):
|
||||||
|
mock_results.return_value = \
|
||||||
|
fakes.group(name="host_exclusivity", type="exclusivity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_group_assignment'][
|
||||||
|
'properties']['group'] = "host_exclusivity"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertTrue(request)
|
||||||
|
|
||||||
|
@mock.patch.object(models.Results, 'first')
|
||||||
|
def test_build_request_host_exclusivity_wrong_tenant(self, mock_results):
|
||||||
|
mock_results.return_value = \
|
||||||
|
fakes.group(name="host_exclusivity", type="exclusivity")
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_group_assignment'][
|
||||||
|
'properties']['group'] = "host_exclusivity"
|
||||||
|
kwargs['tenant_id'] = "bogus_tenant"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertFalse(request)
|
||||||
|
self.assertIn('conflict', self.engine.error_uri)
|
||||||
|
|
||||||
|
def test_build_request_nonexistant_group(self):
|
||||||
|
kwargs = self.build_request_kwargs()
|
||||||
|
kwargs['args']['resources']['test_group_assignment'][
|
||||||
|
'properties']['group'] = "bogus_name"
|
||||||
|
request = self.engine.build_request(**kwargs)
|
||||||
|
self.assertFalse(request)
|
||||||
|
self.assertIn('not_found', self.engine.error_uri)
|
||||||
|
|
||||||
|
@mock.patch.object(ostro_helper, 'uuid')
|
||||||
def test_ping(self, mock_uuid):
|
def test_ping(self, mock_uuid):
|
||||||
"""Validate engine ping by checking engine request equality."""
|
"""Validate engine ping by checking engine request equality."""
|
||||||
mock_uuid.uuid4.return_value = "test_stack_id"
|
mock_uuid.uuid4.return_value = "test_stack_id"
|
||||||
self.ostro.ping()
|
self.engine.ping()
|
||||||
|
|
||||||
self.validate_test(self.ostro.request['stack_id'] == "test_stack_id")
|
self.assertTrue(self.engine.request['stack_id'] == "test_stack_id")
|
||||||
|
|
||||||
def test_is_request_serviceable(self):
|
def test_is_request_serviceable(self):
|
||||||
"""Validate if engine request serviceable."""
|
self.engine.request = {
|
||||||
self.ostro.request = {
|
'resources': {
|
||||||
'resources': {"bla": {'type': "OS::Nova::Server"}}}
|
"bla": {
|
||||||
self.validate_test(self.ostro.is_request_serviceable())
|
'type': "OS::Nova::Server",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertTrue(self.engine.is_request_serviceable())
|
||||||
|
|
||||||
self.ostro.request = {}
|
self.engine.request = {}
|
||||||
self.validate_test(not self.ostro.is_request_serviceable())
|
self.assertFalse(self.engine.is_request_serviceable())
|
||||||
|
|
||||||
def test_replan(self):
|
def test_replan(self):
|
||||||
"""Validate engine replan."""
|
kwargs = {
|
||||||
kwargs = {'args': {'stack_id': 'test_stack_id',
|
'args': {
|
||||||
|
'stack_id': 'test_stack_id',
|
||||||
'locations': 'test_locations',
|
'locations': 'test_locations',
|
||||||
'orchestration_id': 'test_orchestration_id',
|
'orchestration_id': 'test_orchestration_id',
|
||||||
'exclusions': 'test_exclusions'}}
|
'exclusions': 'test_exclusions',
|
||||||
self.ostro.replan(**kwargs)
|
'resource_id': 'test_resource_id',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.engine.replan(**kwargs)
|
||||||
|
|
||||||
self.validate_test(self.ostro.request['stack_id'] == "test_stack_id")
|
self.assertTrue(self.engine.request['stack_id'] == "test_stack_id")
|
||||||
self.validate_test(self.ostro.request['locations'] == "test_locations")
|
self.assertTrue(self.engine.request['locations'] == "test_locations")
|
||||||
|
self.assertTrue(
|
||||||
|
self.engine.request['orchestration_id'] ==
|
||||||
|
"test_orchestration_id")
|
||||||
|
self.assertTrue(
|
||||||
|
self.engine.request['exclusions'] == "test_exclusions")
|
||||||
|
|
||||||
self.validate_test(
|
def test_identify(self):
|
||||||
self.ostro.request['orchestration_id'] == "test_orchestration_id")
|
kwargs = {
|
||||||
|
'args': {
|
||||||
self.validate_test(
|
'stack_id': 'test_stack_id',
|
||||||
self.ostro.request['exclusions'] == "test_exclusions")
|
'orchestration_id': 'test_orchestration_id',
|
||||||
|
'uuid': 'test_uuid',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.engine.identify(**kwargs)
|
||||||
|
self.assertEqual(self.engine.request['stack_id'], "test_stack_id")
|
||||||
|
self.assertEqual(self.engine.request['orchestration_id'],
|
||||||
|
"test_orchestration_id")
|
||||||
|
self.assertEqual(self.engine.request['resource_id'], "test_uuid")
|
||||||
|
self.assertTrue(self.engine.asynchronous)
|
||||||
|
|
||||||
def test_migrate(self):
|
def test_migrate(self):
|
||||||
"""Validate engine migrate."""
|
kwargs = {
|
||||||
kwargs = {'args': {'stack_id': 'test_stack_id',
|
'args': {
|
||||||
|
'stack_id': 'test_stack_id',
|
||||||
|
'tenant_id': 'test_tenant_id',
|
||||||
'excluded_hosts': 'test_excluded_hosts',
|
'excluded_hosts': 'test_excluded_hosts',
|
||||||
'orchestration_id': 'test_orchestration_id'}}
|
'orchestration_id': 'test_orchestration_id',
|
||||||
self.ostro.migrate(**kwargs)
|
}
|
||||||
|
}
|
||||||
|
self.engine.migrate(**kwargs)
|
||||||
|
|
||||||
self.validate_test(self.ostro.request['stack_id'] == "test_stack_id")
|
self.assertTrue(self.engine.request['stack_id'] == "test_stack_id")
|
||||||
|
self.assertTrue(
|
||||||
|
self.engine.request['excluded_hosts'] == "test_excluded_hosts")
|
||||||
|
self.assertTrue(
|
||||||
|
self.engine.request['orchestration_id'] ==
|
||||||
|
"test_orchestration_id")
|
||||||
|
|
||||||
self.validate_test(
|
@mock.patch.object(ostro_helper, 'uuid')
|
||||||
self.ostro.request['excluded_hosts'] == "test_excluded_hosts")
|
|
||||||
|
|
||||||
self.validate_test(
|
|
||||||
self.ostro.request['orchestration_id'] == "test_orchestration_id")
|
|
||||||
|
|
||||||
@mock.patch.object(helper, 'uuid')
|
|
||||||
def test_query(self, mock_uuid):
|
def test_query(self, mock_uuid):
|
||||||
"""Validate test query by validating several engine requests."""
|
"""Validate test query by validating several engine requests."""
|
||||||
mock_uuid.uuid4.return_value = "test_stack_id"
|
mock_uuid.uuid4.return_value = "test_stack_id"
|
||||||
kwargs = {'args': {'type': 'test_type',
|
kwargs = {
|
||||||
'parameters': 'test_parameters'}}
|
'args': {
|
||||||
self.ostro.query(**kwargs)
|
'type': 'test_type',
|
||||||
|
'parameters': 'test_parameters',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.engine.query(**kwargs)
|
||||||
|
|
||||||
self.validate_test(self.ostro.request['stack_id'] == "test_stack_id")
|
self.assertTrue(self.engine.request['stack_id'] == "test_stack_id")
|
||||||
self.validate_test(self.ostro.request['type'] == "test_type")
|
self.assertTrue(self.engine.request['type'] == "test_type")
|
||||||
|
self.assertTrue(
|
||||||
|
self.engine.request['parameters'] == "test_parameters")
|
||||||
|
|
||||||
self.validate_test(
|
@mock.patch.object(ostro_helper, '_log')
|
||||||
self.ostro.request['parameters'] == "test_parameters")
|
@mock.patch.object(ostro_helper.Ostro, '_send')
|
||||||
|
@mock.patch.object(models.ostro, 'PlacementRequest')
|
||||||
def test_send(self):
|
@mock.patch.object(models, 'Query')
|
||||||
"""Validate test send by checking engine server error."""
|
def test_send(self, mock_query, mock_request, mock_send, mock_logger):
|
||||||
self.ostro.args = {'stack_id': 'test_stack_id'}
|
mock_send.return_value = '{"status":{"type":"ok"}}'
|
||||||
self.ostro.send()
|
self.engine.args = {'stack_id': 'test_stack_id'}
|
||||||
self.validate_test("server_error" in self.ostro.error_uri)
|
self.engine.request = {}
|
||||||
|
self.engine.send()
|
||||||
|
self.assertIsNone(self.engine.error_uri)
|
||||||
|
44
valet/tests/unit/api/common/test_validation.py
Normal file
44
valet/tests/unit/api/common/test_validation.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Common Validation Helpers"""
|
||||||
|
|
||||||
|
from valet.api.common import validation
|
||||||
|
from valet.tests.unit.api.v1 import api_base
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidation(api_base.ApiBase):
|
||||||
|
"""Test Harness"""
|
||||||
|
|
||||||
|
uuid = '731056cc-c802-4797-a32b-17eaced354fa'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Initializer"""
|
||||||
|
super(TestValidation, self).setUp()
|
||||||
|
|
||||||
|
def test_is_valid_uuid4(self):
|
||||||
|
"""Test with a valid UUID"""
|
||||||
|
valid = validation.is_valid_uuid4(self.uuid)
|
||||||
|
self.assertTrue(valid)
|
||||||
|
|
||||||
|
def test_is_valid_uuid4_no_hyphens(self):
|
||||||
|
"""Test with a valid UUID, no hyphens"""
|
||||||
|
valid = validation.is_valid_uuid4(self.uuid.replace('-', ''))
|
||||||
|
self.assertTrue(valid)
|
||||||
|
|
||||||
|
def test_is_invalid_uuid4(self):
|
||||||
|
"""Test with an invalid UUID"""
|
||||||
|
valid = validation.is_valid_uuid4("not_a_uuid")
|
||||||
|
self.assertFalse(valid)
|
@ -35,7 +35,8 @@ class TestGroups(ApiBase):
|
|||||||
"""Init a test group object and return."""
|
"""Init a test group object and return."""
|
||||||
mock_insert.return_value = None
|
mock_insert.return_value = None
|
||||||
members = ["me", "you"]
|
members = ["me", "you"]
|
||||||
return Group("test_name", "test_description", "test_type", members)
|
return Group("test_name", "test_description", "test_type",
|
||||||
|
"test_level", members)
|
||||||
|
|
||||||
def test__repr__(self):
|
def test__repr__(self):
|
||||||
"""Validate test name in group repr."""
|
"""Validate test name in group repr."""
|
||||||
|
@ -35,11 +35,9 @@ class TestPlacement(ApiBase):
|
|||||||
def init_Placement(self, mock_insert):
|
def init_Placement(self, mock_insert):
|
||||||
"""Return init test placement object for class init."""
|
"""Return init test placement object for class init."""
|
||||||
mock_insert.return_value = None
|
mock_insert.return_value = None
|
||||||
return Placement("test_name",
|
plan = Plan("plan_name", "stack_id", _insert=False)
|
||||||
"test_orchestration_id",
|
return Placement("test_name", "test_orchestration_id",
|
||||||
plan=Plan("plan_name", "stack_id", _insert=False),
|
plan=plan, location="test_location", _insert=False)
|
||||||
location="test_location",
|
|
||||||
_insert=False)
|
|
||||||
|
|
||||||
def test__repr__(self):
|
def test__repr__(self):
|
||||||
"""Test name from placement repr."""
|
"""Test name from placement repr."""
|
||||||
|
@ -16,18 +16,60 @@
|
|||||||
"""Api Base."""
|
"""Api Base."""
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
from valet.tests.base import Base
|
from valet.tests.base import Base
|
||||||
|
# from valet.tests import db
|
||||||
|
|
||||||
|
|
||||||
class ApiBase(Base):
|
class ApiBase(Base):
|
||||||
"""Api Base Test Class, calls valet tests base."""
|
"""Api Base Test Class, calls valet tests base."""
|
||||||
|
|
||||||
|
# FIXME(jdandrea): No camel-case! Use __init__().
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Setup api base and mock pecan identity/music/state."""
|
"""Setup api base and mock pecan identity/music/state."""
|
||||||
super(ApiBase, self).setUp()
|
super(ApiBase, self).setUp()
|
||||||
pecan.conf.identity = mock.MagicMock()
|
pecan.conf.identity = mock.MagicMock()
|
||||||
pecan.conf.music = mock.MagicMock()
|
pecan.conf.music = mock.MagicMock()
|
||||||
|
|
||||||
|
"""
|
||||||
|
# pecan.conf.music.keyspace = \
|
||||||
|
# mock.PropertyMock(return_value="valet")
|
||||||
|
|
||||||
|
# Set up the mock Music API
|
||||||
|
# TODO(jdandrea): In all honesty, instead of
|
||||||
|
# using a mock object here, it may be better
|
||||||
|
# to mock out only the surface that is being
|
||||||
|
# crossed during a given test. We're most of
|
||||||
|
# the way there. We may end up dumbing down
|
||||||
|
# what the mock object does (vs. having it
|
||||||
|
# do simplified in-memory storage).
|
||||||
|
keyspace = 'valet'
|
||||||
|
engine = db.MusicAPIWithOldMethodNames()
|
||||||
|
|
||||||
|
# FIXME(jdandrea): pecan.conf.music used to be
|
||||||
|
# a MagicMock, however it does not appear possible
|
||||||
|
# to setattr() on a MagicMock (not one that can be
|
||||||
|
# retrieved via obj.get('key') at least). That means
|
||||||
|
# keys/values that were magically handled before are
|
||||||
|
# no longer being handled now. We may end up filling
|
||||||
|
# in the rest of the expected music conf settings
|
||||||
|
# with individual mock object values if necessary.
|
||||||
|
pecan.conf.music = {
|
||||||
|
'keyspace': keyspace,
|
||||||
|
'engine': engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a keyspace and various tables (no schema needed)
|
||||||
|
pecan.conf.music.engine.keyspace_create(keyspace)
|
||||||
|
for table in ('plans', 'placements', 'groups',
|
||||||
|
'placement_requests', 'placement_results',
|
||||||
|
'query'):
|
||||||
|
pecan.conf.music.engine.table_create(
|
||||||
|
keyspace, table, schema=mock.MagicMock())
|
||||||
|
"""
|
||||||
|
|
||||||
self.response = None
|
self.response = None
|
||||||
pecan.core.state = mock.MagicMock()
|
pecan.core.state = mock.MagicMock()
|
||||||
|
|
||||||
|
@ -12,13 +12,14 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pecan
|
import pecan
|
||||||
|
|
||||||
from valet.api.db.models.music.groups import Group
|
from valet.api.db.models.music.groups import Group
|
||||||
from valet.api.db.models.music import Query
|
from valet.api.db.models.music import Query
|
||||||
from valet.api.db.models.music import Results
|
from valet.api.db.models.music import Results
|
||||||
import valet.api.v1.controllers.groups as groups
|
from valet.api.v1.controllers import groups
|
||||||
from valet.api.v1.controllers.groups import GroupsController
|
from valet.api.v1.controllers.groups import GroupsController
|
||||||
from valet.api.v1.controllers.groups import GroupsItemController
|
from valet.api.v1.controllers.groups import GroupsItemController
|
||||||
from valet.api.v1.controllers.groups import MembersController
|
from valet.api.v1.controllers.groups import MembersController
|
||||||
@ -51,10 +52,9 @@ class TestGroups(ApiBase):
|
|||||||
def init_GroupsItemController(self, mock_filter, mock_request):
|
def init_GroupsItemController(self, mock_filter, mock_request):
|
||||||
"""Called by Setup, return GroupsItemController object with id."""
|
"""Called by Setup, return GroupsItemController object with id."""
|
||||||
mock_request.context = {}
|
mock_request.context = {}
|
||||||
mock_filter.return_value = Results([Group("test_name",
|
mock_filter.return_value = Results(
|
||||||
"test_description",
|
[Group("test_name", "test_description", "test_type",
|
||||||
"test_type",
|
"test_level", None)])
|
||||||
None)])
|
|
||||||
contrler = GroupsItemController("group_id")
|
contrler = GroupsItemController("group_id")
|
||||||
|
|
||||||
self.validate_test("test_name" == groups.request.context['group'].name)
|
self.validate_test("test_name" == groups.request.context['group'].name)
|
||||||
@ -69,11 +69,8 @@ class TestGroups(ApiBase):
|
|||||||
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def init_MembersItemController(self, mock_request):
|
def init_MembersItemController(self, mock_request):
|
||||||
"""Called by Setup, return MembersItemController with demo members."""
|
grp = Group("test_member_item_name", "test_description",
|
||||||
grp = Group("test_member_item_name",
|
"test_type", "test_level", None)
|
||||||
"test_description",
|
|
||||||
"test_type",
|
|
||||||
None)
|
|
||||||
grp.members = ["demo members"]
|
grp.members = ["demo members"]
|
||||||
mock_request.context = {'group': grp}
|
mock_request.context = {'group': grp}
|
||||||
|
|
||||||
@ -126,10 +123,9 @@ class TestGroups(ApiBase):
|
|||||||
"""Test members_controller index_put method, check status/tenant_id."""
|
"""Test members_controller index_put method, check status/tenant_id."""
|
||||||
pecan.conf.identity.engine.is_tenant_list_valid.return_value = True
|
pecan.conf.identity.engine.is_tenant_list_valid.return_value = True
|
||||||
|
|
||||||
mock_request.context = {'group': Group("test_name",
|
mock_request.context = {'group': Group(
|
||||||
"test_description",
|
"test_name", "test_description",
|
||||||
"test_type",
|
"test_type", "test_level", None)}
|
||||||
None)}
|
|
||||||
r = self.members_controller.index_put(members=[self.tenant_id])
|
r = self.members_controller.index_put(members=[self.tenant_id])
|
||||||
|
|
||||||
self.validate_test(groups.response.status == 201)
|
self.validate_test(groups.response.status == 201)
|
||||||
@ -143,10 +139,9 @@ class TestGroups(ApiBase):
|
|||||||
"""Test members_controller index_put method with invalid tenants."""
|
"""Test members_controller index_put method with invalid tenants."""
|
||||||
pecan.conf.identity.engine.is_tenant_list_valid.return_value = False
|
pecan.conf.identity.engine.is_tenant_list_valid.return_value = False
|
||||||
|
|
||||||
mock_request.context = {'group': Group("test_name",
|
mock_request.context = {'group': Group(
|
||||||
"test_description",
|
"test_name", "test_description",
|
||||||
"test_type",
|
"test_type", "test_level", None)}
|
||||||
None)}
|
|
||||||
self.members_controller.index_put(members=[self.tenant_id])
|
self.members_controller.index_put(members=[self.tenant_id])
|
||||||
|
|
||||||
self.validate_test("Member list contains invalid tenant IDs" in
|
self.validate_test("Member list contains invalid tenant IDs" in
|
||||||
@ -169,8 +164,8 @@ class TestGroups(ApiBase):
|
|||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def test_index_delete_member_item_controller(self, mock_request,
|
def test_index_delete_member_item_controller(self, mock_request,
|
||||||
mock_func):
|
mock_func):
|
||||||
"""Members_item_controller index_delete, check status and members."""
|
grp = Group("test_name", "test_description",
|
||||||
grp = Group("test_name", "test_description", "test_type", None)
|
"test_type", "test_level", None)
|
||||||
grp.members = ["demo members"]
|
grp.members = ["demo members"]
|
||||||
|
|
||||||
mock_request.context = {'group': grp, 'member_id': "demo members"}
|
mock_request.context = {'group': grp, 'member_id': "demo members"}
|
||||||
@ -184,11 +179,10 @@ class TestGroups(ApiBase):
|
|||||||
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
||||||
@mock.patch.object(groups, 'tenant_servers_in_group')
|
@mock.patch.object(groups, 'tenant_servers_in_group')
|
||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def test_index_delete_member_item_controller_unhappy(self,
|
def test_index_delete_member_item_controller_unhappy(self, mock_request,
|
||||||
mock_request,
|
|
||||||
mock_func):
|
mock_func):
|
||||||
"""Members_item_controller index_delete, check member not found."""
|
grp = Group("test_name", "test_description",
|
||||||
grp = Group("test_name", "test_description", "test_type", None)
|
"test_type", "test_level", None)
|
||||||
grp.members = ["demo members"]
|
grp.members = ["demo members"]
|
||||||
|
|
||||||
mock_request.context = {'group': grp, 'member_id': "demo members"}
|
mock_request.context = {'group': grp, 'member_id': "demo members"}
|
||||||
@ -213,21 +207,18 @@ class TestGroups(ApiBase):
|
|||||||
|
|
||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def test_index_put_groups_item_controller(self, mock_request):
|
def test_index_put_groups_item_controller(self, mock_request):
|
||||||
"""Test index_put for item_controller, check status and description."""
|
mock_request.context = {'group': Group(
|
||||||
mock_request.context = {'group': Group("test_name",
|
"test_name", "test_description",
|
||||||
"test_description",
|
"test_type", "test_level", None)}
|
||||||
"test_type",
|
|
||||||
None)}
|
|
||||||
r = self.groups_item_controller.index_put(
|
r = self.groups_item_controller.index_put(
|
||||||
description="new description")
|
description="new description")
|
||||||
|
|
||||||
self.validate_test(groups.response.status == 201)
|
self.validate_test(groups.response.status == 201)
|
||||||
self.validate_test(r.description == "new description")
|
self.validate_test(r.description == "new description")
|
||||||
|
|
||||||
mock_request.context = {'group': Group("test_name",
|
mock_request.context = {'group': Group(
|
||||||
"test_description",
|
"test_name", "test_description",
|
||||||
"test_type",
|
"test_type", "test_level", None)}
|
||||||
None)}
|
|
||||||
r = self.groups_item_controller.index_put()
|
r = self.groups_item_controller.index_put()
|
||||||
|
|
||||||
self.validate_test(groups.response.status == 201)
|
self.validate_test(groups.response.status == 201)
|
||||||
@ -235,11 +226,9 @@ class TestGroups(ApiBase):
|
|||||||
|
|
||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def test_index_delete_groups_item_controller(self, mock_request):
|
def test_index_delete_groups_item_controller(self, mock_request):
|
||||||
"""Test groups_item_controller index_delete works, check response."""
|
mock_request.context = {'group': Group(
|
||||||
mock_request.context = {'group': Group("test_name",
|
"test_name", "test_description",
|
||||||
"test_description",
|
"test_type", "test_level", None)}
|
||||||
"test_type",
|
|
||||||
None)}
|
|
||||||
self.groups_item_controller.index_delete()
|
self.groups_item_controller.index_delete()
|
||||||
|
|
||||||
self.validate_test(groups.response.status == 204)
|
self.validate_test(groups.response.status == 204)
|
||||||
@ -247,8 +236,8 @@ class TestGroups(ApiBase):
|
|||||||
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
||||||
@mock.patch.object(groups, 'request')
|
@mock.patch.object(groups, 'request')
|
||||||
def test_index_delete_groups_item_controller_unhappy(self, mock_request):
|
def test_index_delete_groups_item_controller_unhappy(self, mock_request):
|
||||||
"""Test to check that you can't delete a group with members."""
|
grp = Group("test_name", "test_description",
|
||||||
grp = Group("test_name", "test_description", "test_type", None)
|
"test_type", "test_level", None)
|
||||||
grp.members = ["demo members"]
|
grp.members = ["demo members"]
|
||||||
mock_request.context = {'group': grp}
|
mock_request.context = {'group': grp}
|
||||||
self.groups_item_controller.index_delete()
|
self.groups_item_controller.index_delete()
|
||||||
@ -265,10 +254,9 @@ class TestGroups(ApiBase):
|
|||||||
mock_all.return_value = all_groups
|
mock_all.return_value = all_groups
|
||||||
response = self.groups_controller.index_get()
|
response = self.groups_controller.index_get()
|
||||||
|
|
||||||
mock_request.context = {'group': Group("test_name",
|
mock_request.context = {'group': Group(
|
||||||
"test_description",
|
"test_name", "test_description",
|
||||||
"test_type",
|
"test_type", "test_level", None)}
|
||||||
None)}
|
|
||||||
item_controller_response = self.groups_item_controller.index_get()
|
item_controller_response = self.groups_item_controller.index_get()
|
||||||
|
|
||||||
self.members_item_controller.index_get()
|
self.members_item_controller.index_get()
|
||||||
@ -281,22 +269,20 @@ class TestGroups(ApiBase):
|
|||||||
self.validate_test(all_groups == response["groups"])
|
self.validate_test(all_groups == response["groups"])
|
||||||
|
|
||||||
def test_index_post(self):
|
def test_index_post(self):
|
||||||
"""Test group_controller index_post, check status and name."""
|
|
||||||
group = self.groups_controller.index_post(
|
group = self.groups_controller.index_post(
|
||||||
name="testgroup",
|
name="testgroup", description="test description",
|
||||||
description="test description",
|
type="testtype", level="test_evel")
|
||||||
type="testtype")
|
|
||||||
|
|
||||||
self.validate_test(groups.response.status == 201)
|
self.validate_test(groups.response.status == 201)
|
||||||
self.validate_test(group.name == "testgroup")
|
self.validate_test(group.name == "testgroup")
|
||||||
|
|
||||||
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
@mock.patch.object(groups, 'error', ApiBase.mock_error)
|
||||||
def test_index_post_unhappy(self):
|
@mock.patch.object(groups.Group, '__init__')
|
||||||
"""Test groups_controller index_post with error."""
|
def test_index_post_unhappy(self, mock_group_init):
|
||||||
pecan.conf.music = None
|
mock_group_init.return_value = Exception()
|
||||||
self.groups_controller.index_post(name="testgroup",
|
self.groups_controller.index_post(
|
||||||
description="test description",
|
name="testgroup", description="test description",
|
||||||
type="testtype")
|
type="testtype", level="test_level")
|
||||||
|
|
||||||
self.validate_test("Unable to create Group" in TestGroups.response)
|
self.validate_test("Unable to create Group" in TestGroups.response)
|
||||||
|
|
||||||
|
@ -14,16 +14,38 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from valet.api.db.models.music.placements import Placement
|
from valet.api.common import ostro_helper
|
||||||
from valet.api.db.models.music.plans import Plan
|
from valet.api.db.models.music.plans import Plan
|
||||||
from valet.api.db.models.music import Query
|
from valet.api.db.models.music import Query
|
||||||
from valet.api.db.models.music import Results
|
from valet.api.db.models.music import Results
|
||||||
import valet.api.v1.controllers.placements as placements
|
import valet.api.v1.controllers.placements as placements
|
||||||
|
from valet.api.v1.controllers.placements import Placement
|
||||||
from valet.api.v1.controllers.placements import PlacementsController
|
from valet.api.v1.controllers.placements import PlacementsController
|
||||||
from valet.api.v1.controllers.placements import PlacementsItemController
|
from valet.api.v1.controllers.placements import PlacementsItemController
|
||||||
from valet.tests.unit.api.v1.api_base import ApiBase
|
from valet.tests.unit.api.v1.api_base import ApiBase
|
||||||
|
|
||||||
|
|
||||||
|
def fake_filter_by(self, **kwargs):
|
||||||
|
"""Fake filter for Music queries.
|
||||||
|
|
||||||
|
FIXME(jdandrea): Find a way to get rid of this. It's here
|
||||||
|
in order to get some of the tests working, but there ought
|
||||||
|
to be a better way that doesn't introduce more surface area.
|
||||||
|
"""
|
||||||
|
if 'id' in kwargs:
|
||||||
|
return Results([Plan("plan_name", "stack_id", _insert=False)])
|
||||||
|
elif 'plan_id' in kwargs:
|
||||||
|
# FIXME(jdandrea) this is duplicated in
|
||||||
|
# init_PlacementsItemController (and there shouldn't be a
|
||||||
|
# separate init; that pattern blurs/confuses things IMO)
|
||||||
|
return Results([
|
||||||
|
Placement("placement_name", "test_orchestration_id",
|
||||||
|
plan=Plan("plan_name", "stack_id", _insert=False),
|
||||||
|
location="test_location", _insert=False)])
|
||||||
|
else:
|
||||||
|
return Results([])
|
||||||
|
|
||||||
|
|
||||||
class TestPlacements(ApiBase):
|
class TestPlacements(ApiBase):
|
||||||
"""Unit tests for valet.api.v1.controllers.placements."""
|
"""Unit tests for valet.api.v1.controllers.placements."""
|
||||||
|
|
||||||
@ -103,27 +125,45 @@ class TestPlacements(ApiBase):
|
|||||||
self.validate_test("plan_name" in response['placement'].plan.name)
|
self.validate_test("plan_name" in response['placement'].plan.name)
|
||||||
self.validate_test("stack_id" in response['placement'].plan.stack_id)
|
self.validate_test("stack_id" in response['placement'].plan.stack_id)
|
||||||
|
|
||||||
@mock.patch.object(placements, 'error', ApiBase.mock_error)
|
@mock.patch.object(ostro_helper, '_log')
|
||||||
@mock.patch.object(Query, 'filter_by', mock.MagicMock)
|
@mock.patch.object(ostro_helper.Ostro, '_send')
|
||||||
@mock.patch.object(placements, 'update_placements')
|
@mock.patch.object(Query, 'filter_by')
|
||||||
def test_index_post(self, mock_plcment):
|
def test_index_post_with_locations(self, mock_filter,
|
||||||
"""Test index_post for placements, validate from response status."""
|
mock_send, mock_logging):
|
||||||
kwargs = {'resource_id': "resource_id", 'locations': ["test_location"]}
|
kwargs = {'resource_id': "resource_id", 'locations': ["test_location"]}
|
||||||
|
mock_filter.return_value = Results([
|
||||||
|
Plan("plan_name", "stack_id", _insert=False)])
|
||||||
|
mock_send.return_value = '{"status":{"type":"ok"}}'
|
||||||
self.placements_item_controller.index_post(**kwargs)
|
self.placements_item_controller.index_post(**kwargs)
|
||||||
self.validate_test(placements.response.status == 201)
|
self.validate_test(placements.response.status == 201)
|
||||||
|
|
||||||
with mock.patch('valet.api.v1.controllers.placements.Ostro') \
|
@mock.patch('valet.api.db.models.music.Query.filter_by',
|
||||||
as mock_ostro:
|
fake_filter_by)
|
||||||
|
@mock.patch.object(placements, 'error', ApiBase.mock_error)
|
||||||
|
@mock.patch.object(ostro_helper, '_log')
|
||||||
|
@mock.patch.object(ostro_helper.Ostro, '_send')
|
||||||
|
def test_index_post_with_engine_error(self, mock_send, mock_logging):
|
||||||
kwargs = {'resource_id': "resource_id", 'locations': [""]}
|
kwargs = {'resource_id': "resource_id", 'locations': [""]}
|
||||||
|
mock_send.return_value = \
|
||||||
|
'{"status":{"type":"error","message":"error"},' \
|
||||||
|
'"resources":{"iterkeys":[]}}'
|
||||||
self.placements_item_controller.index_post(**kwargs)
|
self.placements_item_controller.index_post(**kwargs)
|
||||||
self.validate_test("Ostro error:" in ApiBase.response)
|
self.validate_test("Ostro error:" in ApiBase.response)
|
||||||
|
|
||||||
mock_plcment.return_value = None
|
@mock.patch('valet.api.db.models.music.Query.filter_by',
|
||||||
|
fake_filter_by)
|
||||||
|
@mock.patch.object(ostro_helper, '_log')
|
||||||
|
@mock.patch.object(ostro_helper.Ostro, '_send')
|
||||||
|
@mock.patch.object(placements, 'update_placements')
|
||||||
|
def test_index_post_with_placement_update(self, mock_update,
|
||||||
|
mock_send, mock_logging):
|
||||||
|
kwargs = {'resource_id': "resource_id", 'locations': [""]}
|
||||||
|
mock_update.return_value = None
|
||||||
|
|
||||||
status_type = mock.MagicMock()
|
# FIXME(jdandrea): Why was "iterkeys" used here as a resource??
|
||||||
status_type.response = {"status": {"type": "ok"},
|
# That's a Python iterator reference, not a reasonable resource key.
|
||||||
"resources": {"iterkeys": []}}
|
mock_send.return_value = \
|
||||||
mock_ostro.return_value = status_type
|
'{"status":{"type":"ok"},"resources":{"iterkeys":[]}}'
|
||||||
|
|
||||||
self.placements_item_controller.index_post(**kwargs)
|
self.placements_item_controller.index_post(**kwargs)
|
||||||
self.validate_test(placements.response.status == 201)
|
self.validate_test(placements.response.status == 201)
|
||||||
|
27
valet/tests/unit/fakes.py
Normal file
27
valet/tests/unit/fakes.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#
|
||||||
|
# Copyright 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.
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from valet.api.db.models import music as models
|
||||||
|
|
||||||
|
|
||||||
|
def group(name="mock_group", description="mock group", type="affinity",
|
||||||
|
level="host", members='["test_tenant_id"]'):
|
||||||
|
"""Boilerplate for creating a group"""
|
||||||
|
group = models.groups.Group(name=name, description=description, type=type,
|
||||||
|
level=level, members=members, _insert=False)
|
||||||
|
group.id = str(uuid.uuid4())
|
||||||
|
return group
|
Loading…
x
Reference in New Issue
Block a user