Retire the python-searchlightclient project

As announced in openstack-discuss ML[1], Searchlight project
is retiring in Wallaby cycle.

This commit retires python-searchlightclient repository as per
process deinfed in project-guide[2]. Anyone would like to maintain
it again, please revert back this commit and propose the re-adding it
to governance.

The community wishes to express our thanks and appreciation to all of
those who have contributed to the python-searchlightclient project
over the years.

Depends-On: https://review.opendev.org/c/openstack/project-config/+/764519
Needed-By: https://review.opendev.org/c/openstack/governance/+/764530

[1] http://lists.openstack.org/pipermail/openstack-discuss/2020-November/018637.html
[2] https://docs.openstack.org/project-team-guide/repository.html#retiring-a-repository

Change-Id: If02fcc1c21abc9040a3b81cfa4973c7c6382ea15
This commit is contained in:
Ghanshyam Mann 2020-11-27 21:56:32 -06:00
parent 75bb786669
commit 31410355d4
56 changed files with 8 additions and 4057 deletions

20
.gitignore vendored
View File

@ -1,20 +0,0 @@
.coverage*
.venv
*,cover
cover
*.pyc
AUTHORS
build
dist
ChangeLog
run_tests.err.log
.tox
doc/source/api
doc/build
*.egg
*.eggs
searchlightclient/versioninfo
*.egg-info
*.log
.stestr/
*.swp

View File

@ -1,3 +0,0 @@
[DEFAULT]
test_path=./searchlightclient/tests
top_dir=./

View File

@ -1,8 +0,0 @@
- project:
templates:
- openstack-python3-wallaby-jobs
- openstack-cover-jobs
- publish-openstack-docs-pti
- check-requirements
- openstackclient-plugin-jobs
- openstack-lower-constraints-jobs

View File

@ -1,16 +0,0 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps documented at:
https://docs.openstack.org/infra/manual/developers.html#development-workflow
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
https://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Storyboard, not GitHub:
https://storyboard.openstack.org/#!/project_group/searchlight

175
LICENSE
View File

@ -1,175 +0,0 @@
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.

View File

@ -1,152 +1,10 @@
========================
Team and repository tags
========================
This project is no longer maintained.
.. image:: https://governance.openstack.org/tc/badges/python-searchlightclient.svg
:target: https://governance.openstack.org/tc/reference/tags/index.html
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
.. Change things from this point on
========================
python-searchlightclient
========================
OpenStack Indexing and Search API Client Library
This is a client library for Searchlight built on the Searchlight API. It
provides a Python API (the ``searchlightclient`` module) and a command-line
tool (``searchlight``).
The project is hosted on `Storyboard`_, where bugs can be filed. The code is
hosted on `OpenStack git repository`_. Patches must be submitted using
`Gerrit`_, *not* git repo
pull requests.
.. _OpenStack git repository: https://opendev.org/openstack/python-searchlightclient
.. _Storyboard: https://storyboard.openstack.org/#!/project_group/searchlight
.. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow
python-searchlightclient is licensed under the Apache License like the rest of
OpenStack.
.. contents:: Contents:
:local:
Install the client from PyPI
----------------------------
The program `python-searchlightclient`_ package is published on `PyPI`_ and
so can be installed using the pip tool, which will manage installing all
python dependencies::
$ pip install python-searchlightclient
.. note::
The packages on PyPI may lag behind the git repo in functionality.
.. _PyPI: https://pypi.python.org/pypi/python-searchlightclient/
Setup the client from source
----------------------------
* Clone repository for python-searchlightclient::
$ git clone https://opendev.org/openstack/python-searchlightclient.git
$ cd python-searchlightclient
* Setup a virtualenv
.. note::
This is an optional step, but will allow Searchlightclient's dependencies
to be installed in a contained environment that can be easily deleted
if you choose to start over or uninstall Searchlightclient.
::
$ tox -evenv --notest
Activate the virtual environment whenever you want to work in it.
All further commands in this section should be run with the venv active:
::
$ source .tox/venv/bin/activate
.. note::
When ALL steps are complete, deactivate the virtualenv: $ deactivate
* Install Searchlightclient and its dependencies::
(venv) $ python setup.py develop
Command-line API
----------------
Set Keystone environment variables to execute CLI commands against searchlight.
* To execute CLI commands::
$ export OS_USERNAME=<user>
$ export OS_PASSWORD=<password>
$ export OS_TENANT_NAME=<project>
$ export OS_AUTH_URL='http://localhost:5000/v2.0/'
.. note::
With devstack you just need to $ source openrc <user> <project>. And you can
work with a local installation by passing --os-token <TOKEN> and --os-url
http://localhost:9393. You can also set up a `Openstackclient`_ config file
to work with the CLI.
.. _Openstackclient: https://docs.openstack.org/developer/python-openstackclient/configuration.html#clouds-yaml
::
$ openstack
(openstack) search resource type list
+--------------------------+--------------------------+
| Name | Type |
+--------------------------+--------------------------+
| OS::Designate::RecordSet | OS::Designate::RecordSet |
| OS::Designate::Zone | OS::Designate::Zone |
| OS::Glance::Image | OS::Glance::Image |
| OS::Glance::Metadef | OS::Glance::Metadef |
| OS::Nova::Server | OS::Nova::Server |
+--------------------------+--------------------------+
Here are the full list of subcommands, Use -h to see options:
============================= =======================================
Subcommand Description
============================= =======================================
search facet list List Searchlight Facet
search resource type list List Searchlight Resource Type (Plugin)
search query Search Searchlight resource
============================= =======================================
Python API
----------
To use with keystone as the authentication system::
>>> from keystoneclient.auth.identity import generic
>>> from keystoneclient import session
>>> from searchlightclient import client
>>> auth = generic.Password(auth_url=OS_AUTH_URL, username=OS_USERNAME, password=OS_PASSWORD, tenant_name=OS_TENANT_NAME)
>>> keystone_session = session.Session(auth=auth)
>>> sc = client.Client('1', session=keystone_session)
>>> sc.resource_types.list()
[...]
* License: Apache License, Version 2.0
* Documentation: https://docs.openstack.org/developer/python-searchlightclient
* Source: https://opendev.org/openstack/python-searchlightclient
* Bugs: https://storyboard.openstack.org/#!/project_group/searchlight
Testing
-------
There are multiple test targets that can be run to validate the code.
* tox -e pep8 - style guidelines enforcement
* tox -e py36 - traditional unit testing with python 3.6
* tox -e py37 - traditional unit testing with python 3.7
For any further questions, please email
openstack-discuss@lists.openstack.org or join #openstack-dev on
Freenode.

View File

@ -1,90 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXSOURCE = source
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE)
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-searchlightclient.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-searchlightclient.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -1,7 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
openstackdocstheme>=2.2.0 # Apache-2.0
sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD
sphinx>=2.0.0,!=2.1.0 # BSD

View File

