mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-05 23:51:48 -05:00
Set JT.organization with value from its project
Remove validation requiring JT.organization
Undo some of the additional org definitions in tests
Revert some tests no longer needed for feature
exclude workflow approvals from unified organization field
revert awxkit changes for providing organization
Roll back additional JT creation permission requirement
Fix up more issues by persisting organization field when project is removed
Restrict project org editing, logging, and testing
Grant removed inventory org admin permissions in migration
Add special validate_unique for job templates
this deals with enforcing name-organization uniqueness
Add back in special message where config is unknown
when receiving 403 on job relaunch
Fix logical and performance bugs with data migration
within JT.inventory.organization make-permission-explicit migration
remove nested loops so we do .iterator() on JT queryset
in reverse migration, carefully remove execute role on JT
held by org admins of inventory organization,
as well as the execute_role holders
Use current state of Role model in logic, with 1 notable exception
that is used to filter on ancestors
the ancestor and descentent relationship in the migration model
is not reliable
output of this is saved as an integer list to avoid future
compatibility errors
make the parents rebuilding logic skip over irrelevant models
this is the largest performance gain for small resource numbers
323 lines
11 KiB
Python
323 lines
11 KiB
Python
# Python
|
|
import urllib.parse
|
|
from collections import deque
|
|
# Django
|
|
from django.db import models
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
|
NAMED_URL_RES_DILIMITER = "++"
|
|
NAMED_URL_RES_INNER_DILIMITER = "+"
|
|
NAMED_URL_RES_DILIMITER_ENCODE = "%2B"
|
|
URL_PATH_RESERVED_CHARSET = {}
|
|
for c in ';/?:@=&[]':
|
|
URL_PATH_RESERVED_CHARSET[c] = urllib.parse.quote(c, safe='')
|
|
FK_NAME = 0
|
|
NEXT_NODE = 1
|
|
|
|
NAME_EXCEPTIONS = {
|
|
"custom_inventory_scripts": "inventory_scripts"
|
|
}
|
|
|
|
|
|
class GraphNode(object):
|
|
|
|
def __init__(self, model, fields, adj_list):
|
|
self.model = model
|
|
self.found = False
|
|
self.obj = None
|
|
self.fields = fields
|
|
self.adj_list = adj_list
|
|
self.counter = 0
|
|
|
|
def _handle_unexpected_model_url_names(self, model_url_name):
|
|
if model_url_name in NAME_EXCEPTIONS:
|
|
return NAME_EXCEPTIONS[model_url_name]
|
|
return model_url_name
|
|
|
|
@property
|
|
def model_url_name(self):
|
|
if not hasattr(self, '_model_url_name'):
|
|
self._model_url_name = self.model._meta.verbose_name_plural.replace(' ', '_')
|
|
self._model_url_name = self._handle_unexpected_model_url_names(self._model_url_name)
|
|
return self._model_url_name
|
|
|
|
@property
|
|
def named_url_format(self):
|
|
named_url_components = []
|
|
stack = [self]
|
|
current_fk_name = ''
|
|
while stack:
|
|
if stack[-1].counter == 0:
|
|
named_url_component = NAMED_URL_RES_INNER_DILIMITER.join(
|
|
["<%s>" % (current_fk_name + field)
|
|
for field in stack[-1].fields]
|
|
)
|
|
named_url_components.append(named_url_component)
|
|
if stack[-1].counter >= len(stack[-1].adj_list):
|
|
stack[-1].counter = 0
|
|
stack.pop()
|
|
else:
|
|
to_append = stack[-1].adj_list[stack[-1].counter][NEXT_NODE]
|
|
current_fk_name = "%s." % (stack[-1].adj_list[stack[-1].counter][FK_NAME],)
|
|
stack[-1].counter += 1
|
|
stack.append(to_append)
|
|
return NAMED_URL_RES_DILIMITER.join(named_url_components)
|
|
|
|
@property
|
|
def named_url_repr(self):
|
|
ret = {}
|
|
ret['fields'] = self.fields
|
|
ret['adj_list'] = [[x[FK_NAME], x[NEXT_NODE].model_url_name] for x in self.adj_list]
|
|
return ret
|
|
|
|
def _encode_uri(self, text):
|
|
'''
|
|
Performance assured: http://stackoverflow.com/a/27086669
|
|
'''
|
|
for c in URL_PATH_RESERVED_CHARSET:
|
|
if c in text:
|
|
text = text.replace(c, URL_PATH_RESERVED_CHARSET[c])
|
|
text = text.replace(NAMED_URL_RES_INNER_DILIMITER,
|
|
'[%s]' % NAMED_URL_RES_INNER_DILIMITER)
|
|
return text
|
|
|
|
def generate_named_url(self, obj):
|
|
self.obj = obj
|
|
named_url = []
|
|
stack = [self]
|
|
while stack:
|
|
if stack[-1].counter == 0:
|
|
named_url_item = [self._encode_uri(getattr(stack[-1].obj, field, ''))
|
|
for field in stack[-1].fields]
|
|
named_url.append(NAMED_URL_RES_INNER_DILIMITER.join(named_url_item))
|
|
if stack[-1].counter >= len(stack[-1].adj_list):
|
|
stack[-1].counter = 0
|
|
stack[-1].obj = None
|
|
stack.pop()
|
|
else:
|
|
next_ = stack[-1].adj_list[stack[-1].counter]
|
|
stack[-1].counter += 1
|
|
next_obj = getattr(stack[-1].obj, next_[FK_NAME], None)
|
|
if next_obj is not None:
|
|
next_[NEXT_NODE].obj = next_obj
|
|
stack.append(next_[NEXT_NODE])
|
|
else:
|
|
named_url.append('')
|
|
return NAMED_URL_RES_DILIMITER.join(named_url)
|
|
|
|
|
|
def _process_top_node(self, named_url_names, kwargs, prefixes, stack, idx):
|
|
if stack[-1].counter == 0:
|
|
if idx >= len(named_url_names):
|
|
return idx, False
|
|
if not named_url_names[idx]:
|
|
stack[-1].counter = 0
|
|
stack.pop()
|
|
if prefixes:
|
|
prefixes.pop()
|
|
idx += 1
|
|
return idx, True
|
|
named_url_parts = named_url_names[idx].split(NAMED_URL_RES_INNER_DILIMITER)
|
|
if len(named_url_parts) != len(stack[-1].fields):
|
|
return idx, False
|
|
evolving_prefix = '__'.join(prefixes)
|
|
for attr_name, attr_value in zip(stack[-1].fields, named_url_parts):
|
|
attr_name = ("__%s" % attr_name) if evolving_prefix else attr_name
|
|
if isinstance(attr_value, str):
|
|
attr_value = urllib.parse.unquote(attr_value)
|
|
kwargs[evolving_prefix + attr_name] = attr_value
|
|
idx += 1
|
|
if stack[-1].counter >= len(stack[-1].adj_list):
|
|
stack[-1].counter = 0
|
|
stack.pop()
|
|
if prefixes:
|
|
prefixes.pop()
|
|
else:
|
|
to_append = stack[-1].adj_list[stack[-1].counter]
|
|
stack[-1].counter += 1
|
|
prefixes.append(to_append[FK_NAME])
|
|
stack.append(to_append[NEXT_NODE])
|
|
return idx, True
|
|
|
|
def populate_named_url_query_kwargs(self, kwargs, named_url, ignore_digits=True):
|
|
if ignore_digits and named_url.isdigit() and int(named_url) > 0:
|
|
return False
|
|
named_url = named_url.replace('[%s]' % NAMED_URL_RES_INNER_DILIMITER,
|
|
NAMED_URL_RES_DILIMITER_ENCODE)
|
|
named_url_names = named_url.split(NAMED_URL_RES_DILIMITER)
|
|
prefixes = []
|
|
stack = [self]
|
|
idx = 0
|
|
while stack:
|
|
idx, is_valid = self._process_top_node(
|
|
named_url_names, kwargs, prefixes, stack, idx
|
|
)
|
|
if not is_valid:
|
|
return False
|
|
return idx == len(named_url_names)
|
|
|
|
def add_bindings(self):
|
|
if self.model_url_name not in settings.NAMED_URL_FORMATS:
|
|
settings.NAMED_URL_FORMATS[self.model_url_name] = self.named_url_format
|
|
settings.NAMED_URL_GRAPH_NODES[self.model_url_name] = self.named_url_repr
|
|
settings.NAMED_URL_MAPPINGS[self.model_url_name] = self.model
|
|
|
|
def remove_bindings(self):
|
|
if self.model_url_name in settings.NAMED_URL_FORMATS:
|
|
settings.NAMED_URL_FORMATS.pop(self.model_url_name)
|
|
settings.NAMED_URL_GRAPH_NODES.pop(self.model_url_name)
|
|
settings.NAMED_URL_MAPPINGS.pop(self.model_url_name)
|
|
|
|
|
|
def _get_all_unique_togethers(model):
|
|
queue = deque()
|
|
queue.append(model)
|
|
ret = []
|
|
try:
|
|
if model._meta.get_field('name').unique:
|
|
ret.append(('name',))
|
|
except Exception:
|
|
pass
|
|
while len(queue) > 0:
|
|
model_to_backtrack = queue.popleft()
|
|
uts = model_to_backtrack._meta.unique_together
|
|
if len(uts) > 0 and not isinstance(uts[0], tuple):
|
|
ret.append(uts)
|
|
else:
|
|
ret.extend(uts)
|
|
soft_uts = getattr(model_to_backtrack, 'SOFT_UNIQUE_TOGETHER', [])
|
|
ret.extend(soft_uts)
|
|
for parent_class in model_to_backtrack.__bases__:
|
|
if issubclass(parent_class, models.Model) and\
|
|
hasattr(parent_class, '_meta') and\
|
|
hasattr(parent_class._meta, 'unique_together') and\
|
|
isinstance(parent_class._meta.unique_together, tuple):
|
|
queue.append(parent_class)
|
|
ret.sort(key=lambda x: len(x))
|
|
return tuple(ret)
|
|
|
|
|
|
def _check_unique_together_fields(model, ut):
|
|
has_name = False
|
|
fk_names = []
|
|
fields = []
|
|
is_valid = True
|
|
for field_name in ut:
|
|
field = model._meta.get_field(field_name)
|
|
if field_name == 'name':
|
|
has_name = True
|
|
elif type(field) == models.ForeignKey and field.related_model != model:
|
|
fk_names.append(field_name)
|
|
elif issubclass(type(field), models.CharField) and field.choices:
|
|
fields.append(field_name)
|
|
else:
|
|
is_valid = False
|
|
break
|
|
if not is_valid:
|
|
return (), (), is_valid
|
|
fk_names.sort()
|
|
fields.sort(reverse=True)
|
|
if has_name:
|
|
fields.append('name')
|
|
fields.reverse()
|
|
return tuple(fk_names), tuple(fields), is_valid
|
|
|
|
|
|
def _generate_configurations(nodes):
|
|
if not nodes:
|
|
return
|
|
idx = 0
|
|
stack = [nodes[0][1]]
|
|
idx_stack = [0]
|
|
configuration = {}
|
|
while stack:
|
|
if idx_stack[-1] >= len(stack[-1]):
|
|
stack.pop()
|
|
idx_stack.pop()
|
|
configuration.pop(nodes[idx][0])
|
|
idx -= 1
|
|
else:
|
|
if len(configuration) == len(stack):
|
|
configuration.pop(nodes[idx][0])
|
|
configuration[nodes[idx][0]] = tuple(stack[-1][idx_stack[-1]])
|
|
idx_stack[-1] += 1
|
|
if idx == len(nodes) - 1:
|
|
yield configuration.copy()
|
|
else:
|
|
idx += 1
|
|
stack.append(nodes[idx][1])
|
|
idx_stack.append(0)
|
|
|
|
|
|
def _dfs(configuration, model, graph, dead_ends, new_deadends, parents):
|
|
parents.add(model)
|
|
fields, fk_names = configuration[model][0][:], configuration[model][1][:]
|
|
adj_list = []
|
|
for fk_name in fk_names:
|
|
next_model = model._meta.get_field(fk_name).related_model
|
|
if issubclass(next_model, ContentType):
|
|
continue
|
|
if next_model not in configuration or\
|
|
next_model in dead_ends or\
|
|
next_model in new_deadends or\
|
|
next_model in parents:
|
|
new_deadends.add(model)
|
|
parents.remove(model)
|
|
return False
|
|
if next_model not in graph and\
|
|
not _dfs(
|
|
configuration, next_model, graph,
|
|
dead_ends, new_deadends, parents
|
|
):
|
|
new_deadends.add(model)
|
|
parents.remove(model)
|
|
return False
|
|
adj_list.append((fk_name, graph[next_model]))
|
|
graph[model] = GraphNode(model, fields, adj_list)
|
|
parents.remove(model)
|
|
return True
|
|
|
|
|
|
def _generate_single_graph(configuration, dead_ends):
|
|
new_deadends = set()
|
|
graph = {}
|
|
for model in configuration:
|
|
if model not in graph and model not in new_deadends:
|
|
_dfs(configuration, model, graph, dead_ends, new_deadends, set())
|
|
return graph
|
|
|
|
|
|
def generate_graph(models):
|
|
settings.NAMED_URL_FORMATS = {}
|
|
settings.NAMED_URL_GRAPH_NODES = {}
|
|
settings.NAMED_URL_MAPPINGS = {}
|
|
candidate_nodes = {}
|
|
dead_ends = set()
|
|
for model in models:
|
|
uts = _get_all_unique_togethers(model)
|
|
for ut in uts:
|
|
fk_names, fields, is_valid = _check_unique_together_fields(model, ut)
|
|
if is_valid:
|
|
candidate_nodes.setdefault(model, [])
|
|
candidate_nodes[model].append([fields, fk_names])
|
|
if model not in candidate_nodes:
|
|
dead_ends.add(model)
|
|
candidate_nodes = list(candidate_nodes.items())
|
|
largest_graph = {}
|
|
for configuration in _generate_configurations(candidate_nodes):
|
|
candidate_graph = _generate_single_graph(configuration, dead_ends)
|
|
if len(largest_graph) < len(candidate_graph):
|
|
largest_graph = candidate_graph
|
|
if len(largest_graph) == len(candidate_nodes):
|
|
break
|
|
settings.NAMED_URL_GRAPH = largest_graph
|
|
for node in settings.NAMED_URL_GRAPH.values():
|
|
node.add_bindings()
|
|
|
|
|
|
def reset_counters():
|
|
for node in settings.NAMED_URL_GRAPH.values():
|
|
node.counter = 0
|