Move congress_dashboard folder to this project
Partially-Implements: blueprint enhance-congress-dashboard Closes-Bug: #1653743 Change-Id: I9b2ae92e125181226130de56b09d5588b4cd1755
This commit is contained in:
parent
2a43ed659a
commit
88ef2e3d5a
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
eggs
|
||||||
|
parts
|
||||||
|
bin
|
||||||
|
var
|
||||||
|
sdist
|
||||||
|
develop-eggs
|
||||||
|
.installed.cfg
|
||||||
|
/lib
|
||||||
|
/lib64
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
nosetests.xml
|
||||||
|
.testrepository
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
|
# Complexity
|
||||||
|
output/*.html
|
||||||
|
output/*/index.html
|
||||||
|
|
||||||
|
# Sphinx
|
||||||
|
doc/build
|
||||||
|
|
25
congress_dashboard/README.md
Normal file
25
congress_dashboard/README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
Congress Dashboard
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Congress Dashboard is an extension for OpenStack Dashboard that provides a UI
|
||||||
|
for Congress. With congress-dashboard, a user is able to easily write the
|
||||||
|
policies and rules for governance of cloud.
|
||||||
|
|
||||||
|
Setup Instructions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
This instruction assumes that Horizon is already installed and its
|
||||||
|
installation folder is <horizon>. Detailed information on how to install
|
||||||
|
Horizon can be found at
|
||||||
|
http://docs.openstack.org/developer/horizon/quickstart.html#setup.
|
||||||
|
|
||||||
|
To integrate congress with horizon, copy the files in
|
||||||
|
<congress_dashboard>/enabled to <horizon>/openstack_dashboard/local/enabled/
|
||||||
|
|
||||||
|
$ cp -b <congress_dashboard>/enabled/_50_policy.py <horizon>/openstack_dashboard/local/enabled/
|
||||||
|
$ cp -b <congress_dashboard>/enabled/_60_policies.py <horizon>/openstack_dashboard/local/enabled/
|
||||||
|
$ cp -b <congress_dashboard>/enabled/_70_datasources.py <horizon>/openstack_dashboard/local/enabled/
|
||||||
|
|
||||||
|
Restart Apache server
|
||||||
|
sudo service apache2 restart
|
||||||
|
|
304
congress_dashboard/api/congress.py
Normal file
304
congress_dashboard/api/congress.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from congressclient.v1 import client as congress_client
|
||||||
|
from django.conf import settings
|
||||||
|
import keystoneauth1.identity.v2 as v2
|
||||||
|
import keystoneauth1.identity.v3 as v3
|
||||||
|
import keystoneauth1.session as kssession
|
||||||
|
from openstack_dashboard.api import base
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
|
||||||
|
LITERALS_SEPARATOR = '),'
|
||||||
|
RULE_SEPARATOR = ':-'
|
||||||
|
TABLE_SEPARATOR = ':'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_id_as_name_if_empty(apidict, length=0):
|
||||||
|
try:
|
||||||
|
if not apidict._apidict.get('name'):
|
||||||
|
id = apidict._apidict['id']
|
||||||
|
if length:
|
||||||
|
id = id[:length]
|
||||||
|
apidict._apidict['name'] = '(%s)' % id
|
||||||
|
else:
|
||||||
|
apidict._apidict['name'] = id
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyAPIDictWrapper(base.APIDictWrapper):
|
||||||
|
def set_id_as_name_if_empty(self):
|
||||||
|
_set_id_as_name_if_empty(self)
|
||||||
|
|
||||||
|
def set_id_if_empty(self, id):
|
||||||
|
apidict_id = self._apidict.get('id')
|
||||||
|
if not apidict_id or apidict_id == "None":
|
||||||
|
self._apidict['id'] = id
|
||||||
|
|
||||||
|
def set_value(self, key, value):
|
||||||
|
self._apidict[key] = value
|
||||||
|
|
||||||
|
def delete_by_key(self, key):
|
||||||
|
del self._apidict[key]
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyRule(PolicyAPIDictWrapper):
|
||||||
|
"""Wrapper for a Congress policy's rule."""
|
||||||
|
def set_id_as_name_if_empty(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTable(PolicyAPIDictWrapper):
|
||||||
|
"""Wrapper for a Congress policy's data table."""
|
||||||
|
def set_policy_details(self, policy):
|
||||||
|
self._apidict['policy_name'] = policy['name']
|
||||||
|
self._apidict['policy_owner_id'] = policy['owner_id']
|
||||||
|
|
||||||
|
|
||||||
|
def congressclient(request):
|
||||||
|
"""Instantiate Congress client."""
|
||||||
|
auth_url = getattr(settings, 'OPENSTACK_KEYSTONE_URL')
|
||||||
|
user = request.user
|
||||||
|
session = get_keystone_session(auth_url, user)
|
||||||
|
region_name = user.services_region
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'session': session,
|
||||||
|
'auth': None,
|
||||||
|
'interface': 'publicURL',
|
||||||
|
'service_type': 'policy',
|
||||||
|
'region_name': region_name
|
||||||
|
}
|
||||||
|
return congress_client.Client(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_keystone_session(auth_url, user):
|
||||||
|
if auth_url[-3:] == '/v3':
|
||||||
|
auth = v3.Token(auth_url, user.token.id, project_id=user.tenant_id)
|
||||||
|
else:
|
||||||
|
auth = v2.Token(auth_url, user.token.id, tenant_id=user.tenant_id,
|
||||||
|
tenant_name=user.tenant_name)
|
||||||
|
|
||||||
|
session = kssession.Session(auth=auth)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def policies_list(request):
|
||||||
|
"""List all policies."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policies_list = client.list_policy()
|
||||||
|
results = policies_list['results']
|
||||||
|
policies = []
|
||||||
|
for p in results:
|
||||||
|
policy = PolicyAPIDictWrapper(p)
|
||||||
|
# Policies currently have a name but not necessarily a non-"None" id.
|
||||||
|
# Use the name to identify the policy, needed to differentiate them in
|
||||||
|
# DataTables.
|
||||||
|
policy.set_id_if_empty(policy.get('name'))
|
||||||
|
policies.append(policy)
|
||||||
|
return policies
|
||||||
|
|
||||||
|
|
||||||
|
def policy_create(request, args):
|
||||||
|
"""Create a policy with the given properties."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policy = client.create_policy(args)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def policy_delete(request, policy_id):
|
||||||
|
"""Delete a policy by id."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policy = client.delete_policy(policy_id)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
def policy_get(request, policy_name):
|
||||||
|
"""Get a policy by name."""
|
||||||
|
# TODO(jwy): Use congress.show_policy() once system policies have unique
|
||||||
|
# IDs.
|
||||||
|
policies = policies_list(request)
|
||||||
|
for p in policies:
|
||||||
|
if p['name'] == policy_name:
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def policy_rule_create(request, policy_name, body=None):
|
||||||
|
"""Create a rule in the given policy, with the given properties."""
|
||||||
|
client = congressclient(request)
|
||||||
|
rule = client.create_policy_rule(policy_name, body=body)
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def policy_rule_delete(request, policy_name, rule_id):
|
||||||
|
"""Delete a rule by id, from the given policy."""
|
||||||
|
client = congressclient(request)
|
||||||
|
rule = client.delete_policy_rule(policy_name, rule_id)
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def policy_rules_list(request, policy_name):
|
||||||
|
"""List all rules in a policy, given by name."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policy_rules_list = client.list_policy_rules(policy_name)
|
||||||
|
results = policy_rules_list['results']
|
||||||
|
return [PolicyRule(r) for r in results]
|
||||||
|
|
||||||
|
|
||||||
|
def policy_tables_list(request, policy_name):
|
||||||
|
"""List all data tables in a policy, given by name."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policy_tables_list = client.list_policy_tables(policy_name)
|
||||||
|
results = policy_tables_list['results']
|
||||||
|
return [PolicyTable(t) for t in results]
|
||||||
|
|
||||||
|
|
||||||
|
def policy_table_get(request, policy_name, table_name):
|
||||||
|
"""Get a policy table in a policy, given by name."""
|
||||||
|
client = congressclient(request)
|
||||||
|
return client.show_policy_table(policy_name, table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def policy_rows_list(request, policy_name, table_name):
|
||||||
|
"""List all rows in a policy's data table, given by name."""
|
||||||
|
client = congressclient(request)
|
||||||
|
policy_rows_list = client.list_policy_rows(policy_name, table_name)
|
||||||
|
results = policy_rows_list['results']
|
||||||
|
|
||||||
|
policy_rows = []
|
||||||
|
# Policy table rows currently don't have ids. However, the DataTable object
|
||||||
|
# requires an id for the table to get rendered properly. Otherwise, the
|
||||||
|
# same contents are displayed for every row in the table. Assign the rows
|
||||||
|
# ids here.
|
||||||
|
id = 0
|
||||||
|
for row in results:
|
||||||
|
new_row = PolicyAPIDictWrapper(row)
|
||||||
|
new_row.set_id_if_empty(id)
|
||||||
|
id += 1
|
||||||
|
policy_rows.append(new_row)
|
||||||
|
return policy_rows
|
||||||
|
|
||||||
|
|
||||||
|
def policy_table_schema_get(request, policy_name, table_name):
|
||||||
|
"""Get the schema for a policy table, based on the first matching rule."""
|
||||||
|
column_names = []
|
||||||
|
rules = policy_rules_list(request, policy_name)
|
||||||
|
# There might be multiple rules that use the same name in the head. Pick
|
||||||
|
# the first matching one, which is what the policy engine currently does.
|
||||||
|
for rule in rules:
|
||||||
|
rule_def = rule['rule']
|
||||||
|
head, _ = rule_def.split(RULE_SEPARATOR)
|
||||||
|
if head.strip().startswith('%s(' % table_name):
|
||||||
|
start = head.index('(') + 1
|
||||||
|
end = head.index(')')
|
||||||
|
column_names = head[start:end].split(',')
|
||||||
|
break
|
||||||
|
|
||||||
|
schema = {'table_id': table_name}
|
||||||
|
schema['columns'] = [{'name': name.strip(), 'description': None}
|
||||||
|
for name in column_names]
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def datasources_list(request):
|
||||||
|
"""List all the data sources."""
|
||||||
|
client = congressclient(request)
|
||||||
|
datasources_list = client.list_datasources()
|
||||||
|
datasources = datasources_list['results']
|
||||||
|
return [PolicyAPIDictWrapper(d) for d in datasources]
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_get(request, datasource_id):
|
||||||
|
"""Get a data source by id."""
|
||||||
|
# TODO(jwy): Need API in congress_client to retrieve data source by id.
|
||||||
|
datasources = datasources_list(request)
|
||||||
|
for d in datasources:
|
||||||
|
if d['id'] == datasource_id:
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_get_by_name(request, datasource_name):
|
||||||
|
"""Get a data source by name."""
|
||||||
|
datasources = datasources_list(request)
|
||||||
|
for d in datasources:
|
||||||
|
if d['name'] == datasource_name:
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_tables_list(request, datasource_id):
|
||||||
|
"""List all data tables in a data source, given by id."""
|
||||||
|
client = congressclient(request)
|
||||||
|
datasource_tables_list = client.list_datasource_tables(datasource_id)
|
||||||
|
results = datasource_tables_list['results']
|
||||||
|
return [PolicyAPIDictWrapper(t) for t in results]
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_rows_list(request, datasource_id, table_name):
|
||||||
|
"""List all rows in a data source's data table, given by id."""
|
||||||
|
client = congressclient(request)
|
||||||
|
datasource_rows_list = client.list_datasource_rows(datasource_id,
|
||||||
|
table_name)
|
||||||
|
results = datasource_rows_list['results']
|
||||||
|
datasource_rows = []
|
||||||
|
id = 0
|
||||||
|
for row in results:
|
||||||
|
new_row = PolicyAPIDictWrapper(row)
|
||||||
|
new_row.set_id_if_empty(id)
|
||||||
|
id += 1
|
||||||
|
datasource_rows.append(new_row)
|
||||||
|
return datasource_rows
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_schema_get(request, datasource_id):
|
||||||
|
"""Get the schema for all tables in the given data source."""
|
||||||
|
client = congressclient(request)
|
||||||
|
return client.show_datasource_schema(datasource_id)
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_table_schema_get(request, datasource_id, table_name):
|
||||||
|
"""Get the schema for a data source table."""
|
||||||
|
client = congressclient(request)
|
||||||
|
return client.show_datasource_table_schema(datasource_id, table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_table_schema_get_by_name(request, datasource_name, table_name):
|
||||||
|
"""Get the schema for a data source table."""
|
||||||
|
datasource = datasource_get_by_name(request, datasource_name)
|
||||||
|
client = congressclient(request)
|
||||||
|
return client.show_datasource_table_schema(datasource['id'], table_name)
|
||||||
|
|
||||||
|
|
||||||
|
def datasource_statuses_list(request):
|
||||||
|
client = congressclient(request)
|
||||||
|
datasources_list = client.list_datasources()
|
||||||
|
datasources = datasources_list['results']
|
||||||
|
ds_status = []
|
||||||
|
|
||||||
|
for ds in datasources:
|
||||||
|
try:
|
||||||
|
status = client.list_datasource_status(ds['id'])
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Exception while getting the status")
|
||||||
|
raise
|
||||||
|
wrapper = PolicyAPIDictWrapper(ds)
|
||||||
|
wrapper.set_value('service', ds['name'])
|
||||||
|
for key in status:
|
||||||
|
value = status[key]
|
||||||
|
wrapper.set_value(key, value)
|
||||||
|
ds_status.append(wrapper)
|
||||||
|
return ds_status
|
0
congress_dashboard/datasources/__init__.py
Normal file
0
congress_dashboard/datasources/__init__.py
Normal file
26
congress_dashboard/datasources/panel.py
Normal file
26
congress_dashboard/datasources/panel.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
import horizon
|
||||||
|
from openstack_dashboard.dashboards.admin import dashboard
|
||||||
|
|
||||||
|
|
||||||
|
class DataSources(horizon.Panel):
|
||||||
|
name = _("Data Sources")
|
||||||
|
slug = "datasources"
|
||||||
|
permissions = ('openstack.roles.admin',)
|
||||||
|
|
||||||
|
|
||||||
|
dashboard.Admin.register(DataSources)
|
89
congress_dashboard/datasources/tables.py
Normal file
89
congress_dashboard/datasources/tables.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.template.defaultfilters import unordered_list
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_url(obj):
|
||||||
|
return reverse('horizon:admin:datasources:datasource_table_detail',
|
||||||
|
args=(obj['datasource_id'], obj['table_id']))
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourcesTablesTable(tables.DataTable):
|
||||||
|
name = tables.Column("name", verbose_name=_("Table Name"),
|
||||||
|
link=get_resource_url)
|
||||||
|
datasource_name = tables.Column("datasource_name",
|
||||||
|
verbose_name=_("Service"))
|
||||||
|
datasource_driver = tables.Column("datasource_driver",
|
||||||
|
verbose_name=_("Driver"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "datasources_tables"
|
||||||
|
verbose_name = _("Service Data")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_policy_link(datum):
|
||||||
|
return reverse('horizon:admin:policies:detail',
|
||||||
|
args=(datum['policy_name'],))
|
||||||
|
|
||||||
|
|
||||||
|
def get_policy_table_link(datum):
|
||||||
|
return reverse('horizon:admin:datasources:policy_table_detail',
|
||||||
|
args=(datum['policy_name'], datum['name']))
|
||||||
|
|
||||||
|
|
||||||
|
class PoliciesTablesTable(tables.DataTable):
|
||||||
|
name = tables.Column("name", verbose_name=_("Table Name"),
|
||||||
|
link=get_policy_table_link)
|
||||||
|
policy_name = tables.Column("policy_name", verbose_name=_("Policy"),
|
||||||
|
link=get_policy_link)
|
||||||
|
policy_owner_id = tables.Column("policy_owner_id",
|
||||||
|
verbose_name=_("Owner ID"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "policies_tables"
|
||||||
|
verbose_name = _("Policy Data")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceRowsTable(tables.DataTable):
|
||||||
|
class Meta(object):
|
||||||
|
name = "datasource_rows"
|
||||||
|
verbose_name = _("Rows")
|
||||||
|
hidden_title = False
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceStatusesTable(tables.DataTable):
|
||||||
|
datasource_name = tables.Column("service",
|
||||||
|
verbose_name=_("Service"))
|
||||||
|
last_updated = tables.Column("last_updated",
|
||||||
|
verbose_name=_("Last Updated"))
|
||||||
|
subscriptions = tables.Column("subscriptions",
|
||||||
|
verbose_name=_("Subscriptions"),
|
||||||
|
wrap_list=True, filters=(unordered_list,))
|
||||||
|
last_error = tables.Column("last_error", verbose_name=_("Last Error"))
|
||||||
|
subscribers = tables.Column("subscribers", verbose_name=_("Subscribers"),
|
||||||
|
wrap_list=True, filters=(unordered_list,))
|
||||||
|
initialized = tables.Column("initialized", verbose_name=_("Initialized"))
|
||||||
|
number_of_updates = tables.Column("number_of_updates",
|
||||||
|
verbose_name=_("Number of Updates"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "service_status"
|
||||||
|
verbose_name = _("Service Status")
|
||||||
|
hidden_title = False
|
@ -0,0 +1,22 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<h3>{% trans "Service Status" %}</h3>
|
||||||
|
|
||||||
|
<div class="detail">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt> {% trans "Service Name" %}</dt>
|
||||||
|
<dd>{{ datasource_name }}</dd>
|
||||||
|
<dt>{% trans "Status" %}</dt>
|
||||||
|
<dd>{{ status }}</dd>
|
||||||
|
<dt>{% trans "Subscriptions" %}</dt>
|
||||||
|
<dd>{{ subscriptions }}</dd>
|
||||||
|
<dt>{% trans "Subscribers" %}</dt>
|
||||||
|
<dd>{{ subscribers }}</dd>
|
||||||
|
<dt>{% trans "Number of Updates" %}</dt>
|
||||||
|
<dd>{{ number_of_updates }}</dd>
|
||||||
|
<dt>{% trans "Last Updated" %}</dt>
|
||||||
|
<dd>{{ last_updated }}</dd>
|
||||||
|
<dt>{% trans "Last Error" %}</dt>
|
||||||
|
<dd>{{ last_error }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
@ -0,0 +1,14 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<h3>{% trans "Table Overview" %}</h3>
|
||||||
|
|
||||||
|
<div class="detail">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>{{ datasource_type }} {% trans "Data Source" %}</dt>
|
||||||
|
<dd>{{ datasource_name }}</dd>
|
||||||
|
<dt>{% trans "Name" %}</dt>
|
||||||
|
<dd>{{ table_name }}</dd>
|
||||||
|
<dt>{% trans "ID" %}</dt>
|
||||||
|
<dd>{{ id|default:table_name }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Datasource Overview" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Datasource Details: ")|add:datasource_name %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/datasources/_datasource_overview.html" %}
|
||||||
|
<div id="datasources_tables">
|
||||||
|
{{ datasources_tables_table.render }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Data Source Table Details" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Data Source Table Details: ")|add:table_name %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/datasources/_detail_overview.html" %}
|
||||||
|
<div id="datasource_table_rows">
|
||||||
|
{{ datasource_rows_table.render }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Data Sources" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Data Sources") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div id="policies_tables">
|
||||||
|
{{ policies_tables_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="service_status_tables">
|
||||||
|
{{ service_status_table.render }}
|
||||||
|
</div>
|
||||||
|
<div id="service_tables">
|
||||||
|
{{ datasources_tables_table.render }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
34
congress_dashboard/datasources/urls.py
Normal file
34
congress_dashboard/datasources/urls.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from django.conf.urls import patterns
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from congress_dashboard.datasources import views
|
||||||
|
|
||||||
|
|
||||||
|
SERVICES = (
|
||||||
|
r'^services/(?P<datasource_id>[^/]+)/(?P<service_table_name>[^/]+)/%s$')
|
||||||
|
POLICIES = (
|
||||||
|
r'^policies/(?P<datasource_id>[^/]+)/(?P<policy_table_name>[^/]+)/%s$')
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(SERVICES % 'detail', views.DetailView.as_view(),
|
||||||
|
name='datasource_table_detail'),
|
||||||
|
url(POLICIES % 'detail', views.DetailView.as_view(),
|
||||||
|
name='policy_table_detail'),
|
||||||
|
)
|
187
congress_dashboard/datasources/utils.py
Normal file
187
congress_dashboard/datasources/utils.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_policy_tables(request):
|
||||||
|
# Return all policy tables.
|
||||||
|
all_tables = []
|
||||||
|
try:
|
||||||
|
# Get all the policies.
|
||||||
|
policies = congress.policies_list(request)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get list of policies: %s', str(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
for policy in policies:
|
||||||
|
# Get all the tables in this policy.
|
||||||
|
policy_name = policy['name']
|
||||||
|
policy_tables = congress.policy_tables_list(request,
|
||||||
|
policy_name)
|
||||||
|
# Get the names of the tables.
|
||||||
|
datasource_tables = []
|
||||||
|
for table in policy_tables:
|
||||||
|
table.set_id_as_name_if_empty()
|
||||||
|
table_name = table['name']
|
||||||
|
# Exclude service-derived tables.
|
||||||
|
if congress.TABLE_SEPARATOR not in table_name:
|
||||||
|
datasource_tables.append(table['name'])
|
||||||
|
|
||||||
|
all_tables.append({'datasource': policy_name,
|
||||||
|
'tables': datasource_tables})
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get tables for policy "%s": %s',
|
||||||
|
policy_name, str(e))
|
||||||
|
return all_tables
|
||||||
|
|
||||||
|
|
||||||
|
def _get_service_tables(request):
|
||||||
|
# Return all service tables.
|
||||||
|
all_tables = []
|
||||||
|
try:
|
||||||
|
# Get all the services.
|
||||||
|
services = congress.datasources_list(request)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get list of data sources: %s', str(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
for service in services:
|
||||||
|
# Get all the tables in this service.
|
||||||
|
service_id = service['id']
|
||||||
|
service_tables = congress.datasource_tables_list(request,
|
||||||
|
service_id)
|
||||||
|
# Get the names of the tables.
|
||||||
|
datasource_tables = []
|
||||||
|
for table in service_tables:
|
||||||
|
table.set_id_as_name_if_empty()
|
||||||
|
datasource_tables.append(table['name'])
|
||||||
|
|
||||||
|
all_tables.append({'datasource': service['name'],
|
||||||
|
'tables': datasource_tables})
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get tables for data source "%s": %s',
|
||||||
|
service_id, str(e))
|
||||||
|
return all_tables
|
||||||
|
|
||||||
|
|
||||||
|
def get_datasource_tables(request):
|
||||||
|
"""Get names of all data source tables.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'datasource': 'classification',
|
||||||
|
'tables': ['error']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'datasource': 'neutronv2'
|
||||||
|
'tables': ['networks', 'ports', ...]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
tables = _get_policy_tables(request)
|
||||||
|
tables.extend(_get_service_tables(request))
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def get_datasource_columns(request):
|
||||||
|
"""Get of names of columns from all data sources.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'datasource': 'classification',
|
||||||
|
'tables': [
|
||||||
|
{
|
||||||
|
'table': 'error',
|
||||||
|
'columns': ['name']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'datasource': 'neutronv2',
|
||||||
|
'tables': [
|
||||||
|
{
|
||||||
|
'table': 'networks',
|
||||||
|
'columns': ['id', 'tenant_id', ...],
|
||||||
|
},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
all_columns = []
|
||||||
|
|
||||||
|
# Get all the policy tables.
|
||||||
|
policy_tables = _get_policy_tables(request)
|
||||||
|
try:
|
||||||
|
for policy in policy_tables:
|
||||||
|
# Get all the columns in this policy. Unlike for the services,
|
||||||
|
# there's currently no congress client API to get the schema for
|
||||||
|
# all tables in a policy in a single call.
|
||||||
|
policy_name = policy['datasource']
|
||||||
|
tables = policy['tables']
|
||||||
|
|
||||||
|
datasource_tables = []
|
||||||
|
for table_name in tables:
|
||||||
|
# Get all the columns in this policy table.
|
||||||
|
schema = congress.policy_table_schema_get(request, policy_name,
|
||||||
|
table_name)
|
||||||
|
columns = [c['name'] for c in schema['columns']]
|
||||||
|
datasource_tables.append({'table': table_name,
|
||||||
|
'columns': columns})
|
||||||
|
|
||||||
|
all_columns.append({'datasource': policy_name,
|
||||||
|
'tables': datasource_tables})
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get schema for policy "%s" table "%s": %s',
|
||||||
|
policy_name, table_name, str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get all the services.
|
||||||
|
services = congress.datasources_list(request)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get list of data sources: %s', str(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
for service in services:
|
||||||
|
# Get the schema for this service.
|
||||||
|
service_id = service['id']
|
||||||
|
service_name = service['name']
|
||||||
|
schema = congress.datasource_schema_get(request, service_id)
|
||||||
|
|
||||||
|
datasource_tables = []
|
||||||
|
for table in schema['tables']:
|
||||||
|
# Get the columns for this table.
|
||||||
|
columns = [c['name'] for c in table['columns']]
|
||||||
|
datasource_table = {'table': table['table_id'],
|
||||||
|
'columns': columns}
|
||||||
|
datasource_tables.append(datasource_table)
|
||||||
|
|
||||||
|
all_columns.append({'datasource': service_name,
|
||||||
|
'tables': datasource_tables})
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error('Unable to get schema for data source "%s": %s',
|
||||||
|
service_id, str(e))
|
||||||
|
|
||||||
|
return all_columns
|
272
congress_dashboard/datasources/views.py
Normal file
272
congress_dashboard/datasources/views.py
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# Copyright 2014 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 copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import messages
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
from congress_dashboard.datasources import tables as datasources_tables
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tables.MultiTableView):
|
||||||
|
"""List service and policy defined data."""
|
||||||
|
table_classes = (datasources_tables.DataSourcesTablesTable,
|
||||||
|
datasources_tables.PoliciesTablesTable,
|
||||||
|
datasources_tables.DataSourceStatusesTable,)
|
||||||
|
template_name = 'admin/datasources/index.html'
|
||||||
|
|
||||||
|
def get_datasources_tables_data(self):
|
||||||
|
try:
|
||||||
|
datasources = congress.datasources_list(self.request)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Unable to get services list: %s') % str(e)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
ds_temp = []
|
||||||
|
for ds in datasources:
|
||||||
|
ds_id = ds['id']
|
||||||
|
try:
|
||||||
|
ds_tables = congress.datasource_tables_list(self.request,
|
||||||
|
ds_id)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'ds_id': ds_id, 'error': str(e)}
|
||||||
|
msg = _('Unable to get tables list for service "%(ds_id)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
for table in ds_tables:
|
||||||
|
table.set_value('datasource_id', ds_id)
|
||||||
|
table.set_value('datasource_name', ds['name'])
|
||||||
|
table.set_value('datasource_driver', ds['driver'])
|
||||||
|
table.set_id_as_name_if_empty()
|
||||||
|
# Object ids within a Horizon table must be unique. Otherwise,
|
||||||
|
# Horizon will cache the column values for the object by id and
|
||||||
|
# use the same column values for all rows with the same id.
|
||||||
|
table.set_value('table_id', table['id'])
|
||||||
|
table.set_value('id', '%s-%s' % (ds_id, table['table_id']))
|
||||||
|
ds_temp.append(table)
|
||||||
|
|
||||||
|
logger.debug("ds_temp %s" % ds_temp)
|
||||||
|
return ds_temp
|
||||||
|
|
||||||
|
def get_service_status_data(self):
|
||||||
|
ds = []
|
||||||
|
try:
|
||||||
|
ds = congress.datasource_statuses_list(self.request)
|
||||||
|
logger.debug("ds status : %s " % ds)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Unable to get datasource status list: %s') % str(e)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return ds
|
||||||
|
|
||||||
|
def get_policies_tables_data(self):
|
||||||
|
try:
|
||||||
|
policies = congress.policies_list(self.request)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Unable to get policies list: %s') % str(e)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
policies_tables = []
|
||||||
|
for policy in policies:
|
||||||
|
policy_name = policy['name']
|
||||||
|
try:
|
||||||
|
policy_tables = congress.policy_tables_list(self.request,
|
||||||
|
policy_name)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||||
|
msg = _('Unable to get tables list for policy '
|
||||||
|
'"%(policy_name)s": %(error)s') % msg_args
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
for pt in policy_tables:
|
||||||
|
pt.set_id_as_name_if_empty()
|
||||||
|
pt.set_policy_details(policy)
|
||||||
|
# Object ids within a Horizon table must be unique. Otherwise,
|
||||||
|
# Horizon will cache the column values for the object by id and
|
||||||
|
# use the same column values for all rows with the same id.
|
||||||
|
pt.set_value('table_id', pt['id'])
|
||||||
|
pt.set_value('id', '%s-%s' % (policy_name, pt['table_id']))
|
||||||
|
policies_tables.extend(policy_tables)
|
||||||
|
|
||||||
|
return policies_tables
|
||||||
|
|
||||||
|
|
||||||
|
class DetailView(tables.DataTableView):
|
||||||
|
"""List details about and rows from a data source (service or policy)."""
|
||||||
|
table_class = datasources_tables.DataSourceRowsTable
|
||||||
|
template_name = 'admin/datasources/detail.html'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
datasource_id = self.kwargs['datasource_id']
|
||||||
|
table_name = self.kwargs.get('policy_table_name')
|
||||||
|
is_service = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if table_name:
|
||||||
|
# Policy data table.
|
||||||
|
rows = congress.policy_rows_list(self.request, datasource_id,
|
||||||
|
table_name)
|
||||||
|
if congress.TABLE_SEPARATOR in table_name:
|
||||||
|
table_name_parts = table_name.split(
|
||||||
|
congress.TABLE_SEPARATOR)
|
||||||
|
maybe_datasource_name = table_name_parts[0]
|
||||||
|
datasources = congress.datasources_list(self.request)
|
||||||
|
for datasource in datasources:
|
||||||
|
if datasource['name'] == maybe_datasource_name:
|
||||||
|
# Serivce-derived policy data table.
|
||||||
|
is_service = True
|
||||||
|
datasource_id = datasource['id']
|
||||||
|
table_name = table_name_parts[1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Service data table.
|
||||||
|
is_service = True
|
||||||
|
datasource = congress.datasource_get_by_name(
|
||||||
|
self.request, datasource_id)
|
||||||
|
table_name = self.kwargs['service_table_name']
|
||||||
|
rows = congress.datasource_rows_list(
|
||||||
|
self.request, datasource_id, table_name)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {
|
||||||
|
'table_name': table_name,
|
||||||
|
'ds_id': datasource_id,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
msg = _('Unable to get rows in table "%(table_name)s", data '
|
||||||
|
'source "%(ds_id)s": %(error)s') % msg_args
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse('horizon:admin:datasources:index')
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
# Normally, in Horizon, the columns for a table are defined as
|
||||||
|
# attributes of the Table class. When the class is instantiated,
|
||||||
|
# the columns are processed during the metaclass initialization. To
|
||||||
|
# add columns dynamically, re-create the class from the metaclass
|
||||||
|
# with the added columns, re-create the Table from the new class,
|
||||||
|
# then reassign the Table stored in this View.
|
||||||
|
column_names = []
|
||||||
|
table_class_attrs = copy.deepcopy(dict(self.table_class.__dict__))
|
||||||
|
# Get schema from the server.
|
||||||
|
try:
|
||||||
|
if is_service:
|
||||||
|
schema = congress.datasource_table_schema_get(
|
||||||
|
self.request, datasource_id, table_name)
|
||||||
|
else:
|
||||||
|
schema = congress.policy_table_schema_get(
|
||||||
|
self.request, datasource_id, table_name)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {
|
||||||
|
'table_name': table_name,
|
||||||
|
'ds_id': datasource_id,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
msg = _('Unable to get schema for table "%(table_name)s", '
|
||||||
|
'data source "%(ds_id)s": %(error)s') % msg_args
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse('horizon:admin:datasources:index')
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
columns = schema['columns']
|
||||||
|
row_len = 0
|
||||||
|
if len(rows):
|
||||||
|
row_len = len(rows[0].get('data', []))
|
||||||
|
|
||||||
|
if not row_len or row_len == len(columns):
|
||||||
|
for col in columns:
|
||||||
|
col_name = col['name']
|
||||||
|
# Attribute name for column in the class must be a valid
|
||||||
|
# identifier. Slugify it.
|
||||||
|
col_slug = slugify(col_name)
|
||||||
|
column_names.append(col_slug)
|
||||||
|
table_class_attrs[col_slug] = tables.Column(
|
||||||
|
col_slug, verbose_name=col_name)
|
||||||
|
else:
|
||||||
|
# There could be another table with the same name and different
|
||||||
|
# arity. Divide the rows into unnamed columns. Number them for
|
||||||
|
# internal reference.
|
||||||
|
for i in xrange(0, row_len):
|
||||||
|
col_name = str(i)
|
||||||
|
column_names.append(col_name)
|
||||||
|
table_class_attrs[col_name] = tables.Column(
|
||||||
|
col_name, verbose_name='')
|
||||||
|
|
||||||
|
# Class and object re-creation, using a new class name, the same base
|
||||||
|
# classes, and the new class attributes, which now includes columns.
|
||||||
|
columnized_table_class_name = '%s%sRows' % (
|
||||||
|
slugify(datasource_id).title(), slugify(table_name).title())
|
||||||
|
columnized_table_class = tables.base.DataTableMetaclass(
|
||||||
|
str(columnized_table_class_name), self.table_class.__bases__,
|
||||||
|
table_class_attrs)
|
||||||
|
|
||||||
|
self.table_class = columnized_table_class
|
||||||
|
columnized_table = columnized_table_class(self.request, **self.kwargs)
|
||||||
|
self._tables[columnized_table_class._meta.name] = columnized_table
|
||||||
|
|
||||||
|
# Map columns names to row values.
|
||||||
|
num_cols = len(column_names)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
row_data = row['data']
|
||||||
|
row.delete_by_key('data')
|
||||||
|
for i in xrange(0, num_cols):
|
||||||
|
row.set_value(column_names[i], row_data[i])
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {
|
||||||
|
'table_name': table_name,
|
||||||
|
'ds_id': datasource_id,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
msg = _('Unable to get data for table "%(table_name)s", data '
|
||||||
|
'source "%(ds_id)s": %(error)s') % msg_args
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse('horizon:admin:datasources:index')
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(DetailView, self).get_context_data(**kwargs)
|
||||||
|
if 'policy_table_name' in kwargs:
|
||||||
|
table_name = kwargs.get('policy_table_name')
|
||||||
|
context['datasource_type'] = _('Policy')
|
||||||
|
datasource_name = kwargs['datasource_id']
|
||||||
|
else:
|
||||||
|
table_name = kwargs['service_table_name']
|
||||||
|
context['datasource_type'] = _('Service')
|
||||||
|
try:
|
||||||
|
datasource_id = kwargs['datasource_id']
|
||||||
|
datasource = congress.datasource_get(self.request,
|
||||||
|
datasource_id)
|
||||||
|
datasource_name = datasource['name']
|
||||||
|
except Exception as e:
|
||||||
|
datasource_name = datasource_id
|
||||||
|
logger.info('Failed to get data source "%s": %s' %
|
||||||
|
(datasource_id, str(e)))
|
||||||
|
context['datasource_name'] = datasource_name
|
||||||
|
context['table_name'] = table_name
|
||||||
|
return context
|
3
congress_dashboard/enabled/_50_policy.py
Normal file
3
congress_dashboard/enabled/_50_policy.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
PANEL_GROUP = 'policy'
|
||||||
|
PANEL_GROUP_NAME = 'Policy'
|
||||||
|
PANEL_GROUP_DASHBOARD = 'admin'
|
9
congress_dashboard/enabled/_60_policies.py
Normal file
9
congress_dashboard/enabled/_60_policies.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
PANEL = 'policies'
|
||||||
|
PANEL_DASHBOARD = 'admin'
|
||||||
|
PANEL_GROUP = 'policy'
|
||||||
|
ADD_PANEL = 'congress_dashboard.policies.panel.Policies'
|
||||||
|
ADD_INSTALLED_APPS = [
|
||||||
|
'congress_dashboard',
|
||||||
|
]
|
||||||
|
AUTO_DISCOVER_STATIC_FILES = True
|
||||||
|
ADD_SCSS_FILES = ['congress_dashboard/static/admin/css/policies.css']
|
5
congress_dashboard/enabled/_70_datasources.py
Normal file
5
congress_dashboard/enabled/_70_datasources.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
PANEL = 'datasources'
|
||||||
|
PANEL_DASHBOARD = 'admin'
|
||||||
|
PANEL_GROUP = 'policy'
|
||||||
|
ADD_PANEL = 'congress_dashboard.datasources.panel.DataSources'
|
||||||
|
AUTO_DISCOVER_STATIC_FILES = True
|
0
congress_dashboard/policies/__init__.py
Normal file
0
congress_dashboard/policies/__init__.py
Normal file
69
congress_dashboard/policies/forms.py
Normal file
69
congress_dashboard/policies/forms.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import messages
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
POLICY_KIND_CHOICES = (
|
||||||
|
('nonrecursive', _('Nonrecursive')),
|
||||||
|
('action', _('Action')),
|
||||||
|
('database', _('Database')),
|
||||||
|
('materialized', _('Materialized')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePolicy(forms.SelfHandlingForm):
|
||||||
|
name = forms.CharField(max_length=255, label=_("Policy Name"))
|
||||||
|
kind = forms.ChoiceField(choices=POLICY_KIND_CHOICES, label=_("Kind"),
|
||||||
|
initial='nonrecursive')
|
||||||
|
description = forms.CharField(label=_("Description"), required=False,
|
||||||
|
widget=forms.Textarea(attrs={'rows': 4}))
|
||||||
|
failure_url = 'horizon:admin:policies:index'
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
policy_name = data['name']
|
||||||
|
policy_description = data.get('description')
|
||||||
|
policy_kind = data.pop('kind')
|
||||||
|
LOG.info('User %s creating policy "%s" of type %s in tenant %s',
|
||||||
|
request.user.username, policy_name, policy_kind,
|
||||||
|
request.user.tenant_name)
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
'name': policy_name,
|
||||||
|
'description': policy_description,
|
||||||
|
'kind': policy_kind,
|
||||||
|
}
|
||||||
|
policy = congress.policy_create(request, params)
|
||||||
|
msg = _('Created policy "%s"') % policy_name
|
||||||
|
LOG.info(msg)
|
||||||
|
messages.success(request, msg)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||||
|
msg = _('Failed to create policy "%(policy_name)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse(self.failure_url)
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
return policy
|
26
congress_dashboard/policies/panel.py
Normal file
26
congress_dashboard/policies/panel.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
import horizon
|
||||||
|
from openstack_dashboard.dashboards.admin import dashboard
|
||||||
|
|
||||||
|
|
||||||
|
class Policies(horizon.Panel):
|
||||||
|
name = _("Policies")
|
||||||
|
slug = "policies"
|
||||||
|
permissions = ('openstack.roles.admin',)
|
||||||
|
|
||||||
|
|
||||||
|
dashboard.Admin.register(Policies)
|
0
congress_dashboard/policies/rules/__init__.py
Normal file
0
congress_dashboard/policies/rules/__init__.py
Normal file
112
congress_dashboard/policies/rules/tables.py
Normal file
112
congress_dashboard/policies/rules/tables.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.template.defaultfilters import linebreaksbr
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ungettext_lazy
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import messages
|
||||||
|
from horizon import tables
|
||||||
|
from openstack_dashboard import policy
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateRule(tables.LinkAction):
|
||||||
|
name = 'create_rule'
|
||||||
|
verbose_name = _('Create Rule')
|
||||||
|
url = 'horizon:admin:policies:create_rule'
|
||||||
|
classes = ('ajax-modal',)
|
||||||
|
icon = 'plus'
|
||||||
|
policy_rules = (('policy', 'create_rule'),)
|
||||||
|
|
||||||
|
def get_link_url(self, datum=None):
|
||||||
|
policy_name = self.table.kwargs['policy_name']
|
||||||
|
return reverse(self.url, args=(policy_name,))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRule(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||||
|
@staticmethod
|
||||||
|
def action_present(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u'Delete Rule',
|
||||||
|
u'Delete Rules',
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_past(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u'Deleted rule',
|
||||||
|
u'Deleted rules',
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_url = 'horizon:admin:policies:detail'
|
||||||
|
|
||||||
|
def delete(self, request, obj_id):
|
||||||
|
policy_name = self.table.kwargs['policy_name']
|
||||||
|
LOG.info('User %s deleting policy "%s" rule "%s" in tenant %s',
|
||||||
|
request.user.username, policy_name, obj_id,
|
||||||
|
request.user.tenant_name)
|
||||||
|
try:
|
||||||
|
congress.policy_rule_delete(request, policy_name, obj_id)
|
||||||
|
LOG.info('Deleted policy rule "%s"', obj_id)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'rule_id': obj_id, 'error': str(e)}
|
||||||
|
msg = _('Failed to delete policy rule "%(rule_id)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(request, msg)
|
||||||
|
redirect = reverse(self.redirect_url, args=(policy_name,))
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_rule(rule):
|
||||||
|
"""Make rule's text more human readable."""
|
||||||
|
head_body = rule.split(congress.RULE_SEPARATOR)
|
||||||
|
if len(head_body) < 2:
|
||||||
|
return rule
|
||||||
|
head = head_body[0]
|
||||||
|
body = head_body[1]
|
||||||
|
|
||||||
|
# Add newline after each literal in the body.
|
||||||
|
body_literals = body.split(congress.LITERALS_SEPARATOR)
|
||||||
|
literals_break = congress.LITERALS_SEPARATOR + '\n'
|
||||||
|
new_body = literals_break.join(body_literals)
|
||||||
|
|
||||||
|
# Add newline after the head.
|
||||||
|
rules_break = congress.RULE_SEPARATOR + '\n'
|
||||||
|
return rules_break.join([head, new_body])
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyRulesTable(tables.DataTable):
|
||||||
|
id = tables.Column("id", verbose_name=_("Rule ID"))
|
||||||
|
name = tables.Column("name", verbose_name=_("Name"))
|
||||||
|
comment = tables.Column("comment", verbose_name=_("Comment"))
|
||||||
|
rule = tables.Column("rule", verbose_name=_("Rule"),
|
||||||
|
filters=(_format_rule, linebreaksbr,))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "policy_rules"
|
||||||
|
verbose_name = _("Rules")
|
||||||
|
table_actions = (CreateRule, DeleteRule,)
|
||||||
|
row_actions = (DeleteRule,)
|
||||||
|
hidden_title = False
|
31
congress_dashboard/policies/rules/views.py
Normal file
31
congress_dashboard/policies/rules/views.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from horizon import workflows
|
||||||
|
|
||||||
|
from congress_dashboard.policies.rules import workflows as rule_workflows
|
||||||
|
|
||||||
|
|
||||||
|
class CreateView(workflows.WorkflowView):
|
||||||
|
workflow_class = rule_workflows.CreateRule
|
||||||
|
ajax_template_name = 'admin/policies/rules/create.html'
|
||||||
|
success_url = 'horizon:admin:policies:detail'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(self.success_url,
|
||||||
|
args=(self.kwargs['policy_name'],))
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
return {'policy_name': self.kwargs['policy_name']}
|
441
congress_dashboard/policies/rules/workflows.py
Normal file
441
congress_dashboard/policies/rules/workflows.py
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
# 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, str(e))
|
||||||
|
return str(e)
|
||||||
|
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, str(e))
|
||||||
|
self.context['error'] = str(e)
|
||||||
|
return False
|
||||||
|
return True
|
94
congress_dashboard/policies/tables.py
Normal file
94
congress_dashboard/policies/tables.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Copyright 2014 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
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.translation import ungettext_lazy
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import messages
|
||||||
|
from horizon import tables
|
||||||
|
from openstack_dashboard import policy
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_policy_link(datum):
|
||||||
|
return reverse('horizon:admin:policies:detail', args=(datum['name'],))
|
||||||
|
|
||||||
|
|
||||||
|
class CreatePolicy(tables.LinkAction):
|
||||||
|
name = 'create_policy'
|
||||||
|
verbose_name = _('Create Policy')
|
||||||
|
url = 'horizon:admin:policies:create'
|
||||||
|
classes = ('ajax-modal',)
|
||||||
|
icon = 'plus'
|
||||||
|
|
||||||
|
|
||||||
|
class DeletePolicy(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||||
|
@staticmethod
|
||||||
|
def action_present(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u'Delete Policy',
|
||||||
|
u'Delete Policies',
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def action_past(count):
|
||||||
|
return ungettext_lazy(
|
||||||
|
u'Deleted policy',
|
||||||
|
u'Deleted policies',
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_url = 'horizon:admin:policies:index'
|
||||||
|
|
||||||
|
def delete(self, request, obj_id):
|
||||||
|
LOG.info('User %s deleting policy "%s" in tenant %s',
|
||||||
|
request.user.username, obj_id, request.user.tenant_name)
|
||||||
|
try:
|
||||||
|
congress.policy_delete(request, obj_id)
|
||||||
|
LOG.info('Deleted policy "%s"', obj_id)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'policy_id': obj_id, 'error': str(e)}
|
||||||
|
msg = _('Failed to delete policy "%(policy_id)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(request, msg)
|
||||||
|
redirect = reverse(self.redirect_url)
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
def allowed(self, request, policy=None):
|
||||||
|
# Only user policies can be deleted.
|
||||||
|
if policy:
|
||||||
|
return policy['owner_id'] == 'user'
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class PoliciesTable(tables.DataTable):
|
||||||
|
name = tables.Column("name", verbose_name=_("Name"), link=get_policy_link)
|
||||||
|
description = tables.Column("description", verbose_name=_("Description"))
|
||||||
|
kind = tables.Column("kind", verbose_name=_("Kind"))
|
||||||
|
owner_id = tables.Column("owner_id", verbose_name=_("Owner ID"))
|
||||||
|
|
||||||
|
class Meta(object):
|
||||||
|
name = "policies"
|
||||||
|
verbose_name = _("Policies")
|
||||||
|
table_actions = (CreatePolicy, DeletePolicy)
|
||||||
|
row_actions = (DeletePolicy,)
|
22
congress_dashboard/policies/templates/policies/_create.html
Normal file
22
congress_dashboard/policies/templates/policies/_create.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load url from future %}
|
||||||
|
|
||||||
|
{% block form_id %}create_policy_form{% endblock %}
|
||||||
|
{% block form_action %}{% url 'horizon:admin:policies:create' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal_id %}create_policy_modal{% endblock %}
|
||||||
|
{% block modal-header %}{% trans "Create Policy" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
<div class="left">
|
||||||
|
<fieldset>
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Policy" %}" />
|
||||||
|
<a href="{% url 'horizon:admin:policies:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,18 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<h3>{% trans "Policy Overview" %}</h3>
|
||||||
|
|
||||||
|
<div class="detail">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>{% trans "Name" %}</dt>
|
||||||
|
<dd>{{ policy.name|default:policy.id }}</dd>
|
||||||
|
<dt>{% trans "ID" %}</dt>
|
||||||
|
<dd>{{ policy.id }}</dd>
|
||||||
|
<dt>{% trans "Description" %}</dt>
|
||||||
|
<dd>{{ policy.description }}</dd>
|
||||||
|
<dt>{% trans "Kind" %}</dt>
|
||||||
|
<dd>{{ policy.kind }}</dd>
|
||||||
|
<dt>{% trans "Owner ID" %}</dt>
|
||||||
|
<dd>{{ policy.owner_id|default:"-" }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
11
congress_dashboard/policies/templates/policies/create.html
Normal file
11
congress_dashboard/policies/templates/policies/create.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Create Policy" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Create Policy") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/policies/_create.html" %}
|
||||||
|
{% endblock %}
|
17
congress_dashboard/policies/templates/policies/detail.html
Normal file
17
congress_dashboard/policies/templates/policies/detail.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Policy Details" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Policy Details: ")|add:policy.name %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include "admin/policies/_detail_overview.html" %}
|
||||||
|
<hr>
|
||||||
|
<div id="policy_rules">
|
||||||
|
{{ policy_rules_table.render }}
|
||||||
|
</div>
|
||||||
|
<span id="ds_tables" class="hidden">{{ tables }}</span>
|
||||||
|
<span id="ds_columns" class="hidden">{{ columns }}</span>
|
||||||
|
{% endblock %}
|
13
congress_dashboard/policies/templates/policies/index.html
Normal file
13
congress_dashboard/policies/templates/policies/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Policies" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Policies") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div id="policies">
|
||||||
|
{{ policies_table.render }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,174 @@
|
|||||||
|
<noscript><h3>{{ step }}</h3></noscript>
|
||||||
|
{{ step.get_help_text }}
|
||||||
|
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<table id="mappings_table" class="table table-condensed" data-count="{{ mappings_count }}">
|
||||||
|
<tr>
|
||||||
|
<th colspan="5"> <input class="hidden" id="mappings" name="mappings" type="text" value="{{ form.mappings.value }}" /></th>
|
||||||
|
</tr>
|
||||||
|
{% include 'admin/policies/rules/_mapping_row.html' with form=form count=0 column='' value='' %}
|
||||||
|
{% for column, value in mappings %}{% include 'admin/policies/rules/_mapping_row.html' with form=form count=forloop.counter column=column value=value %}{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="borderless input-errors">
|
||||||
|
{% for error in form.mappings.errors %}
|
||||||
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table id="joins_table" class="table table-condensed">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">Only including rows where:</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% for left, op, right in joins %}<tr id="join_{{ forloop.counter0 }}">
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="join_left_{{ forloop.counter0 }}" class="form-control ac-columns" name="join_left_{{ forloop.counter0 }}" type="text" value="{{ left }}" placeholder="e.g. neutronv2:ports device_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||||
|
<div class="form-control-feedback">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless operator-cell">
|
||||||
|
<select id="join_op_{{ forloop.counter0 }}" class="form-control join-op" name="join_op_{{ forloop.counter0 }}">
|
||||||
|
<option value=""{% if op == '' %} selected="selected"{% endif %}>has same value as</option>
|
||||||
|
<option value="="{% if op == '=' %} selected="selected"{% endif %}>equals</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="join_right_{{ forloop.counter0 }}" class="form-control ac-columns join-right" name="join_right_{{ forloop.counter0 }}" type="text" value="{{ right }}" placeholder="e.g. {% if op %}8, active{% else %}nova:servers id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}{% endif %}" data-column-example="e.g. nova:servers id" data-static-example="e.g. 8, active" data-pattern="{{ column_pattern }}" data-pattern-error="{{ column_pattern_error }}" />
|
||||||
|
<div class="form-control-feedback{% if op != '' %} hidden{% endif %}">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless">
|
||||||
|
<a class="{% if forloop.first %}hidden {% endif %}remove-join-button btn btn-xs btn-primary">–</a>
|
||||||
|
</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
</table>
|
||||||
|
<a id="add_join_button" class="btn btn-xs btn-primary" data-count="{{ joins_count }}">&</a>
|
||||||
|
<table id="negations_table" class="table table-condensed">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">Except for rows where:</label>
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="To exclude a row in the output policy table, select the columns on the left whose values should not exist together in the columns selected on the right."><span class="fa fa-question-circle"></span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% for value, column in negations %}<tr id="negation_{{ forloop.counter0 }}">
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="negation_value_{{ forloop.counter0 }}" class="form-control ac-columns" name="negation_value_{{ forloop.counter0 }}" value="{{ value }}" type="text" placeholder="e.g. nova:servers tenant_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||||
|
<div class="form-control-feedback">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless operator-cell">value is in</td>
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="negation_column_{{ forloop.counter0 }}" class="form-control ac-columns" name="negation_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. neutronv2:ports tenant_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||||
|
<div class="form-control-feedback">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless">
|
||||||
|
<a class="{% if forloop.first %}hidden {% endif %}remove-negation-button btn btn-xs btn-primary">–</a>
|
||||||
|
</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
</table>
|
||||||
|
<a id="add_negation_button" class="btn btn-xs btn-primary" data-count="{{ negations_count }}">&</a>
|
||||||
|
{% comment %}
|
||||||
|
<table id="aliases_table" class="table table-condensed">
|
||||||
|
<tr>
|
||||||
|
<th colspan="5">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label"> </label>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% for column, name in aliases %}<tr id="alias_{{ forloop.counter0 }}">
|
||||||
|
<td class="borderless label-cell">
|
||||||
|
{% if forloop.first %}<div class="form-group">
|
||||||
|
<label class="control-label">Table Aliases:</label>
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="Give an alternate name for a table if more than one instance of it is needed above."><span class="fa fa-question-circle"></span></span>
|
||||||
|
</div>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="alias_column_{{ forloop.counter0 }}" class="form-control ac-tables" name="alias_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. nova:servers" pattern="{{ table_pattern }}" title="{{ table_pattern_error }}" />
|
||||||
|
<div class="form-control-feedback">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless alias-text">as</td>
|
||||||
|
<td class="borderless input-cell">
|
||||||
|
<input id="alias_name_{{ forloop.counter0 }}" class="form-control alias-name-input" name="alias_name_{{ forloop.counter0 }}" type="text" value="{{ name }}" placeholder="e.g. nova:servers2" />
|
||||||
|
</td>
|
||||||
|
<td class="borderless">
|
||||||
|
<a class="{% if forloop.first %}hidden {% endif %}remove-alias-button btn btn-xs btn-primary">–</a>
|
||||||
|
</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
</table>
|
||||||
|
<a id="add_alias_button" class="btn btn-xs btn-primary" data-count="{{ aliases_count }}">+</a>
|
||||||
|
{% endcomment %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Add autocompletion. */
|
||||||
|
$('.ac input.ac-tables').autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_tables').text())
|
||||||
|
});
|
||||||
|
$('.ac input.ac-columns').each(function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var $control = $input.closest('td').find('.form-control-feedback');
|
||||||
|
if (!$control.hasClass('hidden')) {
|
||||||
|
$input.autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_columns').text())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('.ac div.form-control-feedback').click(function() {
|
||||||
|
var $div = $(this);
|
||||||
|
var $input = $div.siblings('.ac-tables, .ac-columns');
|
||||||
|
/* Focus on list now so that clicking outside of it closes it. */
|
||||||
|
$input.autocomplete('search', '').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Combine mapping columns into single param. */
|
||||||
|
$('#mappings_table').closest('form').submit(function() {
|
||||||
|
var columns = [];
|
||||||
|
var incomplete = false;
|
||||||
|
$('#mappings_table').find('.mapping-column-input').not('#mapping_column_0')
|
||||||
|
.each(function() {
|
||||||
|
/* All values are required. */
|
||||||
|
if (incomplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var $input = $(this);
|
||||||
|
var name = $input.val();
|
||||||
|
if (name) {
|
||||||
|
columns.push(name);
|
||||||
|
} else {
|
||||||
|
incomplete = true;
|
||||||
|
columns = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#mappings').val(columns.join(', '));
|
||||||
|
});
|
||||||
|
</script>
|
@ -0,0 +1,65 @@
|
|||||||
|
<noscript><h3>{{ step }}</h3></noscript>
|
||||||
|
{{ step.get_help_text }}
|
||||||
|
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group{% if form.rule_name.errors %} has-error{% endif %} {{ form.rule_name.css_classes }}">
|
||||||
|
<label class="control-label{% if form.rule_name.field.required %} {{ form.required_css_class }}{% endif %}" for="rule_name">{{ form.rule_name.label }}</label>
|
||||||
|
{% if form.rule_name.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.rule_name.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||||
|
{% endif %}
|
||||||
|
<input class="form-control" id="rule_name" maxlength="{{ form.rule_name.field.max_length }}" name="rule_name" type="text" value="{{ form.rule_name.value }}" />
|
||||||
|
{% for error in form.rule_name.errors %}
|
||||||
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group{% if form.comment.errors %} has-error{% endif %} {{ form.comment.css_classes }}">
|
||||||
|
<label class="control-label{% if form.comment.field.required %} {{ form.required_css_class }}{% endif %}" for="comment">{{ form.comment.label }}</label>
|
||||||
|
{% if form.comment.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.comment.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||||
|
{% endif %}
|
||||||
|
<textarea class="form-control" cols="40" id="comment" name="comment" rows="4">{{ form.comment.value }}</textarea>
|
||||||
|
{% for error in form.comment.errors %}
|
||||||
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group{% if form.policy_table.errors %} has-error{% endif %} {{ form.policy_table.css_classes }}">
|
||||||
|
<label class="control-label{% if form.policy_table.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_table">{{ form.policy_table.label }}</label>
|
||||||
|
{% if form.policy_table.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.policy_table.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||||
|
{% endif %}
|
||||||
|
<input class="form-control" id="policy_table" maxlength="{{ form.policy_table.field.max_length }}" name="policy_table" type="text" value="{{ form.policy_table.value }}" placeholder="e.g. error" pattern="[^0-9].*" title="Name cannot begin with a number" />
|
||||||
|
{% for error in form.policy_table.errors %}
|
||||||
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group{% if form.policy_columns.errors %} has-error{% endif %} {{ form.policy_columns.css_classes }}">
|
||||||
|
<label class="control-label{% if form.policy_columns.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_columns">{{ form.policy_columns.label }}</label>
|
||||||
|
{% if form.policy_columns.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.policy_columns.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||||
|
{% endif %}
|
||||||
|
<input class="hidden" id="policy_columns" name="policy_columns" type="text" value="{{ form.policy_columns.value }}" />
|
||||||
|
<table id="policy_columns_table" class="table table-condensed">
|
||||||
|
{% for column in policy_columns_list %}<tr id="policy_column_{{ forloop.counter0 }}">
|
||||||
|
<td class="borderless input-cell">
|
||||||
|
<input class="form-control policy-column-input" name="policy_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. name" pattern="[^0-9].*" title="Name cannot begin with a number" />
|
||||||
|
</td>
|
||||||
|
<td class="borderless button-cell">
|
||||||
|
<a class="{% if forloop.first %}hidden {% endif %}remove-policy-column-button btn btn-xs btn-primary">–</a>
|
||||||
|
</td>
|
||||||
|
</tr>{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="borderless input-errors">
|
||||||
|
{% for error in form.policy_columns.errors %}
|
||||||
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<a id="add_policy_column_button" class="btn btn-xs btn-primary" data-count="{{ policy_columns_count }}">+</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,21 @@
|
|||||||
|
<tr id="mapping_{{ count }}" class="{% if count == 0 %}hidden{% else %}mapping-row{% endif %}">
|
||||||
|
<td class="borderless label-cell">
|
||||||
|
{% if count <= 1 %}<div class="form-group{% if count == 1 and form.mappings.errors %} has-error{% endif %} {{ form.mappings.css_classes }}">
|
||||||
|
<label class="control-label{% if form.mappings.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_columns">{{ form.mappings.label }}</label>
|
||||||
|
{% if form.mappings.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.mappings.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="borderless policy-column-name">{{ column }}</td>
|
||||||
|
<td class="borderless mapping-text">maps to</td>
|
||||||
|
<td class="ui-front borderless input-cell">
|
||||||
|
<div class="has-feedback ac">
|
||||||
|
<input id="mapping_column_{{ count }}" class="form-control ac-columns mapping-column-input" name="mapping_column_{{ count }}" type="text" placeholder="e.g. nova:servers name" value="{{ value }}" />
|
||||||
|
<div class="form-control-feedback">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="borderless"></td>
|
||||||
|
</tr>
|
@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'horizon/common/_workflow.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
{% if workflow.wizard %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-1">
|
||||||
|
<a href="{% url 'horizon:admin:policies:detail' policy_name %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-5 back">
|
||||||
|
<button type="button" class="btn btn-default button-previous">« {% trans "Back" %}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 next">
|
||||||
|
<button type="button" class="btn btn-primary button-next">{% trans "Next" %} »</button>
|
||||||
|
<button type="submit" class="btn btn-primary button-final">{{ workflow.finalize_button_name }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<input class="btn btn-primary pull-right" type="submit" value="{{ workflow.finalize_button_name }}" />
|
||||||
|
{% if modal %}<a class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
32
congress_dashboard/policies/urls.py
Normal file
32
congress_dashboard/policies/urls.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2014 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.
|
||||||
|
|
||||||
|
from django.conf.urls import patterns
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from congress_dashboard.policies.rules import views as rule_views
|
||||||
|
from congress_dashboard.policies import views
|
||||||
|
|
||||||
|
|
||||||
|
POLICY = r'^(?P<policy_name>[^/]+)/%s$'
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'',
|
||||||
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
|
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||||
|
url(POLICY % 'detail', views.DetailView.as_view(), name='detail'),
|
||||||
|
url(POLICY % 'rules/create',
|
||||||
|
rule_views.CreateView.as_view(), name='create_rule'),
|
||||||
|
)
|
133
congress_dashboard/policies/views.py
Normal file
133
congress_dashboard/policies/views.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Copyright 2014 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 json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.template.defaultfilters import dictsort
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import messages
|
||||||
|
from horizon import tables
|
||||||
|
|
||||||
|
from congress_dashboard.api import congress
|
||||||
|
import congress_dashboard.datasources.utils as ds_utils
|
||||||
|
from congress_dashboard.policies import forms as policies_forms
|
||||||
|
from congress_dashboard.policies.rules import tables as rules_tables
|
||||||
|
from congress_dashboard.policies import tables as policies_tables
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(tables.DataTableView):
|
||||||
|
"""List policies."""
|
||||||
|
table_class = policies_tables.PoliciesTable
|
||||||
|
template_name = 'admin/policies/index.html'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
try:
|
||||||
|
policies = congress.policies_list(self.request)
|
||||||
|
except Exception as e:
|
||||||
|
msg = _('Unable to get policies list: %s') % str(e)
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
return []
|
||||||
|
return policies
|
||||||
|
|
||||||
|
|
||||||
|
class CreateView(forms.ModalFormView):
|
||||||
|
form_class = policies_forms.CreatePolicy
|
||||||
|
template_name = 'admin/policies/create.html'
|
||||||
|
success_url = reverse_lazy('horizon:admin:policies:index')
|
||||||
|
|
||||||
|
|
||||||
|
class DetailView(tables.DataTableView):
|
||||||
|
"""List details about and rules in a policy."""
|
||||||
|
table_class = rules_tables.PolicyRulesTable
|
||||||
|
template_name = 'admin/policies/detail.html'
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
policy_name = self.kwargs['policy_name']
|
||||||
|
try:
|
||||||
|
policy_rules = congress.policy_rules_list(self.request,
|
||||||
|
policy_name)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||||
|
msg = _('Unable to get rules in policy "%(policy_name)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse('horizon:admin:policies:index')
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
|
||||||
|
for r in policy_rules:
|
||||||
|
r.set_id_as_name_if_empty()
|
||||||
|
return policy_rules
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(DetailView, self).get_context_data(**kwargs)
|
||||||
|
policy_name = kwargs['policy_name']
|
||||||
|
try:
|
||||||
|
policy = congress.policy_get(self.request, policy_name)
|
||||||
|
except Exception as e:
|
||||||
|
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||||
|
msg = _('Unable to get policy "%(policy_name)s": '
|
||||||
|
'%(error)s') % msg_args
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(self.request, msg)
|
||||||
|
redirect = reverse('horizon:admin:policies:index')
|
||||||
|
raise exceptions.Http302(redirect)
|
||||||
|
context['policy'] = policy
|
||||||
|
|
||||||
|
# Alphabetize and convert list of data source tables and columns into
|
||||||
|
# JSON formatted string consumable by JavaScript. Do this here instead
|
||||||
|
# of in the Create Rule form so that the tables and columns lists
|
||||||
|
# appear in the HTML document before the JavaScript that uses them.
|
||||||
|
all_tables = ds_utils.get_datasource_tables(self.request)
|
||||||
|
sorted_datasources = dictsort(all_tables, 'datasource')
|
||||||
|
tables = []
|
||||||
|
for ds in sorted_datasources:
|
||||||
|
datasource_tables = ds['tables']
|
||||||
|
datasource_tables.sort()
|
||||||
|
for table in ds['tables']:
|
||||||
|
tables.append('%s%s%s' % (ds['datasource'],
|
||||||
|
congress.TABLE_SEPARATOR, table))
|
||||||
|
context['tables'] = json.dumps(tables)
|
||||||
|
|
||||||
|
datasource_columns = ds_utils.get_datasource_columns(self.request)
|
||||||
|
sorted_datasources = dictsort(datasource_columns, 'datasource')
|
||||||
|
columns = []
|
||||||
|
for ds in sorted_datasources:
|
||||||
|
sorted_tables = dictsort(ds['tables'], 'table')
|
||||||
|
for tbl in sorted_tables:
|
||||||
|
# Ignore service-derived tables, which are already included.
|
||||||
|
if congress.TABLE_SEPARATOR in tbl['table']:
|
||||||
|
continue
|
||||||
|
table_columns = tbl['columns']
|
||||||
|
if table_columns:
|
||||||
|
table_columns.sort()
|
||||||
|
else:
|
||||||
|
# Placeholder name for column when the table has none.
|
||||||
|
table_columns = ['_']
|
||||||
|
|
||||||
|
for column in table_columns:
|
||||||
|
columns.append('%s%s%s %s' % (ds['datasource'],
|
||||||
|
congress.TABLE_SEPARATOR,
|
||||||
|
tbl['table'], column))
|
||||||
|
context['columns'] = json.dumps(columns)
|
||||||
|
return context
|
134
congress_dashboard/static/admin/css/policies.css
Normal file
134
congress_dashboard/static/admin/css/policies.css
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/* tables */
|
||||||
|
#policy_columns_table {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
#policy_columns_table td.input-cell {
|
||||||
|
width: 94%;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
#policy_columns_table td.button-cell {
|
||||||
|
padding: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#policy_columns_table td.input-errors,
|
||||||
|
#mappings_table td.input-errors {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#policy_columns_table td.borderless,
|
||||||
|
#mappings_table td.borderless,
|
||||||
|
#joins_table td.borderless,
|
||||||
|
#negations_table td.borderless,
|
||||||
|
#aliases_table td.borderless {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
#mappings_table td.input-cell,
|
||||||
|
#joins_table td.input-cell,
|
||||||
|
#negations_table td.input-cell,
|
||||||
|
#aliases_table td.input-cell {
|
||||||
|
width: 36%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mappings_table td.label-cell {
|
||||||
|
width: 22%;
|
||||||
|
}
|
||||||
|
#mappings_table td.policy-column-name {
|
||||||
|
width: 28%;
|
||||||
|
text-align: right;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
#mappings_table td.mapping-text {
|
||||||
|
width: 10%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#joins_table,
|
||||||
|
#negations_table,
|
||||||
|
#aliases_table {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
#joins_table td.operator-cell,
|
||||||
|
#negations_table td.operator-cell {
|
||||||
|
width: 24%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aliases_table td.label-cell {
|
||||||
|
width: 19%;
|
||||||
|
}
|
||||||
|
#aliases_table td.alias-text {
|
||||||
|
width: 5%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* forms */
|
||||||
|
#mappings_table div.form-group,
|
||||||
|
#joins_table div.form-group,
|
||||||
|
#negations_table div.form-group,
|
||||||
|
#aliases_table div.form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
#mappings_table input.form-control,
|
||||||
|
#joins_table input.form-control,
|
||||||
|
#negations_table input.form-control,
|
||||||
|
#aliases_table input.form-control {
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
#mappings_table div.form-control-feedback,
|
||||||
|
#joins_table div.form-control-feedback,
|
||||||
|
#negations_table div.form-control-feedback,
|
||||||
|
#aliases_table div.form-control-feedback {
|
||||||
|
background-color: #DDDDDD;
|
||||||
|
width: 20px;
|
||||||
|
height: 24px;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
#mappings_table span.caret,
|
||||||
|
#joins_table span.caret,
|
||||||
|
#negations_table span.caret,
|
||||||
|
#aliases_table span.caret {
|
||||||
|
border-width: 5px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
#add_join_button,
|
||||||
|
#add_negation_button,
|
||||||
|
#add_alias_button {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* autocompletion */
|
||||||
|
.ui-autocomplete {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* prevent horizontal scrollbar */
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
/* IE 6 doesn't support max-height
|
||||||
|
* we use height instead, but this forces the menu to always be this tall
|
||||||
|
*/
|
||||||
|
* html .ui-autocomplete {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
.ui-widget {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.ui-state-hover,
|
||||||
|
.ui-widget-content .ui-state-hover,
|
||||||
|
.ui-widget-header .ui-state-hover,
|
||||||
|
.ui-state-focus,
|
||||||
|
.ui-widget-content .ui-state-focus,
|
||||||
|
.ui-widget-header .ui-state-focus,
|
||||||
|
.ui-state-active,
|
||||||
|
.ui-widget-content .ui-state-active,
|
||||||
|
.ui-widget-header .ui-state-active {
|
||||||
|
border: 1px solid #285e8e;
|
||||||
|
background: none;
|
||||||
|
background-color: #3276b1;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
288
congress_dashboard/static/admin/js/policies.js
Normal file
288
congress_dashboard/static/admin/js/policies.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
horizon.policies = {
|
||||||
|
/* Update input attributes for column name autocompletion. */
|
||||||
|
updateColumnAcInput: function($input) {
|
||||||
|
$input.attr({
|
||||||
|
'placeholder': $input.attr('data-column-example'),
|
||||||
|
'pattern': $input.attr('data-pattern'),
|
||||||
|
'title': $input.attr('data-pattern-error')
|
||||||
|
});
|
||||||
|
/* form-control-feedback only hidden, so it still has autocompletion. */
|
||||||
|
$input.closest('td').find('.form-control-feedback')
|
||||||
|
.removeClass('hidden');
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Get column names from conditions mappings. */
|
||||||
|
getMappedColumns: function() {
|
||||||
|
var mappings = [];
|
||||||
|
$('#mappings_table').find('.policy-column-name').each(function() {
|
||||||
|
var $td = $(this);
|
||||||
|
var column = $td.text();
|
||||||
|
if (column) {
|
||||||
|
mappings.push(column);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mappings;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Check if any columns need to be removed from conditions mappings. */
|
||||||
|
scrubMappedColumns: function(columns) {
|
||||||
|
mappings = horizon.policies.getMappedColumns();
|
||||||
|
if (!columns) {
|
||||||
|
columns = [];
|
||||||
|
var $inputs = $('#policy_columns_table').find('.policy-column-input');
|
||||||
|
$inputs.each(function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var name = $input.val();
|
||||||
|
if (name) {
|
||||||
|
columns.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < mappings.length; i++) {
|
||||||
|
var name = mappings[i];
|
||||||
|
if ($.inArray(name, columns) == -1) {
|
||||||
|
$('#mappings_table').find('.policy-column-name:contains(' +
|
||||||
|
name + ')').closest('tr').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Put label back if there's only one row left without it. */
|
||||||
|
var $rows = $('#mappings_table').find('.mapping-row');
|
||||||
|
if ($rows.length == 1 && !$rows.find('.label-cell').text()) {
|
||||||
|
var label = $('#mapping_0').find('.label-cell').html();
|
||||||
|
$rows.find('.label-cell').html(label);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
horizon.addInitFunction(horizon.policies.init = function() {
|
||||||
|
/* Add another policy table column name. */
|
||||||
|
$(document).on('click', '#add_policy_column_button', function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $button = $(this);
|
||||||
|
var $tr = $('#policy_column_0').clone();
|
||||||
|
|
||||||
|
var count = $button.attr('data-count');
|
||||||
|
var cid = parseInt(count);
|
||||||
|
$button.attr('data-count', cid + 1);
|
||||||
|
|
||||||
|
/* Change ids and reset inputs. */
|
||||||
|
$tr.attr('id', 'policy_column_' + cid);
|
||||||
|
$tr.find('input[name]').val('').each(function() {
|
||||||
|
this.name = this.name.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||||
|
});
|
||||||
|
$tr.find('.remove-policy-column-button').removeClass('hidden');
|
||||||
|
/* Add row before the one reserved for errors. */
|
||||||
|
$('#policy_columns_table').find('tr:last').before($tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Remove policy table column name input. */
|
||||||
|
$(document).on('click',
|
||||||
|
'#policy_columns_table a.remove-policy-column-button',
|
||||||
|
function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $a = $(this);
|
||||||
|
var $tr = $a.closest('tr');
|
||||||
|
$tr.remove();
|
||||||
|
horizon.policies.scrubMappedColumns();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add policy table columns to conditions and combine into single param. */
|
||||||
|
$(document).on('change',
|
||||||
|
'#policy_columns_table input.policy-column-input',
|
||||||
|
function() {
|
||||||
|
var mappings = horizon.policies.getMappedColumns();
|
||||||
|
var columns = [];
|
||||||
|
|
||||||
|
var $inputs = $('#policy_columns_table').find('.policy-column-input');
|
||||||
|
$inputs.each(function() {
|
||||||
|
var $input = $(this);
|
||||||
|
var name = $input.val();
|
||||||
|
/* Does not make sense to have multiple of the same column. */
|
||||||
|
if (name && $.inArray(name, columns) == -1) {
|
||||||
|
columns.push(name);
|
||||||
|
|
||||||
|
if ($.inArray(name, mappings) == -1) {
|
||||||
|
/* Add mapping inputs for new policy column. */
|
||||||
|
var $tr = $('#mapping_0').clone();
|
||||||
|
var count = $('#mappings_table').attr('data-count');
|
||||||
|
var cid = parseInt(count);
|
||||||
|
$('#mappings_table').attr('data-count', cid + 1);
|
||||||
|
|
||||||
|
/* Change ids. */
|
||||||
|
$tr.attr('id', 'mapping_' + cid).toggleClass('hidden mapping-row');
|
||||||
|
$tr.find('.policy-column-name').text(name);
|
||||||
|
$tr.find('input[id]').each(function() {
|
||||||
|
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||||
|
this.name = this.id;
|
||||||
|
});
|
||||||
|
/* Remove label if there's already a row with it. */
|
||||||
|
if ($('#mappings_table').find('.mapping-row').length) {
|
||||||
|
$tr.find('.label-cell').empty();
|
||||||
|
}
|
||||||
|
$('#mappings_table').find('tr:last').before($tr);
|
||||||
|
|
||||||
|
/* Add autocompletion. */
|
||||||
|
$('#mapping_column_' + cid).autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_columns').text())
|
||||||
|
});
|
||||||
|
$('#mapping_' + cid).find('.ac div.form-control-feedback')
|
||||||
|
.click(function() {
|
||||||
|
/* Focus on list now so that clicking outside of it closes it. */
|
||||||
|
$('#mapping_column_' + cid).autocomplete('search', '').focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Workflow expects one policy_columns value. */
|
||||||
|
$('#policy_columns').val(columns.join(', '));
|
||||||
|
horizon.policies.scrubMappedColumns(columns);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add another join. */
|
||||||
|
$(document).on('click', '#add_join_button', function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $button = $(this);
|
||||||
|
var $tr = $('#join_0').clone();
|
||||||
|
|
||||||
|
var count = $button.attr('data-count');
|
||||||
|
var cid = parseInt(count);
|
||||||
|
$button.attr('data-count', cid + 1);
|
||||||
|
|
||||||
|
/* Change ids and reset inputs. */
|
||||||
|
$tr.attr('id', 'join_' + cid);
|
||||||
|
$tr.find('input[id], select[id]').val('').each(function() {
|
||||||
|
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||||
|
this.name = this.id;
|
||||||
|
});
|
||||||
|
$tr.find('select').val($tr.find('option:first').val());
|
||||||
|
$tr.find('.remove-join-button').removeClass('hidden');
|
||||||
|
$('#joins_table').append($tr);
|
||||||
|
|
||||||
|
/* Add autocompletion. */
|
||||||
|
$('#join_left_' + cid + ', #join_right_' + cid).autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_columns').text())
|
||||||
|
});
|
||||||
|
horizon.policies.updateColumnAcInput($('#join_right_' + cid));
|
||||||
|
$('#join_' + cid).find('.ac div.form-control-feedback').click(function() {
|
||||||
|
var $div = $(this);
|
||||||
|
/* Focus on list now so that clicking outside of it closes it. */
|
||||||
|
$div.siblings('.ac-columns').autocomplete('search', '').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Remove join input. */
|
||||||
|
$(document).on('click', '#joins_table a.remove-join-button',
|
||||||
|
function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $a = $(this);
|
||||||
|
var $tr = $a.closest('tr');
|
||||||
|
$tr.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Update input attributes based on type selected. */
|
||||||
|
$(document).on('change', '#joins_table select.join-op', function() {
|
||||||
|
var $select = $(this);
|
||||||
|
var $input = $select.closest('tr').find('.join-right').val('');
|
||||||
|
|
||||||
|
if (!$select.val()) {
|
||||||
|
$input.autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_columns').text())
|
||||||
|
});
|
||||||
|
horizon.policies.updateColumnAcInput($input);
|
||||||
|
} else {
|
||||||
|
$input.closest('td').find('.form-control-feedback').addClass('hidden');
|
||||||
|
$input.autocomplete('destroy');
|
||||||
|
$input.attr('placeholder', $input.attr('data-static-example'));
|
||||||
|
$input.removeAttr('pattern').removeAttr('title');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add another negation. */
|
||||||
|
$(document).on('click', '#add_negation_button', function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $button = $(this);
|
||||||
|
var $tr = $('#negation_0').clone();
|
||||||
|
|
||||||
|
var count = $button.attr('data-count');
|
||||||
|
var cid = parseInt(count);
|
||||||
|
$button.attr('data-count', cid + 1);
|
||||||
|
|
||||||
|
/* Change ids and reset inputs. */
|
||||||
|
$tr.attr('id', 'negation_' + cid);
|
||||||
|
$tr.find('input[id], select[id]').val('').each(function() {
|
||||||
|
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||||
|
this.name = this.id;
|
||||||
|
});
|
||||||
|
$tr.find('select').val($tr.find('option:first').val());
|
||||||
|
$tr.find('.remove-negation-button').removeClass('hidden');
|
||||||
|
$('#negations_table').append($tr);
|
||||||
|
|
||||||
|
/* Add autocompletion. */
|
||||||
|
$('#negation_value_' + cid + ', #negation_column_' + cid).autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_columns').text())
|
||||||
|
});
|
||||||
|
$('#negation_' + cid).find('.ac div.form-control-feedback')
|
||||||
|
.click(function() {
|
||||||
|
var $div = $(this);
|
||||||
|
/* Focus on list now so that clicking outside of it closes it. */
|
||||||
|
$div.siblings('.ac-columns').autocomplete('search', '').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Remove negation input. */
|
||||||
|
$(document).on('click', '#negations_table a.remove-negation-button',
|
||||||
|
function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $a = $(this);
|
||||||
|
var $tr = $a.closest('tr');
|
||||||
|
$tr.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add another alias. */
|
||||||
|
$(document).on('click', '#add_alias_button', function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $button = $(this);
|
||||||
|
var $tr = $('#alias_0').clone();
|
||||||
|
|
||||||
|
var count = $button.attr('data-count');
|
||||||
|
var cid = parseInt(count);
|
||||||
|
$button.attr('data-count', cid + 1);
|
||||||
|
|
||||||
|
/* Change ids and reset inputs. */
|
||||||
|
$tr.attr('id', 'alias_' + cid);
|
||||||
|
$tr.find('td:first').empty();
|
||||||
|
$tr.find('input[id]').val('').each(function() {
|
||||||
|
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||||
|
this.name = this.id;
|
||||||
|
});
|
||||||
|
$tr.find('.remove-alias-button').removeClass('hidden');
|
||||||
|
$('#aliases_table').append($tr);
|
||||||
|
|
||||||
|
/* Add autocompletion. */
|
||||||
|
$('#alias_column_' + cid).autocomplete({
|
||||||
|
minLength: 0,
|
||||||
|
source: JSON.parse($('#ds_tables').text())
|
||||||
|
});
|
||||||
|
$('#alias_' + cid).find('.ac div.form-control-feedback')
|
||||||
|
.click(function() {
|
||||||
|
var $div = $(this);
|
||||||
|
/* Focus on list now so that clicking outside of it closes it. */
|
||||||
|
$div.siblings('.ac-tables').autocomplete('search', '').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Remove alias input. */
|
||||||
|
$(document).on('click', '#aliases_table a.remove-alias-button',
|
||||||
|
function(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
var $a = $(this);
|
||||||
|
var $tr = $a.closest('tr');
|
||||||
|
$tr.remove();
|
||||||
|
});
|
||||||
|
});
|
5
congress_dashboard/templates/admin/_scripts.html
Normal file
5
congress_dashboard/templates/admin/_scripts.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'horizon/_scripts.html' %}
|
||||||
|
|
||||||
|
{% block custom_js_files %}
|
||||||
|
<script src='{{ STATIC_URL }}admin/js/policies.js' type='text/javascript' charset='utf-8'></script>
|
||||||
|
{% endblock %}
|
14
congress_dashboard/templates/admin/base.html
Normal file
14
congress_dashboard/templates/admin/base.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{% include "_stylesheets.html" %}
|
||||||
|
|
||||||
|
{% load compress %}
|
||||||
|
{% compress css %}
|
||||||
|
<link href='{{ STATIC_URL }}admin/css/policies.css' type='text/css' media='screen' rel='stylesheet' />
|
||||||
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{% include "admin/_scripts.html" %}
|
||||||
|
{% endblock %}
|
1
tox.ini
1
tox.ini
@ -33,7 +33,6 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasen
|
|||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules
|
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules
|
||||||
max-complexity = 20
|
|
||||||
|
|
||||||
[hacking]
|
[hacking]
|
||||||
import_exceptions = collections.defaultdict,
|
import_exceptions = collections.defaultdict,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user