@ -1,220 +0,0 @@
# 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.
# This file is execfile()d with the current directory set to its containing
# dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'openstackdocstheme',
'sphinxcontrib.rsvgconverter']
autoclass_content = 'both'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
copyright = 'OpenStack Contributors'
# openstackdocstheme options
openstackdocs_repo_name = 'openstack/python-searchlightclient'
openstackdocs_pdf_link = True
openstackdocs_bug_project = 'python-searchlightclient'
openstackdocs_bug_tag = ''
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# Grouping the document tree for man pages.
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
man_pages = [
('man/openstack', 'openstack', 'OpenStack Search command line client',
['OpenStack Contributors'], 1),
]
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'openstackdocs'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-searchlightclientdoc'
# -- Options for LaTeX output -------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
latex_use_xindy = False
latex_domain_indices = False
latex_elements = {
# Additional stuff for the LaTeX preamble.
# openany: Skip blank pages in generated PDFs
'extraclassoptions': 'openany,oneside',
'makeindex': '',
'printindex': '',
'preamble': r'\setcounter{tocdepth}{3}',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual])
# .
latex_documents = [
('index', 'doc-python-searchlightclient.tex',
'python-searchlightclient Documentation',
'OpenStack Foundation', 'manual', True),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True

View File

@ -1,20 +0,0 @@
============
Contributing
============
Code is hosted at `opendev.org`_. Submit bugs to the searchlight project
on `Storyboard`_. Submit code to the openstack/python-searchlightclient project
using `Gerrit`_.
.. _opendev.org: https://opendev.org/openstack/python-searchlightclient
.. _Storyboard: https://storyboard.openstack.org/#!/project_group/93
.. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow
Testing
-------
The preferred way to run the unit tests is using ``tox``.
See `Consistent Testing Interface`_ for more details.
.. _Consistent Testing Interface: https://opendev.org/openstack/governance/tree/reference/project-testing-interface.rst

View File

@ -1,24 +0,0 @@
Python bindings to the OpenStack Searchlight API
================================================
This is a client for OpenStack Searchlight API. There's a Python API
(the :mod:`searchlightclient` module), and a command-line script
(installed as :program:`openstack`).
Contents
--------
.. toctree::
:maxdepth: 2
contribute/index
Man Page
--------
.. toctree::
:maxdepth: 1
man/openstack
man/search

View File

@ -1,7 +0,0 @@
=========
openstack
=========
Please refer to OpenStack command-line interface `OpenStackClient`_.
.. _OpenStackClient: https://docs.openstack.org/python-openstackclient/latest/

View File

@ -1,78 +0,0 @@
======
Search
======
search facet list
-----------------
List Searchlight Facets.
.. program:: search facet list
.. code:: bash
openstack search facet list
[--type <resource-type>]
[--limit-terms <limit>]
[--all-projects]
.. option:: --type <resource-type>
Get facets for a particular resource type.
.. option:: --limit-terms <limit>
Restricts the number of options returned for fields that
support facet terms.
.. option:: --all-projects
Request facet terms for all projects (admin only).
search resource type list
-------------------------
List Searchlight Resource Type (Plugin).
.. program:: search resource type list
.. code:: bash
openstack search resource type list
search query
------------
Search Searchlight resource.
.. program:: search query
.. code:: bash
openstack search query
--type [<resource-type> [<resource-type> ...]]
--all-projects
--source [[<field>,...]]
--json
<query>
.. option:: --type [<resource-type> [<resource-type> ...]]
One or more types to search. Uniquely identifies
resource types. Example: ``--type OS::Glance::Image
OS::Nova::Server``
.. option:: --all-projects
By default searches are restricted to the current project
unless all_projects is set.
.. option:: --source [[<field>,...]]
Whether to display the json source. If not specified,
it will not be displayed. If specified with no argument,
the full source will be displayed. Otherwise, specify
the fields combined with ',' to return the fields you want.
It is recommended that you use the ``--max-width`` argument
with this option.
.. option:: --json
Treat the query argument as a JSON formatted DSL query.

View File

@ -1,71 +0,0 @@
alabaster==0.7.10
appdirs==1.3.0
asn1crypto==0.23.0
Babel==2.3.4
cffi==1.14.0
cliff==2.8.0
cmd2==0.8.0
coverage==4.0
cryptography==2.7
debtcollector==1.2.0
decorator==3.4.0
deprecation==1.0
docutils==0.11
dogpile.cache==0.6.2
dulwich==0.15.0
extras==1.0.0
fixtures==3.0.0
idna==2.6
imagesize==0.7.1
iso8601==0.1.11
Jinja2==2.10
jmespath==0.9.0
jsonpatch==1.16
jsonpointer==1.13
jsonschema==2.6.0
keystoneauth1==3.4.0
linecache2==1.0.0
MarkupSafe==1.0
monotonic==0.6
msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
openstacksdk==0.11.2
os-client-config==1.28.0
os-service-types==1.2.0
osc-lib==1.8.0
oslo.config==5.2.0
oslo.i18n==3.15.3
oslo.serialization==2.18.0
oslo.utils==3.33.0
pbr==2.0.0
positional==1.2.1
prettytable==0.7.1
pycparser==2.18
Pygments==2.2.0
pyOpenSSL==17.1.0
pyparsing==2.1.0
pyperclip==1.5.27
python-cinderclient==3.3.0
python-glanceclient==2.8.0
python-keystoneclient==3.8.0
python-mimeparse==1.6.0
python-novaclient==9.1.0
python-openstackclient==3.12.0
python-subunit==1.0.0
pytz==2013.6
PyYAML==3.13
requests==2.14.2
requestsexceptions==1.2.0
rfc3986==0.3.1
simplejson==3.5.1
snowballstemmer==1.2.1
stevedore==1.20.0
stestr==2.0.0
testrepository==0.0.18
testtools==2.2.0
traceback2==1.4.0
unittest2==1.1.0
warlock==1.2.0
wrapt==1.7.0

View File

@ -1,6 +0,0 @@
---
upgrade:
- |
Python 2.7 support has been dropped. Last release of python-searchlightclient
to support python 2.7 is OpenStack Train. The minimum version of Python now
supported is Python 3.6.

View File

@ -1,15 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
Babel!=2.4.0,>=2.3.4 # BSD
pbr!=2.1.0,>=2.0.0 # Apache-2.0
osc-lib>=1.8.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
oslo.i18n>=3.15.3 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
python-keystoneclient>=3.8.0 # Apache-2.0
python-openstackclient>=3.12.0 # Apache-2.0
PyYAML>=3.13 # MIT
requests>=2.14.2 # Apache-2.0

View File

@ -1,60 +0,0 @@
# 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
from searchlightclient.common import utils
from searchlightclient import exc
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)
def _construct_http_client(**kwargs):
kwargs = kwargs.copy()
if kwargs.get('session') is None:
raise ValueError("A session instance is required")
return SessionClient(
session=kwargs.get('session'),
auth=kwargs.get('auth'),
region_name=kwargs.get('region_name'),
service_type=kwargs.get('service_type', 'search'),
interface=kwargs.get('endpoint_type', 'public').rstrip('URL'),
user_agent=kwargs.get('user_agent', 'python-searchlightclient'),
endpoint_override=kwargs.get('endpoint_override'),
timeout=kwargs.get('timeout')
)
class SessionClient(adapter.LegacyJsonAdapter):
def __init__(self, *args, **kwargs):
self.timeout = kwargs.pop('timeout', None)
super(SessionClient, self).__init__(*args, **kwargs)
def request(self, *args, **kwargs):
kwargs.setdefault('raise_exc', True)
if self.timeout is not None:
kwargs.setdefault('timeout', self.timeout)
kwargs.setdefault('headers', {}).setdefault(
'Content-Type', 'application/json')
resp, body = super(SessionClient, self).request(*args, **kwargs)
if kwargs.get('raise_exc') and resp.status_code >= 400:
raise exc.from_response(resp, body)
return resp

View File

