442 lines
19 KiB
Python
442 lines
19 KiB
Python
# Copyright 2015 VMware.
|
|
#
|
|
# 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 logging
|
|
import re
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django import template
|
|
from django.utils.text import slugify
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from horizon import forms
|
|
from horizon import workflows
|
|
import six
|
|
|
|
from congress_dashboard.api import congress
|
|
|
|
|
|
COLUMN_FORMAT = '<datasource>%s<table> <column>' % congress.TABLE_SEPARATOR
|
|
COLUMN_PATTERN = r'\s*[\w.]+%s[\w.]+\s+[\w.]+\s*$' % congress.TABLE_SEPARATOR
|
|
COLUMN_PATTERN_ERROR = 'Column name must be in "%s" format' % COLUMN_FORMAT
|
|
|
|
TABLE_FORMAT = '<datasource>%s<table>' % congress.TABLE_SEPARATOR
|
|
TABLE_PATTERN = r'\s*[\w.]+%s[\w.]+\s*$' % congress.TABLE_SEPARATOR
|
|
TABLE_PATTERN_ERROR = 'Table name must be in "%s" format' % TABLE_FORMAT
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class CreateOutputAction(workflows.Action):
|
|
policy_name = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
rule_name = forms.CharField(label=_('Rule Name'), max_length=255,
|
|
initial='', required=False)
|
|
comment = forms.CharField(label=_('Rule Comment'), initial='',
|
|
required=False)
|
|
policy_table = forms.CharField(label=_("Policy Table Name"), initial='',
|
|
max_length=255)
|
|
policy_columns = forms.CharField(
|
|
label=_('Policy Table Columns'), initial='',
|
|
help_text=_('Name the columns in the output table, one per textbox.'))
|
|
failure_url = 'horizon:admin:policies:detail'
|
|
|
|
def __init__(self, request, context, *args, **kwargs):
|
|
super(CreateOutputAction, self).__init__(request, context, *args,
|
|
**kwargs)
|
|
self.fields['policy_name'].initial = context['policy_name']
|
|
|
|
class Meta(object):
|
|
name = _('Output')
|
|
|
|
|
|
class CreateOutput(workflows.Step):
|
|
action_class = CreateOutputAction
|
|
contributes = ('policy_name', 'rule_name', 'comment', 'policy_table',
|
|
'policy_columns')
|
|
template_name = 'admin/policies/rules/_create_output.html'
|
|
help_text = _('Information about the rule and the policy table '
|
|
'being created.')
|
|
|
|
def render(self):
|
|
# Overriding parent method to add extra template context variables.
|
|
step_template = template.loader.get_template(self.template_name)
|
|
extra_context = {"form": self.action,
|
|
"step": self}
|
|
context = template.RequestContext(self.workflow.request, extra_context)
|
|
|
|
# Data needed to re-create policy column inputs after an error occurs.
|
|
policy_columns = self.workflow.request.POST.get('policy_columns', '')
|
|
columns_list = policy_columns.split(', ')
|
|
context['policy_columns_list'] = columns_list
|
|
context['policy_columns_count'] = len(columns_list)
|
|
return step_template.render(context)
|
|
|
|
|
|
class CreateConditionsAction(workflows.Action):
|
|
mappings = forms.CharField(label=_('Policy table columns:'), initial='')
|
|
|
|
class Meta(object):
|
|
name = _('Conditions')
|
|
|
|
|
|
class CreateConditions(workflows.Step):
|
|
action_class = CreateConditionsAction
|
|
contributes = ('mappings',)
|
|
template_name = 'admin/policies/rules/_create_conditions.html'
|
|
help_text = _('Sources from which the output policy table will get its '
|
|
'data, plus any constraints.')
|
|
|
|
def _compare_mapping_columns(self, x, y):
|
|
# x = "mapping_column_<int>", y = "mapping_column_<int>"
|
|
return cmp(int(x.split('_')[-1]), int(y.split('_')[-1]))
|
|
|
|
def render(self):
|
|
# Overriding parent method to add extra template context variables.
|
|
step_template = template.loader.get_template(self.template_name)
|
|
extra_context = {"form": self.action,
|
|
"step": self}
|
|
context = template.RequestContext(self.workflow.request, extra_context)
|
|
|
|
# Data needed to re-create mapping column inputs after an error occurs.
|
|
post = self.workflow.request.POST
|
|
mappings = []
|
|
policy_columns = post.get('policy_columns')
|
|
policy_columns_list = []
|
|
# Policy column to data source mappings.
|
|
if policy_columns:
|
|
policy_columns_list = policy_columns.split(', ')
|
|
mapping_columns = []
|
|
for param, value in post.items():
|
|
if (param.startswith('mapping_column_') and
|
|
param != 'mapping_column_0'):
|
|
mapping_columns.append(param)
|
|
|
|
# Mapping columns should be in the same order as the policy columns
|
|
# above to which they match.
|
|
sorted_mapping_columns = sorted(mapping_columns,
|
|
cmp=self._compare_mapping_columns)
|
|
mapping_columns_list = [post.get(c)
|
|
for c in sorted_mapping_columns]
|
|
mappings = zip(policy_columns_list, mapping_columns_list)
|
|
context['mappings'] = mappings
|
|
# Add one for the hidden template row.
|
|
context['mappings_count'] = len(mappings) + 1
|
|
|
|
# Data needed to re-create join, negation, and alias inputs.
|
|
joins = []
|
|
negations = []
|
|
aliases = []
|
|
for param, value in post.items():
|
|
if param.startswith('join_left_') and value:
|
|
join_num = param.split('_')[-1]
|
|
other_value = post.get('join_right_%s' % join_num)
|
|
join_op = post.get('join_op_%s' % join_num)
|
|
if other_value and join_op is not None:
|
|
joins.append((value, join_op, other_value))
|
|
elif param.startswith('negation_value_') and value:
|
|
negation_num = param.split('_')[-1]
|
|
negation_column = post.get('negation_column_%s' %
|
|
negation_num)
|
|
if negation_column:
|
|
negations.append((value, negation_column))
|
|
elif param.startswith('alias_column_') and value:
|
|
alias_num = param.split('_')[-1]
|
|
alias_name = post.get('alias_name_%s' % alias_num)
|
|
if alias_name:
|
|
aliases.append((value, alias_name))
|
|
|
|
# Make sure there's at least one empty row.
|
|
context['joins'] = joins or [('', '')]
|
|
context['joins_count'] = len(joins) or 1
|
|
context['negations'] = negations or [('', '')]
|
|
context['negations_count'] = len(negations) or 1
|
|
context['aliases'] = aliases or [('', '')]
|
|
context['aliases_count'] = len(aliases) or 1
|
|
|
|
# Input validation attributes.
|
|
context['column_pattern'] = COLUMN_PATTERN
|
|
context['column_pattern_error'] = COLUMN_PATTERN_ERROR
|
|
context['table_pattern'] = TABLE_PATTERN
|
|
context['table_pattern_error'] = TABLE_PATTERN_ERROR
|
|
return step_template.render(context)
|
|
|
|
|
|
def _underscore_slugify(name):
|
|
# Slugify given string, except using undesrscores instead of hyphens.
|
|
return slugify(name).replace('-', '_')
|
|
|
|
|
|
class CreateRule(workflows.Workflow):
|
|
slug = 'create_rule'
|
|
name = _('Create Rule')
|
|
finalize_button_name = _('Create')
|
|
success_message = _('Created rule%(rule_name)s.%(error)s')
|
|
failure_message = _('Unable to create rule%(rule_name)s: %(error)s')
|
|
default_steps = (CreateOutput, CreateConditions)
|
|
wizard = True
|
|
|
|
def get_success_url(self):
|
|
policy_name = self.context.get('policy_name')
|
|
return reverse('horizon:admin:policies:detail', args=(policy_name,))
|
|
|
|
def get_failure_url(self):
|
|
policy_name = self.context.get('policy_name')
|
|
return reverse('horizon:admin:policies:detail', args=(policy_name,))
|
|
|
|
def format_status_message(self, message):
|
|
rule_name = self.context.get('rule_name')
|
|
name_str = ''
|
|
if rule_name:
|
|
name_str = ' "%s"' % rule_name
|
|
else:
|
|
rule_id = self.context.get('rule_id')
|
|
if rule_id:
|
|
name_str = ' %s' % rule_id
|
|
return message % {'rule_name': name_str,
|
|
'error': self.context.get('error', '')}
|
|
|
|
def _get_schema_columns(self, request, table):
|
|
table_parts = table.split(congress.TABLE_SEPARATOR)
|
|
datasource = table_parts[0]
|
|
table_name = table_parts[1]
|
|
try:
|
|
schema = congress.datasource_table_schema_get_by_name(
|
|
request, datasource, table_name)
|
|
except Exception:
|
|
# Maybe it's a policy table, not a service.
|
|
try:
|
|
schema = congress.policy_table_schema_get(
|
|
request, datasource, table_name)
|
|
except Exception as e:
|
|
# Nope.
|
|
LOG.error('Unable to get schema for table "%s", '
|
|
'datasource "%s": %s',
|
|
table_name, datasource, e.message)
|
|
return e.message
|
|
return schema['columns']
|
|
|
|
def handle(self, request, data):
|
|
policy_name = data['policy_name']
|
|
username = request.user.username
|
|
project_name = request.user.tenant_name
|
|
|
|
# Output data.
|
|
rule_name = data.get('rule_name')
|
|
comment = data.get('comment')
|
|
policy_table = _underscore_slugify(data['policy_table'])
|
|
if not data['policy_columns']:
|
|
self.context['error'] = 'Missing policy table columns'
|
|
return False
|
|
policy_columns = data['policy_columns'].split(', ')
|
|
|
|
# Conditions data.
|
|
if not data['mappings']:
|
|
self.context['error'] = ('Missing data source column mappings for '
|
|
'policy table columns')
|
|
return False
|
|
mapping_columns = [c.strip() for c in data['mappings'].split(', ')]
|
|
if len(policy_columns) != len(mapping_columns):
|
|
self.context['error'] = ('Missing data source column mappings for '
|
|
'some policy table columns')
|
|
return False
|
|
# Map columns used in rule's head. Every column in the head must also
|
|
# appear in the body.
|
|
head_columns = [_underscore_slugify(c).strip() for c in policy_columns]
|
|
column_variables = dict(zip(mapping_columns, head_columns))
|
|
|
|
# All tables needed in the body.
|
|
body_tables = set()
|
|
negation_tables = set()
|
|
|
|
# Keep track of the tables from the head that need to be in the body.
|
|
for column in mapping_columns:
|
|
if re.match(COLUMN_PATTERN, column) is None:
|
|
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
|
column)
|
|
return False
|
|
table = column.split()[0]
|
|
body_tables.add(table)
|
|
|
|
# Make sure columns that are given a significant variable name are
|
|
# unique names by adding name_count as a suffix.
|
|
name_count = 0
|
|
for param, value in request.POST.items():
|
|
if param.startswith('join_left_') and value:
|
|
if re.match(COLUMN_PATTERN, value) is None:
|
|
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
|
value)
|
|
return False
|
|
value = value.strip()
|
|
|
|
# Get operator and other column used in join.
|
|
join_num = param.split('_')[-1]
|
|
join_op = request.POST.get('join_op_%s' % join_num)
|
|
other_value = request.POST.get('join_right_%s' % join_num)
|
|
other_value = other_value.strip()
|
|
|
|
if join_op == '=':
|
|
try:
|
|
# Check if static value is a number, but keep it as a
|
|
# string, to be used later.
|
|
int(other_value)
|
|
column_variables[value] = other_value
|
|
except ValueError:
|
|
# Pass it along as a quoted string.
|
|
column_variables[value] = '"%s"' % other_value
|
|
else:
|
|
# Join between two columns.
|
|
if not other_value:
|
|
# Ignore incomplete pairing.
|
|
continue
|
|
if re.match(COLUMN_PATTERN, other_value) is None:
|
|
self.context['error'] = ('%s: %s' %
|
|
(COLUMN_PATTERN_ERROR,
|
|
other_value))
|
|
return False
|
|
|
|
# Tables used in the join need to be in the body.
|
|
value_parts = value.split()
|
|
body_tables.add(value_parts[0])
|
|
body_tables.add(other_value.split()[0])
|
|
|
|
# Arbitrarily name the right column the same as the left.
|
|
column_name = value_parts[1]
|
|
# Use existing variable name if there is already one for
|
|
# either column in this join.
|
|
if other_value in column_variables:
|
|
column_variables[value] = column_variables[other_value]
|
|
elif value in column_variables:
|
|
column_variables[other_value] = column_variables[value]
|
|
else:
|
|
variable = '%s_%s' % (column_name, name_count)
|
|
name_count += 1
|
|
column_variables[value] = variable
|
|
column_variables[other_value] = variable
|
|
|
|
elif param.startswith('negation_value_') and value:
|
|
if re.match(COLUMN_PATTERN, value) is None:
|
|
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
|
value)
|
|
return False
|
|
value = value.strip()
|
|
|
|
# Get operator and other column used in negation.
|
|
negation_num = param.split('_')[-1]
|
|
negation_column = request.POST.get('negation_column_%s' %
|
|
negation_num)
|
|
if not negation_column:
|
|
# Ignore incomplete pairing.
|
|
continue
|
|
if re.match(COLUMN_PATTERN, negation_column) is None:
|
|
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
|
negation_column)
|
|
return False
|
|
negation_column = negation_column.strip()
|
|
|
|
# Tables for columns referenced by the negation table must
|
|
# appear in the body.
|
|
value_parts = value.split()
|
|
body_tables.add(value_parts[0])
|
|
|
|
negation_tables.add(negation_column.split()[0])
|
|
# Use existing variable name if there is already one for either
|
|
# column in this negation.
|
|
if negation_column in column_variables:
|
|
column_variables[value] = column_variables[negation_column]
|
|
elif value in column_variables:
|
|
column_variables[negation_column] = column_variables[value]
|
|
else:
|
|
# Arbitrarily name the negated table's column the same as
|
|
# the value column.
|
|
column_name = value_parts[1]
|
|
variable = '%s_%s' % (column_name, name_count)
|
|
name_count += 1
|
|
column_variables[value] = variable
|
|
column_variables[negation_column] = variable
|
|
|
|
LOG.debug('column_variables for rule: %s', column_variables)
|
|
|
|
# Form the literals for all the tables needed in the body. Make sure
|
|
# column that have no relation to any other columns are given a unique
|
|
# variable name, using column_count.
|
|
column_count = 0
|
|
literals = []
|
|
for table in body_tables:
|
|
# Replace column names with variable names that join related
|
|
# columns together.
|
|
columns = self._get_schema_columns(request, table)
|
|
if isinstance(columns, six.string_types):
|
|
self.context['error'] = columns
|
|
return False
|
|
|
|
literal_columns = []
|
|
if columns:
|
|
for column in columns:
|
|
table_column = '%s %s' % (table, column['name'])
|
|
literal_columns.append(
|
|
column_variables.get(table_column, 'col_%s' %
|
|
column_count))
|
|
column_count += 1
|
|
literals.append('%s(%s)' % (table, ', '.join(literal_columns)))
|
|
else:
|
|
# Just the table name, such as for classification:true.
|
|
literals.append(table)
|
|
|
|
# Form the negated tables.
|
|
for table in negation_tables:
|
|
columns = self._get_schema_columns(request, table)
|
|
if isinstance(columns, six.string_types):
|
|
self.context['error'] = columns
|
|
return False
|
|
|
|
literal_columns = []
|
|
num_variables = 0
|
|
for column in columns:
|
|
table_column = '%s %s' % (table, column['name'])
|
|
if table_column in column_variables:
|
|
literal_columns.append(column_variables[table_column])
|
|
num_variables += 1
|
|
else:
|
|
literal_columns.append('col_%s' % column_count)
|
|
column_count += 1
|
|
literal = 'not %s(%s)' % (table, ', '.join(literal_columns))
|
|
literals.append(literal)
|
|
|
|
# Every column in the negated table must appear in a non-negated
|
|
# literal in the body. If there are some variables that have not
|
|
# been used elsewhere, repeat the literal in its non-negated form.
|
|
if num_variables != len(columns) and table not in body_tables:
|
|
literals.append(literal.replace('not ', ''))
|
|
|
|
# All together now.
|
|
rule = '%s(%s) %s %s' % (policy_table, ', '.join(head_columns),
|
|
congress.RULE_SEPARATOR, ', '.join(literals))
|
|
LOG.info('User %s creating policy "%s" rule "%s" in tenant %s: %s',
|
|
username, policy_name, rule_name, project_name, rule)
|
|
try:
|
|
params = {
|
|
'name': rule_name,
|
|
'comment': comment,
|
|
'rule': rule,
|
|
}
|
|
rule = congress.policy_rule_create(request, policy_name,
|
|
body=params)
|
|
LOG.info('Created rule %s', rule['id'])
|
|
self.context['rule_id'] = rule['id']
|
|
except Exception as e:
|
|
LOG.error('Error creating policy "%s" rule "%s": %s',
|
|
policy_name, rule_name, e.message)
|
|
self.context['error'] = e.message
|
|
return False
|
|
return True
|