Browse Source

Split quantumclient out.

changes/29/3129/1
Monty Taylor 10 years ago
parent
commit
5082b6181d
  1. 13
      .gitignore
  2. 4
      .gitreview
  3. 42
      .pylintrc
  4. 176
      LICENSE
  5. 3
      MANIFEST.in
  6. 19
      quantum/__init__.py
  7. 0
      quantum/client/__init__.py
  8. 0
      quantum/client/cli.py
  9. 0
      quantum/client/cli_lib.py
  10. 0
      quantum/client/tests/__init__.py
  11. 0
      quantum/client/tests/unit/__init__.py
  12. 422
      quantum/client/tests/unit/test_cli.py
  13. 625
      quantum/client/tests/unit/test_clientlib.py
  14. 16
      quantum/common/__init__.py
  15. 327
      quantum/common/config.py
  16. 174
      quantum/common/exceptions.py
  17. 249
      quantum/common/flags.py
  18. 153
      quantum/common/serializer.py
  19. 291
      quantum/common/test_lib.py
  20. 268
      quantum/common/utils.py
  21. 12
      setup.cfg
  22. 95
      setup.py
  23. 19
      tools/pip-requires
  24. 7
      tox.ini
  25. 46
      version.py

13
.gitignore

@ -0,0 +1,13 @@
*.pyc
*.DS_Store
build/*
build-stamp
.coverage
cover/*
quantum_client.egg-info/*
quantum/vcsversion.py
run_tests.err.log
run_tests.log
tests/
.venv/
.tox/

4
.gitreview

@ -0,0 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/python-quantumclient.git

42
.pylintrc

@ -0,0 +1,42 @@
# The format of this file isn't really documented; just use --generate-rcfile
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=test
[Messages Control]
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
# C0111: Don't require docstrings on every method
# W0511: TODOs in code comments are fine.
# W0142: *args and **kwargs are fine.
# W0622: Redefining id is fine.
disable=C0111,W0511,W0142,W0622
[Basic]
# Variable names can be 1 to 31 characters long, with lowercase and underscores
variable-rgx=[a-z_][a-z0-9_]{0,30}$
# Argument names can be 2 to 31 characters long, with lowercase and underscores
argument-rgx=[a-z_][a-z0-9_]{1,30}$
# Method names should be at least 3 characters long
# and be lowecased with underscores
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
# Module names matching quantum-* are ok (files in bin/)
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(quantum-[a-z0-9_-]+))$
# Don't require docstrings on tests.
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
[Design]
max-public-methods=100
min-public-methods=0
max-args=6
[Variables]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
# _ is used by our localization
additional-builtins=_

176
LICENSE

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

3
MANIFEST.in

@ -0,0 +1,3 @@
include tox.ini
include version.py
include tools/*

19
quantum/__init__.py

@ -0,0 +1,19 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC
# 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 pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

0
__init__.py → quantum/client/__init__.py

0
cli.py → quantum/client/cli.py

0
cli_lib.py → quantum/client/cli_lib.py

0
quantum/client/tests/__init__.py

0
quantum/client/tests/unit/__init__.py

422
quantum/client/tests/unit/test_cli.py

@ -0,0 +1,422 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 ????
# 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.
# @author: Salvatore Orlando, Citrix Systems
""" Module containing unit tests for Quantum
command line interface
"""
import logging
import sys
import unittest
from quantum import api as server
from quantum.client import cli_lib as cli
from quantum.client import Client
from quantum.db import api as db
from quantum.tests.unit.client_tools import stubs as client_stubs
LOG = logging.getLogger('quantum.tests.test_cli')
FORMAT = 'json'
class CLITest(unittest.TestCase):
def setUp(self):
"""Prepare the test environment"""
options = {}
options['plugin_provider'] = \
'quantum.plugins.sample.SamplePlugin.FakePlugin'
#TODO: make the version of the API router configurable
self.api = server.APIRouterV11(options)
self.tenant_id = "test_tenant"
self.network_name_1 = "test_network_1"
self.network_name_2 = "test_network_2"
# Prepare client and plugin manager
self.client = Client(tenant=self.tenant_id, format=FORMAT,
testingStub=client_stubs.FakeHTTPConnection)
# Redirect stdout
self.fake_stdout = client_stubs.FakeStdout()
sys.stdout = self.fake_stdout
def tearDown(self):
"""Clear the test environment"""
db.clear_db()
sys.stdout = sys.__stdout__
def _verify_list_networks(self):
# Verification - get raw result from db
nw_list = db.network_list(self.tenant_id)
networks = [dict(id=nw.uuid, name=nw.name) for nw in nw_list]
# Fill CLI template
output = cli.prepare_output('list_nets', self.tenant_id,
dict(networks=networks))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_create_network(self):
# Verification - get raw result from db
nw_list = db.network_list(self.tenant_id)
if len(nw_list) != 1:
self.fail("No network created")
network_id = nw_list[0].uuid
# Fill CLI template
output = cli.prepare_output('create_net', self.tenant_id,
dict(network_id=network_id))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_delete_network(self, network_id):
# Verification - get raw result from db
nw_list = db.network_list(self.tenant_id)
if len(nw_list) != 0:
self.fail("DB should not contain any network")
# Fill CLI template
output = cli.prepare_output('delete_net', self.tenant_id,
dict(network_id=network_id))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_update_network(self):
# Verification - get raw result from db
nw_list = db.network_list(self.tenant_id)
network_data = {'id': nw_list[0].uuid,
'name': nw_list[0].name}
# Fill CLI template
output = cli.prepare_output('update_net', self.tenant_id,
dict(network=network_data))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_show_network(self):
# Verification - get raw result from db
nw = db.network_list(self.tenant_id)[0]
network = dict(id=nw.uuid, name=nw.name)
# Fill CLI template
output = cli.prepare_output('show_net', self.tenant_id,
dict(network=network))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_list_ports(self, network_id):
# Verification - get raw result from db
port_list = db.port_list(network_id)
ports = [dict(id=port.uuid, state=port.state)
for port in port_list]
# Fill CLI template
output = cli.prepare_output('list_ports', self.tenant_id,
dict(network_id=network_id,
ports=ports))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_create_port(self, network_id):
# Verification - get raw result from db
port_list = db.port_list(network_id)
if len(port_list) != 1:
self.fail("No port created")
port_id = port_list[0].uuid
# Fill CLI template
output = cli.prepare_output('create_port', self.tenant_id,
dict(network_id=network_id,
port_id=port_id))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_delete_port(self, network_id, port_id):
# Verification - get raw result from db
port_list = db.port_list(network_id)
if len(port_list) != 0:
self.fail("DB should not contain any port")
# Fill CLI template
output = cli.prepare_output('delete_port', self.tenant_id,
dict(network_id=network_id,
port_id=port_id))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_update_port(self, network_id, port_id):
# Verification - get raw result from db
port = db.port_get(port_id, network_id)
port_data = {'id': port.uuid, 'state': port.state}
# Fill CLI template
output = cli.prepare_output('update_port', self.tenant_id,
dict(network_id=network_id,
port=port_data))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_show_port(self, network_id, port_id):
# Verification - get raw result from db
# TODO(salvatore-orlando): Must resolve this issue with
# attachment in separate bug fix.
port = db.port_get(port_id, network_id)
port_data = {'id': port.uuid, 'state': port.state,
'attachment': "<none>"}
if port.interface_id is not None:
port_data['attachment'] = port.interface_id
# Fill CLI template
output = cli.prepare_output('show_port', self.tenant_id,
dict(network_id=network_id,
port=port_data))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_plug_iface(self, network_id, port_id):
# Verification - get raw result from db
port = db.port_get(port_id, network_id)
# Fill CLI template
output = cli.prepare_output("plug_iface", self.tenant_id,
dict(network_id=network_id,
port_id=port['uuid'],
attachment=port['interface_id']))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def _verify_unplug_iface(self, network_id, port_id):
# Verification - get raw result from db
port = db.port_get(port_id, network_id)
# Fill CLI template
output = cli.prepare_output("unplug_iface", self.tenant_id,
dict(network_id=network_id,
port_id=port['uuid']))
# Verify!
# Must add newline at the end to match effect of print call
self.assertEquals(self.fake_stdout.make_string(), output + '\n')
def test_list_networks(self):
try:
# Pre-populate data for testing using db api
db.network_create(self.tenant_id, self.network_name_1)
db.network_create(self.tenant_id, self.network_name_2)
cli.list_nets(self.client, self.tenant_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_list_networks failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_list_networks()
def test_create_network(self):
try:
cli.create_net(self.client, self.tenant_id, "test")
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_create_network failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_create_network()
def test_delete_network(self):
try:
db.network_create(self.tenant_id, self.network_name_1)
network_id = db.network_list(self.tenant_id)[0]['uuid']
cli.delete_net(self.client, self.tenant_id, network_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_delete_network failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_delete_network(network_id)
def test_show_network(self):
try:
# Load some data into the datbase
net = db.network_create(self.tenant_id, self.network_name_1)
cli.show_net(self.client, self.tenant_id, net['uuid'])
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_detail_network failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_show_network()
def test_update_network(self):
try:
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
cli.update_net(self.client, self.tenant_id,
network_id, 'name=%s' % self.network_name_2)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_update_network failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_update_network()
def test_list_ports(self):
try:
# Pre-populate data for testing using db api
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
db.port_create(network_id)
db.port_create(network_id)
cli.list_ports(self.client, self.tenant_id, network_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_list_ports failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_list_ports(network_id)
def test_create_port(self):
network_id = None
try:
# Pre-populate data for testing using db api
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
cli.create_port(self.client, self.tenant_id, network_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_create_port failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_create_port(network_id)
def test_delete_port(self):
network_id = None
port_id = None
try:
# Pre-populate data for testing using db api
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(network_id)
port_id = port['uuid']
cli.delete_port(self.client, self.tenant_id, network_id, port_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_delete_port failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_delete_port(network_id, port_id)
def test_update_port(self):
try:
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(network_id)
port_id = port['uuid']
# Default state is DOWN - change to ACTIVE.
cli.update_port(self.client, self.tenant_id, network_id,
port_id, 'state=ACTIVE')
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_update_port failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_update_port(network_id, port_id)
def test_show_port_no_attach(self):
network_id = None
port_id = None
try:
# Pre-populate data for testing using db api
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(network_id)
port_id = port['uuid']
cli.show_port(self.client, self.tenant_id, network_id, port_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_show_port_no_attach failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_show_port(network_id, port_id)
def test_show_port_with_attach(self):
network_id = None
port_id = None
iface_id = "flavor crystals"
try:
# Pre-populate data for testing using db api
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(network_id)
port_id = port['uuid']
db.port_set_attachment(port_id, network_id, iface_id)
cli.show_port(self.client, self.tenant_id, network_id, port_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_show_port_with_attach failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_show_port(network_id, port_id)
def test_plug_iface(self):
network_id = None
port_id = None
try:
# Load some data into the datbase
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(net['uuid'])
port_id = port['uuid']
cli.plug_iface(self.client, self.tenant_id, network_id,
port_id, "test_iface_id")
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_plug_iface failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_plug_iface(network_id, port_id)
def test_unplug_iface(self):
network_id = None
port_id = None
try:
# Load some data into the datbase
net = db.network_create(self.tenant_id, self.network_name_1)
network_id = net['uuid']
port = db.port_create(net['uuid'])
port_id = port['uuid']
db.port_set_attachment(port_id, network_id, "test_iface_id")
cli.unplug_iface(self.client, self.tenant_id, network_id, port_id)
except:
LOG.exception("Exception caught: %s", sys.exc_info())
self.fail("test_plug_iface failed due to an exception")
LOG.debug("Operation completed. Verifying result")
LOG.debug(self.fake_stdout.content)
self._verify_unplug_iface(network_id, port_id)

625
quantum/client/tests/unit/test_clientlib.py

@ -0,0 +1,625 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Cisco Systems
# 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.
# @author: Tyler Smith, Cisco Systems
import logging
import unittest
import re
from quantum.common.serializer import Serializer
from quantum.client import Client
LOG = logging.getLogger('quantum.tests.test_api')
# Set a couple tenants to use for testing
TENANT_1 = 'totore'
TENANT_2 = 'totore2'
class ServerStub():
"""This class stubs a basic server for the API client to talk to"""
class Response(object):
"""This class stubs a basic response to send the API client"""
def __init__(self, content=None, status=None):
self.content = content
self.status = status
def read(self):
return self.content
def status(self):
return self.status
# To test error codes, set the host to 10.0.0.1, and the port to the code
def __init__(self, host, port=9696, key_file="", cert_file=""):
self.host = host
self.port = port
self.key_file = key_file
self.cert_file = cert_file
def request(self, method, action, body, headers):
self.method = method
self.action = action
self.body = body
def status(self, status=None):
return status or 200
def getresponse(self):
res = self.Response(status=self.status())
# If the host is 10.0.0.1, return the port as an error code
if self.host == "10.0.0.1":
res.status = self.port
return res
# Extract important information from the action string to assure sanity
match = re.search('tenants/(.+?)/(.+)\.(json|xml)$', self.action)
tenant = match.group(1)
path = match.group(2)
format = match.group(3)
data = {'data': {'method': self.method, 'action': self.action,
'body': self.body, 'tenant': tenant, 'path': path,
'format': format, 'key_file': self.key_file,
'cert_file': self.cert_file}}
# Serialize it to the proper format so the API client can handle it
if data['data']['format'] == 'json':
res.content = Serializer().serialize(data, "application/json")
else:
res.content = Serializer().serialize(data, "application/xml")
return res
class APITest(unittest.TestCase):
def setUp(self):
""" Setups a test environment for the API client """
HOST = '127.0.0.1'
PORT = 9696
USE_SSL = False
self.client = Client(HOST, PORT, USE_SSL, TENANT_1, 'json', ServerStub)
def _assert_sanity(self, call, status, method, path, data=[], params={}):
""" Perform common assertions to test the sanity of client requests """
# Handle an error case first
if status != 200:
(self.client.host, self.client.port) = ("10.0.0.1", status)
self.assertRaises(Exception, call, *data, **params)
return
# Make the call, then get the data from the root node and assert it
data = call(*data, **params)['data']
self.assertEqual(data['method'], method)
self.assertEqual(data['format'], params['format'])
self.assertEqual(data['tenant'], params['tenant'])
self.assertEqual(data['path'], path)
return data
def _test_list_networks(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_list_networks - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.list_networks,
status,
"GET",
"networks",
data=[],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_list_networks - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_show_network_details(self,
tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_show_network_details - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.show_network_details,
status,
"GET",
"networks/001",
data=["001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_show_network_details - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_create_network(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_create_network - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.create_network,
status,
"POST",
"networks",
data=[{'network': {'net-name': 'testNetwork'}}],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_create_network - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_update_network(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_update_network - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.update_network,
status,
"PUT",
"networks/001",
data=["001",
{'network': {'net-name': 'newName'}}],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_update_network - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_delete_network(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_delete_network - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.delete_network,
status,
"DELETE",
"networks/001",
data=["001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_delete_network - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_list_ports(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_list_ports - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.list_ports,
status,
"GET",
"networks/001/ports",
data=["001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_list_ports - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_show_port_details(self,
tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_show_port_details - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.show_port_details,
status,
"GET",
"networks/001/ports/001",
data=["001", "001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_show_port_details - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_create_port(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_create_port - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.create_port,
status,
"POST",
"networks/001/ports",
data=["001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_create_port - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_delete_port(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_delete_port - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.delete_port,
status,
"DELETE",
"networks/001/ports/001",
data=["001", "001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_delete_port - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_update_port(self, tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_update_port - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.update_port,
status,
"PUT",
"networks/001/ports/001",
data=["001", "001",
{'port': {'state': 'ACTIVE'}}],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_update_port - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_show_port_attachment(self,
tenant=TENANT_1, format='json', status=200):
LOG.debug("_test_show_port_attachment - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.show_port_attachment,
status,
"GET",
"networks/001/ports/001/attachment",
data=["001", "001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_show_port_attachment - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_attach_resource(self, tenant=TENANT_1,
format='json', status=200):
LOG.debug("_test_attach_resource - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.attach_resource,
status,
"PUT",
"networks/001/ports/001/attachment",
data=["001", "001",
{'resource': {'id': '1234'}}],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_attach_resource - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_detach_resource(self, tenant=TENANT_1,
format='json', status=200):
LOG.debug("_test_detach_resource - tenant:%s "\
"- format:%s - START", format, tenant)
self._assert_sanity(self.client.detach_resource,
status,
"DELETE",
"networks/001/ports/001/attachment",
data=["001", "001"],
params={'tenant': tenant, 'format': format})
LOG.debug("_test_detach_resource - tenant:%s "\
"- format:%s - END", format, tenant)
def _test_ssl_certificates(self, tenant=TENANT_1,
format='json', status=200):
LOG.debug("_test_ssl_certificates - tenant:%s "\
"- format:%s - START", format, tenant)
# Set SSL, and our cert file
self.client.use_ssl = True
cert_file = "/fake.cert"
self.client.key_file = self.client.cert_file = cert_file
data = self._assert_sanity(self.client.list_networks,
status,
"GET",
"networks",
data=[],
params={'tenant': tenant, 'format': format})
self.assertEquals(data["key_file"], cert_file)
self.assertEquals(data["cert_file"], cert_file)
LOG.debug("_test_ssl_certificates - tenant:%s "\
"- format:%s - END", format, tenant)
def test_list_networks_json(self):
self._test_list_networks(format='json')
def test_list_networks_xml(self):
self._test_list_networks(format='xml')
def test_list_networks_alt_tenant(self):
self._test_list_networks(tenant=TENANT_2)
def test_list_networks_error_470(self):
self._test_list_networks(status=470)
def test_list_networks_error_401(self):
self._test_list_networks(status=401)
def test_show_network_details_json(self):
self._test_show_network_details(format='json')
def test_show_network_details_xml(self):
self._test_show_network_details(format='xml')
def test_show_network_details_alt_tenant(self):
self._test_show_network_details(tenant=TENANT_2)
def test_show_network_details_error_470(self):
self._test_show_network_details(status=470)
def test_show_network_details_error_401(self):
self._test_show_network_details(status=401)
def test_show_network_details_error_420(self):
self._test_show_network_details(status=420)
def test_create_network_json(self):
self._test_create_network(format='json')
def test_create_network_xml(self):
self._test_create_network(format='xml')
def test_create_network_alt_tenant(self):
self._test_create_network(tenant=TENANT_2)
def test_create_network_error_470(self):
self._test_create_network(status=470)
def test_create_network_error_401(self):
self._test_create_network(status=401)
def test_create_network_error_400(self):
self._test_create_network(status=400)
def test_create_network_error_422(self):
self._test_create_network(status=422)
def test_update_network_json(self):
self._test_update_network(format='json')
def test_update_network_xml(self):
self._test_update_network(format='xml')
def test_update_network_alt_tenant(self):
self._test_update_network(tenant=TENANT_2)
def test_update_network_error_470(self):
self._test_update_network(status=470)
def test_update_network_error_401(self):
self._test_update_network(status=401)
def test_update_network_error_400(self):
self._test_update_network(status=400)
def test_update_network_error_420(self):
self._test_update_network(status=420)
def test_update_network_error_422(self):
self._test_update_network(status=422)
def test_delete_network_json(self):
self._test_delete_network(format='json')
def test_delete_network_xml(self):
self._test_delete_network(format='xml')
def test_delete_network_alt_tenant(self):
self._test_delete_network(tenant=TENANT_2)
def test_delete_network_error_470(self):
self._test_delete_network(status=470)
def test_delete_network_error_401(self):
self._test_delete_network(status=401)
def test_delete_network_error_420(self):
self._test_delete_network(status=420)
def test_delete_network_error_421(self):
self._test_delete_network(status=421)
def test_list_ports_json(self):
self._test_list_ports(format='json')
def test_list_ports_xml(self):
self._test_list_ports(format='xml')
def test_list_ports_alt_tenant(self):
self._test_list_ports(tenant=TENANT_2)
def test_list_ports_error_470(self):
self._test_list_ports(status=470)
def test_list_ports_error_401(self):
self._test_list_ports(status=401)
def test_list_ports_error_420(self):
self._test_list_ports(status=420)
def test_show_port_details_json(self):
self._test_list_ports(format='json')
def test_show_port_details_xml(self):
self._test_list_ports(format='xml')
def test_show_port_details_alt_tenant(self):
self._test_list_ports(tenant=TENANT_2)
def test_show_port_details_error_470(self):
self._test_show_port_details(status=470)
def test_show_port_details_error_401(self):
self._test_show_port_details(status=401)
def test_show_port_details_error_420(self):
self._test_show_port_details(status=420)
def test_show_port_details_error_430(self):
self._test_show_port_details(status=430)
def test_create_port_json(self):
self._test_create_port(format='json')
def test_create_port_xml(self):
self._test_create_port(format='xml')
def test_create_port_alt_tenant(self):
self._test_create_port(tenant=TENANT_2)
def test_create_port_error_470(self):
self._test_create_port(status=470)
def test_create_port_error_401(self):
self._test_create_port(status=401)
def test_create_port_error_400(self):
self._test_create_port(status=400)
def test_create_port_error_420(self):
self._test_create_port(status=420)
def test_create_port_error_430(self):
self._test_create_port(status=430)
def test_create_port_error_431(self):
self._test_create_port(status=431)
def test_delete_port_json(self):
self._test_delete_port(format='json')
def test_delete_port_xml(self):
self._test_delete_port(format='xml')
def test_delete_port_alt_tenant(self):
self._test_delete_port(tenant=TENANT_2)
def test_delete_port_error_470(self):
self._test_delete_port(status=470)
def test_delete_port_error_401(self):
self._test_delete_port(status=401)
def test_delete_port_error_420(self):
self._test_delete_port(status=420)
def test_delete_port_error_430(self):
self._test_delete_port(status=430)
def test_delete_port_error_432(self):
self._test_delete_port(status=432)
def test_update_port_json(self):
self._test_update_port(format='json')
def test_update_port_xml(self):
self._test_update_port(format='xml')
def test_update_port_alt_tenant(self):
self._test_update_port(tenant=TENANT_2)
def test_update_port_error_470(self):
self._test_update_port(status=470)
def test_update_port_error_401(self):
self._test_update_port(status=401)
def test_update_port_error_400(self):
self._test_update_port(status=400)
def test_update_port_error_420(self):
self._test_update_port(status=420)
def test_update_port_error_430(self):
self._test_update_port(status=430)
def test_update_port_error_431(self):
self._test_update_port(status=431)
def test_show_port_attachment_json(self):
self._test_show_port_attachment(format='json')
def test_show_port_attachment_xml(self):
self._test_show_port_attachment(format='xml')
def test_show_port_attachment_alt_tenant(self):
self._test_show_port_attachment(tenant=TENANT_2)
def test_show_port_attachment_error_470(self):
self._test_show_port_attachment(status=470)
def test_show_port_attachment_error_401(self):
self._test_show_port_attachment(status=401)
def test_show_port_attachment_error_400(self):
self._test_show_port_attachment(status=400)
def test_show_port_attachment_error_420(self):
self._test_show_port_attachment(status=420)
def test_show_port_attachment_error_430(self):
self._test_show_port_attachment(status=430)
def test_attach_resource_json(self):
self._test_attach_resource(format='json')
def test_attach_resource_xml(self):
self._test_attach_resource(format='xml')
def test_attach_resource_alt_tenant(self):
self._test_attach_resource(tenant=TENANT_2)
def test_attach_resource_error_470(self):
self._test_attach_resource(status=470)
def test_attach_resource_error_401(self):
self._test_attach_resource(status=401)
def test_attach_resource_error_400(self):
self._test_attach_resource(status=400)
def test_attach_resource_error_420(self):
self._test_attach_resource(status=420)
def test_attach_resource_error_430(self):
self._test_attach_resource(status=430)
def test_attach_resource_error_432(self):
self._test_attach_resource(status=432)
def test_attach_resource_error_440(self):
self._test_attach_resource(status=440)
def test_detach_resource_json(self):
self._test_detach_resource(format='json')
def test_detach_resource_xml(self):
self._test_detach_resource(format='xml')
def test_detach_resource_alt_tenant(self):
self._test_detach_resource(tenant=TENANT_2)
def test_detach_resource_error_470(self):
self._test_detach_resource(status=470)
def test_detach_resource_error_401(self):
self._test_detach_resource(status=401)
def test_detach_resource_error_420(self):
self._test_detach_resource(status=420)
def test_detach_resource_error_430(self):
self._test_detach_resource(status=430)
def test_ssl_certificates(self):
self._test_ssl_certificates()

16
quantum/common/__init__.py

@ -0,0 +1,16 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nicira Networks, Inc.
# 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.
# @author: Somik Behera, Nicira Networks, Inc.

327
quantum/common/config.py

@ -0,0 +1,327 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nicira Networks, Inc.
# 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.
"""
Routines for configuring Quantum
"""
import ConfigParser
import logging
import logging.config
import logging.handlers
import optparse
import os
import re
import sys
from paste import deploy
from quantum.common import flags
from quantum.common import exceptions as exception
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
FLAGS = flags.FLAGS
LOG = logging.getLogger('quantum.wsgi')
def parse_options(parser, cli_args=None):
"""
Returns the parsed CLI options, command to run and its arguments, merged
with any same-named options found in a configuration file.
The function returns a tuple of (options, args), where options is a
mapping of option key/str(value) pairs, and args is the set of arguments
(not options) supplied on the command-line.
The reason that the option values are returned as strings only is that
ConfigParser and paste.deploy only accept string values...
:param parser: The option parser
:param cli_args: (Optional) Set of arguments to process. If not present,
sys.argv[1:] is used.
:retval tuple of (options, args)
"""
(options, args) = parser.parse_args(cli_args)
return (vars(options), args)
def add_common_options(parser):
"""
Given a supplied optparse.OptionParser, adds an OptionGroup that
represents all common configuration options.
:param parser: optparse.OptionParser
"""
help_text = "The following configuration options are common to "\
"all quantum programs."
group = optparse.OptionGroup(parser, "Common Options", help_text)
group.add_option('-v', '--verbose', default=False, dest="verbose",
action="store_true",
help="Print more verbose output")
group.add_option('-d', '--debug', default=False, dest="debug",
action="store_true",
help="Print debugging output")
group.add_option('--config-file', default=None, metavar="PATH",
help="Path to the config file to use. When not specified "
"(the default), we generally look at the first "
"argument specified to be a config file, and if "
"that is also missing, we search standard "
"directories for a config file.")
parser.add_option_group(group)
def add_log_options(parser):
"""
Given a supplied optparse.OptionParser, adds an OptionGroup that
represents all the configuration options around logging.
:param parser: optparse.OptionParser
"""
help_text = "The following configuration options are specific to logging "\
"functionality for this program."
group = optparse.OptionGroup(parser, "Logging Options", help_text)
group.add_option('--log-config', default=None, metavar="PATH",
help="If this option is specified, the logging "
"configuration file specified is used and overrides "
"any other logging options specified. Please see "
"the Python logging module documentation for "
"details on logging configuration files.")
group.add_option('--log-date-format', metavar="FORMAT",
default=DEFAULT_LOG_DATE_FORMAT,
help="Format string for %(asctime)s in log records. "
"Default: %default")
group.add_option('--log-file', default=None, metavar="PATH",
help="(Optional) Name of log file to output to. "
"If not set, logging will go to stdout.")
group.add_option("--log-dir", default=None,
help="(Optional) The directory to keep log files in "
"(will be prepended to --logfile)")
parser.add_option_group(group)
def setup_logging(options, conf):
"""
Sets up the logging options for a log with supplied name
:param options: Mapping of typed option key/values
:param conf: Mapping of untyped key/values from config file
"""
if options.get('log_config', None):
# Use a logging configuration file for all settings...
if os.path.exists(options['log_config']):
logging.config.fileConfig(options['log_config'])
return
else:
raise RuntimeError("Unable to locate specified logging "
"config file: %s" % options['log_config'])
# If either the CLI option or the conf value
# is True, we set to True
debug = options.get('debug') or \
get_option(conf, 'debug', type='bool', default=False)
verbose = options.get('verbose') or \
get_option(conf, 'verbose', type='bool', default=False)
root_logger = logging.root
if debug:
root_logger.setLevel(logging.DEBUG)
elif verbose:
root_logger.setLevel(logging.INFO)
else:
root_logger.setLevel(logging.WARNING)
# Set log configuration from options...
# Note that we use a hard-coded log format in the options
# because of Paste.Deploy bug #379
# http://trac.pythonpaste.org/pythonpaste/ticket/379
log_format = options.get('log_format', DEFAULT_LOG_FORMAT)
log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
formatter = logging.Formatter(log_format, log_date_format)
logfile = options.get('log_file')
if not logfile:
logfile = conf.get('log_file')
if logfile:
logdir = options.get('log_dir')
if not logdir:
logdir = conf.get('log_dir')
if logdir:
logfile = os.path.join(logdir, logfile)
logfile = logging.FileHandler(logfile)
logfile.setFormatter(formatter)
logfile.setFormatter(formatter)
root_logger.addHandler(logfile)
else:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
root_logger.addHandler(handler)
def find_config_file(options, args, config_file='quantum.conf'):
"""
Return the first config file found.
We search for the paste config file in the following order:
* If --config-file option is used, use that
* If args[0] is a file, use that
* Search for the configuration file in standard directories:
* .
* ~.quantum/
* ~
* $FLAGS.state_path/etc/quantum
* $FLAGS.state_path/etc
:retval Full path to config file, or None if no config file found
"""
fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
if options.get('config_file'):
if os.path.exists(options['config_file']):
return fix_path(options['config_file'])
elif args:
if os.path.exists(args[0]):
return fix_path(args[0])
dir_to_common = os.path.dirname(os.path.abspath(__file__))
root = os.path.join(dir_to_common, '..', '..', '..', '..')
# Handle standard directory search for the config file
config_file_dirs = [fix_path(os.path.join(os.getcwd(), 'etc')),
fix_path(os.path.join('~', '.quantum-venv', 'etc',
'quantum')),
fix_path('~'),
os.path.join(FLAGS.state_path, 'etc'),
os.path.join(FLAGS.state_path, 'etc', 'quantum'),
fix_path(os.path.join('~', '.local',
'etc', 'quantum')),
'/usr/etc/quantum',
'/usr/local/etc/quantum',
'/etc/quantum/',
'/etc']
if 'plugin