@ -1,530 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 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.
"""
Base utilities to build API operation managers and objects on top of.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
from urllib import parse
from searchlightclient.common import exceptions
from searchlightclient.i18n import _
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param args: args to be passed to every hook function
:param kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key=None, obj_class=None, json=None):
"""List the collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
:param obj_class: class for constructing the returned objects
(self.resource_class will be used by default)
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
"""
if json:
body = self.client.post(url, json=json).json()
else:
body = self.client.get(url).json()
if obj_class is None:
obj_class = self.resource_class
data = body[response_key] if response_key is not None else body
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
try:
data = data['values']
except (KeyError, TypeError):
pass
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key=None):
"""Get an object from collection.
:param url: a partial URL, e.g., '/servers'
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
"""
body = self.client.get(url).json()
data = body[response_key] if response_key is not None else body
return self.resource_class(self, data, loaded=True)
def _head(self, url):
"""Retrieve request headers for an object.
:param url: a partial URL, e.g., '/servers'
"""
resp = self.client.head(url)
return resp.status_code == 204
def _post(self, url, json, response_key=None, return_raw=False):
"""Create an object.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'server'. If response_key is None - all response body
will be used.
:param return_raw: flag to force returning raw JSON instead of
Python object of self.resource_class
"""
body = self.client.post(url, json=json).json()
data = body[response_key] if response_key is not None else body
if return_raw:
return data
return self.resource_class(self, data)
def _put(self, url, json=None, response_key=None):
"""Update an object with PUT method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
resp = self.client.put(url, json=json)
# PUT requests may not return a body
if resp.content:
body = resp.json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _patch(self, url, json=None, response_key=None):
"""Update an object with PATCH method.
:param url: a partial URL, e.g., '/servers'
:param json: data that will be encoded as JSON and passed in POST
request (GET will be sent by default)
:param response_key: the key to be looked up in response dictionary,
e.g., 'servers'. If response_key is None - all response body
will be used.
"""
body = self.client.patch(url, json=json).json()
if response_key is not None:
return self.resource_class(self, body[response_key])
else:
return self.resource_class(self, body)
def _delete(self, url):
"""Delete an object.
:param url: a partial URL, e.g., '/servers/my-server'
"""
return self.client.delete(url)
class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in kwargs.copy().items():
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return rl[0]
class Extension(HookableMixin):
"""Extension descriptor."""
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
manager_class = None
def __init__(self, name, module):
super(Extension, self).__init__()
self.name = name
self.module = module
self._parse_extension_module()
def _parse_extension_module(self):
self.manager_class = None
for attr_name, attr_value in self.module.__dict__.items():
if attr_name in self.SUPPORTED_HOOKS:
self.add_hook(attr_name, attr_value)
else:
try:
if issubclass(attr_value, BaseManager):
self.manager_class = attr_value
except TypeError:
pass
def __repr__(self):
return "<Extension '%s'>" % self.name
class Resource(object):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
"""
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__
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)
@property
def human_id(self):
"""Human-readable ID which can be used for bash completion.
"""
if self.HUMAN_ID:
name = getattr(self, self.NAME_ATTR, None)
if name is not None:
return strutils.to_slug(name)
return None
def _add_details(self, info):
for (k, v) in info.items():
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
self._add_details(
{'x_request_id': self.manager.client.last_request_id})
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -1,477 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 Nebula, Inc.
# Copyright 2013 Alessio Ababilov
# Copyright 2013 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.
"""
Exception definitions.
"""
########################################################################
#
# THIS MODULE IS DEPRECATED
#
# Please refer to
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
# the discussion leading to this deprecation.
#
# We recommend checking out the python-openstacksdk project
# (https://launchpad.net/python-openstacksdk) instead.
#
########################################################################
import inspect
import sys
from searchlightclient.i18n import _
class ClientException(Exception):
"""The base exception class for all exceptions this library raises.
"""
pass
class ValidationError(ClientException):
"""Error in validation on API client side."""
pass
class UnsupportedVersion(ClientException):
"""User is trying to use an unsupported version of the API."""
pass
class CommandError(ClientException):
"""Error in CLI tool."""
pass
class AuthorizationFailure(ClientException):
"""Cannot authorize API client."""
pass
class ConnectionError(ClientException):
"""Cannot connect to API service."""
pass
class ConnectionRefused(ConnectionError):
"""Connection refused while trying to connect to API service."""
pass
class AuthPluginOptionsMissing(AuthorizationFailure):
"""Auth plugin misses some options."""
def __init__(self, opt_names):
super(AuthPluginOptionsMissing, self).__init__(
_("Authentication failed. Missing options: %s") %
", ".join(opt_names))
self.opt_names = opt_names
class AuthSystemNotFound(AuthorizationFailure):
"""User has specified an AuthSystem that is not installed."""
def __init__(self, auth_system):
super(AuthSystemNotFound, self).__init__(
_("AuthSystemNotFound: %r") % auth_system)
self.auth_system = auth_system
class NoUniqueMatch(ClientException):
"""Multiple entities found instead of one."""
pass
class EndpointException(ClientException):
"""Something is rotten in Service Catalog."""
pass
class EndpointNotFound(EndpointException):
"""Could not find requested endpoint in Service Catalog."""
pass
class AmbiguousEndpoints(EndpointException):
"""Found more than one matching endpoint in Service Catalog."""
def __init__(self, endpoints=None):
super(AmbiguousEndpoints, self).__init__(
_("AmbiguousEndpoints: %r") % endpoints)
self.endpoints = endpoints
class HttpError(ClientException):
"""The base exception class for all HTTP exceptions.
"""
http_status = 0
message = _("HTTP Error")
def __init__(self, message=None, details=None,
response=None, request_id=None,
url=None, method=None, http_status=None):
self.http_status = http_status or self.http_status
self.message = message or self.message
self.details = details
self.request_id = request_id
self.response = response
self.url = url
self.method = method
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
if request_id:
formatted_string += " (Request-ID: %s)" % request_id
super(HttpError, self).__init__(formatted_string)
class HTTPRedirection(HttpError):
"""HTTP Redirection."""
message = _("HTTP Redirection")
class HTTPClientError(HttpError):
"""Client-side HTTP error.
Exception for cases in which the client seems to have erred.
"""
message = _("HTTP Client Error")
class HttpServerError(HttpError):
"""Server-side HTTP error.
Exception for cases in which the server is aware that it has
erred or is incapable of performing the request.
"""
message = _("HTTP Server Error")
class MultipleChoices(HTTPRedirection):
"""HTTP 300 - Multiple Choices.
Indicates multiple options for the resource that the client may follow.
"""
http_status = 300
message = _("Multiple Choices")
class BadRequest(HTTPClientError):
"""HTTP 400 - Bad Request.
The request cannot be fulfilled due to bad syntax.
"""
http_status = 400
message = _("Bad Request")
class Unauthorized(HTTPClientError):
"""HTTP 401 - Unauthorized.
Similar to 403 Forbidden, but specifically for use when authentication
is required and has failed or has not yet been provided.
"""
http_status = 401
message = _("Unauthorized")
class PaymentRequired(HTTPClientError):
"""HTTP 402 - Payment Required.
Reserved for future use.
"""
http_status = 402
message = _("Payment Required")
class Forbidden(HTTPClientError):
"""HTTP 403 - Forbidden.
The request was a valid request, but the server is refusing to respond
to it.
"""
http_status = 403
message = _("Forbidden")
class NotFound(HTTPClientError):
"""HTTP 404 - Not Found.
The requested resource could not be found but may be available again
in the future.
"""
http_status = 404
message = _("Not Found")
class MethodNotAllowed(HTTPClientError):
"""HTTP 405 - Method Not Allowed.
A request was made of a resource using a request method not supported
by that resource.
"""
http_status = 405
message = _("Method Not Allowed")
class NotAcceptable(HTTPClientError):
"""HTTP 406 - Not Acceptable.
The requested resource is only capable of generating content not
acceptable according to the Accept headers sent in the request.
"""
http_status = 406
message = _("Not Acceptable")
class ProxyAuthenticationRequired(HTTPClientError):
"""HTTP 407 - Proxy Authentication Required.
The client must first authenticate itself with the proxy.
"""
http_status = 407
message = _("Proxy Authentication Required")
class RequestTimeout(HTTPClientError):
"""HTTP 408 - Request Timeout.
The server timed out waiting for the request.
"""
http_status = 408
message = _("Request Timeout")
class Conflict(HTTPClientError):
"""HTTP 409 - Conflict.
Indicates that the request could not be processed because of conflict
in the request, such as an edit conflict.
"""
http_status = 409
message = _("Conflict")
class Gone(HTTPClientError):
"""HTTP 410 - Gone.
Indicates that the resource requested is no longer available and will
not be available again.
"""
http_status = 410
message = _("Gone")
class LengthRequired(HTTPClientError):
"""HTTP 411 - Length Required.
The request did not specify the length of its content, which is
required by the requested resource.
"""
http_status = 411
message = _("Length Required")
class PreconditionFailed(HTTPClientError):
"""HTTP 412 - Precondition Failed.
The server does not meet one of the preconditions that the requester
put on the request.
"""
http_status = 412
message = _("Precondition Failed")
class RequestEntityTooLarge(HTTPClientError):
"""HTTP 413 - Request Entity Too Large.
The request is larger than the server is willing or able to process.
"""
http_status = 413
message = _("Request Entity Too Large")
def __init__(self, *args, **kwargs):
try:
self.retry_after = int(kwargs.pop('retry_after'))
except (KeyError, ValueError):
self.retry_after = 0
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
class RequestUriTooLong(HTTPClientError):
"""HTTP 414 - Request-URI Too Long.
The URI provided was too long for the server to process.
"""
http_status = 414
message = _("Request-URI Too Long")
class UnsupportedMediaType(HTTPClientError):
"""HTTP 415 - Unsupported Media Type.
The request entity has a media type which the server or resource does
not support.
"""
http_status = 415
message = _("Unsupported Media Type")
class RequestedRangeNotSatisfiable(HTTPClientError):
"""HTTP 416 - Requested Range Not Satisfiable.
The client has asked for a portion of the file, but the server cannot
supply that portion.
"""
http_status = 416
message = _("Requested Range Not Satisfiable")
class ExpectationFailed(HTTPClientError):
"""HTTP 417 - Expectation Failed.
The server cannot meet the requirements of the Expect request-header field.
"""
http_status = 417
message = _("Expectation Failed")
class UnprocessableEntity(HTTPClientError):
"""HTTP 422 - Unprocessable Entity.
The request was well-formed but was unable to be followed due to semantic
errors.
"""
http_status = 422
message = _("Unprocessable Entity")
class InternalServerError(HttpServerError):
"""HTTP 500 - Internal Server Error.
A generic error message, given when no more specific message is suitable.
"""
http_status = 500
message = _("Internal Server Error")
# NotImplemented is a python keyword.
class HttpNotImplemented(HttpServerError):
"""HTTP 501 - Not Implemented.
The server either does not recognize the request method, or it lacks
the ability to fulfill the request.
"""
http_status = 501
message = _("Not Implemented")
class BadGateway(HttpServerError):
"""HTTP 502 - Bad Gateway.
The server was acting as a gateway or proxy and received an invalid
response from the upstream server.
"""
http_status = 502
message = _("Bad Gateway")
class ServiceUnavailable(HttpServerError):
"""HTTP 503 - Service Unavailable.
The server is currently unavailable.
"""
http_status = 503
message = _("Service Unavailable")
class GatewayTimeout(HttpServerError):
"""HTTP 504 - Gateway Timeout.
The server was acting as a gateway or proxy and did not receive a timely
response from the upstream server.
"""
http_status = 504
message = _("Gateway Timeout")
class HttpVersionNotSupported(HttpServerError):
"""HTTP 505 - HttpVersion Not Supported.
The server does not support the HTTP protocol version used in the request.
"""
http_status = 505
message = _("HTTP Version Not Supported")
# _code_map contains all the classes that have http_status attribute.
_code_map = dict(
(getattr(obj, 'http_status', None), obj)
for name, obj in vars(sys.modules[__name__]).items()
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, method, url):
"""Returns an instance of :class:`HttpError` or subclass based on response.
:param response: instance of `requests.Response` class
:param method: HTTP method used for request
:param url: URL used for request
"""
req_id = response.headers.get("x-openstack-request-id")
# NOTE(hdd) true for older versions of nova and cinder
if not req_id:
req_id = response.headers.get("x-compute-request-id")
kwargs = {
"http_status": response.status_code,
"response": response,
"method": method,
"url": url,
"request_id": req_id,
}
if "retry-after" in response.headers:
kwargs["retry_after"] = response.headers["retry-after"]
content_type = response.headers.get("Content-Type", "")
if content_type.startswith("application/json"):
try:
body = response.json()
except ValueError:
pass
else:
if isinstance(body, dict):
error = body.get(list(body)[0])
if isinstance(error, dict):
kwargs["message"] = (error.get("message") or
error.get("faultstring"))
kwargs["details"] = (error.get("details") or
str(body))
elif content_type.startswith("text/"):
kwargs["details"] = getattr(response, 'text', '')
try:
cls = _code_map[response.status_code]
except KeyError:
if 500 <= response.status_code < 600:
cls = HttpServerError
elif 400 <= response.status_code < 500:
cls = HTTPClientError
else:
cls = HttpError
return cls(**kwargs)

View File

@ -1,313 +0,0 @@
# Copyright 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.
import base64
import logging
import os
import textwrap
import uuid
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import importutils
import prettytable
from urllib import error
from urllib import parse
from urllib import request
import yaml
from searchlightclient import exc
from searchlightclient.i18n import _
LOG = logging.getLogger(__name__)
supported_formats = {
"json": lambda x: jsonutils.dumps(x, indent=2),
"yaml": yaml.safe_dump
}
def env(*args, **kwargs):
"""Returns the first environment variable set.
If all are empty, defaults to '' or keyword arg `default`.
"""
for arg in args:
value = os.environ.get(arg)
if value:
return value
return kwargs.get('default', '')
def link_formatter(links):
def format_link(l):
if 'rel' in l:
return "%s (%s)" % (l.get('href', ''), l.get('rel', ''))
else:
return "%s" % (l.get('href', ''))
return '\n'.join(format_link(l) for l in links or [])
def resource_nested_identifier(rsrc):
nested_link = [l for l in rsrc.links or []
if l.get('rel') == 'nested']
if nested_link:
nested_href = nested_link[0].get('href')
nested_identifier = nested_href.split("/")[-2:]
return "/".join(nested_identifier)
def json_formatter(js):
return jsonutils.dumps(js, indent=2, ensure_ascii=False,
separators=(', ', ': '))
def yaml_formatter(js):
return yaml.safe_dump(js, default_flow_style=False)
def text_wrap_formatter(d):
return '\n'.join(textwrap.wrap(d or '', 55))
def newline_list_formatter(r):
return '\n'.join(r or [])
def print_dict(d, formatters=None):
formatters = formatters or {}
pt = prettytable.PrettyTable(['Property', 'Value'],
caching=False, print_empty=False)
pt.align = 'l'
for field in d:
if field in formatters:
pt.add_row([field, formatters[field](d[field])])
else:
pt.add_row([field, d[field]])
print(pt.get_string(sortby='Property'))
def event_log_formatter(events):
"""Return the events in log format."""
event_log = []
log_format = _("%(event_date)s %(event_time)s %(event_id)s "
"[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s")
for event in events:
event_time = getattr(event, 'event_time', '')
time_date = event_time.split('T')
try:
event_date = time_date[0]
event_time = time_date[1]
except IndexError:
event_time = event_date = ''
log = log_format % {
'event_date': event_date, 'event_time': event_time,
'event_id': getattr(event, 'id', ''),
'rsrc_name': getattr(event, 'resource_name', ''),
'rsrc_status': getattr(event, 'resource_status', ''),
'rsrc_status_reason': getattr(event, 'resource_status_reason', '')
}
event_log.append(log)
return "\n".join(event_log)
def print_update_list(lst, fields, formatters=None):
"""Print the stack-update --dry-run output as a table.
This function is necessary to print the stack-update --dry-run
output, which contains additional information about the update.
"""
formatters = formatters or {}
pt = prettytable.PrettyTable(fields, caching=False, print_empty=False)
pt.align = 'l'
for change in lst:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](change.get(field, None)))
else:
row.append(change.get(field, None))
pt.add_row(row)
print(encodeutils.safe_encode(pt.get_string()).decode())
def find_resource(manager, name_or_id):
"""Helper for the _find_* methods."""
# first try to get entity as integer id
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
except exc.NotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id)
except (ValueError, exc.NotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exc.NotFound:
msg = _("No %(name)s with a name or ID of "
"'%(name_or_id)s' exists.") % \
{
'name': manager.resource_class.__name__.lower(),
'name_or_id': name_or_id}
raise exc.CommandError(msg)
def import_versioned_module(version, submodule=None):
module = 'searchlightclient.v%s' % version
if submodule:
module = '.'.join((module, submodule))
return importutils.import_module(module)
def format_parameters(params, parse_semicolon=True):
'''Reformat parameters into dict of format expected by the API.'''
if not params:
return {}
if parse_semicolon:
# expect multiple invocations of --parameters but fall back
# to ; delimited if only one --parameters is specified
if len(params) == 1:
params = params[0].split(';')
parameters = {}
for p in params:
try:
(n, v) = p.split(('='), 1)
except ValueError:
msg = _('Malformed parameter(%s). Use the key=value format.') % p
raise exc.CommandError(msg)
if n not in parameters:
parameters[n] = v
else:
if not isinstance(parameters[n], list):
parameters[n] = [parameters[n]]
parameters[n].append(v)
return parameters
def format_all_parameters(params, param_files,
template_file=None, template_url=None):
parameters = {}
parameters.update(format_parameters(params))
parameters.update(format_parameter_file(
param_files,
template_file,
template_url))
return parameters
def format_parameter_file(param_files, template_file=None,
template_url=None):
'''Reformat file parameters into dict of format expected by the API.'''
if not param_files:
return {}
params = format_parameters(param_files, False)
template_base_url = None
if template_file or template_url:
template_base_url = base_url_for_url(get_template_url(
template_file, template_url))
param_file = {}
for key, value in iter(params.items()):
param_file[key] = resolve_param_get_file(value,
template_base_url)
return param_file
def resolve_param_get_file(file, base_url):
if base_url and not base_url.endswith('/'):
base_url = base_url + '/'
str_url = parse.urljoin(base_url, file)
return read_url_content(str_url)
def format_output(output, format='yaml'):
"""Format the supplied dict as specified."""
output_format = format.lower()
try:
return supported_formats[output_format](output)
except KeyError:
raise exc.HTTPUnsupported(_("The format(%s) is unsupported.")
% output_format)
def parse_query_url(url):
base_url, query_params = url.split('?')
return base_url, parse.parse_qs(query_params)
def get_template_url(template_file=None, template_url=None):
if template_file:
template_url = normalise_file_path_to_url(template_file)
return template_url
def read_url_content(url):
try:
content = request.urlopen(url).read()
except error.URLError:
raise exc.CommandError(_('Could not fetch contents for %s') % url)
if content:
try:
content.decode('utf-8')
except ValueError:
content = base64.encodestring(content)
return content
def base_url_for_url(url):
parsed = parse.urlparse(url)
parsed_dir = os.path.dirname(parsed.path)
return parse.urljoin(url, parsed_dir)
def normalise_file_path_to_url(path):
if parse.urlparse(path).scheme:
return path
path = os.path.abspath(path)
return parse.urljoin('file:', request.pathname2url(path))
def get_response_body(resp):
body = resp.content
if 'application/json' in resp.headers.get('content-type', ''):
try:
body = resp.json()
except ValueError:
LOG.error('Could not decode response body as JSON')
else:
body = None
return body

View File

@ -1,191 +0,0 @@
# 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
from oslo_serialization import jsonutils
from searchlightclient.i18n import _
verbose = 0
class BaseException(Exception):
"""An error occurred."""
def __init__(self, message=None):
self.message = message
def __str__(self):
return self.message or self.__class__.__doc__
class CommandError(BaseException):
"""Invalid usage of CLI."""
class InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""
class CommunicationError(BaseException):
"""Unable to communicate with server."""
class HTTPException(BaseException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, message=None):
super(HTTPException, self).__init__(message)
try:
self.error = jsonutils.loads(message)
if 'error' not in self.error:
raise KeyError(_('Key "error" not exists'))
except KeyError:
# NOTE(jianingy): If key 'error' happens not exist,
# self.message becomes no sense. In this case, we
# return doc of current exception class instead.
self.error = {'error':
{'message': self.__class__.__doc__}}
except Exception:
self.error = {'error':
{'message': self.message or self.__class__.__doc__}}
def __str__(self):
message = self.error['error'].get('message', 'Internal Error')
if verbose:
traceback = self.error['error'].get('traceback', '')
return (_('ERROR: %(message)s\n%(traceback)s') %
{'message': message, 'traceback': traceback})
else:
return _('ERROR: %s') % message
class HTTPMultipleChoices(HTTPException):
code = 300
def __str__(self):
self.details = _("Requested version of Searchlight API is not"
"available.")
return (_("%(name)s (HTTP %(code)s) %(details)s") %
{
'name': self.__class__.__name__,
'code': self.code,
'details': self.details})
class BadRequest(HTTPException):
"""DEPRECATED."""
code = 400
class HTTPBadRequest(BadRequest):
pass
class Unauthorized(HTTPException):
"""DEPRECATED."""
code = 401
class HTTPUnauthorized(Unauthorized):
pass
class Forbidden(HTTPException):
"""DEPRECATED."""
code = 403
class HTTPForbidden(Forbidden):
pass
class NotFound(HTTPException):
"""DEPRECATED."""
code = 404
class HTTPNotFound(NotFound):
pass
class HTTPMethodNotAllowed(HTTPException):
code = 405
class Conflict(HTTPException):
"""DEPRECATED."""
code = 409
class HTTPConflict(Conflict):
pass
class OverLimit(HTTPException):
"""DEPRECATED."""
code = 413
class HTTPOverLimit(OverLimit):
pass
class HTTPUnsupported(HTTPException):
code = 415
class HTTPInternalServerError(HTTPException):
code = 500
class HTTPNotImplemented(HTTPException):
code = 501
class HTTPBadGateway(HTTPException):
code = 502
class ServiceUnavailable(HTTPException):
"""DEPRECATED."""
code = 503
class HTTPServiceUnavailable(ServiceUnavailable):
pass
# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
# classes
_code_map = {}
for obj_name in dir(sys.modules[__name__]):
if obj_name.startswith('HTTP'):
obj = getattr(sys.modules[__name__], obj_name)
_code_map[obj.code] = obj
def from_response(response):
"""Return an instance of an HTTPException based on requests response."""
cls = _code_map.get(response.status_code, HTTPException)
return cls(response.content)
class NoTokenLookupException(Exception):
"""DEPRECATED."""
pass
class EndpointNotFound(Exception):
"""DEPRECATED."""
pass

View File

@ -1,25 +0,0 @@
# 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.
"""oslo_i18n integration module for searchlightclient.
See https://docs.openstack.org/oslo.i18n/latest/user/index.html.
"""
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='searchlightclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary

View File

@ -1,61 +0,0 @@
# 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.
#
"""OpenStackClient plugin for Search service."""
import logging
from osc_lib import utils
DEFAULT_SEARCH_API_VERSION = '1'
API_VERSION_OPTION = 'os_search_api_version'
API_NAME = 'search'
API_VERSIONS = {
'1': 'searchlightclient.v1.client.Client',
}
def make_client(instance):
"""Returns a search service client"""
search_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
# Set client http_log_debug to True if verbosity level is high enough
http_log_debug = utils.get_effective_log_level() <= logging.DEBUG
# Remember interface only if it is set
kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface)
client = search_client(
session=instance.session,
http_log_debug=http_log_debug,
region_name=instance._region_name,
**kwargs
)
return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-search-api-version',
metavar='<search-api-version>',
default=utils.env(
'OS_SEARCH_API_VERSION',
default=DEFAULT_SEARCH_API_VERSION),
help='Search API version, default=' +
DEFAULT_SEARCH_API_VERSION +
' (Env: OS_SEARCH_API_VERSION)')
return parser

View File

@ -1,79 +0,0 @@
# 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.
#
"""Searchlight v1 Facet action implementations"""
import logging
from osc_lib.command import command
from osc_lib import utils
class ListFacet(command.Lister):
"""List Searchlight Facet."""
log = logging.getLogger(__name__ + ".ListFacet")
def get_parser(self, prog_name):
parser = super(ListFacet, self).get_parser(prog_name)
parser.add_argument(
"--type",
metavar="<resource-type>",
help="Get facets for a particular resource type"
)
parser.add_argument(
"--limit-terms",
metavar="<limit>",
help="Restricts the number of options returned for "
"fields that support facet terms"
)
parser.add_argument(
"--all-projects",
action='store_true',
default=False,
help="Request facet terms for all projects (admin only)"
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
search_client = self.app.client_manager.search
columns = (
"Resource Type",
"Type",
"Name",
"Options"
)
params = {
"type": parsed_args.type,
"limit_terms": parsed_args.limit_terms,
"all_projects": parsed_args.all_projects
}
data = search_client.facets.list(**params)
result = []
for resource_type, values in data.items():
if isinstance(values, list):
# Cope with pre-1.0 service APIs
facets = values
else:
facets = values['facets']
for s in facets:
options = []
for o in s.get('options', []):
options.append(
str(o['key']) + '(' + str(o['doc_count']) + ')')
s["options"] = ', '.join(options)
s["resource_type"] = resource_type
result.append(utils.get_dict_properties(s, columns))
return (columns, result)

View File

@ -1,49 +0,0 @@
# 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.
#
"""Searchlight v1 Resource Type action implementations"""
import logging
from osc_lib.command import command
class ListResourceType(command.Lister):
"""List Searchlight Resource Type (Plugin)."""
log = logging.getLogger(__name__ + ".ListResourceType")
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
search_client = self.app.client_manager.search
columns = (
"Alias Searching",
"Alias Indexing",
"Type"
)
data = search_client.resource_types.list()
return (columns,
(self.get_item_properties(
s, columns,
) for s in data))
def get_item_properties(self, item, fields):
# osc_lib.utils.get_item_properties doesn't work because
# the field names are using "-" instead of "_".
row = []
for field in fields:
field_name = field.lower().replace(' ', '-')
data = getattr(item, field_name, '')
row.append(data)
return tuple(row)

View File

@ -1,136 +0,0 @@
# 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.
#
"""Searchlight v1 Search action implementations"""
import logging
from oslo_serialization import jsonutils
from osc_lib.command import command
from osc_lib import utils
class SearchResource(command.Lister):
"""Search Searchlight resource."""
log = logging.getLogger(__name__ + ".SearchResource")
def get_parser(self, prog_name):
parser = super(SearchResource, self).get_parser(prog_name)
parser.add_argument(
"query",
metavar="<query>",
help="Query resources by Elasticsearch query string or json "
"format DSL. Query string example: 'name: cirros AND "
"updated_at: [now-1y TO now]'. DSL example: "
"'{\"term\": {\"name\": \"cirros\"}}'. "
"See Elasticsearch DSL or Searchlight documentation for "
"more detail."
)
parser.add_argument(
"--json",
action='store_true',
default=False,
help="Treat the query argument as a JSON formatted DSL query."
)
parser.add_argument(
"--type",
nargs='*',
metavar="<resource-type>",
help="One or more types to search. Uniquely identifies resource "
"types. Example: --type OS::Glance::Image "
"OS::Nova::Server"
)
parser.add_argument(
"--all-projects",
action='store_true',
default=False,
help="By default searches are restricted to the current project "
"unless all_projects is set"
)
parser.add_argument(
"--source",
nargs='?',
const='all_sources',
metavar="[<field>,...]",
help="Whether to display the json source. If not specified, "
"it will not be displayed. If specified with no argument, "
"the full source will be displayed. Otherwise, specify the "
"fields combined with ',' to return the fields you want. "
"It is recommended that you use the --max-width argument "
"with this option."
)
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)", parsed_args)
search_client = self.app.client_manager.search
mapping = {"_score": "score", "_type": "type", "_id": "id",
"_index": "index", "_source": "source"}
params = {
"type": parsed_args.type,
"all_projects": parsed_args.all_projects
}
source = parsed_args.source
if source:
columns = ("ID", "Score", "Type", "Source")
if source != "all_sources":
params["_source"] = (["id"] +
[s for s in source.split(",") if s != 'id'])
else:
columns = ("ID", "Name", "Score", "Type", "Updated")
# Only return the required fields when source not specified.
params["_source"] = ["id", "name", "updated_at"]
if parsed_args.query:
if parsed_args.json:
query = jsonutils.loads(parsed_args.query)
else:
try:
jsonutils.loads(parsed_args.query)
print("You should use the --json flag when specifying "
"a JSON object.")
exit(1)
except Exception:
qs = self._modify_query_string(parsed_args.query)
query = {"query_string": {"query": qs}}
params['query'] = query
data = search_client.search.search(**params)
result = []
for r in data.hits['hits']:
converted = {}
extra = {}
# hit._id may include extra information appended after _,
# so use r['_source']['id'] for safe.
r['_id'] = r.get('_source', {}).get('id')
for k, v in r.items():
map_key = mapping.get(k)
if map_key is not None:
converted[map_key] = v
if k == "_source" and not parsed_args.source:
converted["name"] = v.get("name")
converted["updated"] = v.get("updated_at")
else:
extra[k] = v
if extra:
self.log.debug("extra info returned: %s", extra)
result.append(utils.get_dict_properties(converted, columns))
return (columns, result)
def _modify_query_string(self, query_string):
return query_string.replace(r'/', r'\/')

View File

@ -1,67 +0,0 @@
# 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
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
class FakeClientManager(object):
def __init__(self):
self.session = None
self.auth_ref = None
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 info.items():
setattr(self, k, v)
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__ 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,93 +0,0 @@
# 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 fixtures
import sys
import testtools
from searchlightclient.tests.unit.osc 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 osc_lib 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

@ -1,89 +0,0 @@
# Copyright 2014 OpenStack Foundation
#
# 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 unittest import mock
from searchlightclient.tests.unit.osc import fakes
from searchlightclient.tests.unit.osc import utils
ResourceType = {
"alias-searching": "searchlight-search",
"alias-indexing": "searchlight-listener",
"type": "OS::Nova::Server"
}
OldFacet = {
"OS::Nova::Server":
[
{"type": "string", "name": "id"},
{"type": "date", "name": "created_at"},
],
}
# The alternate facet format introduced in
# https://blueprints.launchpad.net/searchlight/+spec/count-endpoint
Facet = {
"OS::Nova::Server":
{
"doc_count": 2,
"facets": [
{"type": "string", "name": "id"},
{"type": "date", "name": "created_at"},
]
}
}
Resource = {
"hits":
{"hits":
[
{"_score": 0.3, "_type": "OS::Glance::Image", "_id": "1",
"_source": {"id": "1", "name": "image1",
"updated_at": "2016-01-01T00:00:00Z"}},
{"_score": 0.3, "_type": "OS::Nova::Server", "_id": "2_ADMIN",
"_source": {"id": "2", "name": "instance1",
"updated_at": "2016-01-01T00:00:00Z"}},
],
"_shards": {"successful": 5, "failed": 0, "total": 5},
"took": 5, "timed_out": False
}
}
class FakeSearchv1Client(object):
def __init__(self, **kwargs):
self.http_client = mock.Mock()
self.http_client.auth_token = kwargs['token']
self.http_client.management_url = kwargs['endpoint']
self.resource_types = mock.Mock()
self.resource_types.list = mock.Mock(return_value=[])
self.facets = mock.Mock()
self.facets.list = mock.Mock(return_value=[])
self.search = mock.Mock()
self.search.search = mock.Mock(return_value=[])
class TestSearchv1(utils.TestCommand):
def setUp(self):
super(TestSearchv1, self).setUp()
self.app.client_manager.search = FakeSearchv1Client(
endpoint=fakes.AUTH_URL,
token=fakes.AUTH_TOKEN,
)

View File

@ -1,70 +0,0 @@
# 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 searchlightclient.osc.v1 import facet
from searchlightclient.tests.unit.osc.v1 import fakes as searchlight_fakes
class TestFacet(searchlight_fakes.TestSearchv1):
def setUp(self):
super(TestFacet, self).setUp()
self.facet_client = self.app.client_manager.search.facets
class TestFacetListBase(TestFacet):
def setUp(self):
super(TestFacetListBase, self).setUp()
self.cmd = facet.ListFacet(self.app, None)
self.facet_client.list.return_value = searchlight_fakes.Facet
def _test_list(self, arglist, **assertArgs):
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.facet_client.list.assert_called_with(**assertArgs)
collist = ('Resource Type', 'Type', 'Name', 'Options')
self.assertEqual(collist, columns)
datalist = (
('OS::Nova::Server', 'string', 'id', ''),
('OS::Nova::Server', 'date', 'created_at', ''),
)
self.assertEqual(datalist, tuple(data))
class TestFacetList(TestFacetListBase):
def test_list(self):
self._test_list([], all_projects=False, limit_terms=None, type=None)
def test_list_all_projects(self):
self._test_list(['--all-projects'],
all_projects=True, limit_terms=None, type=None)
def test_list_with_type(self):
self._test_list(['--type', 'fake_res_type'],
all_projects=False,
limit_terms=None, type='fake_res_type')
def test_list_with_limit_terms(self):
self._test_list(['--limit-terms', 'fake_limit'],
all_projects=False,
limit_terms='fake_limit', type=None)
class TestOldFacetList(TestFacetListBase):
def setUp(self):
super(TestOldFacetList, self).setUp()
self.facet_client.list.return_value = searchlight_fakes.OldFacet
def test_list(self):
self._test_list([], all_projects=False, limit_terms=None, type=None)

View File

@ -1,51 +0,0 @@
# 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
from searchlightclient.osc.v1 import resource_type
from searchlightclient.tests.unit.osc import fakes
from searchlightclient.tests.unit.osc.v1 import fakes as searchlight_fakes
class TestResourceType(searchlight_fakes.TestSearchv1):
def setUp(self):
super(TestResourceType, self).setUp()
self.rtype_client = self.app.client_manager.search.resource_types
class TestResourceTypeList(TestResourceType):
def setUp(self):
super(TestResourceTypeList, self).setUp()
self.cmd = resource_type.ListResourceType(self.app, None)
self.rtype_client.list.return_value = [
fakes.FakeResource(
None,
copy.deepcopy(searchlight_fakes.ResourceType),
loaded=True,
),
]
def test_list(self):
parsed_args = self.check_parser(self.cmd, [], [])
columns, data = self.cmd.take_action(parsed_args)
self.rtype_client.list.assert_called_with()
collist = ('Alias Searching', 'Alias Indexing', 'Type')
self.assertEqual(collist, columns)
datalist = (('searchlight-search',
'searchlight-listener',
'OS::Nova::Server'),)
self.assertEqual(datalist, tuple(data))

View File

@ -1,133 +0,0 @@
# 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
from searchlightclient.osc.v1 import search
from searchlightclient.tests.unit.osc import fakes
from searchlightclient.tests.unit.osc.v1 import fakes as searchlight_fakes
class TestSearch(searchlight_fakes.TestSearchv1):
def setUp(self):
super(TestSearch, self).setUp()
self.search_client = self.app.client_manager.search.search
class TestSearchResource(TestSearch):
def setUp(self):
super(TestSearchResource, self).setUp()
self.cmd = search.SearchResource(self.app, None)
fake_data = copy.deepcopy(searchlight_fakes.Resource)
fake_data['hits']['hits'][0]['is_not_processed'] = 'foo'
self.search_client.search.return_value = \
fakes.FakeResource(None, fake_data, loaded=True)
def _test_search(self, arglist, **assertArgs):
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
details = False
if assertArgs.get("source"):
details = True
if assertArgs.get("source") == "all_resources":
assertArgs.pop("source")
else:
assertArgs["_source"] = assertArgs.pop("source")
self.search_client.search.assert_called_with(**assertArgs)
if details:
collist = ("ID", "Score", "Type", "Source")
datalist = (
('1', 0.3, 'OS::Glance::Image',
{'id': '1', 'name': 'image1',
'updated_at': '2016-01-01T00:00:00Z'}),
('2', 0.3, 'OS::Nova::Server',
{'id': '2', 'name': 'instance1',
'updated_at': '2016-01-01T00:00:00Z'}))
else:
collist = ("ID", "Name", "Score", "Type", "Updated")
datalist = (('1', 'image1', 0.3, 'OS::Glance::Image',
'2016-01-01T00:00:00Z'),
('2', 'instance1', 0.3, 'OS::Nova::Server',
'2016-01-01T00:00:00Z'))
self.assertEqual(collist, columns)
self.assertEqual(datalist, tuple(data))
def test_search(self):
self._test_search(["name: fake"],
query={"query_string": {"query": "name: fake"}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_search_resource(self):
self._test_search(["name: fake", "--type", "res1", "res2"],
query={"query_string": {"query": "name: fake"}},
_source=['id', 'name', 'updated_at'],
type=["res1", "res2"],
all_projects=False)
def test_search_query_string(self):
self._test_search(["name: fake"],
query={"query_string": {"query": "name: fake"}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_search_regexp_slashes_in_query_string(self):
"""Escape slashes in querystrings so not to be treated as regexp"""
self._test_search(["this/has/some/slashes"],
query={"query_string": {"query": r"this\/has\/some\/slashes"}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_search_regexp_slashes_in_query(self):
"""Don't escape slashes in DSL queries"""
self._test_search(['--json',
'{"term": {"name": "this/has/some/slashes"}}'],
query={"term": {"name": "this/has/some/slashes"}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_search_query_dsl(self):
self._test_search(['--json',
'{"term": {"status": "active"}}'],
query={'term': {'status': 'active'}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_search_query_dsl_no_json_flag(self):
self.assertRaises(
SystemExit, self._test_search,
['{"term": {"status": "active"}}'],
query={'term': {'status': 'active'}},
_source=['id', 'name', 'updated_at'],
all_projects=False, type=None)
def test_list_all_projects(self):
self._test_search(["name: fake", "--all-projects"],
query={"query_string": {"query": "name: fake"}},
_source=['id', 'name', 'updated_at'],
all_projects=True, type=None)
def test_list_source(self):
self._test_search(["name: fake", "--source"],
query={"query_string": {"query": "name: fake"}},
all_projects=False, source="all_resources",
type=None)
def test_list_optional_source(self):
self._test_search(["name: fake", "--source", "f1,f2"],
query={"query_string": {"query": "name: fake"}},
all_projects=False, source=["id", "f1", "f2"],
type=None)

View File

@ -1,44 +0,0 @@
# 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 searchlightclient.v1 import facets
import testtools
from unittest import mock
class FacetsManagerTest(testtools.TestCase):
def setUp(self):
super(FacetsManagerTest, self).setUp()
self.manager = facets.FacetsManager(None)
self.manager.client = mock.Mock()
def test_list_all_projects(self):
self.manager.list(all_projects=True)
self.manager.client.get.assert_called_once_with(
'/v1/search/facets?all_projects=True')
def test_list_by_type(self):
self.manager.list(type='fake_type')
self.manager.client.get.assert_called_once_with(
'/v1/search/facets?type=fake_type')
def test_list_by_index(self):
self.manager.list(index='fake_index')
self.manager.client.get.assert_called_once_with(
'/v1/search/facets?index=fake_index')
def test_list_by_limit_terms(self):
self.manager.list(limit_terms='10')
self.manager.client.get.assert_called_once_with(
'/v1/search/facets?limit_terms=10')

View File

@ -1,25 +0,0 @@
# 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 searchlightclient.v1 import resource_types
import testtools
from unittest import mock
class ResourceTypeManagerTest(testtools.TestCase):
def test_list(self):
manager = resource_types.ResourceTypeManager(None)
manager._list = mock.Mock()
manager.list()
manager._list.assert_called_once_with('/v1/search/plugins', 'plugins')

View File

@ -1,73 +0,0 @@
# 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 searchlightclient.v1 import search
import testtools
from unittest import mock
class SearchManagerTest(testtools.TestCase):
def setUp(self):
super(SearchManagerTest, self).setUp()
self.manager = search.SearchManager(None)
self.manager._post = mock.Mock()
def test_search_with_query(self):
query_string = {
'query_string': {
'query': 'database'
}
}
self.manager.search(query=query_string)
self.manager._post.assert_called_once_with(
'/v1/search', {'query': query_string})
def test_search_with_type(self):
self.manager.search(type='fake_type')
self.manager._post.assert_called_once_with(
'/v1/search', {'type': 'fake_type'})
def test_search_with_offset(self):
self.manager.search(offset='fake_offset')
self.manager._post.assert_called_once_with(
'/v1/search', {'offset': 'fake_offset'})
def test_search_with_limit(self):
self.manager.search(limit=10)
self.manager._post.assert_called_once_with(
'/v1/search', {'limit': 10})
def test_search_with_sort(self):
self.manager.search(sort='asc')
self.manager._post.assert_called_once_with(
'/v1/search', {'sort': 'asc'})
def test_search_with_source(self):
self.manager.search(_source=['fake_source'])
self.manager._post.assert_called_once_with(
'/v1/search', {'_source': ['fake_source']})
def test_search_with_highlight(self):
self.manager.search(highlight='fake_highlight')
self.manager._post.assert_called_once_with(
'/v1/search', {'highlight': 'fake_highlight'})
def test_search_with_all_projects(self):
self.manager.search(all_projects=True)
self.manager._post.assert_called_once_with(
'/v1/search', {'all_projects': True})
def test_search_with_invalid_option(self):
self.manager.search(invalid='fake')
self.manager._post.assert_called_once_with('/v1/search', {})

View File

@ -1,42 +0,0 @@
# 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 searchlightclient import client
from searchlightclient.v1 import facets
from searchlightclient.v1 import resource_types
from searchlightclient.v1 import search
class Client(object):
"""Client for the Searchlight v1 API.
:param session: a keystoneauth/keystoneclient session object
:type session: keystoneclient.session.Session
:param str service_type: The default service_type for URL discovery
:param str interface: The default interface for URL discovery
(Default: public)
:param str region_name: The default region_name for URL discovery
:param str endpoint_override: Always use this endpoint URL for requests
for this ceiloclient
:param auth: An auth plugin to use instead of the session one
:type auth: keystoneclient.auth.base.BaseAuthPlugin
:param str user_agent: The User-Agent string to set
(Default is python-searchlightclient)
"""
def __init__(self, *args, **kwargs):
"""Initialize a new client for the Searchlight v1 API."""
self.http_client = client._construct_http_client(*args, **kwargs)
self.resource_types = resource_types.ResourceTypeManager(
self.http_client)
self.facets = facets.FacetsManager(self.http_client)
self.search = search.SearchManager(self.http_client)

View File

@ -1,53 +0,0 @@
# 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 urllib import parse
from searchlightclient.common import base
class Facets(base.Resource):
def __repr__(self):
return "<Facets %s>" % self._info
def list(self, **kwargs):
return self.manager.list(self, **kwargs)
class FacetsManager(base.BaseManager):
resource_class = Facets
def list(self, **kwargs):
"""Get a list of facets.
:param index: Index name to query.
:param all_projects: By default, facet terms are limited to the
currently scoped project. Administrators are
able to request facet terms for all projects
by specify all_projects=True.
:param limit_terms: Limit the number of options returned for fields
that support facet terms.
:param type: Request facets for a particular type by adding a type
query parameter.
:rtype: dict of {resource_type: {'facets': [:class:`Facets`],
'doc_count':, :class:int}}
"""
params = {}
if kwargs.get('index'):
params['index'] = kwargs['index']
if kwargs.get('type'):
params['type'] = kwargs['type']
if kwargs.get('limit_terms'):
params['limit_terms'] = kwargs['limit_terms']
if kwargs.get('all_projects') is not None:
params['all_projects'] = kwargs['all_projects']
url = '/v1/search/facets?%s' % parse.urlencode(params, True)
return self.client.get(url).json()

View File

@ -1,31 +0,0 @@
# 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 searchlightclient.common import base
class ResourceType(base.Resource):
def __repr__(self):
return "<ResourceType %s>" % self._info
def list(self, **kwargs):
return self.manager.list(self, **kwargs)
class ResourceTypeManager(base.BaseManager):
resource_class = ResourceType
def list(self, **kwargs):
"""Get a list of plugins.
:rtype: list of :class:`ResourceType`
"""
return self._list('/v1/search/plugins', 'plugins')

View File

@ -1,58 +0,0 @@
# 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 searchlightclient.common import base
class Search(base.Resource):
def __repr__(self):
return "<Resource %s>" % self._info
def search(self, **kwargs):
return self.manager.search(self, **kwargs)
class SearchManager(base.BaseManager):
resource_class = Search
def search(self, **kwargs):
"""Executes a search query against searchlight and returns the 'hits'
from the response. Currently accepted parameters are (all optional):
:param query: see Elasticsearch DSL or Searchlight documentation;
defaults to match everything
:param type: one or more types to search. Uniquely identifies resource
types. Example: OS::Glance::Image
:param offset: skip over this many results
:param limit: return this many results
:param sort: sort by one or more fields
:param _source: restrict the fields returned for each document
:param highlight: add an Elasticsearch highlight clause
:param all_projects: by default searches are restricted to the
current project unless all_projects is set
:param simplified: return only _source data
"""
search_params = {}
for k, v in kwargs.items():
if k in ('query', 'type', 'offset',
'limit', 'sort', '_source', 'highlight', 'all_projects'):
search_params[k] = v
resources = self._post('/v1/search', search_params)
# NOTE: This could be done at the server side to reduce data
# transfer, since the data have been wrapped several times
# before transfer, and the data overhead is pretty small comparing
# to the data payload('_source'), it is done here for simplicity.
if 'simplified' in kwargs and kwargs['simplified']:
resources = [h['_source'] for h in resources.hits['hits']]
return resources

View File

@ -1,35 +0,0 @@
[metadata]
name = python-searchlightclient
summary = OpenStack Indexing and Search API Client Library
description-file =
README.rst
author = OpenStack
author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/python-searchlightclient/latest
python-requires = >=3.6
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
[files]
packages =
searchlightclient
[entry_points]
openstack.cli.extension =
search = searchlightclient.osc.plugin
openstack.search.v1 =
search_resource_type_list = searchlightclient.osc.v1.resource_type:ListResourceType
search_facet_list = searchlightclient.osc.v1.facet:ListFacet
search_query = searchlightclient.osc.v1.search:SearchResource

View File

@ -1,20 +0,0 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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 setuptools
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

View File

@ -1,10 +0,0 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Hacking already pins down pep8, pyflakes and flake8
hacking>=3.0.1,<3.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
stestr>=2.0.0 # Apache-2.0
testtools>=2.2.0 # MIT

View File

@ -1,75 +0,0 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2010 OpenStack Foundation
# Copyright 2013 IBM Corp.
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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 ConfigParser
import os
import sys
import install_venv_common as install_venv # flake8: noqa
def print_help(project, venv, root):
help = """
%(project)s development environment setup is complete.
%(project)s development uses virtualenv to track and manage Python
dependencies while in development and testing.
To activate the %(project)s virtualenv for the extent of your current
shell session you can run:
$ source %(venv)s/bin/activate
Or, if you prefer, you can run commands in the virtualenv on a case by
case basis by running:
$ %(root)s/tools/with_venv.sh <your command>
"""
print(help % dict(project=project, venv=venv, root=root))
def main(argv):
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if os.environ.get('tools_path'):
root = os.environ['tools_path']
venv = os.path.join(root, '.venv')
if os.environ.get('venv'):
venv = os.environ['venv']
pip_requires = os.path.join(root, 'requirements.txt')
test_requires = os.path.join(root, 'test-requirements.txt')
py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
setup_cfg = ConfigParser.ConfigParser()
setup_cfg.read('setup.cfg')
project = setup_cfg.get('metadata', 'name')
install = install_venv.InstallVenv(
root, venv, pip_requires, test_requires, py_version, project)
options = install.parse_args(argv)
install.check_python_version()
install.check_dependencies()
install.create_virtualenv(no_site_packages=options.no_site_packages)
install.install_dependencies()
print_help(project, venv, root)
if __name__ == '__main__':
main(sys.argv)

View File

@ -1,170 +0,0 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
#
# 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.
"""Provides methods needed by installation script for OpenStack development
virtual environments.
Since this script is used to bootstrap a virtualenv from the system's Python
environment, it should be kept strictly compatible with Python 2.6.
Synced in from openstack-common
"""
import optparse
import os
import subprocess
import sys
class InstallVenv(object):
def __init__(self, root, venv, requirements,
test_requirements, py_version,
project):
self.root = root
self.venv = venv
self.requirements = requirements
self.test_requirements = test_requirements
self.py_version = py_version
self.project = project
def die(self, message, *args):
print(message % args, file=sys.stderr)
sys.exit(1)
def check_python_version(self):
if sys.version_info < (2, 6):
self.die("Need Python Version >= 2.6")
def run_command_with_code(self, cmd, redirect_output=True,
check_exit_code=True):
"""Runs a command in an out-of-process shell.
Returns the output of that command. Working directory is self.root.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
return (output, proc.returncode)
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
return self.run_command_with_code(cmd, redirect_output,
check_exit_code)[0]
def get_distro(self):
if (os.path.exists('/etc/fedora-release') or
os.path.exists('/etc/redhat-release')):
return Fedora(
self.root, self.venv, self.requirements,
self.test_requirements, self.py_version, self.project)
else:
return Distro(
self.root, self.venv, self.requirements,
self.test_requirements, self.py_version, self.project)
def check_dependencies(self):
self.get_distro().install_virtualenv()
def create_virtualenv(self, no_site_packages=True):
"""Creates the virtual environment and installs PIP.
Creates the virtual environment and installs PIP only into the
virtual environment.
"""
if not os.path.isdir(self.venv):
print('Creating venv...', end=' ')
if no_site_packages:
self.run_command(['virtualenv', '-q', '--no-site-packages',
self.venv])
else:
self.run_command(['virtualenv', '-q', self.venv])
print('done.')
else:
print("venv already exists...")
pass
def pip_install(self, *args):
self.run_command(['tools/with_venv.sh',
'pip', 'install', '--upgrade'] + list(args),
redirect_output=False)
def install_dependencies(self):
print('Installing dependencies with pip (this can take a while)...')
# First things first, make sure our venv has the latest pip and
# setuptools and pbr
self.pip_install('pip>=1.4')
self.pip_install('setuptools')
self.pip_install('pbr')
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
def parse_args(self, argv):
"""Parses command-line arguments."""
parser = optparse.OptionParser()
parser.add_option('-n', '--no-site-packages',
action='store_true',
help="Do not inherit packages from global Python "
"install")
return parser.parse_args(argv[1:])[0]
class Distro(InstallVenv):
def check_cmd(self, cmd):
return bool(self.run_command(['which', cmd],
check_exit_code=False).strip())
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if self.check_cmd('easy_install'):
print('Installing virtualenv via easy_install...', end=' ')
if self.run_command(['easy_install', 'virtualenv']):
print('Succeeded')
return
else:
print('Failed')
self.die('ERROR: virtualenv not found.\n\n%s development'
' requires virtualenv, please install it using your'
' favorite package management tool' % self.project)
class Fedora(Distro):
"""This covers all Fedora-based distributions.
Includes: Fedora, RHEL, CentOS, Scientific Linux
"""
def check_pkg(self, pkg):
return self.run_command_with_code(['rpm', '-q', pkg],
check_exit_code=False)[1] == 0
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if not self.check_pkg('python-virtualenv'):
self.die("Please install 'python-virtualenv'.")
super(Fedora, self).install_virtualenv()

View File

@ -1,10 +0,0 @@
#!/bin/bash
command -v tox > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo 'This script requires "tox" to run.'
echo 'You can install it with "pip install tox".'
exit 1;
fi
tox -evenv -- $@

77
tox.ini
View File

@ -1,77 +0,0 @@
[tox]
envlist = py37,pep8
minversion = 3.1.1
skipsdist = True
ignore_basepython_conflict = True
[testenv]
basepython = python3
usedevelop = True
install_command = pip install {opts} {packages}
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
find . -type f -name "*.pyc" -delete
stestr run --slowest {posargs}
whitelist_externals = find
[testenv:pep8]
sitepackages = False
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:functional]
setenv =
OS_TEST_PATH = ./searchlightclient/tests/functional
passenv = OS_*
[testenv:cover]
setenv =
VIRTUAL_ENV={envdir}
PYTHON=coverage run --source searchlightclient --parallel-mode
commands =
stestr run {posargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
[testenv:docs]
whitelist_externals =
rm
deps = -r{toxinidir}/doc/requirements.txt
commands =
rm -rf doc/build
sphinx-build --keep-going -b html -d doc/build/doctrees doc/source doc/build/html
[testenv:pdf-docs]
deps = -r{toxinidir}/doc/requirements.txt
envdir = {toxworkdir}/docs
whitelist_externals =
make
commands =
sphinx-build -W -b latex doc/source doc/build/pdf
make -C doc/build/pdf
[flake8]
# E128 continuation line under-indented for visual indent
# E265 block comment should start with '# '
# H405: multi line docstring summary not separated with an empty line
# W504 line break after binary operator
ignore = E128,E265,H405,W504
show-source = True
exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build
max-complexity=20
[hacking]
import_exceptions = searchlightclient.openstack.common._i18n
[testenv:lower-constraints]
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt