Initial congressclient

This patch adds client support for congress with keystone and supports
the following api commands:

	$ openstack congress policy list
	$ openstack congress policy row get
	$ openstack congress policy rule create
	$ openstack congress policy rule delete
	$ openstack congress policy rules list

Change-Id: I624b54f6cf2614eaec970c8bdd3766291dcc9489
This commit is contained in:
Aaron Rosen 2014-09-03 14:27:16 -07:00
parent d44349d9f0
commit c0612c0c6c
20 changed files with 787 additions and 43 deletions

View File

View File

@ -0,0 +1,94 @@
# Copyright 2012-2013 OpenStack, LLC.
#
# 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 os
import sys
from congressclient import exceptions
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
def get_client_class(api_name, version, version_map):
"""Returns the client class for the requested API version
:param api_name: the name of the API, e.g. 'compute', 'image', etc
:param version: the requested API version
:param version_map: a dict of client classes keyed by version
:rtype: a client class for the requested API version
"""
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = "Invalid %s client version '%s'. must be one of: %s" % (
(api_name, version, ', '.join(version_map.keys())))
raise exceptions.UnsupportedVersion(msg)
return import_class(client_path)
def import_class(import_str):
"""Returns a class from a string including module and class
:param import_str: a string representation of the class name
:rtype: the requested class
"""
mod_str, _sep, class_str = import_str.rpartition('.')
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
def format_list(data):
"""Return a formatted strings
:param data: a list of strings
:rtype: a string formatted to a,b,c
"""
return ', '.join(data)
def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
"""Return a tuple containing the item properties.
:param item: a single dict resource
:param fields: tuple of strings with the desired field names
:param mixed_case_fields: tuple of field names to preserve case
:param formatters: dictionary mapping field names to callables
to format the values
"""
row = []
for field in fields:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = item[field_name] if field_name in item else ''
if field in formatters:
row.append(formatters[field](data))
else:
row.append(data)
return tuple(row)

View File

@ -0,0 +1,16 @@
# Copyright (c) 2012 OpenStack Foundation.
# All Rights Reserved.
#
# 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.openstack.common.apiclient.exceptions import * # noqa

View File

View File

@ -0,0 +1,61 @@
# 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
import keystoneclient
from congressclient.common import utils
LOG = logging.getLogger(__name__)
DEFAULT_COMPUTE_API_VERSION = '1'
API_VERSION_OPTION = 'os_policy_api_version'
API_NAME = 'congressclient'
API_VERSIONS = {
'1': 'congressclient.v1.client.Client',
}
def make_client(instance):
"""Returns a congress service client."""
congress_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
auth = keystoneclient.auth.identity.v2.Password(
auth_url=instance._auth_url,
username=instance._username,
password=instance._password, tenant_name=instance._project_name)
session = keystoneclient.session.Session(auth=auth)
LOG.debug('instantiating congress client: %s', congress_client)
return congress_client(session=session,
auth=None,
interface='publicURL',
service_type='policy',
region_name=instance._region_name)
def build_option_parser(parser):
"""Hook to add global options."""
parser.add_argument(
'--os-policy-api-version',
metavar='<policy-api-version>',
default=utils.env(
'OS_POLICY_API_VERSION',
default=DEFAULT_COMPUTE_API_VERSION),
help='Policy API version, default=' +
DEFAULT_COMPUTE_API_VERSION +
' (Env: OS_POLICY_API_VERSION)')
return parser

View File

View File

@ -0,0 +1,162 @@
# Copyright 2012-2013 OpenStack, LLC.
#
# 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.
"""Policy action implemenations"""
import logging
from cliff import command
from cliff import lister
from cliff import show
import six
from congressclient.common import utils
from congressclient.openstack.common import jsonutils
def _format_rule(rule):
"""Break up rule string so it fits on screen."""
rule_split = jsonutils.dumps(rule).split(":-")
formatted_string = rule_split[0] + ":-\n"
for rule in rule_split[1].split("), "):
formatted_string += rule + '\n'
return formatted_string
class CreatePolicyRule(show.ShowOne):
"""Create a policy rule."""
log = logging.getLogger(__name__ + '.CreatePolicyRule')
def get_parser(self, prog_name):
parser = super(CreatePolicyRule, self).get_parser(prog_name)
parser.add_argument(
'policy_name',
metavar="<policy-name>",
help="Name or identifier of the policy")
parser.add_argument(
'rule',
metavar="<rule>",
help="Policy rule")
return parser
def take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
client = self.app.client_manager.congressclient
body = {'rule': parsed_args.rule}
data = client.create_policy_rule(parsed_args.policy_name, body)
data['rule'] = _format_rule(data['rule'])
return zip(*sorted(six.iteritems(data)))
class DeletePolicyRule(command.Command):
"""Delete a policy rule."""
log = logging.getLogger(__name__ + '.DeletePolicyRule')
def get_parser(self, prog_name):
parser = super(DeletePolicyRule, self).get_parser(prog_name)
parser.add_argument(
'policy_name',
metavar="<policy-name>",
help="Name of the policy to delete")
parser.add_argument(
'rule_id',
metavar="<rule-id>",
help="ID of the policy to delete")
return parser
def take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
client = self.app.client_manager.congressclient
client.delete_policy_rule(parsed_args.policy_name,
parsed_args.rule_id)
class ListPolicyRules(lister.Lister):
"""List policy rules."""
log = logging.getLogger(__name__ + '.ListPolicyRules')
def get_parser(self, prog_name):
parser = super(ListPolicyRules, self).get_parser(prog_name)
parser.add_argument(
'policy_name',
metavar="<policy-name>",
help="Name of the policy")
return parser
def take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
client = self.app.client_manager.congressclient
data = client.list_policy_rules(parsed_args.policy_name)['results']
columns = ['id', 'comment', 'rule']
formatters = {'PolicyRules': utils.format_list}
return (columns,
(utils.get_dict_properties(s, columns,
formatters=formatters)
for s in data))
class ListPolicy(lister.Lister):
"""List Policy."""
log = logging.getLogger(__name__ + '.ListPolicy')
def get_parser(self, prog_name):
parser = super(ListPolicy, self).get_parser(prog_name)
return parser
def take_action(self, parsed_args):
client = self.app.client_manager.congressclient
data = client.list_policy()['results']
columns = ['id', 'owner_id']
formatters = {'Policies': utils.format_list}
return (columns,
(utils.get_dict_properties(s, columns,
formatters=formatters)
for s in data))
class GetPolicyRow(lister.Lister):
"""Get policy row."""
log = logging.getLogger(__name__ + '.GetPolicyRow')
def get_parser(self, prog_name):
parser = super(GetPolicyRow, self).get_parser(prog_name)
parser.add_argument(
'policy_name',
metavar="<policy-name>",
help="Name of the policy to show")
parser.add_argument(
'table',
metavar="<table>",
help="Table to get the policy row from")
return parser
def take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
client = self.app.client_manager.congressclient
data = client.get_policy_rows(parsed_args.policy_name,
parsed_args.table)['results']
columns = ['data']
formatters = {'Policies': utils.format_list}
return (columns,
(utils.get_dict_properties(s, columns,
formatters=formatters)
for s in data))

View File

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
# 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.

View File

@ -0,0 +1,53 @@
# 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 argparse
import mock
from congressclient.tests import utils
class TestCongressBase(utils.TestCommand):
def setUp(self):
super(TestCongressBase, self).setUp()
self.app = mock.Mock(name='app')
self.app.client_manager = mock.Mock(name='client_manager')
self.namespace = argparse.Namespace()
given_show_options = [
'-f',
'shell',
'-c',
'id',
'--prefix',
'TST',
]
then_show_options = [
('formatter', 'shell'),
('columns', ['id']),
('prefix', 'TST'),
]
given_list_options = [
'-f',
'csv',
'-c',
'id',
'--quote',
'all',
]
then_list_options = [
('formatter', 'csv'),
('columns', ['id']),
('quote_mode', 'all'),
]

View File

@ -0,0 +1,81 @@
# Copyright 2013 Nebula Inc.
#
# 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 sys
import six
AUTH_TOKEN = "foobar"
AUTH_URL = "http://0.0.0.0"
class FakeStdout:
def __init__(self):
self.content = []
def write(self, text):
self.content.append(text)
def make_string(self):
result = ''
for line in self.content:
result = result + line
return result
class FakeApp(object):
def __init__(self, _stdout):
self.stdout = _stdout
self.client_manager = None
self.stdin = sys.stdin
self.stdout = _stdout or sys.stdout
self.stderr = sys.stderr
self.restapi = None
class FakeClientManager(object):
def __init__(self):
self.compute = None
self.identity = None
self.image = None
self.object = None
self.volume = None
self.network = None
self.auth_ref = None
class FakeModule(object):
def __init__(self, name, version):
self.name = name
self.__version__ = version
class FakeResource(object):
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def _add_details(self, info):
for (k, v) in six.iteritems(info):
setattr(self, k, v)
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_congressclient
----------------------------------
Tests for `congressclient` module.
"""
from congressclient.tests import base
class TestCongressclient(base.TestCase):
def test_something(self):
pass

View File

@ -0,0 +1,93 @@
# Copyright 2012-2013 OpenStack Foundation
# Copyright 2013 Nebula Inc.
#
# 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 os
import sys
import fixtures
import testtools
from congressclient.tests import fakes
class TestCase(testtools.TestCase):
def setUp(self):
testtools.TestCase.setUp(self)
if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or
os.environ.get("OS_STDOUT_CAPTURE") == "1"):
stdout = self.useFixture(fixtures.StringStream("stdout")).stream
self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
if (os.environ.get("OS_STDERR_CAPTURE") == "True" or
os.environ.get("OS_STDERR_CAPTURE") == "1"):
stderr = self.useFixture(fixtures.StringStream("stderr")).stream
self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
def assertNotCalled(self, m, msg=None):
"""Assert a function was not called."""
if m.called:
if not msg:
msg = 'method %s should not have been called' % m
self.fail(msg)
# 2.6 doesn't have the assert dict equals so make sure that it exists
if tuple(sys.version_info)[0:2] < (2, 7):
def assertIsInstance(self, obj, cls, msg=None):
"""self.assertTrue(isinstance(obj, cls)), with a nicer message."""
if not isinstance(obj, cls):
standardMsg = '%s is not an instance of %r' % (obj, cls)
self.fail(self._formatMessage(msg, standardMsg))
def assertDictEqual(self, d1, d2, msg=None):
# Simple version taken from 2.7
self.assertIsInstance(d1, dict,
'First argument is not a dictionary')
self.assertIsInstance(d2, dict,
'Second argument is not a dictionary')
if d1 != d2:
if msg:
self.fail(msg)
else:
standardMsg = '%r != %r' % (d1, d2)
self.fail(standardMsg)
class TestCommand(TestCase):
"""Test cliff command classes."""
def setUp(self):
super(TestCommand, self).setUp()
# Build up a fake app
self.fake_stdout = fakes.FakeStdout()
self.app = fakes.FakeApp(self.fake_stdout)
self.app.client_manager = fakes.FakeClientManager()
def check_parser(self, cmd, args, verify_args):
cmd_parser = cmd.get_parser('check_parser')
try:
parsed_args = cmd_parser.parse_args(args)
except SystemExit:
raise Exception("Argument parse failed")
for av in verify_args:
attr, value = av
if attr:
self.assertIn(attr, parsed_args)
self.assertEqual(getattr(parsed_args, attr), value)
return parsed_args

View File

View File

@ -0,0 +1,147 @@
# 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 mock
from congressclient.osc.v1 import policy
from congressclient.tests import common
class TestCreatePolicyRule(common.TestCongressBase):
def test_create_policy_rule(self):
policy_name = 'classification'
rule = ("port_security_group(port, security_group_name) :-"
"neutron:ports(addr_pairs, security_groups, extra_dhcp_opts,"
"binding_cap, status, name, admin_state_up, network_id, "
"tenant_id, binding_vif, device_owner, mac_address, "
"fixed_ips, port, device_id, binding_host_id1), "
"neutron:ports.security_groups(security_groups, "
"security_group_id), neutron:security_groups(tenant_id2, "
"security_group_name, desc2, security_group_id)")
response = {"comment": "None",
"id": "e531f2b3-3d97-42c0-b3b5-b7b6ab532018",
"rule": rule}
arglist = [policy_name, rule]
verifylist = [
('policy_name', policy_name),
('rule', rule),
]
mocker = mock.Mock(return_value=response)
self.app.client_manager.congressclient.create_policy_rule = mocker
cmd = policy.CreatePolicyRule(self.app, self.namespace)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = list(cmd.take_action(parsed_args))
filtered = [('comment', 'id', 'rule'),
('None', 'e531f2b3-3d97-42c0-b3b5-b7b6ab532018',
policy._format_rule(rule))]
self.assertEqual(filtered, result)
class TestDeletePolicyRule(common.TestCongressBase):
def test_delete_policy_rule(self):
policy_name = 'classification'
rule_id = 'e531f2b3-3d97-42c0-b3b5-b7b6ab532018'
arglist = [
policy_name, rule_id
]
verifylist = [
('policy_name', policy_name),
('rule_id', rule_id)
]
mocker = mock.Mock(return_value=None)
self.app.client_manager.congressclient.delete_policy_rule = mocker
cmd = policy.DeletePolicyRule(self.app, self.namespace)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
mocker.assert_called_with(policy_name, rule_id)
self.assertEqual(None, result)
class TestListPolicyRules(common.TestCongressBase):
def test_list_policy_rules(self):
policy_name = 'classification'
rule_id = 'e531f2b3-3d97-42c0-b3b5-b7b6ab532018'
arglist = [
policy_name
]
verifylist = [
('policy_name', policy_name)
]
response = {
"results": [{"comment": "None",
"id": rule_id,
"rule": "security_group(port, security_group_name)"
}]
}
lister = mock.Mock(return_value=response)
self.app.client_manager.congressclient.list_policy_rules = lister
cmd = policy.ListPolicyRules(self.app, self.namespace)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
lister.assert_called_with(policy_name)
self.assertEqual(['id', 'comment', 'rule'], result[0])
class ListPolicy(common.TestCongressBase):
def test_list_policy_rules(self):
policy_name = 'classification'
arglist = [
]
verifylist = [
]
response = {
"results": [{"id": policy_name,
"owner": "system"
}]}
lister = mock.Mock(return_value=response)
self.app.client_manager.congressclient.list_policy = lister
cmd = policy.ListPolicy(self.app, self.namespace)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
lister.assert_called_with()
self.assertEqual(['id', 'owner_id'], result[0])
class GetPolicyRow(common.TestCongressBase):
def test_list_policy_rules(self):
policy_name = 'classification'
table_name = 'port_security_group'
arglist = [
policy_name, table_name
]
verifylist = [
]
response = {"results":
[{"data": ["69abc88b-c950-4625-801b-542e84381509",
"default"]}]}
lister = mock.Mock(return_value=response)
self.app.client_manager.congressclient.get_policy_rows = lister
cmd = policy.GetPolicyRow(self.app, self.namespace)
parsed_args = self.check_parser(cmd, arglist, verifylist)
result = cmd.take_action(parsed_args)
lister.assert_called_with(policy_name, table_name)
self.assertEqual(['data'], result[0])

View File

View File

@ -0,0 +1,52 @@
# 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 keystoneclient import adapter
class Client(object):
policy_rules_path = '/policies/%s/rules'
policy_rules_paths = '/policies/%s/rules/%s'
policy_rows = '/policies/%s/tables/%s/rows'
policy_rules = '/policies/%s/rules'
policies = '/policies'
def __init__(self, **kwargs):
super(Client, self).__init__()
kwargs.setdefault('user_agent', 'python-congressclient')
self.httpclient = adapter.LegacyJsonAdapter(**kwargs)
def create_policy_rule(self, policy_name, body=None):
resp, body = self.httpclient.post(
self.policy_rules_path % policy_name, body=body)
return body
def delete_policy_rule(self, policy_name, rule_id):
resp, body = self.httpclient.delete(
self.policy_rules_paths % (policy_name, rule_id))
return body
def get_policy_rows(self, policy_name, table):
resp, body = self.httpclient.get(self.policy_rows % (policy_name,
table))
return body
def list_policy_rules(self, policy_name):
resp, body = self.httpclient.get(self.policy_rules % (policy_name))
return body
def list_policy(self):
resp, body = self.httpclient.get(self.policies)
return body

View File

@ -1,2 +1,7 @@
pbr>=0.6,!=0.7,<1.0 pbr>=0.6,!=0.7,<1.0
Babel>=1.3 Babel>=1.3
cliff>=1.6.0
oslo.i18n>=0.3.0
python-keystoneclient>=0.10.0
requests>=1.2.1
six>=1.7.0

View File

@ -23,6 +23,17 @@ classifier =
packages = packages =
congressclient congressclient
[entry_points]
openstack.cli.extension =
congressclient = congressclient.osc.osc_plugin
openstack.congressclient.v1 =
congress_policy_rule_create = congressclient.osc.v1.policy:CreatePolicyRule
congress_policy_rule_delete = congressclient.osc.v1.policy:DeletePolicyRule
congress_policy_rules_list = congressclient.osc.v1.policy:ListPolicyRules
congress_policy_list = congressclient.osc.v1.policy:ListPolicy
congress_policy_row_get = congressclient.osc.v1.policy:GetPolicyRow
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source
build-dir = doc/build build-dir = doc/build

8
tenant-list.log Normal file
View File

@ -0,0 +1,8 @@
+----------------------------------+--------------------+---------+
| id | name | enabled |
+----------------------------------+--------------------+---------+
| 8918ef508b3a48a1ad963cec1d7bec18 | admin | True |
| e41ef6f35dab448699a5ca69eff60692 | demo | True |
| c01bea38e55d44d3a87bb018b050338c | invisible_to_admin | True |
| 3a1208bc143a4ed589288057ea8e0735 | service | True |
+----------------------------------+--------------------+---------+

View File

@ -1,4 +1,4 @@
hacking>=0.5.6,<0.8 hacking>=0.9.2,<0.10
coverage>=3.6 coverage>=3.6
discover discover
@ -9,3 +9,5 @@ oslosphinx>=2.2.0.0a2
testrepository>=0.0.18 testrepository>=0.0.18
testscenarios>=0.4 testscenarios>=0.4
testtools>=0.9.34 testtools>=0.9.34
mock>=1.0
WebOb>=1.2.3