Retire stackforge/billingstack
This commit is contained in:
parent
c378381306
commit
d6f0ac4234
@ -1,7 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = billingstack
|
||||
omit = billingstack/tests/*,billingstack/openstack/*
|
||||
|
||||
[report]
|
||||
ignore-errors = True
|
58
.gitignore
vendored
58
.gitignore
vendored
@ -1,58 +0,0 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
.testrepository
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
.venv
|
||||
.codeintel
|
||||
|
||||
doc/source/api/*
|
||||
doc/build/*
|
||||
AUTHORS
|
||||
TAGS
|
||||
ChangeLog
|
||||
|
||||
# Project specific
|
||||
etc/billingstack/*.ini
|
||||
etc/billingstack/*.conf
|
||||
billingstack/versioninfo
|
||||
*.sqlite
|
||||
|
||||
|
||||
billingstack-screenrc
|
||||
status
|
||||
logs
|
||||
.ropeproject
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=stackforge/billingstack.git
|
42
.pylintrc
42
.pylintrc
@ -1,42 +0,0 @@
|
||||
# The format of this file isn't really documented; just use --generate-rcfile
|
||||
[MASTER]
|
||||
# Add <file or directory> to the black list. It should be a base name, not a
|
||||
# path. You may set this option multiple times.
|
||||
ignore=test
|
||||
|
||||
[Messages Control]
|
||||
# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future
|
||||
# C0111: Don't require docstrings on every method
|
||||
# W0511: TODOs in code comments are fine.
|
||||
# W0142: *args and **kwargs are fine.
|
||||
# W0622: Redefining id is fine.
|
||||
disable=C0111,W0511,W0142,W0622
|
||||
|
||||
[Basic]
|
||||
# Variable names can be 1 to 31 characters long, with lowercase and underscores
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
|
||||
# Argument names can be 2 to 31 characters long, with lowercase and underscores
|
||||
argument-rgx=[a-z_][a-z0-9_]{1,30}$
|
||||
|
||||
# Method names should be at least 3 characters long
|
||||
# and be lowecased with underscores
|
||||
method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$
|
||||
|
||||
# Module names matching billingstack-* are ok (files in bin/)
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(billingstack-[a-z0-9_-]+))$
|
||||
|
||||
# Don't require docstrings on tests.
|
||||
no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$
|
||||
|
||||
[Design]
|
||||
max-public-methods=100
|
||||
min-public-methods=0
|
||||
max-args=6
|
||||
|
||||
[Variables]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
# _ is used by our localization
|
||||
additional-builtins=_
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
253
HACKING.rst
253
HACKING.rst
@ -1,253 +0,0 @@
|
||||
BillingStack Style Commandments
|
||||
===============================
|
||||
|
||||
- Step 1: Read http://www.python.org/dev/peps/pep-0008/
|
||||
- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
|
||||
- Step 3: Read on
|
||||
|
||||
|
||||
General
|
||||
-------
|
||||
- Put two newlines between top-level code (funcs, classes, etc)
|
||||
- Put one newline between methods in classes and anywhere else
|
||||
- Do not write "except:", use "except Exception:" at the very least
|
||||
- Include your name with TODOs as in "#TODO(termie)"
|
||||
- Do not name anything the same name as a built-in or reserved word
|
||||
- Use the "is not" operator when testing for unequal identities. Example::
|
||||
|
||||
if not X is Y: # BAD, intended behavior is ambiguous
|
||||
pass
|
||||
|
||||
if X is not Y: # OKAY, intuitive
|
||||
pass
|
||||
|
||||
- Use the "not in" operator for evaluating membership in a collection. Example::
|
||||
|
||||
if not X in Y: # BAD, intended behavior is ambiguous
|
||||
pass
|
||||
|
||||
if X not in Y: # OKAY, intuitive
|
||||
pass
|
||||
|
||||
if not (X in Y or X in Z): # OKAY, still better than all those 'not's
|
||||
pass
|
||||
|
||||
|
||||
Imports
|
||||
-------
|
||||
- Do not make relative imports
|
||||
- Order your imports by the full module path
|
||||
- Organize your imports according to the following template
|
||||
|
||||
Example::
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
{{stdlib imports in human alphabetical order}}
|
||||
\n
|
||||
{{third-party lib imports in human alphabetical order}}
|
||||
\n
|
||||
{{billingstack imports in human alphabetical order}}
|
||||
\n
|
||||
\n
|
||||
{{begin your code}}
|
||||
|
||||
|
||||
Human Alphabetical Order Examples
|
||||
---------------------------------
|
||||
Example::
|
||||
|
||||
import httplib
|
||||
import logging
|
||||
import random
|
||||
import StringIO
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import eventlet
|
||||
import webob.exc
|
||||
|
||||
from billingstack.api import v1
|
||||
from billingstack.central import rpc_api
|
||||
from billingstack.rater import rpc_api
|
||||
|
||||
|
||||
Docstrings
|
||||
----------
|
||||
|
||||
Docstrings are required for all functions and methods.
|
||||
|
||||
Docstrings should ONLY use triple-double-quotes (``"""``)
|
||||
|
||||
Single-line docstrings should NEVER have extraneous whitespace
|
||||
between enclosing triple-double-quotes.
|
||||
|
||||
**INCORRECT** ::
|
||||
|
||||
""" There is some whitespace between the enclosing quotes :( """
|
||||
|
||||
**CORRECT** ::
|
||||
|
||||
"""There is no whitespace between the enclosing quotes :)"""
|
||||
|
||||
Docstrings that span more than one line should look like this:
|
||||
|
||||
Example::
|
||||
|
||||
"""
|
||||
Start the docstring on the line following the opening triple-double-quote
|
||||
|
||||
If you are going to describe parameters and return values, use Sphinx, the
|
||||
appropriate syntax is as follows.
|
||||
|
||||
:param foo: the foo parameter
|
||||
:param bar: the bar parameter
|
||||
:returns: return_type -- description of the return value
|
||||
:returns: description of the return value
|
||||
:raises: AttributeError, KeyError
|
||||
"""
|
||||
|
||||
**DO NOT** leave an extra newline before the closing triple-double-quote.
|
||||
|
||||
|
||||
Dictionaries/Lists
|
||||
------------------
|
||||
If a dictionary (dict) or list object is longer than 80 characters, its items
|
||||
should be split with newlines. Embedded iterables should have their items
|
||||
indented. Additionally, the last item in the dictionary should have a trailing
|
||||
comma. This increases readability and simplifies future diffs.
|
||||
|
||||
Example::
|
||||
|
||||
my_dictionary = {
|
||||
"image": {
|
||||
"name": "Just a Snapshot",
|
||||
"size": 2749573,
|
||||
"properties": {
|
||||
"user_id": 12,
|
||||
"arch": "x86_64",
|
||||
},
|
||||
"things": [
|
||||
"thing_one",
|
||||
"thing_two",
|
||||
],
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Calling Methods
|
||||
---------------
|
||||
Calls to methods 80 characters or longer should format each argument with
|
||||
newlines. This is not a requirement, but a guideline::
|
||||
|
||||
unnecessarily_long_function_name('string one',
|
||||
'string two',
|
||||
kwarg1=constants.ACTIVE,
|
||||
kwarg2=['a', 'b', 'c'])
|
||||
|
||||
|
||||
Rather than constructing parameters inline, it is better to break things up::
|
||||
|
||||
list_of_strings = [
|
||||
'what_a_long_string',
|
||||
'not as long',
|
||||
]
|
||||
|
||||
dict_of_numbers = {
|
||||
'one': 1,
|
||||
'two': 2,
|
||||
'twenty four': 24,
|
||||
}
|
||||
|
||||
object_one.call_a_method('string three',
|
||||
'string four',
|
||||
kwarg1=list_of_strings,
|
||||
kwarg2=dict_of_numbers)
|
||||
|
||||
|
||||
Internationalization (i18n) Strings
|
||||
-----------------------------------
|
||||
In order to support multiple languages, we have a mechanism to support
|
||||
automatic translations of exception and log strings.
|
||||
|
||||
Example::
|
||||
|
||||
msg = _("An error occurred")
|
||||
raise HTTPBadRequest(explanation=msg)
|
||||
|
||||
If you have a variable to place within the string, first internationalize the
|
||||
template string then do the replacement.
|
||||
|
||||
Example::
|
||||
|
||||
msg = _("Missing parameter: %s") % ("flavor",)
|
||||
LOG.error(msg)
|
||||
|
||||
If you have multiple variables to place in the string, use keyword parameters.
|
||||
This helps our translators reorder parameters when needed.
|
||||
|
||||
Example::
|
||||
|
||||
msg = _("The server with id %(s_id)s has no key %(m_key)s")
|
||||
LOG.error(msg % {"s_id": "1234", "m_key": "imageId"})
|
||||
|
||||
|
||||
Creating Unit Tests
|
||||
-------------------
|
||||
For every new feature, unit tests should be created that both test and
|
||||
(implicitly) document the usage of said feature. If submitting a patch for a
|
||||
bug that had no unit test, a new passing unit test should be added. If a
|
||||
submitted bug fix does have a unit test, be sure to add a new one that fails
|
||||
without the patch and passes with the patch.
|
||||
|
||||
|
||||
Commit Messages
|
||||
---------------
|
||||
Using a common format for commit messages will help keep our git history
|
||||
readable. Follow these guidelines:
|
||||
|
||||
First, provide a brief summary of 50 characters or less. Summaries
|
||||
of greater then 72 characters will be rejected by the gate.
|
||||
|
||||
The first line of the commit message should provide an accurate
|
||||
description of the change, not just a reference to a bug or
|
||||
blueprint. It must be followed by a single blank line.
|
||||
|
||||
Following your brief summary, provide a more detailed description of
|
||||
the patch, manually wrapping the text at 72 characters. This
|
||||
description should provide enough detail that one does not have to
|
||||
refer to external resources to determine its high-level functionality.
|
||||
|
||||
Once you use 'git review', two lines will be appended to the commit
|
||||
message: a blank line followed by a 'Change-Id'. This is important
|
||||
to correlate this commit with a specific review in Gerrit, and it
|
||||
should not be modified.
|
||||
|
||||
For further information on constructing high quality commit messages,
|
||||
and how to split up commits into a series of changes, consult the
|
||||
project wiki:
|
||||
|
||||
http://wiki.openstack.org/GitCommitMessages
|
||||
|
||||
|
||||
openstack-common
|
||||
----------------
|
||||
|
||||
A number of modules from openstack-common are imported into the project.
|
||||
|
||||
These modules are "incubating" in openstack-common and are kept in sync
|
||||
with the help of openstack-common's update.py script. See:
|
||||
|
||||
http://wiki.openstack.org/CommonLibrary#Incubation
|
||||
|
||||
The copy of the code should never be directly modified here. Please
|
||||
always update openstack-common first and then run the script to copy
|
||||
the changes across.
|
||||
|
||||
|
||||
Logging
|
||||
-------
|
||||
Use __name__ as the name of your logger and name your module-level logger
|
||||
objects 'LOG'::
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
175
LICENSE
175
LICENSE
@ -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.
|
11
MANIFEST.in
11
MANIFEST.in
@ -1,11 +0,0 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
include billingstack/versioninfo
|
||||
include *.txt *.ini *.cfg *.rst *.md
|
||||
include etc/billingstack/*.sample
|
||||
include etc/billingstack/policy.json
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
exclude *.sublime-project
|
||||
global-exclude *.pyc
|
11
README.rst
11
README.rst
@ -1,8 +1,7 @@
|
||||
BillingStack
|
||||
============
|
||||
This project is no longer maintained.
|
||||
|
||||
Site: www.billingstack.org
|
||||
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".
|
||||
|
||||
Docs: http://billingstack.rtfd.org
|
||||
Github: http://github.com/stackforge/billingstack
|
||||
Bugs: http://launchpad.net/billingstack
|
||||
|
@ -1,59 +0,0 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"file_exclude_patterns":
|
||||
[
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.exe",
|
||||
"*.dll",
|
||||
"*.obj",
|
||||
"*.o",
|
||||
"*.a",
|
||||
"*.lib",
|
||||
"*.so",
|
||||
"*.dylib",
|
||||
"*.ncb",
|
||||
"*.sdf",
|
||||
"*.suo",
|
||||
"*.pdb",
|
||||
"*.idb",
|
||||
".DS_Store",
|
||||
"*.class",
|
||||
"*.psd",
|
||||
"*.db",
|
||||
".vagrant",
|
||||
".noseids"
|
||||
],
|
||||
"folder_exclude_patterns":
|
||||
[
|
||||
".svn",
|
||||
".git",
|
||||
".hg",
|
||||
"CVS",
|
||||
"*.egg",
|
||||
"*.egg-info",
|
||||
".tox",
|
||||
"venv",
|
||||
".venv",
|
||||
"doc/build",
|
||||
"doc/source/api"
|
||||
],
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"default_line_ending": "unix",
|
||||
"detect_indentation": false,
|
||||
"ensure_newline_at_eof_on_save": true,
|
||||
"rulers":
|
||||
[
|
||||
79
|
||||
],
|
||||
"tab_size": 4,
|
||||
"translate_tabs_to_spaces": true,
|
||||
"trim_trailing_white_space_on_save": true
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.
|
@ -1,31 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2013 Woorea Solutions, S.L
|
||||
#
|
||||
# Author: Luis Gervaso <luis@woorea.es>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copied: Moniker
|
||||
from oslo.config import cfg
|
||||
|
||||
API_SERVICE_OPTS = [
|
||||
cfg.IntOpt('api_port', default=9091,
|
||||
help='The port for the billing API server'),
|
||||
cfg.IntOpt('api_listen', default='0.0.0.0', help='Bind to address'),
|
||||
cfg.StrOpt('auth_strategy', default='noauth',
|
||||
help='The strategy to use for auth. Supports noauth or '
|
||||
'keystone'),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(API_SERVICE_OPTS, 'service:api')
|
@ -1,91 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
import os
|
||||
import pecan
|
||||
from oslo.config import cfg
|
||||
from wsgiref import simple_server
|
||||
|
||||
from billingstack import service
|
||||
from billingstack.api import hooks
|
||||
from billingstack.openstack.common import log
|
||||
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config():
|
||||
conf = {
|
||||
'app': {
|
||||
'root': 'billingstack.api.v2.controllers.root.RootController',
|
||||
'modules': ['designate.api.v2'],
|
||||
}
|
||||
}
|
||||
return pecan.configuration.conf_from_dict(conf)
|
||||
|
||||
|
||||
def setup_app(pecan_config=None, extra_hooks=None):
|
||||
app_hooks = [
|
||||
hooks.NoAuthHook()
|
||||
]
|
||||
|
||||
if extra_hooks:
|
||||
app_hooks.extend(extra_hooks)
|
||||
|
||||
pecan_config = pecan_config or get_config()
|
||||
|
||||
pecan.configuration.set_config(dict(pecan_config), overwrite=True)
|
||||
|
||||
app = pecan.make_app(
|
||||
pecan_config.app.root,
|
||||
debug=cfg.CONF.debug,
|
||||
hooks=app_hooks,
|
||||
force_canonical=getattr(pecan_config.app, 'force_canonical', True)
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
class VersionSelectorApplication(object):
|
||||
def __init__(self):
|
||||
self.v2 = setup_app()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.v2(environ, start_response)
|
||||
|
||||
|
||||
def start():
|
||||
service.prepare_service()
|
||||
|
||||
root = VersionSelectorApplication()
|
||||
|
||||
host = cfg.CONF['service:api'].api_listen
|
||||
port = cfg.CONF['service:api'].api_port
|
||||
|
||||
srv = simple_server.make_server(host, port, root)
|
||||
|
||||
LOG.info('Starting server in PID %s' % os.getpid())
|
||||
LOG.info("Configuration:")
|
||||
cfg.CONF.log_opt_values(LOG, logging.INFO)
|
||||
|
||||
if host == '0.0.0.0':
|
||||
LOG.info('serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' %
|
||||
(port, port))
|
||||
else:
|
||||
LOG.info("serving on http://%s:%s" % (host, port))
|
||||
|
||||
srv.serve_forever()
|
@ -1,158 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan.rest
|
||||
|
||||
from wsme.types import Base, Enum, UserType, text, Unset, wsproperty
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import log
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.StrOpt('cors_allowed_origin', default='*', help='Allowed CORS Origin'),
|
||||
cfg.IntOpt('cors_max_age', default=3600)])
|
||||
|
||||
|
||||
CORS_ALLOW_HEADERS = [
|
||||
'origin',
|
||||
'authorization',
|
||||
'accept',
|
||||
'content-type',
|
||||
'x-requested-with'
|
||||
]
|
||||
|
||||
|
||||
class RestController(pecan.rest.RestController):
|
||||
def _handle_patch(self, method, remainder):
|
||||
return self._handle_post(method, remainder)
|
||||
|
||||
|
||||
class Property(UserType):
|
||||
"""
|
||||
A Property that just passes the value around...
|
||||
"""
|
||||
def tonativetype(self, value):
|
||||
return value
|
||||
|
||||
def fromnativetype(self, value):
|
||||
return value
|
||||
|
||||
|
||||
property_type = Property()
|
||||
|
||||
|
||||
def _query_to_criterion(query, storage_func=None, **kw):
|
||||
"""
|
||||
Iterate over the query checking against the valid signatures (later).
|
||||
|
||||
:param query: A list of queries.
|
||||
:param storage_func: The name of the storage function to very against.
|
||||
"""
|
||||
translation = {
|
||||
'customer': 'customer_id'
|
||||
}
|
||||
|
||||
criterion = {}
|
||||
for q in query:
|
||||
key = translation.get(q.field, q.field)
|
||||
criterion[key] = q.as_dict()
|
||||
|
||||
criterion.update(kw)
|
||||
|
||||
return criterion
|
||||
|
||||
|
||||
operation_kind = Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||
|
||||
|
||||
class Query(Base):
|
||||
"""
|
||||
Query filter.
|
||||
"""
|
||||
|
||||
_op = None # provide a default
|
||||
|
||||
def get_op(self):
|
||||
return self._op or 'eq'
|
||||
|
||||
def set_op(self, value):
|
||||
self._op = value
|
||||
|
||||
field = text
|
||||
"The name of the field to test"
|
||||
|
||||
#op = wsme.wsattr(operation_kind, default='eq')
|
||||
# this ^ doesn't seem to work.
|
||||
op = wsproperty(operation_kind, get_op, set_op)
|
||||
"The comparison operator. Defaults to 'eq'."
|
||||
|
||||
value = text
|
||||
"The value to compare against the stored data"
|
||||
|
||||
def __repr__(self):
|
||||
# for LOG calls
|
||||
return '<Query %r %s %r>' % (self.field, self.op, self.value)
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(field='resource_id',
|
||||
op='eq',
|
||||
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
'op': self.op,
|
||||
'field': self.field,
|
||||
'value': self.value
|
||||
}
|
||||
|
||||
|
||||
class ModelBase(Base):
|
||||
def as_dict(self):
|
||||
"""
|
||||
Return this model as a dict
|
||||
"""
|
||||
data = {}
|
||||
|
||||
for attr in self._wsme_attributes:
|
||||
value = attr.__get__(self, self.__class__)
|
||||
if value is not Unset:
|
||||
if isinstance(value, Base) and hasattr(value, "as_dict"):
|
||||
value = value.as_dict()
|
||||
data[attr.name] = value
|
||||
return data
|
||||
|
||||
def to_db(self):
|
||||
"""
|
||||
Returns this Model object as it's DB form
|
||||
|
||||
Example
|
||||
'currency' vs 'currency_name'
|
||||
"""
|
||||
return self.as_dict()
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, values):
|
||||
"""
|
||||
Return a class of this object from values in the from_db
|
||||
"""
|
||||
return cls(**values)
|
@ -1,40 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import hooks
|
||||
|
||||
from billingstack.openstack.common.context import RequestContext
|
||||
|
||||
|
||||
class NoAuthHook(hooks.PecanHook):
|
||||
"""
|
||||
Simple auth - all requests will be is_admin=True
|
||||
"""
|
||||
def merchant_id(self, path):
|
||||
"""
|
||||
Get merchant id from url
|
||||
"""
|
||||
parts = [p for p in path.split('/') if p]
|
||||
try:
|
||||
index = parts.index('merchants') + 1
|
||||
return parts[index]
|
||||
except ValueError:
|
||||
return
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
def before(self, state):
|
||||
merchant_id = self.merchant_id(state.request.path_url)
|
||||
state.request.ctxt = RequestContext(tenant=merchant_id, is_admin=True)
|
@ -1,9 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>BillingStack Diagnostics</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Diagnostics</h1>
|
||||
<p>Here you'll find some basic information about your BillingStack server</p>
|
||||
</body>
|
||||
</html>
|
@ -1,64 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copied: http://flask.pocoo.org/snippets/56/
|
||||
from datetime import timedelta
|
||||
from flask import make_response, request, current_app
|
||||
import functools
|
||||
|
||||
|
||||
def crossdomain(origin=None, methods=None, headers=None,
|
||||
max_age=21600, attach_to_all=True,
|
||||
automatic_options=True):
|
||||
if methods is not None:
|
||||
methods = ', '.join(sorted(x.upper() for x in methods))
|
||||
if headers is not None and not isinstance(headers, basestring):
|
||||
headers = ', '.join(x.upper() for x in headers)
|
||||
if not isinstance(origin, basestring):
|
||||
origin = ', '.join(origin)
|
||||
if isinstance(max_age, timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
|
||||
def get_methods():
|
||||
if methods is not None:
|
||||
return methods
|
||||
|
||||
options_resp = current_app.make_default_options_response()
|
||||
return options_resp.headers['allow']
|
||||
|
||||
def decorator(f):
|
||||
def wrapped_function(*args, **kw):
|
||||
if automatic_options and request.method == 'OPTIONS':
|
||||
resp = current_app.make_default_options_response()
|
||||
else:
|
||||
resp = make_response(f(*args, **kw))
|
||||
if not attach_to_all and request.method != 'OPTIONS':
|
||||
return resp
|
||||
|
||||
h = resp.headers
|
||||
|
||||
h['Access-Control-Allow-Origin'] = origin
|
||||
h['Access-Control-Allow-Credentials'] = 'true'
|
||||
h['Access-Control-Allow-Methods'] = get_methods()
|
||||
h['Access-Control-Max-Age'] = str(max_age)
|
||||
if headers is not None:
|
||||
h['Access-Control-Allow-Headers'] = headers
|
||||
return resp
|
||||
|
||||
f.provide_automatic_options = False
|
||||
f.required_methods = ['OPTIONS']
|
||||
return functools.update_wrapper(wrapped_function, f)
|
||||
return decorator
|
@ -1,18 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
@ -1,15 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.
|
@ -1,67 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class CurrencyController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Currency)
|
||||
def get_all(self):
|
||||
row = central_api.get_currency(request.ctxt, self.id_)
|
||||
|
||||
return models.Currency.from_db(row)
|
||||
|
||||
@wsme.validate(models.Currency)
|
||||
@wsme_pecan.wsexpose(models.Currency, body=models.Currency)
|
||||
def patch(self, body):
|
||||
row = central_api.update_currency(request.ctxt, self.id_, body.to_db())
|
||||
return models.Currency.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_currency(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class CurrenciesController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, currency_id, *remainder):
|
||||
return CurrencyController(currency_id), remainder
|
||||
|
||||
@wsme.validate(models.Currency)
|
||||
@wsme_pecan.wsexpose(models.Currency, body=models.Currency,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_currency(request.ctxt, body.to_db())
|
||||
|
||||
return models.Currency.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Currency], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = central_api.list_currencies(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Currency.from_db, rows)
|
@ -1,74 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.api.v2.controllers.payment import PaymentMethodsController
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class CustomerController(RestController):
|
||||
payment_methods = PaymentMethodsController()
|
||||
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['customer_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Customer)
|
||||
def get_all(self):
|
||||
row = central_api.get_customer(request.ctxt, self.id_)
|
||||
|
||||
return models.Customer.from_db(row)
|
||||
|
||||
@wsme.validate(models.Customer)
|
||||
@wsme_pecan.wsexpose(models.Customer, body=models.Customer)
|
||||
def patch(self, body):
|
||||
row = central_api.update_customer(request.ctxt, self.id_, body.to_db())
|
||||
return models.Customer.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_customer(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class CustomersController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, customer_id, *remainder):
|
||||
return CustomerController(customer_id), remainder
|
||||
|
||||
@wsme.validate(models.Customer)
|
||||
@wsme_pecan.wsexpose(models.Customer, body=models.Customer,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_customer(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Customer.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Customer], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = central_api.list_customers(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Customer.from_db, rows)
|
@ -1,73 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.biller.rpcapi import biller_api
|
||||
|
||||
|
||||
class InvoiceController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['invoice_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Invoice)
|
||||
def get_all(self):
|
||||
row = biller_api.get_invoice(request.ctxt, self.id_)
|
||||
|
||||
return models.Invoice.from_db(row)
|
||||
|
||||
@wsme.validate(models.Invoice)
|
||||
@wsme_pecan.wsexpose(models.Invoice, body=models.Invoice)
|
||||
def patch(self, body):
|
||||
row = biller_api.update_invoice(request.ctxt, self.id_, body.to_db())
|
||||
|
||||
return models.Invoice.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
biller_api.delete_invoice(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class InvoicesController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, invoice_id, *remainder):
|
||||
return InvoiceController(invoice_id), remainder
|
||||
|
||||
@wsme.validate(models.Invoice)
|
||||
@wsme_pecan.wsexpose(models.Invoice, body=models.Invoice, status_code=202)
|
||||
def post(self, body):
|
||||
row = biller_api.create_invoice(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Invoice.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Invoice], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q,
|
||||
merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = biller_api.list_invoices(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Invoice.from_db, rows)
|
@ -1,68 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.biller.rpcapi import biller_api
|
||||
|
||||
|
||||
class InvoiceStateController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.InvoiceState)
|
||||
def get_all(self):
|
||||
row = biller_api.get_invoice_state(request.ctxt, self.id_)
|
||||
|
||||
return models.InvoiceState.from_db(row)
|
||||
|
||||
@wsme.validate(models.InvoiceState)
|
||||
@wsme_pecan.wsexpose(models.InvoiceState, body=models.InvoiceState)
|
||||
def patch(self, body):
|
||||
row = biller_api.update_invoice_state(
|
||||
request.ctxt, self.id_, body.to_db())
|
||||
return models.InvoiceState.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
biller_api.delete_invoice_state(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class InvoiceStatesController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, invoice_state_id, *remainder):
|
||||
return InvoiceStateController(invoice_state_id), remainder
|
||||
|
||||
@wsme.validate(models.InvoiceState)
|
||||
@wsme_pecan.wsexpose(models.InvoiceState, body=models.InvoiceState,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = biller_api.create_invoice_state(request.ctxt, body.to_db())
|
||||
|
||||
return models.InvoiceState.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.InvoiceState], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = biller_api.list_invoice_states(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.InvoiceState.from_db, rows)
|
@ -1,67 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class LanguageController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Language)
|
||||
def get_all(self):
|
||||
row = central_api.get_language(request.ctxt, self.id_)
|
||||
|
||||
return models.Language.from_db(row)
|
||||
|
||||
@wsme.validate(models.InvoiceState)
|
||||
@wsme_pecan.wsexpose(models.Language, body=models.Language)
|
||||
def patch(self, body):
|
||||
row = central_api.update_language(request.ctxt, self.id_, body.to_db())
|
||||
return models.Language.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_language(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class LanguagesController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, language_id, *remainder):
|
||||
return LanguageController(language_id), remainder
|
||||
|
||||
@wsme.validate(models.InvoiceState)
|
||||
@wsme_pecan.wsexpose(models.Language, body=models.Language,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_language(request.ctxt, body.to_db())
|
||||
|
||||
return models.Language.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Language], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = central_api.list_languages(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Language.from_db, rows)
|
@ -1,85 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
from billingstack.api.v2.controllers.customer import CustomersController
|
||||
from billingstack.api.v2.controllers.payment import PGConfigsController
|
||||
from billingstack.api.v2.controllers.plan import PlansController
|
||||
from billingstack.api.v2.controllers.product import ProductsController
|
||||
from billingstack.api.v2.controllers.subscription import \
|
||||
SubscriptionsController
|
||||
from billingstack.api.v2.controllers.invoice import InvoicesController
|
||||
from billingstack.api.v2.controllers.usage import UsagesController
|
||||
|
||||
|
||||
class MerchantController(RestController):
|
||||
customers = CustomersController()
|
||||
payment_gateway_configurations = PGConfigsController()
|
||||
plans = PlansController()
|
||||
products = ProductsController()
|
||||
subscriptions = SubscriptionsController()
|
||||
|
||||
invoices = InvoicesController()
|
||||
usage = UsagesController()
|
||||
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['merchant_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Merchant)
|
||||
def get_all(self):
|
||||
row = central_api.get_merchant(request.ctxt, self.id_)
|
||||
|
||||
return models.Merchant.from_db(row)
|
||||
|
||||
@wsme.validate(models.InvoiceState)
|
||||
@wsme_pecan.wsexpose(models.Merchant, body=models.Merchant)
|
||||
def patch(self, body):
|
||||
row = central_api.update_merchant(request.ctxt, self.id_, body.to_db())
|
||||
return models.Merchant.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_merchant(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class MerchantsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, merchant_id, *remainder):
|
||||
return MerchantController(merchant_id), remainder
|
||||
|
||||
@wsme.validate(models.Merchant)
|
||||
@wsme_pecan.wsexpose(models.Merchant, body=models.Merchant,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_merchant(request.ctxt, body.to_db())
|
||||
|
||||
return models.Merchant.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Merchant], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = central_api.list_merchants(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Merchant.from_db, rows)
|
@ -1,141 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.collector.rpcapi import collector_api
|
||||
|
||||
|
||||
class PGProviders(RestController):
|
||||
@wsme_pecan.wsexpose([models.PGProvider], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(q)
|
||||
|
||||
rows = collector_api.list_pg_providers(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.PGProvider.from_db, rows)
|
||||
|
||||
|
||||
class PGConfigController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.PGConfig)
|
||||
def get_all(self):
|
||||
row = collector_api.get_pg_config(request.ctxt, self.id_)
|
||||
|
||||
return models.PGConfig.from_db(row)
|
||||
|
||||
@wsme.validate(models.PGConfig)
|
||||
@wsme_pecan.wsexpose(models.PGConfig, body=models.PGConfig)
|
||||
def patch(self, body):
|
||||
row = collector_api.update_pg_config(
|
||||
request.ctxt,
|
||||
self.id_,
|
||||
body.to_db())
|
||||
|
||||
return models.PGConfig.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
collector_api.delete_pg_config(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class PGConfigsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, method_id, *remainder):
|
||||
return PGConfigController(method_id), remainder
|
||||
|
||||
@wsme.validate(models.PGConfig)
|
||||
@wsme_pecan.wsexpose(models.PGConfig, body=models.PGConfig,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
values = body.to_db()
|
||||
values['merchant_id'] = request.context['merchant_id']
|
||||
|
||||
row = collector_api.create_pg_config(
|
||||
request.ctxt,
|
||||
values)
|
||||
|
||||
return models.PGConfig.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.PGConfig], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q, merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = collector_api.list_pg_configs(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.PGConfig.from_db, rows)
|
||||
|
||||
|
||||
class PaymentMethodController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['payment_method_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.PaymentMethod)
|
||||
def get_all(self):
|
||||
row = collector_api.get_payment_method(request.ctxt, self.id_)
|
||||
|
||||
return models.PaymentMethod.from_db(row)
|
||||
|
||||
@wsme.validate(models.PaymentMethod)
|
||||
@wsme_pecan.wsexpose(models.PaymentMethod, body=models.PaymentMethod)
|
||||
def patch(self, body):
|
||||
row = collector_api.update_payment_method(
|
||||
request.ctxt,
|
||||
self.id_,
|
||||
body.to_db())
|
||||
|
||||
return models.PaymentMethod.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
collector_api.delete_payment_method(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class PaymentMethodsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, method_id, *remainder):
|
||||
return PaymentMethodController(method_id), remainder
|
||||
|
||||
@wsme.validate(models.PaymentMethod)
|
||||
@wsme_pecan.wsexpose(models.PaymentMethod, body=models.PaymentMethod,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
values = body.to_db()
|
||||
values['customer_id'] = request.context['customer_id']
|
||||
|
||||
row = collector_api.create_payment_method(request.ctxt, values)
|
||||
|
||||
return models.PaymentMethod.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.PaymentMethod], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q, merchant_id=request.context['merchant_id'],
|
||||
customer_id=request.context['customer_id'])
|
||||
|
||||
rows = collector_api.list_payment_methods(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.PaymentMethod.from_db, rows)
|
@ -1,116 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class ItemController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
|
||||
@wsme.validate(models.PlanItem)
|
||||
@wsme_pecan.wsexpose(models.PlanItem, body=models.PlanItem)
|
||||
def put(self, body):
|
||||
values = {
|
||||
'plan_id': request.context['plan_id'],
|
||||
'product_id': self.id_
|
||||
}
|
||||
|
||||
row = central_api.create_plan_item(request.ctxt, values)
|
||||
|
||||
return models.PlanItem.from_db(row)
|
||||
|
||||
@wsme.validate(models.PlanItem)
|
||||
@wsme_pecan.wsexpose(models.PlanItem, body=models.PlanItem)
|
||||
def patch(self, body):
|
||||
row = central_api.update_plan_item(
|
||||
request.ctxt,
|
||||
request.context['plan_id'],
|
||||
self.id_,
|
||||
body.to_db())
|
||||
|
||||
return models.PlanItem.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self, id_):
|
||||
central_api.delete_plan_item(
|
||||
request.ctxt,
|
||||
request.context['plan_id'],
|
||||
id_)
|
||||
|
||||
|
||||
class ItemsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, id_, *remainder):
|
||||
return ItemController(id_), remainder
|
||||
|
||||
|
||||
class PlanController(RestController):
|
||||
items = ItemsController()
|
||||
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['plan_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Plan)
|
||||
def get_all(self):
|
||||
row = central_api.get_plan(request.ctxt, self.id_)
|
||||
|
||||
return models.Plan.from_db(row)
|
||||
|
||||
@wsme.validate(models.Plan)
|
||||
@wsme_pecan.wsexpose(models.Plan, body=models.Plan)
|
||||
def patch(self, body):
|
||||
row = central_api.update_plan(request.ctxt, self.id_, body.to_db())
|
||||
|
||||
return models.Plan.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_plan(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class PlansController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, plan_id, *remainder):
|
||||
return PlanController(plan_id), remainder
|
||||
|
||||
@wsme.validate(models.Plan)
|
||||
@wsme_pecan.wsexpose(models.Plan, body=models.Plan, status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_plan(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Plan.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Plan], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q,
|
||||
merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = central_api.list_plans(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Plan.from_db, rows)
|
@ -1,74 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class ProductController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['product_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Product)
|
||||
def get_all(self):
|
||||
row = central_api.get_product(request.ctxt, self.id_)
|
||||
|
||||
return models.Product.from_db(row)
|
||||
|
||||
@wsme.validate(models.Product)
|
||||
@wsme_pecan.wsexpose(models.Product, body=models.Product)
|
||||
def patch(self, body):
|
||||
row = central_api.update_product(request.ctxt, self.id_, body.to_db())
|
||||
|
||||
return models.Product.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_product(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class ProductsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, product_id, *remainder):
|
||||
return ProductController(product_id), remainder
|
||||
|
||||
@wsme.validate(models.Product)
|
||||
@wsme_pecan.wsexpose(models.Product, body=models.Product,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_product(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Product.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Product], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q,
|
||||
merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = central_api.list_products(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Product.from_db, rows)
|
@ -1,42 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 billingstack.openstack.common import log
|
||||
from billingstack.api.v2.controllers.currency import CurrenciesController
|
||||
from billingstack.api.v2.controllers.language import LanguagesController
|
||||
from billingstack.api.v2.controllers.merchant import MerchantsController
|
||||
from billingstack.api.v2.controllers.invoice_state import \
|
||||
InvoiceStatesController
|
||||
from billingstack.api.v2.controllers.payment import PGProviders
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class V2Controller(object):
|
||||
# Central
|
||||
currencies = CurrenciesController()
|
||||
languages = LanguagesController()
|
||||
merchants = MerchantsController()
|
||||
|
||||
# Biller
|
||||
invoice_states = InvoiceStatesController()
|
||||
|
||||
# Collector
|
||||
payment_gateway_providers = PGProviders()
|
||||
|
||||
|
||||
class RootController(object):
|
||||
v2 = V2Controller()
|
@ -1,75 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.central.rpcapi import central_api
|
||||
|
||||
|
||||
class SubscriptionController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['subscription_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Subscription)
|
||||
def get_all(self):
|
||||
row = central_api.get_subscription(request.ctxt, self.id_)
|
||||
|
||||
return models.Subscription.from_db(row)
|
||||
|
||||
@wsme.validate(models.Subscription)
|
||||
@wsme_pecan.wsexpose(models.Subscription, body=models.Subscription)
|
||||
def patch(self, body):
|
||||
row = central_api.update_subscription(request.ctxt, self.id_,
|
||||
body.to_db())
|
||||
|
||||
return models.Subscription.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
central_api.delete_subscription(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class SubscriptionsController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, subscription_id, *remainder):
|
||||
return SubscriptionController(subscription_id), remainder
|
||||
|
||||
@wsme.validate(models.Subscription)
|
||||
@wsme_pecan.wsexpose(models.Subscription, body=models.Subscription,
|
||||
status_code=202)
|
||||
def post(self, body):
|
||||
row = central_api.create_subscription(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Subscription.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Subscription], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q,
|
||||
merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = central_api.list_subscriptions(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Subscription.from_db, rows)
|
@ -1,73 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 pecan import expose, request
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
|
||||
from billingstack.api.base import Query, _query_to_criterion, RestController
|
||||
from billingstack.api.v2 import models
|
||||
from billingstack.rater.rpcapi import rater_api
|
||||
|
||||
|
||||
class UsageController(RestController):
|
||||
def __init__(self, id_):
|
||||
self.id_ = id_
|
||||
request.context['usage_id'] = id_
|
||||
|
||||
@wsme_pecan.wsexpose(models.Usage)
|
||||
def get_all(self):
|
||||
row = rater_api.get_usage(request.ctxt, self.id_)
|
||||
|
||||
return models.Usage.from_db(row)
|
||||
|
||||
@wsme.validate(models.Usage)
|
||||
@wsme_pecan.wsexpose(models.Usage, body=models.Usage)
|
||||
def patch(self, body):
|
||||
row = rater_api.update_usage(request.ctxt, self.id_, body.to_db())
|
||||
|
||||
return models.Usage.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose(None, status_code=204)
|
||||
def delete(self):
|
||||
rater_api.delete_usage(request.ctxt, self.id_)
|
||||
|
||||
|
||||
class UsagesController(RestController):
|
||||
@expose()
|
||||
def _lookup(self, usage_id, *remainder):
|
||||
return UsageController(usage_id), remainder
|
||||
|
||||
@wsme.validate(models.Usage)
|
||||
@wsme_pecan.wsexpose(models.Usage, body=models.Usage, status_code=202)
|
||||
def post(self, body):
|
||||
row = rater_api.create_usage(
|
||||
request.ctxt,
|
||||
request.context['merchant_id'],
|
||||
body.to_db())
|
||||
|
||||
return models.Usage.from_db(row)
|
||||
|
||||
@wsme_pecan.wsexpose([models.Usage], [Query])
|
||||
def get_all(self, q=[]):
|
||||
criterion = _query_to_criterion(
|
||||
q,
|
||||
merchant_id=request.context['merchant_id'])
|
||||
|
||||
rows = rater_api.list_usages(
|
||||
request.ctxt, criterion=criterion)
|
||||
|
||||
return map(models.Usage.from_db, rows)
|
@ -1,221 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 wsme.types import text, DictType
|
||||
from datetime import datetime
|
||||
|
||||
from billingstack.api.base import ModelBase, property_type
|
||||
from billingstack.openstack.common import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class Base(ModelBase):
|
||||
id = text
|
||||
|
||||
|
||||
class DescribedBase(Base):
|
||||
name = text
|
||||
title = text
|
||||
description = text
|
||||
|
||||
|
||||
def change_suffixes(data, keys, shorten=True, suffix='_name'):
|
||||
"""
|
||||
Loop thro the keys foreach key setting for example
|
||||
'currency_name' > 'currency'
|
||||
"""
|
||||
for key in keys:
|
||||
if shorten:
|
||||
new, old = key, key + suffix
|
||||
else:
|
||||
new, old = key + suffix, key
|
||||
if old in data:
|
||||
if new in data:
|
||||
raise RuntimeError("Can't override old key with new key")
|
||||
|
||||
data[new] = data.pop(old)
|
||||
|
||||
|
||||
class Currency(DescribedBase):
|
||||
pass
|
||||
|
||||
|
||||
class Language(DescribedBase):
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceState(DescribedBase):
|
||||
pass
|
||||
|
||||
|
||||
class PGProvider(DescribedBase):
|
||||
def __init__(self, **kw):
|
||||
#kw['methods'] = [PGMethod.from_db(m) for m in kw.get('methods', [])]
|
||||
super(PGProvider, self).__init__(**kw)
|
||||
|
||||
methods = [DictType(key_type=text, value_type=property_type)]
|
||||
properties = DictType(key_type=text, value_type=property_type)
|
||||
|
||||
|
||||
class ContactInfo(Base):
|
||||
id = text
|
||||
first_name = text
|
||||
last_name = text
|
||||
company = text
|
||||
address1 = text
|
||||
address2 = text
|
||||
address3 = text
|
||||
locality = text
|
||||
region = text
|
||||
country_name = text
|
||||
postal_code = text
|
||||
|
||||
phone = text
|
||||
email = text
|
||||
website = text
|
||||
|
||||
|
||||
class PlanItem(ModelBase):
|
||||
name = text
|
||||
title = text
|
||||
description = text
|
||||
|
||||
plan_id = text
|
||||
product_id = text
|
||||
|
||||
pricing = [DictType(key_type=text, value_type=property_type)]
|
||||
|
||||
|
||||
class Plan(DescribedBase):
|
||||
def __init__(self, **kw):
|
||||
if 'items' in kw:
|
||||
kw['items'] = map(PlanItem.from_db, kw.pop('items'))
|
||||
super(Plan, self).__init__(**kw)
|
||||
|
||||
items = [PlanItem]
|
||||
properties = DictType(key_type=text, value_type=property_type)
|
||||
|
||||
|
||||
class Product(DescribedBase):
|
||||
properties = DictType(key_type=text, value_type=property_type)
|
||||
pricing = [DictType(key_type=text, value_type=property_type)]
|
||||
|
||||
|
||||
class InvoiceLine(Base):
|
||||
description = text
|
||||
price = float
|
||||
quantity = float
|
||||
sub_total = float
|
||||
invoice_id = text
|
||||
|
||||
|
||||
class Invoice(Base):
|
||||
identifier = text
|
||||
sub_total = float
|
||||
tax_percentage = float
|
||||
tax_total = float
|
||||
total = float
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
billing_day = int
|
||||
resource_id = text
|
||||
resource_type = text
|
||||
|
||||
plan_id = text
|
||||
customer_id = text
|
||||
payment_method_id = text
|
||||
|
||||
|
||||
class Usage(Base):
|
||||
measure = text
|
||||
start_timestamp = datetime
|
||||
end_timestamp = datetime
|
||||
price = float
|
||||
total = float
|
||||
value = float
|
||||
merchant_id = text
|
||||
product_id = text
|
||||
subscription_id = text
|
||||
|
||||
|
||||
class PGConfig(Base):
|
||||
name = text
|
||||
title = text
|
||||
|
||||
merchant_id = text
|
||||
provider_id = text
|
||||
|
||||
state = text
|
||||
|
||||
properties = DictType(key_type=text, value_type=property_type)
|
||||
|
||||
|
||||
class PaymentMethod(Base):
|
||||
name = text
|
||||
identifier = text
|
||||
expires = text
|
||||
|
||||
merchant_id = text
|
||||
customer_id = text
|
||||
provider_config_id = text
|
||||
|
||||
state = text
|
||||
|
||||
properties = DictType(key_type=text, value_type=property_type)
|
||||
|
||||
|
||||
class Account(Base):
|
||||
_keys = ['currency', 'language']
|
||||
|
||||
currency = text
|
||||
language = text
|
||||
|
||||
name = text
|
||||
|
||||
|
||||
class Merchant(Account):
|
||||
default_gateway = text
|
||||
|
||||
def to_db(self):
|
||||
values = self.as_dict()
|
||||
change_suffixes(values, self._keys, shorten=False)
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, values):
|
||||
change_suffixes(values, cls._keys)
|
||||
return cls(**values)
|
||||
|
||||
|
||||
class Customer(Account):
|
||||
merchant_id = text
|
||||
contact_info = [ContactInfo]
|
||||
|
||||
def __init__(self, **kw):
|
||||
infos = kw.get('contact_info', {})
|
||||
kw['contact_info'] = [ContactInfo.from_db(i) for i in infos]
|
||||
super(Customer, self).__init__(**kw)
|
||||
|
||||
def to_db(self):
|
||||
values = self.as_dict()
|
||||
change_suffixes(values, self._keys, shorten=False)
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, values):
|
||||
change_suffixes(values, cls._keys)
|
||||
return cls(**values)
|
@ -1,27 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='service:biller', title="Configuration for Biller Service"
|
||||
))
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.IntOpt('workers', default=None,
|
||||
help='Number of worker processes to spawn'),
|
||||
cfg.StrOpt('storage-driver', default='sqlalchemy',
|
||||
help='The storage driver to use'),
|
||||
], group='service:biller')
|
@ -1,94 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common.rpc import proxy
|
||||
|
||||
rpcapi_opts = [
|
||||
cfg.StrOpt('biller_topic', default='biller',
|
||||
help='the topic biller nodes listen on')
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(rpcapi_opts)
|
||||
|
||||
|
||||
class BillerAPI(proxy.RpcProxy):
|
||||
BASE_RPC_VERSION = '1.0'
|
||||
|
||||
def __init__(self):
|
||||
super(BillerAPI, self).__init__(
|
||||
topic=cfg.CONF.biller_topic,
|
||||
default_version=self.BASE_RPC_VERSION)
|
||||
|
||||
# Invoice States
|
||||
def create_invoice_state(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_invoice_state',
|
||||
values=values))
|
||||
|
||||
def list_invoice_states(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_invoice_states',
|
||||
criterion=criterion))
|
||||
|
||||
def get_invoice_state(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_invoice_state', id_=id_))
|
||||
|
||||
def update_invoice_state(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_invoice_state',
|
||||
id_=id_, values=values))
|
||||
|
||||
def delete_invoice_state(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_invoice_state', id_=id_))
|
||||
|
||||
# Invoices
|
||||
def create_invoice(self, ctxt, merchant_id, values):
|
||||
return self.call(ctxt, self.make_msg('create_invoice',
|
||||
merchant_id=merchant_id, values=values))
|
||||
|
||||
def list_invoices(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_invoices',
|
||||
criterion=criterion))
|
||||
|
||||
def get_invoice(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_invoice', id_=id_))
|
||||
|
||||
def update_invoice(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_invoice', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_invoice(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_invoice', id_=id_))
|
||||
|
||||
# Invoice lines
|
||||
def create_invoice_line(self, ctxt, invoice_id, values):
|
||||
return self.call(ctxt, self.make_msg('create_invoice_line',
|
||||
invoice_id=invoice_id, values=values))
|
||||
|
||||
def list_invoice_lines(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_invoice_lines',
|
||||
criterion=criterion))
|
||||
|
||||
def get_invoice_line(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_invoice_line', id_=id_))
|
||||
|
||||
def update_invoice_line(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_invoice_line', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_invoice_line(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_invoice_line', id_=id_))
|
||||
|
||||
|
||||
biller_api = BillerAPI()
|
@ -1,105 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.config import cfg
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common import service as os_service
|
||||
from billingstack.openstack.common.rpc import service as rpc_service
|
||||
from billingstack.storage.utils import get_connection
|
||||
from billingstack import service as bs_service
|
||||
|
||||
|
||||
cfg.CONF.import_opt('biller_topic', 'billingstack.biller.rpcapi')
|
||||
cfg.CONF.import_opt('host', 'billingstack.netconf')
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Service(rpc_service.Service):
|
||||
"""
|
||||
Biller service
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.update(
|
||||
host=cfg.CONF.host,
|
||||
topic=cfg.CONF.biller_topic,
|
||||
)
|
||||
|
||||
super(Service, self).__init__(*args, **kwargs)
|
||||
|
||||
def start(self):
|
||||
self.storage_conn = get_connection('biller')
|
||||
super(Service, self).start()
|
||||
|
||||
def wait(self):
|
||||
super(Service, self).wait()
|
||||
self.conn.consumer_thread.wait()
|
||||
|
||||
def create_invoice_state(self, ctxt, values):
|
||||
return self.storage_conn.create_invoice_state(ctxt, values)
|
||||
|
||||
def list_invoice_states(self, ctxt, **kw):
|
||||
return self.storage_conn.list_invoice_states(ctxt, **kw)
|
||||
|
||||
def get_invoice_state(self, ctxt, id_):
|
||||
return self.storage_conn.get_invoice_state(ctxt, id_)
|
||||
|
||||
def update_invoice_state(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_invoice_state(ctxt, id_, values)
|
||||
|
||||
def delete_invoice_state(self, ctxt, id_):
|
||||
return self.storage_conn.delete_invoice_state(ctxt, id_)
|
||||
|
||||
def create_invoice(self, ctxt, merchant_id, values):
|
||||
return self.storage_conn.create_invoice_state(
|
||||
ctxt, merchant_id, values)
|
||||
|
||||
def list_invoices(self, ctxt, **kw):
|
||||
return self.storage_conn.list_invoices(ctxt, **kw)
|
||||
|
||||
def get_invoice(self, ctxt, id_):
|
||||
return self.storage_conn.get_invoice(ctxt, id_)
|
||||
|
||||
def update_invoice(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_invoice(ctxt, id_, values)
|
||||
|
||||
def delete_invoice(self, ctxt, id_):
|
||||
return self.storage_conn.delete_invoice(ctxt, id_)
|
||||
|
||||
def create_invoice_line(self, ctxt, invoice_id, values):
|
||||
return self.storage_conn.create_invoice_line_state(
|
||||
ctxt, invoice_id, values)
|
||||
|
||||
def list_invoice_lines(self, ctxt, **kw):
|
||||
return self.storage_conn.list_invoice_lines(ctxt, **kw)
|
||||
|
||||
def get_invoice_line(self, ctxt, id_):
|
||||
return self.storage_conn.get_invoice_line(ctxt, id_)
|
||||
|
||||
def update_invoice_line(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_invoice_line(ctxt, id_, values)
|
||||
|
||||
def delete_invoice_line(self, ctxt, id_):
|
||||
return self.storage_conn.delete_invoice_line(ctxt, id_)
|
||||
|
||||
|
||||
def launch():
|
||||
bs_service.prepare_service(sys.argv)
|
||||
launcher = os_service.launch(Service(),
|
||||
cfg.CONF['service:biller'].workers)
|
||||
launcher.wait()
|
@ -1,26 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 billingstack.storage import base
|
||||
|
||||
|
||||
class StorageEngine(base.StorageEngine):
|
||||
"""Base class for the biller storage"""
|
||||
__plugin_ns__ = 'billingstack.biller.storage'
|
||||
|
||||
|
||||
class Connection(base.Connection):
|
||||
"""Define the base API for biller storage"""
|
@ -1,246 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
A Usage plugin using sqlalchemy...
|
||||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import DateTime, Float, Unicode
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.sqlalchemy.types import UUID
|
||||
from billingstack.sqlalchemy import api, model_base, session
|
||||
|
||||
from billingstack.biller.storage import Connection, StorageEngine
|
||||
from billingstack.central import rpcapi as central_api
|
||||
|
||||
# DB SCHEMA
|
||||
BASE = declarative_base(cls=model_base.ModelBase)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='biller:sqlalchemy', title='Config for biller sqlalchemy plugin'))
|
||||
|
||||
|
||||
cfg.CONF.register_opts(session.SQLOPTS, group='biller:sqlalchemy')
|
||||
|
||||
|
||||
class InvoiceState(BASE):
|
||||
"""
|
||||
A State representing the currented state a Invoice is in
|
||||
|
||||
Example:
|
||||
Completed, Failed
|
||||
"""
|
||||
name = Column(Unicode(60), nullable=False, primary_key=True)
|
||||
title = Column(Unicode(100), nullable=False)
|
||||
description = Column(Unicode(255))
|
||||
|
||||
|
||||
class Invoice(BASE, model_base.BaseMixin):
|
||||
"""
|
||||
An invoice
|
||||
"""
|
||||
identifier = Column(Unicode(255), nullable=False)
|
||||
due = Column(DateTime, )
|
||||
|
||||
sub_total = Column(Float)
|
||||
tax_percentage = Column(Float)
|
||||
tax_total = Column(Float)
|
||||
total = Column(Float)
|
||||
|
||||
customer_id = Column(UUID, nullable=False)
|
||||
|
||||
line_items = relationship('InvoiceLine', backref='invoice_lines')
|
||||
|
||||
state = relationship('InvoiceState', backref='invoices')
|
||||
state_id = Column(Unicode(60), ForeignKey('invoice_state.name'),
|
||||
nullable=False)
|
||||
|
||||
# Keep track of the currency and merchant
|
||||
currency_name = Column(Unicode(10), nullable=False)
|
||||
merchant_id = Column(UUID, nullable=False)
|
||||
|
||||
|
||||
class InvoiceLine(BASE, model_base.BaseMixin):
|
||||
"""
|
||||
A Line item in which makes up the Invoice
|
||||
"""
|
||||
description = Column(Unicode(255))
|
||||
price = Column(Float)
|
||||
quantity = Column(Float)
|
||||
sub_total = Column(Float)
|
||||
|
||||
invoice_id = Column(UUID, ForeignKey('invoice.id', ondelete='CASCADE',
|
||||
onupdate='CASCADE'), nullable=False)
|
||||
|
||||
|
||||
class SQLAlchemyEngine(StorageEngine):
|
||||
__plugin_name__ = 'sqlalchemy'
|
||||
|
||||
def get_connection(self):
|
||||
return Connection()
|
||||
|
||||
|
||||
class Connection(Connection, api.HelpersMixin):
|
||||
def __init__(self):
|
||||
self.setup('biller:sqlalchemy')
|
||||
|
||||
def base(self):
|
||||
return BASE
|
||||
|
||||
# Invoice States
|
||||
def create_invoice_state(self, ctxt, values):
|
||||
"""
|
||||
Add a supported invoice_state to the database
|
||||
"""
|
||||
row = InvoiceState(**values)
|
||||
self._save(row)
|
||||
return dict(row)
|
||||
|
||||
def list_invoice_states(self, ctxt, **kw):
|
||||
rows = self._list(InvoiceState, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_invoice_state(self, ctxt, id_):
|
||||
row = self._get_id_or_name(InvoiceState, id_)
|
||||
return dict(row)
|
||||
|
||||
def update_invoice_state(self, ctxt, id_, values):
|
||||
row = self._update(InvoiceState, id_, values, by_name=True)
|
||||
return dict(row)
|
||||
|
||||
def delete_invoice_state(self, ctxt, id_):
|
||||
self._delete(InvoiceState, id_, by_name=True)
|
||||
|
||||
# Invoices
|
||||
def _invoice(self, row):
|
||||
invoice = dict(row)
|
||||
return invoice
|
||||
|
||||
def create_invoice(self, ctxt, merchant_id, values):
|
||||
"""
|
||||
Add a new Invoice
|
||||
|
||||
:param merchant_id: The Merchant
|
||||
:param values: Values describing the new Invoice
|
||||
"""
|
||||
merchant = central_api.get_merchant(merchant_id)
|
||||
|
||||
invoice = Invoice(**values)
|
||||
invoice.merchant = merchant
|
||||
|
||||
self._save(invoice)
|
||||
return self._invoice(invoice)
|
||||
|
||||
def list_invoices(self, ctxt, **kw):
|
||||
"""
|
||||
List Invoices
|
||||
"""
|
||||
rows = self._list(Invoice, **kw)
|
||||
return map(self._invoice, rows)
|
||||
|
||||
def get_invoice(self, ctxt, id_):
|
||||
"""
|
||||
Get a Invoice
|
||||
|
||||
:param id_: The Invoice ID
|
||||
"""
|
||||
row = self._get(Invoice, id_)
|
||||
return self.invoice(row)
|
||||
|
||||
def update_invoice(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Invoice
|
||||
|
||||
:param id_: The Invoice ID
|
||||
:param values: Values to update with
|
||||
"""
|
||||
row = self._get(Invoice, id_)
|
||||
row.update(values)
|
||||
|
||||
self._save(row)
|
||||
return self._invoice(row)
|
||||
|
||||
def delete_invoice(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Invoice
|
||||
|
||||
:param id_: Invoice ID
|
||||
"""
|
||||
self._delete(Invoice, id_)
|
||||
|
||||
# Invoices Items
|
||||
def _invoice_line(self, row):
|
||||
line = dict(row)
|
||||
return line
|
||||
|
||||
def create_invoice_items(self, ctxt, invoice_id, values):
|
||||
"""
|
||||
Add a new Invoice
|
||||
|
||||
:param invoice_id: The Invoice
|
||||
:param values: Values describing the new Invoice Line
|
||||
"""
|
||||
invoice = self._get(Invoice, invoice_id)
|
||||
|
||||
line = InvoiceLine(**values)
|
||||
line.invoice = invoice
|
||||
|
||||
self._save(line)
|
||||
return self._invoice_line(line)
|
||||
|
||||
def list_invoice_lines(self, ctxt, **kw):
|
||||
"""
|
||||
List Invoice Lines
|
||||
"""
|
||||
rows = self._list(InvoiceLine, **kw)
|
||||
return map(self._invoice_line, rows)
|
||||
|
||||
def get_invoice_line(self, ctxt, id_):
|
||||
"""
|
||||
Get a Invoice Line
|
||||
|
||||
:param id_: The Invoice Line ID
|
||||
"""
|
||||
row = self._get(InvoiceLine, id_)
|
||||
return self._invoice_line(row)
|
||||
|
||||
def update_invoice_line(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Invoice Line
|
||||
|
||||
:param id_: The Invoice ID
|
||||
:param values: Values to update with
|
||||
"""
|
||||
row = self._get(InvoiceLine, id_)
|
||||
row.update(values)
|
||||
|
||||
self._save(row)
|
||||
return self._invoice_line(row)
|
||||
|
||||
def delete_invoice_line(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Invoice Line
|
||||
|
||||
:param id_: Invoice Line ID
|
||||
"""
|
||||
self._delete(InvoiceLine, id_)
|
@ -1,28 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='service:central', title="Configuration for Central Service"
|
||||
))
|
||||
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.IntOpt('workers', default=None,
|
||||
help='Number of worker processes to spawn'),
|
||||
cfg.StrOpt('storage-driver', default='sqlalchemy',
|
||||
help='The storage driver to use'),
|
||||
], group='service:central')
|
@ -1,43 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in co68mpliance 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 taskflow.patterns import linear_flow
|
||||
|
||||
from billingstack import tasks
|
||||
from billingstack.openstack.common import log
|
||||
|
||||
ACTION = 'merchant:create'
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class EntryCreateTask(tasks.RootTask):
|
||||
def __init__(self, storage, **kw):
|
||||
super(EntryCreateTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, values):
|
||||
return self.storage.create_merchant(ctxt, values)
|
||||
|
||||
|
||||
def create_flow(storage):
|
||||
flow = linear_flow.Flow(ACTION)
|
||||
|
||||
entry_task = EntryCreateTask(storage, provides='merchant', prefix=ACTION)
|
||||
flow.add(entry_task)
|
||||
|
||||
return flow
|
@ -1,211 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common.rpc import proxy
|
||||
|
||||
rpcapi_opts = [
|
||||
cfg.StrOpt('central_topic', default='central',
|
||||
help='the topic central nodes listen on')
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(rpcapi_opts)
|
||||
|
||||
|
||||
class CentralAPI(proxy.RpcProxy):
|
||||
BASE_RPC_VERSION = '1.0'
|
||||
|
||||
def __init__(self):
|
||||
super(CentralAPI, self).__init__(
|
||||
topic=cfg.CONF.central_topic,
|
||||
default_version=self.BASE_RPC_VERSION)
|
||||
|
||||
# Currency
|
||||
def create_currency(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_currency', values=values))
|
||||
|
||||
def list_currencies(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_currencies',
|
||||
criterion=criterion))
|
||||
|
||||
def get_currency(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_currency',
|
||||
id_=id_))
|
||||
|
||||
def update_currency(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_currency',
|
||||
id_=id_, values=values))
|
||||
|
||||
def delete_currency(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_currency',
|
||||
id_=id_))
|
||||
|
||||
# Language
|
||||
def create_language(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_language', values=values))
|
||||
|
||||
def list_languages(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_languages',
|
||||
criterion=criterion))
|
||||
|
||||
def get_language(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_language', id_=id_))
|
||||
|
||||
def update_language(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_language',
|
||||
id_=id_, values=values))
|
||||
|
||||
def delete_language(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_language', id_=id_))
|
||||
|
||||
# Contact Info
|
||||
def create_contact_info(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('create_contact_info', id_=id_,
|
||||
values=values))
|
||||
|
||||
def get_contact_info(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_contact_info', id_))
|
||||
|
||||
def update_contact_info(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_contact_info', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_contact_info(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_contact_info', id_=id_))
|
||||
|
||||
# Merchant
|
||||
def create_merchant(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_merchant', values=values))
|
||||
|
||||
def list_merchants(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_merchants',
|
||||
criterion=criterion))
|
||||
|
||||
def get_merchant(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_merchant', id_=id_))
|
||||
|
||||
def update_merchant(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_merchant',
|
||||
id_=id_, values=values))
|
||||
|
||||
def delete_merchant(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_merchant',
|
||||
id_=id_))
|
||||
|
||||
# Customer
|
||||
def create_customer(self, ctxt, merchant_id, values):
|
||||
return self.call(ctxt, self.make_msg('create_customer',
|
||||
merchant_id=merchant_id, values=values))
|
||||
|
||||
def list_customers(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_customers',
|
||||
criterion=criterion))
|
||||
|
||||
def get_customer(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_customer', id_=id_))
|
||||
|
||||
def update_customer(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_customer',
|
||||
id_=id_, values=values))
|
||||
|
||||
def delete_customer(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_customer', id_=id_))
|
||||
|
||||
# Plans
|
||||
def create_plan(self, ctxt, merchant_id, values):
|
||||
return self.call(ctxt, self.make_msg('create_plan',
|
||||
merchant_id=merchant_id, values=values))
|
||||
|
||||
def list_plans(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_plans',
|
||||
criterion=criterion))
|
||||
|
||||
def get_plan(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_plan', id_=id_))
|
||||
|
||||
def update_plan(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_plan', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_plan(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_plan', id_=id_))
|
||||
|
||||
def get_plan_by_subscription(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_plan_by_subscription',
|
||||
id_=id_))
|
||||
|
||||
# PlanItems
|
||||
def create_plan_item(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_plan_item',
|
||||
values=values))
|
||||
|
||||
def list_plan_items(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_plan_items',
|
||||
criterion=criterion))
|
||||
|
||||
def get_plan_item(self, ctxt, plan_id, product_id):
|
||||
return self.call(ctxt, self.make_msg('get_plan_item',
|
||||
plan_id=plan_id, product_id=product_id))
|
||||
|
||||
def update_plan_item(self, ctxt, plan_id, product_id, values):
|
||||
return self.call(ctxt, self.make_msg('update_plan_item',
|
||||
plan_id=plan_id, product_id=product_id,
|
||||
values=values))
|
||||
|
||||
def delete_plan_item(self, ctxt, plan_id, product_id):
|
||||
return self.call(ctxt, self.make_msg('delete_plan_item',
|
||||
plan_id=plan_id, product_id=product_id))
|
||||
|
||||
# Products
|
||||
def create_product(self, ctxt, merchant_id, values):
|
||||
return self.call(ctxt, self.make_msg('create_product',
|
||||
merchant_id=merchant_id, values=values))
|
||||
|
||||
def list_products(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_products',
|
||||
criterion=criterion))
|
||||
|
||||
def get_product(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_product', id_=id_))
|
||||
|
||||
def update_product(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_product', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_product(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_product', id_=id_))
|
||||
|
||||
# Subscriptions
|
||||
def create_subscription(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_subscription',
|
||||
values=values))
|
||||
|
||||
def list_subscriptions(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_subscriptions',
|
||||
criterion=criterion))
|
||||
|
||||
def get_subscription(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_subscription', id_=id_))
|
||||
|
||||
def update_subscription(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_subscription', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_subscription(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_subscription', id_=id_))
|
||||
|
||||
|
||||
central_api = CentralAPI()
|
@ -1,215 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.config import cfg
|
||||
from taskflow.engines import run as run_flow
|
||||
|
||||
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common.rpc import service as rpc_service
|
||||
from billingstack.openstack.common import service as os_service
|
||||
from billingstack.central.flows import merchant
|
||||
from billingstack.storage.utils import get_connection
|
||||
from billingstack import service as bs_service
|
||||
|
||||
|
||||
cfg.CONF.import_opt('central_topic', 'billingstack.central.rpcapi')
|
||||
cfg.CONF.import_opt('host', 'billingstack.netconf')
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Service(rpc_service.Service):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.update(
|
||||
host=cfg.CONF.host,
|
||||
topic=cfg.CONF.central_topic,
|
||||
)
|
||||
|
||||
super(Service, self).__init__(*args, **kwargs)
|
||||
|
||||
def start(self):
|
||||
self.storage_conn = get_connection('central')
|
||||
super(Service, self).start()
|
||||
|
||||
def wait(self):
|
||||
super(Service, self).wait()
|
||||
self.conn.consumer_thread.wait()
|
||||
|
||||
# Currency
|
||||
def create_currency(self, ctxt, values):
|
||||
return self.storage_conn.create_currency(ctxt, values)
|
||||
|
||||
def list_currencies(self, ctxt, **kw):
|
||||
return self.storage_conn.list_currencies(ctxt, **kw)
|
||||
|
||||
def get_currency(self, ctxt, id_):
|
||||
return self.storage_conn.get_currency(ctxt, id_)
|
||||
|
||||
def update_currency(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_currency(ctxt, id_, values)
|
||||
|
||||
def delete_currency(self, ctxt, id_):
|
||||
return self.storage_conn.delete_currency(ctxt, id_)
|
||||
|
||||
# Language
|
||||
def create_language(self, ctxt, values):
|
||||
return self.storage_conn.create_language(ctxt, values)
|
||||
|
||||
def list_languages(self, ctxt, **kw):
|
||||
return self.storage_conn.list_languages(ctxt, **kw)
|
||||
|
||||
def get_language(self, ctxt, id_):
|
||||
return self.storage_conn.get_language(ctxt, id_)
|
||||
|
||||
def update_language(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_language(ctxt, id_, values)
|
||||
|
||||
def delete_language(self, ctxt, id_):
|
||||
return self.storage_conn.delete_language(ctxt, id_)
|
||||
|
||||
# Contact Info
|
||||
def create_contact_info(self, ctxt, obj, values, cls=None,
|
||||
rel_attr='contact_info'):
|
||||
return self.storage_conn.create_contact_info(ctxt, values)
|
||||
|
||||
def get_contact_info(self, ctxt, id_):
|
||||
return self.storage_conn.get_contact_info(ctxt, id_)
|
||||
|
||||
def update_contact_info(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_contact_info(ctxt, values)
|
||||
|
||||
def delete_contact_info(self, ctxt, id_):
|
||||
return self.storage_conn.delete_contact_info(ctxt, id_)
|
||||
|
||||
# PGP
|
||||
def list_pg_providers(self, ctxt, **kw):
|
||||
return self.storage_conn.list_pg_providers(ctxt, **kw)
|
||||
|
||||
def get_pg_provider(self, ctxt, pgp_id):
|
||||
return self.storage_conn.get_pg_provider(ctxt, pgp_id)
|
||||
|
||||
# Merchant
|
||||
def create_merchant(self, ctxt, values):
|
||||
flow = merchant.create_flow(self.storage_conn)
|
||||
result = run_flow(flow, engine_conf="parallel",
|
||||
store={'values': values, 'ctxt': ctxt})
|
||||
return result['merchant']
|
||||
|
||||
def list_merchants(self, ctxt, **kw):
|
||||
return self.storage_conn.list_merchants(ctxt, **kw)
|
||||
|
||||
def get_merchant(self, ctxt, id_):
|
||||
return self.storage_conn.get_merchant(ctxt, id_)
|
||||
|
||||
def update_merchant(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_merchant(ctxt, id_, values)
|
||||
|
||||
def delete_merchant(self, ctxt, id_):
|
||||
return self.storage_conn.delete_merchant(ctxt, id_)
|
||||
|
||||
# Customer
|
||||
def create_customer(self, ctxt, merchant_id, values):
|
||||
return self.storage_conn.create_customer(ctxt, merchant_id, values)
|
||||
|
||||
def list_customers(self, ctxt, **kw):
|
||||
return self.storage_conn.list_customers(ctxt, **kw)
|
||||
|
||||
def get_customer(self, ctxt, id_):
|
||||
return self.storage_conn.get_customer(ctxt, id_)
|
||||
|
||||
def update_customer(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_customer(ctxt, id_, values)
|
||||
|
||||
def delete_customer(self, ctxt, id_):
|
||||
return self.storage_conn.delete_customer(ctxt, id_)
|
||||
|
||||
# Plans
|
||||
def create_plan(self, ctxt, merchant_id, values):
|
||||
return self.storage_conn.create_plan(ctxt, merchant_id, values)
|
||||
|
||||
def list_plans(self, ctxt, **kw):
|
||||
return self.storage_conn.list_plans(ctxt, **kw)
|
||||
|
||||
def get_plan(self, ctxt, id_):
|
||||
return self.storage_conn.get_plan(ctxt, id_)
|
||||
|
||||
def update_plan(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_plan(ctxt, id_, values)
|
||||
|
||||
def delete_plan(self, ctxt, id_):
|
||||
return self.storage_conn.delete_plan(ctxt, id_)
|
||||
|
||||
def get_plan_by_subscription(self, ctxt, id_):
|
||||
return self.storage_conn.get_plan_by_subscription(ctxt, id_)
|
||||
|
||||
# PlanItems
|
||||
def create_plan_item(self, ctxt, values):
|
||||
return self.storage_conn.create_plan_item(ctxt, values)
|
||||
|
||||
def list_plan_items(self, ctxt, **kw):
|
||||
return self.storage_conn.list_plan_items(ctxt, **kw)
|
||||
|
||||
def get_plan_item(self, ctxt, plan_id, product_id):
|
||||
return self.storage_conn.get_plan_item(ctxt, plan_id, product_id)
|
||||
|
||||
def update_plan_item(self, ctxt, plan_id, product_id, values):
|
||||
return self.storage_conn.update_plan_item(
|
||||
ctxt, plan_id, product_id, values)
|
||||
|
||||
def delete_plan_item(self, ctxt, plan_id, product_id):
|
||||
return self.storage_conn.delete_plan_item(ctxt, plan_id, product_id)
|
||||
|
||||
# Products
|
||||
def create_product(self, ctxt, merchant_id, values):
|
||||
return self.storage_conn.create_product(ctxt, merchant_id, values)
|
||||
|
||||
def list_products(self, ctxt, **kw):
|
||||
return self.storage_conn.list_products(ctxt, **kw)
|
||||
|
||||
def get_product(self, ctxt, id_):
|
||||
return self.storage_conn.get_product(ctxt, id_)
|
||||
|
||||
def update_product(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_product(ctxt, id_, values)
|
||||
|
||||
def delete_product(self, ctxt, id_):
|
||||
return self.storage_conn.delete_product(ctxt, id_)
|
||||
|
||||
# Subscriptions
|
||||
def create_subscription(self, ctxt, values):
|
||||
return self.storage_conn.create_subscription(ctxt, values)
|
||||
|
||||
def list_subscriptions(self, ctxt, **kw):
|
||||
return self.storage_conn.list_subscriptions(ctxt, **kw)
|
||||
|
||||
def get_subscription(self, ctxt, id_):
|
||||
return self.storage_conn.get_subscription(ctxt, id_)
|
||||
|
||||
def update_subscription(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_subscription(ctxt, id_, values)
|
||||
|
||||
def delete_subscription(self, ctxt, id_):
|
||||
return self.storage_conn.delete_subscription(ctxt, id_)
|
||||
|
||||
|
||||
def launch():
|
||||
bs_service.prepare_service(sys.argv)
|
||||
launcher = os_service.launch(Service(),
|
||||
cfg.CONF['service:central'].workers)
|
||||
launcher.wait()
|
@ -1,31 +0,0 @@
|
||||
# Copyright 2012 Managed I.T.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@managedit.ie>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copied: Moniker
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.storage import base
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StorageEngine(base.StorageEngine):
|
||||
__plugin_type__ = 'central'
|
||||
__plugin_ns__ = 'billingstack.central.storage'
|
||||
|
||||
|
||||
class Connection(base.Connection):
|
||||
pass
|
@ -1,502 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 sqlalchemy.orm import exc
|
||||
from oslo.config import cfg
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack import exceptions
|
||||
from billingstack import utils as common_utils
|
||||
from billingstack.sqlalchemy import utils as db_utils, api
|
||||
from billingstack.sqlalchemy.session import SQLOPTS
|
||||
from billingstack.central.storage import Connection, StorageEngine
|
||||
from billingstack.central.storage.impl_sqlalchemy import models
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='central:sqlalchemy', title="Configuration for SQLAlchemy Storage"
|
||||
))
|
||||
|
||||
cfg.CONF.register_opts(SQLOPTS, group='central:sqlalchemy')
|
||||
|
||||
|
||||
class SQLAlchemyEngine(StorageEngine):
|
||||
__plugin_name__ = 'sqlalchemy'
|
||||
|
||||
def get_connection(self):
|
||||
return Connection(self.name)
|
||||
|
||||
|
||||
class Connection(Connection, api.HelpersMixin):
|
||||
"""
|
||||
SQLAlchemy connection
|
||||
"""
|
||||
def __init__(self, config_group):
|
||||
self.setup(config_group)
|
||||
|
||||
def base(self):
|
||||
return models.BASE
|
||||
|
||||
def set_properties(self, obj, properties, cls=None, rel_attr='properties',
|
||||
purge=False):
|
||||
"""
|
||||
Set's a dict with key values on a relation on the row
|
||||
|
||||
:param obj: Either a row object or a id to use in connection with cls
|
||||
:param properties: Key and Value dict with props to set. 1 row item.
|
||||
:param cls: The class to use if obj isn't a row to query.
|
||||
:param rel_attr: The relation attribute name to get the class to use
|
||||
:param purge: Purge entries that doesn't exist in existing but in DB
|
||||
"""
|
||||
row = self._get_row(obj, cls=cls)
|
||||
|
||||
existing = self._kv_rows(row[rel_attr])
|
||||
|
||||
for key, value in properties.items():
|
||||
values = {'name': key, 'value': value}
|
||||
|
||||
if key not in existing:
|
||||
rel_row = self._make_rel_row(row, rel_attr, values)
|
||||
row[rel_attr].append(rel_row)
|
||||
else:
|
||||
existing[key].update(values)
|
||||
|
||||
if purge:
|
||||
for key in existing:
|
||||
if not key in properties:
|
||||
row[rel_attr].remove(existing[key])
|
||||
|
||||
# Currency
|
||||
def create_currency(self, ctxt, values):
|
||||
"""
|
||||
Add a supported currency to the database
|
||||
"""
|
||||
data = common_utils.get_currency(values['name'])
|
||||
row = models.Currency(**data)
|
||||
self._save(row)
|
||||
return dict(row)
|
||||
|
||||
def list_currencies(self, ctxt, **kw):
|
||||
rows = self._list(models.Currency, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_currency(self, ctxt, id_):
|
||||
row = self._get_id_or_name(models.Currency, id_)
|
||||
return dict(row)
|
||||
|
||||
def update_currency(self, ctxt, id_, values):
|
||||
row = self._update(models.Currency, id_, values, by_name=True)
|
||||
return dict(row)
|
||||
|
||||
def delete_currency(self, ctxt, id_):
|
||||
self._delete(models.Currency, id_, by_name=True)
|
||||
|
||||
# Language
|
||||
def create_language(self, ctxt, values):
|
||||
"""
|
||||
Add a supported language to the database
|
||||
"""
|
||||
data = common_utils.get_language(values['name'])
|
||||
row = models.Language(**data)
|
||||
self._save(row)
|
||||
return dict(row)
|
||||
|
||||
def list_languages(self, ctxt, **kw):
|
||||
rows = self._list(models.Language, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_language(self, ctxt, id_):
|
||||
row = self._get_id_or_name(models.Language, id_)
|
||||
return dict(row)
|
||||
|
||||
def update_language(self, ctxt, id_, values):
|
||||
row = self._update(models.Language, id_, values, by_name=True)
|
||||
return dict(row)
|
||||
|
||||
def delete_language(self, ctxt, id_):
|
||||
self._delete(models.Language, id_, by_name=True)
|
||||
|
||||
# ContactInfo
|
||||
def create_contact_info(self, ctxt, obj, values, cls=None,
|
||||
rel_attr='contact_info'):
|
||||
"""
|
||||
:param entity: The object to add the contact_info to
|
||||
:param values: The values to add
|
||||
"""
|
||||
row = self._get_row(obj, cls=cls)
|
||||
|
||||
rel_row = self._make_rel_row(obj, rel_attr, values)
|
||||
|
||||
local, remote = db_utils.get_prop_names(row)
|
||||
|
||||
if rel_attr in remote:
|
||||
if isinstance(row[rel_attr], list):
|
||||
row[rel_attr].append(rel_row)
|
||||
else:
|
||||
row[rel_attr] = rel_row
|
||||
else:
|
||||
msg = 'Attempted to set non-relation %s' % rel_attr
|
||||
raise exceptions.BadRequest(msg)
|
||||
|
||||
if cls:
|
||||
self._save(rel_row)
|
||||
return dict(rel_row)
|
||||
else:
|
||||
return rel_row
|
||||
|
||||
def get_contact_info(self, ctxt, id_):
|
||||
self._get(models.ContactInfo, id_)
|
||||
|
||||
def update_contact_info(self, ctxt, id_, values):
|
||||
return self._update(models.ContactInfo, id_, values)
|
||||
|
||||
def delete_contact_info(self, ctxt, id_):
|
||||
self._delete(models.ContactInfo, id_)
|
||||
|
||||
# Merchant
|
||||
def create_merchant(self, ctxt, values):
|
||||
row = models.Merchant(**values)
|
||||
|
||||
self._save(row)
|
||||
return dict(row)
|
||||
|
||||
def list_merchants(self, ctxt, **kw):
|
||||
rows = self._list(models.Merchant, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_merchant(self, ctxt, id_):
|
||||
row = self._get(models.Merchant, id_)
|
||||
return dict(row)
|
||||
|
||||
def update_merchant(self, ctxt, id_, values):
|
||||
row = self._update(models.Merchant, id_, values)
|
||||
return dict(row)
|
||||
|
||||
def delete_merchant(self, ctxt, id_):
|
||||
self._delete(models.Merchant, id_)
|
||||
|
||||
# Customer
|
||||
def _customer(self, row):
|
||||
data = dict(row)
|
||||
|
||||
data['contact_info'] = [dict(i) for i in row.contact_info]
|
||||
data['default_info'] = dict(row.default_info) if row.default_info\
|
||||
else {}
|
||||
return data
|
||||
|
||||
def create_customer(self, ctxt, merchant_id, values):
|
||||
merchant = self._get(models.Merchant, merchant_id)
|
||||
|
||||
contact_info = values.pop('contact_info', None)
|
||||
customer = models.Customer(**values)
|
||||
merchant.customers.append(customer)
|
||||
|
||||
if contact_info:
|
||||
info_row = self.create_contact_info(ctxt, customer, contact_info)
|
||||
customer.default_info = info_row
|
||||
|
||||
self._save(customer)
|
||||
return self._customer(customer)
|
||||
|
||||
def list_customers(self, ctxt, **kw):
|
||||
rows = self._list(models.Customer, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_customer(self, ctxt, id_):
|
||||
row = self._get(models.Customer, id_)
|
||||
return self._customer(row)
|
||||
|
||||
def update_customer(self, ctxt, id_, values):
|
||||
row = self._update(models.Customer, id_, values)
|
||||
return self._customer(row)
|
||||
|
||||
def delete_customer(self, ctxt, id_):
|
||||
return self._delete(models.Customer, id_)
|
||||
|
||||
def _entity(self, row):
|
||||
"""
|
||||
Helper to serialize a entity like a Product or a Plan
|
||||
|
||||
:param row: The Row.
|
||||
"""
|
||||
entity = dict(row)
|
||||
if hasattr(row, 'properties'):
|
||||
entity['properties'] = self._kv_rows(
|
||||
row.properties, func=lambda i: i['value'])
|
||||
if hasattr(row, 'pricing'):
|
||||
entity['pricing'] = row.pricing or []
|
||||
return entity
|
||||
|
||||
# Plan
|
||||
def _plan(self, row):
|
||||
plan = self._entity(row)
|
||||
plan['items'] = map(self._plan_item, row.plan_items) if row.plan_items\
|
||||
else []
|
||||
return plan
|
||||
|
||||
def create_plan(self, ctxt, merchant_id, values):
|
||||
"""
|
||||
Add a new Plan
|
||||
|
||||
:param merchant_id: The Merchant
|
||||
:param values: Values describing the new Plan
|
||||
"""
|
||||
merchant = self._get(models.Merchant, merchant_id)
|
||||
|
||||
properties = values.pop('properties', {})
|
||||
|
||||
plan = models.Plan(**values)
|
||||
|
||||
plan.merchant = merchant
|
||||
self.set_properties(plan, properties)
|
||||
|
||||
self._save(plan)
|
||||
return self._plan(plan)
|
||||
|
||||
def list_plans(self, ctxt, **kw):
|
||||
"""
|
||||
List Plan
|
||||
|
||||
:param merchant_id: The Merchant to list it for
|
||||
"""
|
||||
rows = self._list(models.Plan, **kw)
|
||||
return map(self._plan, rows)
|
||||
|
||||
def get_plan(self, ctxt, id_):
|
||||
"""
|
||||
Get a Plan
|
||||
|
||||
:param id_: The Plan ID
|
||||
"""
|
||||
row = self._get(models.Plan, id_)
|
||||
return self._plan(row)
|
||||
|
||||
def update_plan(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Plan
|
||||
|
||||
:param id_: The Plan ID
|
||||
:param values: Values to update with
|
||||
"""
|
||||
properties = values.pop('properties', {})
|
||||
|
||||
row = self._get(models.Plan, id_)
|
||||
row.update(values)
|
||||
|
||||
self.set_properties(row, properties)
|
||||
|
||||
self._save(row)
|
||||
return self._plan(row)
|
||||
|
||||
def delete_plan(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Plan
|
||||
|
||||
:param id_: Plan ID
|
||||
"""
|
||||
self._delete(models.Plan, id_)
|
||||
|
||||
def get_plan_by_subscription(self, ctxt, subscription_id):
|
||||
q = self.session.query(models.Plan).join(models.Subscription)\
|
||||
.filter(models.Subscription.id == subscription_id)
|
||||
try:
|
||||
row = q.one()
|
||||
except exc.NoResultFound:
|
||||
msg = 'Couldn\'t find any Plan for subscription %s' % \
|
||||
subscription_id
|
||||
raise exceptions.NotFound(msg)
|
||||
return self._plan(row)
|
||||
|
||||
# PlanItemw
|
||||
def _plan_item(self, row):
|
||||
entity = self._entity(row)
|
||||
entity['name'] = row.product.name
|
||||
entity['title'] = row.title or row.product.title
|
||||
entity['description'] = row.description or row.product.description
|
||||
return entity
|
||||
|
||||
def create_plan_item(self, ctxt, values):
|
||||
row = models.PlanItem(**values)
|
||||
self._save(row)
|
||||
return self._entity(row)
|
||||
|
||||
def list_plan_items(self, ctxt, **kw):
|
||||
return self._list(models.PlanItem, **kw)
|
||||
|
||||
def get_plan_item(self, ctxt, plan_id, product_id, criterion={}):
|
||||
criterion.update({'plan_id': plan_id, 'product_id': product_id})
|
||||
row = self._get(models.PlanItem, criterion=criterion)
|
||||
return self._entity(row)
|
||||
|
||||
def update_plan_item(self, ctxt, plan_id, product_id, values):
|
||||
criterion = {'plan_id': plan_id, 'product_id': product_id}
|
||||
row = self._get(models.PlanItem, criterion=criterion)
|
||||
row.update(values)
|
||||
self._save(row)
|
||||
return self._entity(row)
|
||||
|
||||
def delete_plan_item(self, ctxt, plan_id, product_id):
|
||||
"""
|
||||
Remove a Product from a Plan by deleting the PlanItem.
|
||||
|
||||
:param plan_id: The Plan's ID.
|
||||
:param product_id: The Product's ID.
|
||||
"""
|
||||
query = self.session.query(models.PlanItem).\
|
||||
filter_by(plan_id=plan_id, product_id=product_id)
|
||||
|
||||
count = query.delete()
|
||||
if count == 0:
|
||||
msg = 'Couldn\'t match plan_id %s or product_id %s' % (
|
||||
plan_id, product_id)
|
||||
raise exceptions.NotFound(msg)
|
||||
|
||||
# Products
|
||||
def _product(self, row):
|
||||
product = self._entity(row)
|
||||
return product
|
||||
|
||||
def create_product(self, ctxt, merchant_id, values):
|
||||
"""
|
||||
Add a new Product
|
||||
|
||||
:param merchant_id: The Merchant
|
||||
:param values: Values describing the new Product
|
||||
"""
|
||||
values = values.copy()
|
||||
|
||||
merchant = self._get(models.Merchant, merchant_id)
|
||||
|
||||
properties = values.pop('properties', {})
|
||||
|
||||
product = models.Product(**values)
|
||||
product.merchant = merchant
|
||||
|
||||
self.set_properties(product, properties)
|
||||
|
||||
self._save(product)
|
||||
return self._product(product)
|
||||
|
||||
def list_products(self, ctxt, **kw):
|
||||
"""
|
||||
List Products
|
||||
|
||||
:param merchant_id: The Merchant to list it for
|
||||
"""
|
||||
rows = self._list(models.Product, **kw)
|
||||
return map(self._product, rows)
|
||||
|
||||
def get_product(self, ctxt, id_):
|
||||
"""
|
||||
Get a Product
|
||||
|
||||
:param id_: The Product ID
|
||||
"""
|
||||
row = self._get(models.Product, id_)
|
||||
return self._product(row)
|
||||
|
||||
def update_product(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Product
|
||||
|
||||
:param id_: The Product ID
|
||||
:param values: Values to update with
|
||||
"""
|
||||
values = values.copy()
|
||||
properties = values.pop('properties', {})
|
||||
|
||||
row = self._get(models.Product, id_)
|
||||
row.update(values)
|
||||
|
||||
self.set_properties(row, properties)
|
||||
|
||||
self._save(row)
|
||||
return self._product(row)
|
||||
|
||||
def delete_product(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Product
|
||||
|
||||
:param id_: Product ID
|
||||
"""
|
||||
self._delete(models.Product, id_)
|
||||
|
||||
# Subscriptions
|
||||
def _subscription(self, row):
|
||||
subscription = dict(row)
|
||||
return subscription
|
||||
|
||||
def create_subscription(self, ctxt, values):
|
||||
"""
|
||||
Add a new Subscription
|
||||
|
||||
:param merchant_id: The Merchant
|
||||
:param values: Values describing the new Subscription
|
||||
"""
|
||||
subscription = models.Subscription(**values)
|
||||
|
||||
self._save(subscription)
|
||||
return self._subscription(subscription)
|
||||
|
||||
def list_subscriptions(self, ctxt, criterion=None, **kw):
|
||||
"""
|
||||
List Subscriptions
|
||||
|
||||
:param merchant_id: The Merchant to list it for
|
||||
"""
|
||||
query = self.session.query(models.Subscription)
|
||||
|
||||
# NOTE: Filter needs to be joined for merchant_id
|
||||
query = db_utils.filter_merchant_by_join(
|
||||
query, models.Customer, criterion)
|
||||
|
||||
rows = self._list(
|
||||
query=query,
|
||||
cls=models.Subscription,
|
||||
criterion=criterion,
|
||||
**kw)
|
||||
|
||||
return map(self._subscription, rows)
|
||||
|
||||
def get_subscription(self, ctxt, id_):
|
||||
"""
|
||||
Get a Subscription
|
||||
|
||||
:param id_: The Subscription ID
|
||||
"""
|
||||
row = self._get(models.Subscription, id_)
|
||||
return self._subscription(row)
|
||||
|
||||
def update_subscription(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Subscription
|
||||
|
||||
:param id_: The Subscription ID
|
||||
:param values: Values to update with
|
||||
"""
|
||||
row = self._get(models.Subscription, id_)
|
||||
row.update(values)
|
||||
|
||||
self._save(row)
|
||||
return self._subscription(row)
|
||||
|
||||
def delete_subscription(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Subscription
|
||||
|
||||
:param id_: Subscription ID
|
||||
"""
|
||||
self._delete(models.Subscription, id_)
|
@ -1,94 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author Mark McClain (DreamHost)
|
||||
|
||||
The migrations in the alembic/versions contain the changes needed to migrate
|
||||
from older billingstack releases to newer versions. A migration occurs by executing
|
||||
a script that details the changes needed to upgrade/downgrade the database. The
|
||||
migration scripts are ordered so that multiple scripts can run sequentially to
|
||||
update the database. The scripts are executed by billingstack's migration wrapper
|
||||
which uses the Alembic library to manage the migration. billingstack supports
|
||||
migration from Folsom or later.
|
||||
|
||||
|
||||
If you are a deployer or developer and want to migrate from Folsom to Grizzly
|
||||
or later you must first add version tracking to the database:
|
||||
|
||||
$ billingstack-db-manage -config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini stamp folsom
|
||||
|
||||
You can then upgrade to the latest database version via:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini upgrade head
|
||||
|
||||
To check the current database version:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini current
|
||||
|
||||
To create a script to run the migration offline:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini upgrade head --sql
|
||||
|
||||
To run the offline migration between specific migration versions:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini upgrade \
|
||||
<start version>:<end version> --sql
|
||||
|
||||
Upgrade the database incrementally:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini upgrade --delta <# of revs>
|
||||
|
||||
Downgrade the database by a certain number of revisions:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini downgrade --delta <# of revs>
|
||||
|
||||
|
||||
DEVELOPERS:
|
||||
A database migration script is required when you submit a change to billingstack
|
||||
that alters the database model definition. The migration script is a special
|
||||
python file that includes code to update/downgrade the database to match the
|
||||
changes in the model definition. Alembic will execute these scripts in order to
|
||||
provide a linear migration path between revision. The billingstack-db-manage command
|
||||
can be used to generate migration template for you to complete. The operations
|
||||
in the template are those supported by the Alembic migration library.
|
||||
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini revision \
|
||||
-m "description of revision" \
|
||||
--autogenerate
|
||||
|
||||
This generates a prepopulated template with the changes needed to match the
|
||||
database state with the models. You should inspect the autogenerated template
|
||||
to ensure that the proper models have been altered.
|
||||
|
||||
In rare circumstances, you may want to start with an empty migration template
|
||||
and manually author the changes necessary for an upgrade/downgrade. You can
|
||||
create a blank file via:
|
||||
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini revision \
|
||||
-m "description of revision"
|
||||
|
||||
The migration timeline should remain linear so that there is a clear path when
|
||||
upgrading/downgrading. To verify that the timeline does branch, you can run
|
||||
this command:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini check_migration
|
||||
|
||||
If the migration path does branch, you can find the branch point via:
|
||||
$ billingstack-db-manage --config-file /path/to/quantum.conf \
|
||||
--config-file /path/to/plugin/config.ini history
|
@ -1,52 +0,0 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# default to an empty string because the Quantum migration cli will
|
||||
# extract the correct value and set it programatically before alemic is fully
|
||||
# invoked.
|
||||
sqlalchemy.url =
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
@ -1,91 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
# Copied: Quantum
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import create_engine, pool
|
||||
|
||||
from billingstack.central.storage.impl_sqlalchemy.models import ModelBase
|
||||
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
billingstack_config = config.billingstack_config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# set the target for 'autogenerate' support
|
||||
target_metadata = ModelBase.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
context.configure(url=billingstack_config['central:sqlalchemy']
|
||||
.database_connection)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations(options=build_options())
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
engine = create_engine(
|
||||
billingstack_config['central:sqlalchemy'].database_connection,
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations(options=build_options())
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def build_options():
|
||||
return {}
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
@ -1,40 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright ${create_date.year} OpenStack LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade(options=None):
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade(config=None):
|
||||
${downgrades if downgrades else "pass"}
|
@ -1,3 +0,0 @@
|
||||
This directory contains the migration scripts for the billingstack project. Please
|
||||
see the README in billinstack/db/migration on how to use and generate new
|
||||
migrations.
|
@ -1,125 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
# Copied: Quantum
|
||||
import os
|
||||
|
||||
from alembic import command as alembic_command
|
||||
from alembic import config as alembic_config
|
||||
from alembic import util as alembic_util
|
||||
|
||||
from oslo.config import cfg
|
||||
from billingstack.openstack.common.gettextutils import _
|
||||
|
||||
|
||||
_db_opts = [
|
||||
cfg.StrOpt('database_connection',
|
||||
default='',
|
||||
help=_('URL to database')),
|
||||
]
|
||||
|
||||
CONF = cfg.ConfigOpts()
|
||||
CONF.register_opts(_db_opts, 'central:sqlalchemy')
|
||||
|
||||
|
||||
def do_alembic_command(config, cmd, *args, **kwargs):
|
||||
try:
|
||||
getattr(alembic_command, cmd)(config, *args, **kwargs)
|
||||
except alembic_util.CommandError, e:
|
||||
alembic_util.err(str(e))
|
||||
|
||||
|
||||
def do_check_migration(config, cmd):
|
||||
do_alembic_command(config, 'branches')
|
||||
|
||||
|
||||
def do_upgrade_downgrade(config, cmd):
|
||||
if not CONF.command.revision and not CONF.command.delta:
|
||||
raise SystemExit(_('You must provide a revision or relative delta'))
|
||||
|
||||
revision = CONF.command.revision
|
||||
|
||||
if CONF.command.delta:
|
||||
sign = '+' if CONF.command.name == 'upgrade' else '-'
|
||||
revision = sign + str(CONF.command.delta)
|
||||
else:
|
||||
revision = CONF.command.revision
|
||||
|
||||
do_alembic_command(config, cmd, revision, sql=CONF.command.sql)
|
||||
|
||||
|
||||
def do_stamp(config, cmd):
|
||||
do_alembic_command(config, cmd,
|
||||
CONF.command.revision,
|
||||
sql=CONF.command.sql)
|
||||
|
||||
|
||||
def do_revision(config, cmd):
|
||||
do_alembic_command(config, cmd,
|
||||
message=CONF.command.message,
|
||||
autogenerate=CONF.command.autogenerate,
|
||||
sql=CONF.command.sql)
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
for name in ['current', 'history', 'branches']:
|
||||
parser = subparsers.add_parser(name)
|
||||
parser.set_defaults(func=do_alembic_command)
|
||||
|
||||
parser = subparsers.add_parser('check_migration')
|
||||
parser.set_defaults(func=do_check_migration)
|
||||
|
||||
for name in ['upgrade', 'downgrade']:
|
||||
parser = subparsers.add_parser(name)
|
||||
parser.add_argument('--delta', type=int)
|
||||
parser.add_argument('--sql', action='store_true')
|
||||
parser.add_argument('revision', nargs='?')
|
||||
parser.set_defaults(func=do_upgrade_downgrade)
|
||||
|
||||
parser = subparsers.add_parser('stamp')
|
||||
parser.add_argument('--sql', action='store_true')
|
||||
parser.add_argument('revision')
|
||||
parser.set_defaults(func=do_stamp)
|
||||
|
||||
parser = subparsers.add_parser('revision')
|
||||
parser.add_argument('-m', '--message')
|
||||
parser.add_argument('--autogenerate', action='store_true')
|
||||
parser.add_argument('--sql', action='store_true')
|
||||
parser.set_defaults(func=do_revision)
|
||||
|
||||
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
help=_('Available commands'),
|
||||
handler=add_command_parsers)
|
||||
|
||||
CONF.register_cli_opt(command_opt)
|
||||
|
||||
|
||||
def main():
|
||||
config = alembic_config.Config(
|
||||
os.path.join(os.path.dirname(__file__), 'alembic.ini')
|
||||
)
|
||||
config.set_main_option(
|
||||
'script_location',
|
||||
'billingstack.central.storage'
|
||||
'.impl_sqlalchemy.migration:alembic_migrations')
|
||||
# attach the Quantum conf to the Alembic conf
|
||||
config.billingstack_config = CONF
|
||||
|
||||
CONF()
|
||||
CONF.command.func(config, CONF.command.name)
|
@ -1,228 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 sqlalchemy import Column, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy import Integer, Unicode
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||
|
||||
from billingstack import utils
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.sqlalchemy.types import JSON, UUID
|
||||
from billingstack.sqlalchemy.model_base import (
|
||||
ModelBase, BaseMixin, PropertyMixin)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE = declarative_base(cls=ModelBase)
|
||||
|
||||
|
||||
class Currency(BASE):
|
||||
"""
|
||||
Allowed currency
|
||||
"""
|
||||
name = Column(Unicode(10), nullable=False, primary_key=True)
|
||||
title = Column(Unicode(100), nullable=False)
|
||||
|
||||
|
||||
class Language(BASE):
|
||||
"""
|
||||
A Language
|
||||
"""
|
||||
name = Column(Unicode(10), nullable=False, primary_key=True)
|
||||
title = Column(Unicode(100), nullable=False)
|
||||
|
||||
|
||||
class ContactInfo(BASE, BaseMixin):
|
||||
"""
|
||||
Contact Information about an entity like a User, Customer etc...
|
||||
"""
|
||||
|
||||
@declared_attr
|
||||
def __mapper_args__(cls):
|
||||
name = unicode(utils.capital_to_underscore(cls.__name__))
|
||||
return {"polymorphic_on": "info_type", "polymorphic_identity": name}
|
||||
|
||||
info_type = Column(Unicode(20), nullable=False)
|
||||
|
||||
first_name = Column(Unicode(100))
|
||||
last_name = Column(Unicode(100))
|
||||
company = Column(Unicode(100))
|
||||
address1 = Column(Unicode(255))
|
||||
address2 = Column(Unicode(255))
|
||||
address3 = Column(Unicode(255))
|
||||
locality = Column(Unicode(60))
|
||||
region = Column(Unicode(60))
|
||||
country_name = Column(Unicode(100))
|
||||
postal_code = Column(Unicode(40))
|
||||
|
||||
phone = Column(Unicode(100))
|
||||
email = Column(Unicode(100))
|
||||
website = Column(Unicode(100))
|
||||
|
||||
|
||||
class CustomerInfo(ContactInfo):
|
||||
id = Column(UUID, ForeignKey("contact_info.id",
|
||||
onupdate='CASCADE', ondelete='CASCADE'),
|
||||
primary_key=True)
|
||||
|
||||
customer_id = Column(UUID, ForeignKey('customer.id'), nullable=False)
|
||||
|
||||
|
||||
class Merchant(BASE, BaseMixin):
|
||||
"""
|
||||
A Merchant is like a Account in Recurly
|
||||
"""
|
||||
name = Column(Unicode(60), nullable=False)
|
||||
title = Column(Unicode(60))
|
||||
|
||||
customers = relationship('Customer', backref='merchant')
|
||||
|
||||
plans = relationship('Plan', backref='merchant')
|
||||
products = relationship('Product', backref='merchant')
|
||||
|
||||
currency = relationship('Currency', uselist=False, backref='merchants')
|
||||
currency_name = Column(Unicode(10), ForeignKey('currency.name'),
|
||||
nullable=False)
|
||||
|
||||
language = relationship('Language', uselist=False, backref='merchants')
|
||||
language_name = Column(Unicode(10), ForeignKey('language.name'),
|
||||
nullable=False)
|
||||
|
||||
|
||||
class Customer(BASE, BaseMixin):
|
||||
"""
|
||||
A Customer is linked to a Merchant and can have Users related to it
|
||||
"""
|
||||
name = Column(Unicode(60), nullable=False)
|
||||
title = Column(Unicode(60))
|
||||
|
||||
merchant_id = Column(UUID, ForeignKey('merchant.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
contact_info = relationship(
|
||||
'CustomerInfo',
|
||||
backref='customer',
|
||||
primaryjoin='Customer.id == CustomerInfo.customer_id',
|
||||
lazy='joined')
|
||||
|
||||
default_info = relationship(
|
||||
'CustomerInfo',
|
||||
primaryjoin='Customer.default_info_id == CustomerInfo.id',
|
||||
uselist=False,
|
||||
post_update=True)
|
||||
default_info_id = Column(
|
||||
UUID,
|
||||
ForeignKey('customer_info.id', use_alter=True,
|
||||
onupdate='CASCADE', name='default_info'))
|
||||
|
||||
currency = relationship('Currency', uselist=False, backref='customers')
|
||||
currency_name = Column(Unicode(10), ForeignKey('currency.name'))
|
||||
|
||||
language = relationship('Language', uselist=False, backref='customers')
|
||||
language_name = Column(Unicode(10), ForeignKey('language.name'))
|
||||
|
||||
|
||||
class Plan(BASE, BaseMixin):
|
||||
"""
|
||||
A Product collection like a "Virtual Web Cluster" with 10 servers
|
||||
"""
|
||||
name = Column(Unicode(60), nullable=False)
|
||||
title = Column(Unicode(100))
|
||||
description = Column(Unicode(255))
|
||||
#provider = Column(Unicode(255), nullable=False)
|
||||
|
||||
plan_items = relationship('PlanItem', backref='plan')
|
||||
|
||||
merchant_id = Column(UUID, ForeignKey('merchant.id',
|
||||
ondelete='CASCADE'), nullable=False)
|
||||
|
||||
|
||||
class PlanProperty(BASE, PropertyMixin):
|
||||
__table_args__ = (UniqueConstraint('name', 'plan_id', name='plan'),)
|
||||
|
||||
plan = relationship('Plan', backref='properties', lazy='joined')
|
||||
plan_id = Column(
|
||||
UUID,
|
||||
ForeignKey('plan.id',
|
||||
ondelete='CASCADE',
|
||||
onupdate='CASCADE'))
|
||||
|
||||
|
||||
class PlanItem(BASE, BaseMixin):
|
||||
__table_args__ = (UniqueConstraint('plan_id', 'product_id', name='item'),)
|
||||
|
||||
title = Column(Unicode(100))
|
||||
description = Column(Unicode(255))
|
||||
|
||||
pricing = Column(JSON)
|
||||
|
||||
plan_id = Column(UUID, ForeignKey('plan.id', ondelete='CASCADE'),
|
||||
onupdate='CASCADE', primary_key=True)
|
||||
|
||||
product = relationship('Product', backref='plan_items', uselist=False)
|
||||
product_id = Column(UUID, ForeignKey('product.id', onupdate='CASCADE'),
|
||||
primary_key=True)
|
||||
|
||||
|
||||
class Product(BASE, BaseMixin):
|
||||
"""
|
||||
A sellable Product, like vCPU hours, bandwidth units
|
||||
"""
|
||||
name = Column(Unicode(60), nullable=False)
|
||||
title = Column(Unicode(100))
|
||||
description = Column(Unicode(255))
|
||||
|
||||
pricing = Column(JSON)
|
||||
|
||||
merchant_id = Column(UUID, ForeignKey('merchant.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
|
||||
class ProductProperty(BASE, PropertyMixin):
|
||||
"""
|
||||
A Metadata row for something like Product or PlanItem
|
||||
"""
|
||||
__table_args__ = (UniqueConstraint('name', 'product_id', name='product'),)
|
||||
|
||||
product = relationship('Product', backref='properties', lazy='joined')
|
||||
product_id = Column(
|
||||
UUID,
|
||||
ForeignKey('product.id',
|
||||
ondelete='CASCADE',
|
||||
onupdate='CASCADE'))
|
||||
|
||||
|
||||
class Subscription(BASE, BaseMixin):
|
||||
"""
|
||||
The thing that ties together stuff that is to be billed
|
||||
|
||||
In other words a Plan which is a collection of Products or a Product.
|
||||
"""
|
||||
billing_day = Column(Integer)
|
||||
|
||||
resource_id = Column(Unicode(255), nullable=False)
|
||||
resource_type = Column(Unicode(255), nullable=True)
|
||||
|
||||
plan = relationship('Plan', backref='subscriptions', uselist=False)
|
||||
plan_id = Column(UUID, ForeignKey('plan.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
customer = relationship('Customer', backref='subscriptions')
|
||||
customer_id = Column(UUID, ForeignKey('customer.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
payment_method_id = Column(UUID)
|
@ -1,27 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='service:collector', title="Configuration for collector Service"
|
||||
))
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.IntOpt('workers', default=None,
|
||||
help='Number of worker processes to spawn'),
|
||||
cfg.StrOpt('storage-driver', default='sqlalchemy',
|
||||
help='The storage driver to use'),
|
||||
], group='service:collector')
|
@ -1,17 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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.
|
@ -1,97 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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 taskflow.patterns import linear_flow
|
||||
|
||||
from billingstack import exceptions
|
||||
from billingstack import tasks
|
||||
from billingstack.collector import states
|
||||
from billingstack.openstack.common import log
|
||||
from billingstack.payment_gateway import get_provider
|
||||
|
||||
|
||||
ACTION = 'gateway_configuration:create'
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class EntryCreateTask(tasks.RootTask):
|
||||
def __init__(self, storage, **kw):
|
||||
super(EntryCreateTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, values):
|
||||
values['state'] = states.VERIFYING
|
||||
return self.storage.create_pg_config(ctxt, values)
|
||||
|
||||
|
||||
class PrerequirementsTask(tasks.RootTask):
|
||||
"""
|
||||
Fetch provider information for use in the next task.
|
||||
"""
|
||||
def __init__(self, storage, **kw):
|
||||
super(PrerequirementsTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, gateway_config):
|
||||
return self.storage.get_pg_provider(
|
||||
ctxt, gateway_config['provider_id'])
|
||||
|
||||
|
||||
class BackendVerifyTask(tasks.RootTask):
|
||||
"""
|
||||
This is the verification task that runs in a threaded flow.
|
||||
|
||||
1. Load the Provider Plugin via entrypoints
|
||||
2. Instantiate the Plugin with the Config
|
||||
3. Execute verify_config call
|
||||
4. Update storage accordingly
|
||||
"""
|
||||
def __init__(self, storage, **kw):
|
||||
super(BackendVerifyTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, gateway_config, gateway_provider):
|
||||
gateway_provider_cls = get_provider(gateway_provider['name'])
|
||||
gateway_provider_obj = gateway_provider_cls(gateway_config)
|
||||
|
||||
try:
|
||||
gateway_provider_obj.verify_config()
|
||||
except exceptions.ConfigurationError:
|
||||
self.storage.update_pg_config(
|
||||
ctxt, gateway_config['id'], {'state': states.INVALID})
|
||||
raise
|
||||
self.storage.update_pg_config(
|
||||
ctxt, gateway_config['id'], {'state': states.ACTIVE})
|
||||
|
||||
|
||||
def create_flow(storage):
|
||||
flow = linear_flow.Flow(ACTION + ':initial')
|
||||
|
||||
entry_task = EntryCreateTask(
|
||||
storage, provides='gateway_config', prefix=ACTION)
|
||||
flow.add(entry_task)
|
||||
|
||||
backend_flow = linear_flow.Flow(ACTION + ':backend')
|
||||
prereq_task = PrerequirementsTask(
|
||||
storage, provides='gateway_provider', prefix=ACTION)
|
||||
backend_flow.add(prereq_task)
|
||||
backend_flow.add(BackendVerifyTask(storage, prefix=ACTION))
|
||||
|
||||
flow.add(backend_flow)
|
||||
|
||||
return flow
|
@ -1,103 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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 taskflow.patterns import linear_flow
|
||||
|
||||
from billingstack import exceptions
|
||||
from billingstack import tasks
|
||||
from billingstack.collector import states
|
||||
from billingstack.openstack.common import log
|
||||
from billingstack.payment_gateway import get_provider
|
||||
|
||||
|
||||
ACTION = 'payment_method:create'
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class EntryCreateTask(tasks.RootTask):
|
||||
"""
|
||||
Create the initial entry in the database
|
||||
"""
|
||||
def __init__(self, storage, **kw):
|
||||
super(EntryCreateTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, values):
|
||||
values['state'] = states.PENDING
|
||||
return self.storage.create_payment_method(ctxt, values)
|
||||
|
||||
|
||||
class PrerequirementsTask(tasks.RootTask):
|
||||
"""
|
||||
Task to get the config and the provider from the catalog / database.
|
||||
"""
|
||||
def __init__(self, storage, **kw):
|
||||
super(PrerequirementsTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, values):
|
||||
data = {}
|
||||
data['gateway_config'] = self.storage.get_pg_config(
|
||||
ctxt, values['provider_config_id'])
|
||||
data['gateway_provider'] = self.storage.get_pg_provider(
|
||||
ctxt, data['gateway_config']['provider_id'])
|
||||
return data
|
||||
|
||||
|
||||
class BackendCreateTask(tasks.RootTask):
|
||||
def __init__(self, storage, **kw):
|
||||
super(BackendCreateTask, self).__init__(**kw)
|
||||
self.storage = storage
|
||||
|
||||
def execute(self, ctxt, payment_method, gateway_config, gateway_provider):
|
||||
gateway_provider_cls = get_provider(gateway_provider['name'])
|
||||
gateway_provider_obj = gateway_provider_cls(gateway_config)
|
||||
|
||||
try:
|
||||
gateway_provider_obj.create_payment_method(
|
||||
payment_method['customer_id'],
|
||||
payment_method)
|
||||
except exceptions.BadRequest:
|
||||
self.storage.update_payment_method(
|
||||
ctxt, payment_method['id'], {'status': states.INVALID})
|
||||
raise
|
||||
|
||||
|
||||
def create_flow(storage):
|
||||
"""
|
||||
The flow for the service to start
|
||||
"""
|
||||
flow = linear_flow.Flow(ACTION + ':initial')
|
||||
|
||||
entry_task = EntryCreateTask(storage, provides='payment_method',
|
||||
prefix=ACTION)
|
||||
flow.add(entry_task)
|
||||
|
||||
backend_flow = linear_flow.Flow(ACTION + ':backend')
|
||||
prereq_task = PrerequirementsTask(
|
||||
storage,
|
||||
provides=set([
|
||||
'gateway_config',
|
||||
'gateway_provider']),
|
||||
prefix=ACTION)
|
||||
backend_flow.add(prereq_task)
|
||||
backend_flow.add(BackendCreateTask(storage, prefix=ACTION))
|
||||
|
||||
flow.add(backend_flow)
|
||||
|
||||
return flow
|
@ -1,94 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common.rpc import proxy
|
||||
|
||||
rpcapi_opts = [
|
||||
cfg.StrOpt('collector_topic', default='collector',
|
||||
help='the topic collector nodes listen on')
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(rpcapi_opts)
|
||||
|
||||
|
||||
class CollectorAPI(proxy.RpcProxy):
|
||||
BASE_RPC_VERSION = '1.0'
|
||||
|
||||
def __init__(self):
|
||||
super(CollectorAPI, self).__init__(
|
||||
topic=cfg.CONF.collector_topic,
|
||||
default_version=self.BASE_RPC_VERSION)
|
||||
|
||||
# PGP
|
||||
def list_pg_providers(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_pg_providers',
|
||||
criterion=criterion))
|
||||
|
||||
def get_pg_provider(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_pg_provider', id_=id_))
|
||||
|
||||
# PGM
|
||||
def list_pg_methods(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_pg_methods',
|
||||
criterion=criterion))
|
||||
|
||||
def get_pg_method(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_pg_method', id_=id_))
|
||||
|
||||
def delete_pg_method(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_pg_method', id_=id_))
|
||||
|
||||
# PGC
|
||||
def create_pg_config(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_pg_config',
|
||||
values=values))
|
||||
|
||||
def list_pg_configs(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_pg_configs',
|
||||
criterion=criterion))
|
||||
|
||||
def get_pg_config(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_pg_config', id_=id_))
|
||||
|
||||
def update_pg_config(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_pg_config', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_pg_config(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_pg_config', id_=id_))
|
||||
|
||||
# PaymentMethod
|
||||
def create_payment_method(self, ctxt, values):
|
||||
return self.call(ctxt, self.make_msg('create_payment_method',
|
||||
values=values))
|
||||
|
||||
def list_payment_methods(self, ctxt, criterion=None):
|
||||
return self.call(ctxt, self.make_msg('list_payment_methods',
|
||||
criterion=criterion))
|
||||
|
||||
def get_payment_method(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('get_payment_method', id_=id_))
|
||||
|
||||
def update_payment_method(self, ctxt, id_, values):
|
||||
return self.call(ctxt, self.make_msg('update_payment_method', id_=id_,
|
||||
values=values))
|
||||
|
||||
def delete_payment_method(self, ctxt, id_):
|
||||
return self.call(ctxt, self.make_msg('delete_payment_method', id_=id_))
|
||||
|
||||
|
||||
collector_api = CollectorAPI()
|
@ -1,108 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
A service that does calls towards the PGP web endpoint or so
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo.config import cfg
|
||||
from taskflow.engines import run as run_flow
|
||||
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common.rpc import service as rpc_service
|
||||
from billingstack.openstack.common import service as os_service
|
||||
from billingstack.storage.utils import get_connection
|
||||
from billingstack.central.rpcapi import CentralAPI
|
||||
from billingstack import service as bs_service
|
||||
from billingstack.collector.flows import (
|
||||
gateway_configuration, payment_method)
|
||||
|
||||
|
||||
cfg.CONF.import_opt('host', 'billingstack.netconf')
|
||||
cfg.CONF.import_opt('collector_topic', 'billingstack.collector.rpcapi')
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Service(rpc_service.Service):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.update(
|
||||
host=cfg.CONF.host,
|
||||
topic=cfg.CONF.collector_topic,
|
||||
)
|
||||
|
||||
super(Service, self).__init__(*args, **kwargs)
|
||||
|
||||
# Get a storage connection
|
||||
self.central_api = CentralAPI()
|
||||
|
||||
def start(self):
|
||||
self.storage_conn = get_connection('collector')
|
||||
super(Service, self).start()
|
||||
|
||||
def wait(self):
|
||||
super(Service, self).wait()
|
||||
self.conn.consumer_thread.wait()
|
||||
|
||||
# PGP
|
||||
def list_pg_providers(self, ctxt, **kw):
|
||||
return self.storage_conn.list_pg_providers(ctxt, **kw)
|
||||
|
||||
# PGC
|
||||
def create_pg_config(self, ctxt, values):
|
||||
flow = gateway_configuration.create_flow(self.storage_conn)
|
||||
results = run_flow(flow, store={'values': values, 'ctxt': ctxt})
|
||||
return results['gateway_config']
|
||||
|
||||
def list_pg_configs(self, ctxt, **kw):
|
||||
return self.storage_conn.list_pg_configs(ctxt, **kw)
|
||||
|
||||
def get_pg_config(self, ctxt, id_):
|
||||
return self.storage_conn.get_pg_config(ctxt, id_)
|
||||
|
||||
def update_pg_config(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_pg_config(ctxt, id_, values)
|
||||
|
||||
def delete_pg_config(self, ctxt, id_):
|
||||
return self.storage_conn.delete_pg_config(ctxt, id_)
|
||||
|
||||
# PM
|
||||
def create_payment_method(self, ctxt, values):
|
||||
flow = payment_method.create_flow(self.storage_conn)
|
||||
results = run_flow(flow, store={'values': values, 'ctxt': ctxt})
|
||||
return results['payment_method']
|
||||
|
||||
def list_payment_methods(self, ctxt, **kw):
|
||||
return self.storage_conn.list_payment_methods(ctxt, **kw)
|
||||
|
||||
def get_payment_method(self, ctxt, id_, **kw):
|
||||
return self.storage_conn.get_payment_method(ctxt, id_)
|
||||
|
||||
def update_payment_method(self, ctxt, id_, values):
|
||||
return self.storage_conn.update_payment_method(ctxt, id_, values)
|
||||
|
||||
def delete_payment_method(self, ctxt, id_):
|
||||
return self.storage_conn.delete_payment_method(ctxt, id_)
|
||||
|
||||
|
||||
def launch():
|
||||
bs_service.prepare_service(sys.argv)
|
||||
launcher = os_service.launch(Service(),
|
||||
cfg.CONF['service:collector'].workers)
|
||||
launcher.wait()
|
@ -1,21 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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.
|
||||
PENDING = u'PENDING'
|
||||
VERIFYING = u'VERIFYING'
|
||||
ACTIVE = u'ACTIVE'
|
||||
INVALID = u'INVALID'
|
@ -1,108 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 billingstack.storage import base
|
||||
|
||||
|
||||
class StorageEngine(base.StorageEngine):
|
||||
"""Base class for the collector storage"""
|
||||
__plugin_ns__ = 'billingstack.collector.storage'
|
||||
|
||||
|
||||
class Connection(base.Connection):
|
||||
"""Define the base API for collector storage"""
|
||||
def pg_provider_register(self):
|
||||
"""
|
||||
Register a Provider and it's Methods
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_pg_providers(self, ctxt, **kw):
|
||||
"""
|
||||
List available PG Providers
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_pg_provider(self, ctxt, id_):
|
||||
"""
|
||||
Get a PaymentGateway Provider
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def pg_provider_deregister(self, ctxt, id_):
|
||||
"""
|
||||
De-register a PaymentGateway Provider (Plugin) and all it's methods
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_pg_config(self, ctxt, values):
|
||||
"""
|
||||
Create a PaymentGateway Configuration
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_pg_configs(self, ctxt, **kw):
|
||||
"""
|
||||
List PaymentGateway Configurations
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_pg_config(self, ctxt, id_):
|
||||
"""
|
||||
Get a PaymentGateway Configuration
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_pg_config(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a PaymentGateway Configuration
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_pg_config(self, ctxt, id_):
|
||||
"""
|
||||
Delete a PaymentGateway Configuration
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_payment_method(self, ctxt, values):
|
||||
"""
|
||||
Configure a PaymentMethod like a CreditCard
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_payment_methods(self, ctxt, criterion=None, **kw):
|
||||
"""
|
||||
List a Customer's PaymentMethods
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_payment_method(self, ctxt, id_, **kw):
|
||||
"""
|
||||
Get a Customer's PaymentMethod
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_payment_method(self, ctxt, id_, values):
|
||||
"""
|
||||
Update a Customer's PaymentMethod
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_payment_method(self, ctxt, id_):
|
||||
"""
|
||||
Delete a Customer's PaymentMethod
|
||||
"""
|
||||
raise NotImplementedError
|
@ -1,263 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import Unicode
|
||||
from sqlalchemy.orm import exc, relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from billingstack.collector import states
|
||||
from billingstack.collector.storage import Connection, StorageEngine
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.sqlalchemy.types import JSON, UUID
|
||||
from billingstack.sqlalchemy import api, model_base, session, utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE = declarative_base(cls=model_base.ModelBase)
|
||||
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='collector:sqlalchemy',
|
||||
title='Config for collector sqlalchemy plugin'))
|
||||
|
||||
cfg.CONF.register_opts(session.SQLOPTS, group='collector:sqlalchemy')
|
||||
|
||||
|
||||
class PGProvider(BASE, model_base.BaseMixin):
|
||||
"""
|
||||
A Payment Gateway - The thing that processes a Payment Method
|
||||
|
||||
This is registered either by the Admin or by the PaymentGateway plugin
|
||||
"""
|
||||
__tablename__ = 'pg_provider'
|
||||
|
||||
name = Column(Unicode(60), nullable=False)
|
||||
title = Column(Unicode(100))
|
||||
description = Column(Unicode(255))
|
||||
|
||||
properties = Column(JSON)
|
||||
|
||||
methods = relationship(
|
||||
'PGMethod',
|
||||
backref='provider',
|
||||
lazy='joined')
|
||||
|
||||
def method_map(self):
|
||||
return self.attrs_map(['provider_methods'])
|
||||
|
||||
|
||||
class PGMethod(BASE, model_base.BaseMixin):
|
||||
"""
|
||||
This represents a PaymentGatewayProviders method with some information
|
||||
like name, type etc to describe what is in other settings known as a
|
||||
"CreditCard"
|
||||
|
||||
Example:
|
||||
A Visa card: {"type": "creditcard", "visa"}
|
||||
"""
|
||||
__tablename__ = 'pg_method'
|
||||
|
||||
name = Column(Unicode(100), nullable=False)
|
||||
title = Column(Unicode(100))
|
||||
description = Column(Unicode(255))
|
||||
|
||||
type = Column(Unicode(100), nullable=False)
|
||||
properties = Column(JSON)
|
||||
|
||||
# NOTE: This is so a PGMethod can be "owned" by a Provider, meaning that
|
||||
# other Providers should not be able to use it.
|
||||
provider_id = Column(UUID, ForeignKey(
|
||||
'pg_provider.id',
|
||||
ondelete='CASCADE',
|
||||
onupdate='CASCADE'))
|
||||
|
||||
@staticmethod
|
||||
def make_key(data):
|
||||
return '%(type)s:%(name)s' % data
|
||||
|
||||
def key(self):
|
||||
return self.make_key(self)
|
||||
|
||||
|
||||
class PGConfig(BASE, model_base.BaseMixin):
|
||||
"""
|
||||
A Merchant's configuration of a PaymentGateway like api keys, url and more
|
||||
"""
|
||||
__tablename__ = 'pg_config'
|
||||
|
||||
name = Column(Unicode(100), nullable=False)
|
||||
title = Column(Unicode(100))
|
||||
|
||||
properties = Column(JSON)
|
||||
|
||||
# Link to the Merchant
|
||||
merchant_id = Column(UUID, nullable=False)
|
||||
|
||||
provider = relationship('PGProvider',
|
||||
backref='merchant_configurations')
|
||||
provider_id = Column(UUID, ForeignKey('pg_provider.id',
|
||||
onupdate='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
state = Column(Unicode(20), default=states.PENDING)
|
||||
|
||||
|
||||
class PaymentMethod(BASE, model_base.BaseMixin):
|
||||
name = Column(Unicode(255), nullable=False)
|
||||
|
||||
identifier = Column(Unicode(255), nullable=False)
|
||||
expires = Column(Unicode(255))
|
||||
|
||||
properties = Column(JSON)
|
||||
|
||||
customer_id = Column(UUID, nullable=False)
|
||||
|
||||
provider_config = relationship('PGConfig', backref='payment_methods',
|
||||
lazy='joined')
|
||||
provider_config_id = Column(UUID, ForeignKey('pg_config.id',
|
||||
onupdate='CASCADE'), nullable=False)
|
||||
|
||||
state = Column(Unicode(20), default=states.PENDING)
|
||||
|
||||
|
||||
class SQLAlchemyEngine(StorageEngine):
|
||||
__plugin_name__ = 'sqlalchemy'
|
||||
|
||||
def get_connection(self):
|
||||
return Connection()
|
||||
|
||||
|
||||
class Connection(Connection, api.HelpersMixin):
|
||||
def __init__(self):
|
||||
self.setup('collector:sqlalchemy')
|
||||
|
||||
def base(self):
|
||||
return BASE
|
||||
|
||||
# Payment Gateway Providers
|
||||
def pg_provider_register(self, ctxt, values):
|
||||
values = values.copy()
|
||||
methods = values.pop('methods', [])
|
||||
|
||||
query = self.session.query(PGProvider)\
|
||||
.filter_by(name=values['name'])
|
||||
|
||||
try:
|
||||
provider = query.one()
|
||||
except exc.NoResultFound:
|
||||
provider = PGProvider()
|
||||
|
||||
provider.update(values)
|
||||
|
||||
self._set_provider_methods(ctxt, provider, methods)
|
||||
|
||||
self._save(provider)
|
||||
return self._dict(provider, extra=['methods'])
|
||||
|
||||
def list_pg_providers(self, ctxt, **kw):
|
||||
rows = self._list(PGProvider, **kw)
|
||||
return [self._dict(r, extra=['methods']) for r in rows]
|
||||
|
||||
def get_pg_provider(self, ctxt, id_, **kw):
|
||||
row = self._get(PGProvider, id_)
|
||||
return self._dict(row, extra=['methods'])
|
||||
|
||||
def pg_provider_deregister(self, ctxt, id_):
|
||||
self._delete(PGProvider, id_)
|
||||
|
||||
def _get_provider_methods(self, provider):
|
||||
"""
|
||||
Used internally to form a "Map" of the Providers methods
|
||||
"""
|
||||
methods = {}
|
||||
for m in provider.methods:
|
||||
methods[m.key()] = m
|
||||
return methods
|
||||
|
||||
def _set_provider_methods(self, ctxt, provider, config_methods):
|
||||
"""Helper method for setting the Methods for a Provider"""
|
||||
existing = self._get_provider_methods(provider)
|
||||
for method in config_methods:
|
||||
self._set_method(provider, method, existing)
|
||||
|
||||
def _set_method(self, provider, method, existing):
|
||||
key = PGMethod.make_key(method)
|
||||
|
||||
if key in existing:
|
||||
existing[key].update(method)
|
||||
else:
|
||||
row = PGMethod(**method)
|
||||
provider.methods.append(row)
|
||||
|
||||
# Payment Gateway Configuration
|
||||
def create_pg_config(self, ctxt, values):
|
||||
row = PGConfig(**values)
|
||||
|
||||
self._save(row)
|
||||
return dict(row)
|
||||
|
||||
def list_pg_configs(self, ctxt, **kw):
|
||||
rows = self._list(PGConfig, **kw)
|
||||
return map(dict, rows)
|
||||
|
||||
def get_pg_config(self, ctxt, id_, **kw):
|
||||
row = self._get(PGConfig, id_, **kw)
|
||||
return dict(row)
|
||||
|
||||
def update_pg_config(self, ctxt, id_, values):
|
||||
row = self._update(PGConfig, id_, values)
|
||||
return dict(row)
|
||||
|
||||
def delete_pg_config(self, ctxt, id_):
|
||||
self._delete(PGConfig, id_)
|
||||
|
||||
# PaymentMethod
|
||||
def create_payment_method(self, ctxt, values):
|
||||
row = PaymentMethod(**values)
|
||||
|
||||
self._save(row)
|
||||
return self._dict(row)
|
||||
|
||||
def list_payment_methods(self, ctxt, criterion=None, **kw):
|
||||
query = self.session.query(PaymentMethod)
|
||||
|
||||
# NOTE: Filter needs to be joined for merchant_id
|
||||
query = utils.filter_merchant_by_join(
|
||||
query, PGConfig, criterion)
|
||||
|
||||
rows = self._list(
|
||||
cls=PaymentMethod,
|
||||
query=query,
|
||||
criterion=criterion,
|
||||
**kw)
|
||||
|
||||
return [self._dict(row) for row in rows]
|
||||
|
||||
def get_payment_method(self, ctxt, id_, **kw):
|
||||
row = self._get_id_or_name(PaymentMethod, id_)
|
||||
return self._dict(row)
|
||||
|
||||
def update_payment_method(self, ctxt, id_, values):
|
||||
row = self._update(PaymentMethod, id_, values)
|
||||
return self._dict(row)
|
||||
|
||||
def delete_payment_method(self, ctxt, id_):
|
||||
self._delete(PaymentMethod, id_)
|
@ -1,31 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import rpc
|
||||
|
||||
cfg.CONF.register_opts([
|
||||
cfg.StrOpt('pybasedir',
|
||||
default=os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'../')),
|
||||
help='Directory where the nova python module is installed'),
|
||||
cfg.StrOpt('state-path', default='$pybasedir',
|
||||
help='Top-level directory for maintaining state')
|
||||
])
|
||||
|
||||
|
||||
rpc.set_defaults(control_exchange='billingstack')
|
@ -1,89 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2012 Managed I.T.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@managedit.ie>
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 re
|
||||
|
||||
|
||||
class Base(Exception):
|
||||
error_code = 500
|
||||
message_tmpl = None
|
||||
|
||||
def __init__(self, message='', *args, **kw):
|
||||
self.message = message % kw if self.message_tmpl else message
|
||||
|
||||
self.errors = kw.pop('errors', None)
|
||||
super(Base, self).__init__(self.message)
|
||||
|
||||
@property
|
||||
def error_type(self):
|
||||
name = "_".join(l.lower() for l in re.findall('[A-Z][^A-Z]*',
|
||||
self.__class__.__name__))
|
||||
name = re.sub('_+remote$', '', name)
|
||||
return name
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
def get_message(self):
|
||||
"""
|
||||
Return the exception message or None
|
||||
"""
|
||||
if unicode(self):
|
||||
return unicode(self)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class NotImplemented(Base, NotImplementedError):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigurationError(Base):
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Base):
|
||||
error_code = 400
|
||||
|
||||
|
||||
class InvalidObject(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSortKey(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQueryField(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidOperator(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class Forbidden(Base):
|
||||
pass
|
||||
|
||||
|
||||
class Duplicate(Base):
|
||||
error_code = 409
|
||||
|
||||
|
||||
class NotFound(Base):
|
||||
error_code = 404
|
@ -1,33 +0,0 @@
|
||||
# Copyright 2012 Managed I.T.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@managedit.ie>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copied: Moniker
|
||||
from oslo.config import cfg
|
||||
from cliff.app import App
|
||||
from cliff.commandmanager import CommandManager
|
||||
from billingstack.version import version_info as version
|
||||
|
||||
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
|
||||
class Shell(App):
|
||||
def __init__(self):
|
||||
super(Shell, self).__init__(
|
||||
description='BillingStack Management CLI',
|
||||
version=version.version_string(),
|
||||
command_manager=CommandManager('billingstack.manage')
|
||||
)
|
@ -1,86 +0,0 @@
|
||||
# Copyright 2012 Managed I.T.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@managedit.ie>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Copied: Moniker
|
||||
from cliff.command import Command as CliffCommand
|
||||
from cliff.lister import Lister
|
||||
from cliff.show import ShowOne
|
||||
from billingstack import utils
|
||||
|
||||
|
||||
class Command(CliffCommand):
|
||||
|
||||
def run(self, parsed_args):
|
||||
#self.context = billingstackContext.get_admin_context()
|
||||
|
||||
return super(Command, self).run(parsed_args)
|
||||
|
||||
def execute(self, parsed_args):
|
||||
"""
|
||||
Execute something, this is since we overload self.take_action()
|
||||
in order to format the data
|
||||
|
||||
This method __NEEDS__ to be overloaded!
|
||||
|
||||
:param parsed_args: The parsed args that are given by take_action()
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def post_execute(self, data):
|
||||
"""
|
||||
Format the results locally if needed, by default we just return data
|
||||
|
||||
:param data: Whatever is returned by self.execute()
|
||||
"""
|
||||
return data
|
||||
|
||||
def setup(self, parsed_args):
|
||||
pass
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
# TODO: Common Exception Handling Here
|
||||
self.setup(parsed_args)
|
||||
results = self.execute(parsed_args)
|
||||
return self.post_execute(results)
|
||||
|
||||
|
||||
class ListCommand(Command, Lister):
|
||||
def post_execute(self, results):
|
||||
if len(results) > 0:
|
||||
columns = utils.get_columns(results)
|
||||
data = [utils.get_item_properties(i, columns) for i in results]
|
||||
return columns, data
|
||||
else:
|
||||
return [], ()
|
||||
|
||||
|
||||
class GetCommand(Command, ShowOne):
|
||||
def post_execute(self, results):
|
||||
return results.keys(), results.values()
|
||||
|
||||
|
||||
class CreateCommand(Command, ShowOne):
|
||||
def post_execute(self, results):
|
||||
return results.keys(), results.values()
|
||||
|
||||
|
||||
class UpdateCommand(Command, ShowOne):
|
||||
def post_execute(self, results):
|
||||
return results.keys(), results.values()
|
||||
|
||||
|
||||
class DeleteCommand(Command):
|
||||
pass
|
@ -1,34 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import log
|
||||
from billingstack.manage.base import Command
|
||||
from billingstack.storage.utils import get_connection
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
cfg.CONF.import_opt('state_path', 'billingstack.paths')
|
||||
|
||||
|
||||
class DatabaseCommand(Command):
|
||||
"""
|
||||
A Command that uses a storage connection to do some stuff
|
||||
"""
|
||||
def get_connection(self, service):
|
||||
return get_connection(service)
|
@ -1,42 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@gmail.com>
|
||||
#
|
||||
# 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 billingstack.openstack.common.context import get_admin_context
|
||||
from billingstack.payment_gateway import register_providers
|
||||
from billingstack.manage.base import ListCommand
|
||||
from billingstack.manage.database import DatabaseCommand
|
||||
|
||||
|
||||
class ProvidersRegister(DatabaseCommand):
|
||||
"""
|
||||
Register Payment Gateway Providers
|
||||
"""
|
||||
def execute(self, parsed_args):
|
||||
context = get_admin_context()
|
||||
register_providers(context)
|
||||
|
||||
|
||||
class ProvidersList(DatabaseCommand, ListCommand):
|
||||
def execute(self, parsed_args):
|
||||
context = get_admin_context()
|
||||
conn = self.get_connection('collector')
|
||||
|
||||
data = conn.list_pg_providers(context)
|
||||
|
||||
for p in data:
|
||||
keys = ['type', 'name']
|
||||
methods = [":".join([m[k] for k in keys]) for m in p['methods']]
|
||||
p['methods'] = ", ".join(methods)
|
||||
return data
|
@ -1,59 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2012 Red Hat, 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 socket
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _get_my_ip():
|
||||
"""
|
||||
Returns the actual ip of the local machine.
|
||||
|
||||
This code figures out what source address would be used if some traffic
|
||||
were to be sent out to some well known address on the Internet. In this
|
||||
case, a Google DNS server is used, but the specific address does not
|
||||
matter much. No traffic is actually sent.
|
||||
"""
|
||||
try:
|
||||
csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
csock.connect(('8.8.8.8', 80))
|
||||
(addr, port) = csock.getsockname()
|
||||
csock.close()
|
||||
return addr
|
||||
except socket.error:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
netconf_opts = [
|
||||
cfg.StrOpt('my_ip',
|
||||
default=_get_my_ip(),
|
||||
help='ip address of this host'),
|
||||
cfg.StrOpt('host',
|
||||
default=socket.getfqdn(),
|
||||
help='Name of this node. This can be an opaque identifier. '
|
||||
'It is not necessarily a hostname, FQDN, or IP address. '
|
||||
'However, the node name must be valid within '
|
||||
'an AMQP key, and if using ZeroMQ, a valid '
|
||||
'hostname, FQDN, or IP address')
|
||||
]
|
||||
|
||||
CONF.register_opts(netconf_opts)
|
@ -1,86 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""
|
||||
Simple class that stores security context information in the web request.
|
||||
|
||||
Projects should subclass this class if they wish to enhance the request
|
||||
context or provide additional information in their specific WSGI pipeline.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
|
||||
from billingstack.openstack.common import uuidutils
|
||||
|
||||
|
||||
def generate_request_id():
|
||||
return 'req-%s' % uuidutils.generate_uuid()
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
|
||||
"""Helper class to represent useful information about a request context.
|
||||
|
||||
Stores information about the security context under which the user
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token=None, user=None, tenant=None, is_admin=False,
|
||||
read_only=False, show_deleted=False, request_id=None,
|
||||
instance_uuid=None):
|
||||
self.auth_token = auth_token
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.is_admin = is_admin
|
||||
self.read_only = read_only
|
||||
self.show_deleted = show_deleted
|
||||
self.instance_uuid = instance_uuid
|
||||
if not request_id:
|
||||
request_id = generate_request_id()
|
||||
self.request_id = request_id
|
||||
|
||||
def to_dict(self):
|
||||
return {'user': self.user,
|
||||
'tenant': self.tenant,
|
||||
'is_admin': self.is_admin,
|
||||
'read_only': self.read_only,
|
||||
'show_deleted': self.show_deleted,
|
||||
'auth_token': self.auth_token,
|
||||
'request_id': self.request_id,
|
||||
'instance_uuid': self.instance_uuid}
|
||||
|
||||
|
||||
def get_admin_context(show_deleted=False):
|
||||
context = RequestContext(None,
|
||||
tenant=None,
|
||||
is_admin=True,
|
||||
show_deleted=show_deleted)
|
||||
return context
|
||||
|
||||
|
||||
def get_context_from_function_and_args(function, args, kwargs):
|
||||
"""Find an arg of type RequestContext and return it.
|
||||
|
||||
This is useful in a couple of decorators where we don't
|
||||
know much about the function we're wrapping.
|
||||
"""
|
||||
|
||||
for arg in itertools.chain(kwargs.values(), args):
|
||||
if isinstance(arg, RequestContext):
|
||||
return arg
|
||||
|
||||
return None
|
@ -1,179 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Red Hat, 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 base64
|
||||
|
||||
from Crypto.Hash import HMAC
|
||||
from Crypto import Random
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import importutils
|
||||
|
||||
|
||||
class CryptoutilsException(Exception):
|
||||
"""Generic Exception for Crypto utilities."""
|
||||
|
||||
message = _("An unknown error occurred in crypto utils.")
|
||||
|
||||
|
||||
class CipherBlockLengthTooBig(CryptoutilsException):
|
||||
"""The block size is too big."""
|
||||
|
||||
def __init__(self, requested, permitted):
|
||||
msg = _("Block size of %(given)d is too big, max = %(maximum)d")
|
||||
message = msg % {'given': requested, 'maximum': permitted}
|
||||
super(CryptoutilsException, self).__init__(message)
|
||||
|
||||
|
||||
class HKDFOutputLengthTooLong(CryptoutilsException):
|
||||
"""The amount of Key Material asked is too much."""
|
||||
|
||||
def __init__(self, requested, permitted):
|
||||
msg = _("Length of %(given)d is too long, max = %(maximum)d")
|
||||
message = msg % {'given': requested, 'maximum': permitted}
|
||||
super(CryptoutilsException, self).__init__(message)
|
||||
|
||||
|
||||
class HKDF(object):
|
||||
"""An HMAC-based Key Derivation Function implementation (RFC5869)
|
||||
|
||||
This class creates an object that allows to use HKDF to derive keys.
|
||||
"""
|
||||
|
||||
def __init__(self, hashtype='SHA256'):
|
||||
self.hashfn = importutils.import_module('Crypto.Hash.' + hashtype)
|
||||
self.max_okm_length = 255 * self.hashfn.digest_size
|
||||
|
||||
def extract(self, ikm, salt=None):
|
||||
"""An extract function that can be used to derive a robust key given
|
||||
weak Input Key Material (IKM) which could be a password.
|
||||
Returns a pseudorandom key (of HashLen octets)
|
||||
|
||||
:param ikm: input keying material (ex a password)
|
||||
:param salt: optional salt value (a non-secret random value)
|
||||
"""
|
||||
if salt is None:
|
||||
salt = '\x00' * self.hashfn.digest_size
|
||||
|
||||
return HMAC.new(salt, ikm, self.hashfn).digest()
|
||||
|
||||
def expand(self, prk, info, length):
|
||||
"""An expand function that will return arbitrary length output that can
|
||||
be used as keys.
|
||||
Returns a buffer usable as key material.
|
||||
|
||||
:param prk: a pseudorandom key of at least HashLen octets
|
||||
:param info: optional string (can be a zero-length string)
|
||||
:param length: length of output keying material (<= 255 * HashLen)
|
||||
"""
|
||||
if length > self.max_okm_length:
|
||||
raise HKDFOutputLengthTooLong(length, self.max_okm_length)
|
||||
|
||||
N = (length + self.hashfn.digest_size - 1) / self.hashfn.digest_size
|
||||
|
||||
okm = ""
|
||||
tmp = ""
|
||||
for block in range(1, N + 1):
|
||||
tmp = HMAC.new(prk, tmp + info + chr(block), self.hashfn).digest()
|
||||
okm += tmp
|
||||
|
||||
return okm[:length]
|
||||
|
||||
|
||||
MAX_CB_SIZE = 256
|
||||
|
||||
|
||||
class SymmetricCrypto(object):
|
||||
"""Symmetric Key Crypto object.
|
||||
|
||||
This class creates a Symmetric Key Crypto object that can be used
|
||||
to encrypt, decrypt, or sign arbitrary data.
|
||||
|
||||
:param enctype: Encryption Cipher name (default: AES)
|
||||
:param hashtype: Hash/HMAC type name (default: SHA256)
|
||||
"""
|
||||
|
||||
def __init__(self, enctype='AES', hashtype='SHA256'):
|
||||
self.cipher = importutils.import_module('Crypto.Cipher.' + enctype)
|
||||
self.hashfn = importutils.import_module('Crypto.Hash.' + hashtype)
|
||||
|
||||
def new_key(self, size):
|
||||
return Random.new().read(size)
|
||||
|
||||
def encrypt(self, key, msg, b64encode=True):
|
||||
"""Encrypt the provided msg and returns the cyphertext optionally
|
||||
base64 encoded.
|
||||
|
||||
Uses AES-128-CBC with a Random IV by default.
|
||||
|
||||
The plaintext is padded to reach blocksize length.
|
||||
The last byte of the block is the length of the padding.
|
||||
The length of the padding does not include the length byte itself.
|
||||
|
||||
:param key: The Encryption key.
|
||||
:param msg: the plain text.
|
||||
|
||||
:returns encblock: a block of encrypted data.
|
||||
"""
|
||||
iv = Random.new().read(self.cipher.block_size)
|
||||
cipher = self.cipher.new(key, self.cipher.MODE_CBC, iv)
|
||||
|
||||
# CBC mode requires a fixed block size. Append padding and length of
|
||||
# padding.
|
||||
if self.cipher.block_size > MAX_CB_SIZE:
|
||||
raise CipherBlockLengthTooBig(self.cipher.block_size, MAX_CB_SIZE)
|
||||
r = len(msg) % self.cipher.block_size
|
||||
padlen = self.cipher.block_size - r - 1
|
||||
msg += '\x00' * padlen
|
||||
msg += chr(padlen)
|
||||
|
||||
enc = iv + cipher.encrypt(msg)
|
||||
if b64encode:
|
||||
enc = base64.b64encode(enc)
|
||||
return enc
|
||||
|
||||
def decrypt(self, key, msg, b64decode=True):
|
||||
"""Decrypts the provided ciphertext, optionally base 64 encoded, and
|
||||
returns the plaintext message, after padding is removed.
|
||||
|
||||
Uses AES-128-CBC with an IV by default.
|
||||
|
||||
:param key: The Encryption key.
|
||||
:param msg: the ciphetext, the first block is the IV
|
||||
"""
|
||||
if b64decode:
|
||||
msg = base64.b64decode(msg)
|
||||
iv = msg[:self.cipher.block_size]
|
||||
cipher = self.cipher.new(key, self.cipher.MODE_CBC, iv)
|
||||
|
||||
padded = cipher.decrypt(msg[self.cipher.block_size:])
|
||||
l = ord(padded[-1]) + 1
|
||||
plain = padded[:-l]
|
||||
return plain
|
||||
|
||||
def sign(self, key, msg, b64encode=True):
|
||||
"""Signs a message string and returns a base64 encoded signature.
|
||||
|
||||
Uses HMAC-SHA-256 by default.
|
||||
|
||||
:param key: The Signing key.
|
||||
:param msg: the message to sign.
|
||||
"""
|
||||
h = HMAC.new(key, msg, self.hashfn)
|
||||
out = h.digest()
|
||||
if b64encode:
|
||||
out = base64.b64encode(out)
|
||||
return out
|
@ -1,16 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Cloudscaling Group, Inc
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
@ -1,106 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2013 Rackspace Hosting
|
||||
# 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.
|
||||
|
||||
"""Multiple DB API backend support.
|
||||
|
||||
Supported configuration options:
|
||||
|
||||
The following two parameters are in the 'database' group:
|
||||
`backend`: DB backend name or full module path to DB backend module.
|
||||
`use_tpool`: Enable thread pooling of DB API calls.
|
||||
|
||||
A DB backend module should implement a method named 'get_backend' which
|
||||
takes no arguments. The method can return any object that implements DB
|
||||
API methods.
|
||||
|
||||
*NOTE*: There are bugs in eventlet when using tpool combined with
|
||||
threading locks. The python logging module happens to use such locks. To
|
||||
work around this issue, be sure to specify thread=False with
|
||||
eventlet.monkey_patch().
|
||||
|
||||
A bug for eventlet has been filed here:
|
||||
|
||||
https://bitbucket.org/eventlet/eventlet/issue/137/
|
||||
"""
|
||||
import functools
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import importutils
|
||||
from billingstack.openstack.common import lockutils
|
||||
|
||||
|
||||
db_opts = [
|
||||
cfg.StrOpt('backend',
|
||||
default='sqlalchemy',
|
||||
deprecated_name='db_backend',
|
||||
deprecated_group='DEFAULT',
|
||||
help='The backend to use for db'),
|
||||
cfg.BoolOpt('use_tpool',
|
||||
default=False,
|
||||
deprecated_name='dbapi_use_tpool',
|
||||
deprecated_group='DEFAULT',
|
||||
help='Enable the experimental use of thread pooling for '
|
||||
'all DB API calls')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(db_opts, 'database')
|
||||
|
||||
|
||||
class DBAPI(object):
|
||||
def __init__(self, backend_mapping=None):
|
||||
if backend_mapping is None:
|
||||
backend_mapping = {}
|
||||
self.__backend = None
|
||||
self.__backend_mapping = backend_mapping
|
||||
|
||||
@lockutils.synchronized('dbapi_backend', 'billingstack-')
|
||||
def __get_backend(self):
|
||||
"""Get the actual backend. May be a module or an instance of
|
||||
a class. Doesn't matter to us. We do this synchronized as it's
|
||||
possible multiple greenthreads started very quickly trying to do
|
||||
DB calls and eventlet can switch threads before self.__backend gets
|
||||
assigned.
|
||||
"""
|
||||
if self.__backend:
|
||||
# Another thread assigned it
|
||||
return self.__backend
|
||||
backend_name = CONF.database.backend
|
||||
self.__use_tpool = CONF.database.use_tpool
|
||||
if self.__use_tpool:
|
||||
from eventlet import tpool
|
||||
self.__tpool = tpool
|
||||
# Import the untranslated name if we don't have a
|
||||
# mapping.
|
||||
backend_path = self.__backend_mapping.get(backend_name,
|
||||
backend_name)
|
||||
backend_mod = importutils.import_module(backend_path)
|
||||
self.__backend = backend_mod.get_backend()
|
||||
return self.__backend
|
||||
|
||||
def __getattr__(self, key):
|
||||
backend = self.__backend or self.__get_backend()
|
||||
attr = getattr(backend, key)
|
||||
if not self.__use_tpool or not hasattr(attr, '__call__'):
|
||||
return attr
|
||||
|
||||
def tpool_wrapper(*args, **kwargs):
|
||||
return self.__tpool.execute(attr, *args, **kwargs)
|
||||
|
||||
functools.update_wrapper(tpool_wrapper, attr)
|
||||
return tpool_wrapper
|
@ -1,51 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""DB related custom exceptions."""
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
|
||||
class DBError(Exception):
|
||||
"""Wraps an implementation specific exception."""
|
||||
def __init__(self, inner_exception=None):
|
||||
self.inner_exception = inner_exception
|
||||
super(DBError, self).__init__(str(inner_exception))
|
||||
|
||||
|
||||
class DBDuplicateEntry(DBError):
|
||||
"""Wraps an implementation specific exception."""
|
||||
def __init__(self, columns=[], inner_exception=None):
|
||||
self.columns = columns
|
||||
super(DBDuplicateEntry, self).__init__(inner_exception)
|
||||
|
||||
|
||||
class DBDeadlock(DBError):
|
||||
def __init__(self, inner_exception=None):
|
||||
super(DBDeadlock, self).__init__(inner_exception)
|
||||
|
||||
|
||||
class DBInvalidUnicodeParameter(Exception):
|
||||
message = _("Invalid Parameter: "
|
||||
"Unicode is not supported by the current database.")
|
||||
|
||||
|
||||
class DbMigrationError(DBError):
|
||||
"""Wraps migration specific exception."""
|
||||
def __init__(self, message=None):
|
||||
super(DbMigrationError, self).__init__(str(message))
|
@ -1,16 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Cloudscaling Group, Inc
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
@ -1,103 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2012 Cloudscaling Group, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
SQLAlchemy models.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy.orm import object_mapper
|
||||
|
||||
from billingstack.openstack.common.db.sqlalchemy.session import get_session
|
||||
from billingstack.openstack.common import timeutils
|
||||
|
||||
|
||||
class ModelBase(object):
|
||||
"""Base class for models."""
|
||||
__table_initialized__ = False
|
||||
created_at = Column(DateTime, default=timeutils.utcnow)
|
||||
updated_at = Column(DateTime, onupdate=timeutils.utcnow)
|
||||
metadata = None
|
||||
|
||||
def save(self, session=None):
|
||||
"""Save this object."""
|
||||
if not session:
|
||||
session = get_session()
|
||||
# NOTE(boris-42): This part of code should be look like:
|
||||
# sesssion.add(self)
|
||||
# session.flush()
|
||||
# But there is a bug in sqlalchemy and eventlet that
|
||||
# raises NoneType exception if there is no running
|
||||
# transaction and rollback is called. As long as
|
||||
# sqlalchemy has this bug we have to create transaction
|
||||
# explicity.
|
||||
with session.begin(subtransactions=True):
|
||||
session.add(self)
|
||||
session.flush()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return getattr(self, key, default)
|
||||
|
||||
def __iter__(self):
|
||||
columns = dict(object_mapper(self).columns).keys()
|
||||
# NOTE(russellb): Allow models to specify other keys that can be looked
|
||||
# up, beyond the actual db columns. An example would be the 'name'
|
||||
# property for an Instance.
|
||||
if hasattr(self, '_extra_keys'):
|
||||
columns.extend(self._extra_keys())
|
||||
self._i = iter(columns)
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
n = self._i.next()
|
||||
return n, getattr(self, n)
|
||||
|
||||
def update(self, values):
|
||||
"""Make the model object behave like a dict."""
|
||||
for k, v in values.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def iteritems(self):
|
||||
"""Make the model object behave like a dict.
|
||||
|
||||
Includes attributes from joins."""
|
||||
local = dict(self)
|
||||
joined = dict([(k, v) for k, v in self.__dict__.iteritems()
|
||||
if not k[0] == '_'])
|
||||
local.update(joined)
|
||||
return local.iteritems()
|
||||
|
||||
|
||||
class SoftDeleteMixin(object):
|
||||
deleted_at = Column(DateTime)
|
||||
deleted = Column(Integer, default=0)
|
||||
|
||||
def soft_delete(self, session=None):
|
||||
"""Mark this object as deleted."""
|
||||
self.deleted = self.id
|
||||
self.deleted_at = timeutils.utcnow()
|
||||
self.save(session=session)
|
@ -1,132 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# Copyright 2012 Justin Santa Barbara
|
||||
# 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.
|
||||
|
||||
"""Implementation of paginate query."""
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
from openstack.common.gettextutils import _
|
||||
from openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InvalidSortKey(Exception):
|
||||
message = _("Sort key supplied was not valid.")
|
||||
|
||||
|
||||
# copy from glance/db/sqlalchemy/api.py
|
||||
def paginate_query(query, model, limit, sort_keys, marker=None,
|
||||
sort_dir=None, sort_dirs=None):
|
||||
"""Returns a query with sorting / pagination criteria added.
|
||||
|
||||
Pagination works by requiring a unique sort_key, specified by sort_keys.
|
||||
(If sort_keys is not unique, then we risk looping through values.)
|
||||
We use the last row in the previous page as the 'marker' for pagination.
|
||||
So we must return values that follow the passed marker in the order.
|
||||
With a single-valued sort_key, this would be easy: sort_key > X.
|
||||
With a compound-values sort_key, (k1, k2, k3) we must do this to repeat
|
||||
the lexicographical ordering:
|
||||
(k1 > X1) or (k1 == X1 && k2 > X2) or (k1 == X1 && k2 == X2 && k3 > X3)
|
||||
|
||||
We also have to cope with different sort_directions.
|
||||
|
||||
Typically, the id of the last row is used as the client-facing pagination
|
||||
marker, then the actual marker object must be fetched from the db and
|
||||
passed in to us as marker.
|
||||
|
||||
:param query: the query object to which we should add paging/sorting
|
||||
:param model: the ORM model class
|
||||
:param limit: maximum number of items to return
|
||||
:param sort_keys: array of attributes by which results should be sorted
|
||||
:param marker: the last item of the previous page; we returns the next
|
||||
results after this value.
|
||||
:param sort_dir: direction in which results should be sorted (asc, desc)
|
||||
:param sort_dirs: per-column array of sort_dirs, corresponding to sort_keys
|
||||
|
||||
:rtype: sqlalchemy.orm.query.Query
|
||||
:return: The query with sorting/pagination added.
|
||||
"""
|
||||
|
||||
if 'id' not in sort_keys:
|
||||
# TODO(justinsb): If this ever gives a false-positive, check
|
||||
# the actual primary key, rather than assuming its id
|
||||
LOG.warn(_('Id not in sort_keys; is sort_keys unique?'))
|
||||
|
||||
assert(not (sort_dir and sort_dirs))
|
||||
|
||||
# Default the sort direction to ascending
|
||||
if sort_dirs is None and sort_dir is None:
|
||||
sort_dir = 'asc'
|
||||
|
||||
# Ensure a per-column sort direction
|
||||
if sort_dirs is None:
|
||||
sort_dirs = [sort_dir for _sort_key in sort_keys]
|
||||
|
||||
assert(len(sort_dirs) == len(sort_keys))
|
||||
|
||||
# Add sorting
|
||||
for current_sort_key, current_sort_dir in zip(sort_keys, sort_dirs):
|
||||
sort_dir_func = {
|
||||
'asc': sqlalchemy.asc,
|
||||
'desc': sqlalchemy.desc,
|
||||
}[current_sort_dir]
|
||||
|
||||
try:
|
||||
sort_key_attr = getattr(model, current_sort_key)
|
||||
except AttributeError:
|
||||
raise InvalidSortKey()
|
||||
query = query.order_by(sort_dir_func(sort_key_attr))
|
||||
|
||||
# Add pagination
|
||||
if marker is not None:
|
||||
marker_values = []
|
||||
for sort_key in sort_keys:
|
||||
v = getattr(marker, sort_key)
|
||||
marker_values.append(v)
|
||||
|
||||
# Build up an array of sort criteria as in the docstring
|
||||
criteria_list = []
|
||||
for i in xrange(0, len(sort_keys)):
|
||||
crit_attrs = []
|
||||
for j in xrange(0, i):
|
||||
model_attr = getattr(model, sort_keys[j])
|
||||
crit_attrs.append((model_attr == marker_values[j]))
|
||||
|
||||
model_attr = getattr(model, sort_keys[i])
|
||||
if sort_dirs[i] == 'desc':
|
||||
crit_attrs.append((model_attr < marker_values[i]))
|
||||
elif sort_dirs[i] == 'asc':
|
||||
crit_attrs.append((model_attr > marker_values[i]))
|
||||
else:
|
||||
raise ValueError(_("Unknown sort direction, "
|
||||
"must be 'desc' or 'asc'"))
|
||||
|
||||
criteria = sqlalchemy.sql.and_(*crit_attrs)
|
||||
criteria_list.append(criteria)
|
||||
|
||||
f = sqlalchemy.sql.or_(*criteria_list)
|
||||
query = query.filter(f)
|
||||
|
||||
if limit is not None:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query
|
@ -1,146 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 OpenStack Foundation.
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import errno
|
||||
import gc
|
||||
import os
|
||||
import pprint
|
||||
import socket
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import eventlet
|
||||
import eventlet.backdoor
|
||||
import greenlet
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import log as logging
|
||||
|
||||
help_for_backdoor_port = (
|
||||
"Acceptable values are 0, <port>, and <start>:<end>, where 0 results "
|
||||
"in listening on a random tcp port number; <port> results in listening "
|
||||
"on the specified port number (and not enabling backdoor if that port "
|
||||
"is in use); and <start>:<end> results in listening on the smallest "
|
||||
"unused port number within the specified range of port numbers. The "
|
||||
"chosen port is displayed in the service's log file.")
|
||||
eventlet_backdoor_opts = [
|
||||
cfg.StrOpt('backdoor_port',
|
||||
default=None,
|
||||
help="Enable eventlet backdoor. %s" % help_for_backdoor_port)
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(eventlet_backdoor_opts)
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventletBackdoorConfigValueError(Exception):
|
||||
def __init__(self, port_range, help_msg, ex):
|
||||
msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. '
|
||||
'%(help)s' %
|
||||
{'range': port_range, 'ex': ex, 'help': help_msg})
|
||||
super(EventletBackdoorConfigValueError, self).__init__(msg)
|
||||
self.port_range = port_range
|
||||
|
||||
|
||||
def _dont_use_this():
|
||||
print("Don't use this, just disconnect instead")
|
||||
|
||||
|
||||
def _find_objects(t):
|
||||
return filter(lambda o: isinstance(o, t), gc.get_objects())
|
||||
|
||||
|
||||
def _print_greenthreads():
|
||||
for i, gt in enumerate(_find_objects(greenlet.greenlet)):
|
||||
print(i, gt)
|
||||
traceback.print_stack(gt.gr_frame)
|
||||
print()
|
||||
|
||||
|
||||
def _print_nativethreads():
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
print(threadId)
|
||||
traceback.print_stack(stack)
|
||||
print()
|
||||
|
||||
|
||||
def _parse_port_range(port_range):
|
||||
if ':' not in port_range:
|
||||
start, end = port_range, port_range
|
||||
else:
|
||||
start, end = port_range.split(':', 1)
|
||||
try:
|
||||
start, end = int(start), int(end)
|
||||
if end < start:
|
||||
raise ValueError
|
||||
return start, end
|
||||
except ValueError as ex:
|
||||
raise EventletBackdoorConfigValueError(port_range, ex,
|
||||
help_for_backdoor_port)
|
||||
|
||||
|
||||
def _listen(host, start_port, end_port, listen_func):
|
||||
try_port = start_port
|
||||
while True:
|
||||
try:
|
||||
return listen_func((host, try_port))
|
||||
except socket.error as exc:
|
||||
if (exc.errno != errno.EADDRINUSE or
|
||||
try_port >= end_port):
|
||||
raise
|
||||
try_port += 1
|
||||
|
||||
|
||||
def initialize_if_enabled():
|
||||
backdoor_locals = {
|
||||
'exit': _dont_use_this, # So we don't exit the entire process
|
||||
'quit': _dont_use_this, # So we don't exit the entire process
|
||||
'fo': _find_objects,
|
||||
'pgt': _print_greenthreads,
|
||||
'pnt': _print_nativethreads,
|
||||
}
|
||||
|
||||
if CONF.backdoor_port is None:
|
||||
return None
|
||||
|
||||
start_port, end_port = _parse_port_range(str(CONF.backdoor_port))
|
||||
|
||||
# NOTE(johannes): The standard sys.displayhook will print the value of
|
||||
# the last expression and set it to __builtin__._, which overwrites
|
||||
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
||||
# since it won't interact poorly with gettext, and it's easier to
|
||||
# read the output too.
|
||||
def displayhook(val):
|
||||
if val is not None:
|
||||
pprint.pprint(val)
|
||||
sys.displayhook = displayhook
|
||||
|
||||
sock = _listen('localhost', start_port, end_port, eventlet.listen)
|
||||
|
||||
# In the case of backdoor port being zero, a port number is assigned by
|
||||
# listen(). In any case, pull the port number out here.
|
||||
port = sock.getsockname()[1]
|
||||
LOG.info(_('Eventlet backdoor listening on %(port)s for process %(pid)d') %
|
||||
{'port': port, 'pid': os.getpid()})
|
||||
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
||||
locals=backdoor_locals)
|
||||
return port
|
@ -1,139 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""
|
||||
Exceptions common to OpenStack projects
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
_FATAL_EXCEPTION_FORMAT_ERRORS = False
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
def __init__(self, message=None):
|
||||
super(Error, self).__init__(message)
|
||||
|
||||
|
||||
class ApiError(Error):
|
||||
def __init__(self, message='Unknown', code='Unknown'):
|
||||
self.api_message = message
|
||||
self.code = code
|
||||
super(ApiError, self).__init__('%s: %s' % (code, message))
|
||||
|
||||
|
||||
class NotFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownScheme(Error):
|
||||
|
||||
msg_fmt = "Unknown scheme '%s' found in URI"
|
||||
|
||||
def __init__(self, scheme):
|
||||
msg = self.msg_fmt % scheme
|
||||
super(UnknownScheme, self).__init__(msg)
|
||||
|
||||
|
||||
class BadStoreUri(Error):
|
||||
|
||||
msg_fmt = "The Store URI %s was malformed. Reason: %s"
|
||||
|
||||
def __init__(self, uri, reason):
|
||||
msg = self.msg_fmt % (uri, reason)
|
||||
super(BadStoreUri, self).__init__(msg)
|
||||
|
||||
|
||||
class Duplicate(Error):
|
||||
pass
|
||||
|
||||
|
||||
class NotAuthorized(Error):
|
||||
pass
|
||||
|
||||
|
||||
class NotEmpty(Error):
|
||||
pass
|
||||
|
||||
|
||||
class Invalid(Error):
|
||||
pass
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingArgumentError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseMigrationError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class ClientConnectionError(Exception):
|
||||
"""Error resulting from a client connecting to a server"""
|
||||
pass
|
||||
|
||||
|
||||
def wrap_exception(f):
|
||||
def _wrap(*args, **kw):
|
||||
try:
|
||||
return f(*args, **kw)
|
||||
except Exception as e:
|
||||
if not isinstance(e, Error):
|
||||
logging.exception(_('Uncaught exception'))
|
||||
raise Error(str(e))
|
||||
raise
|
||||
_wrap.func_name = f.func_name
|
||||
return _wrap
|
||||
|
||||
|
||||
class OpenstackException(Exception):
|
||||
"""Base Exception class.
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'msg_fmt' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
"""
|
||||
msg_fmt = "An unknown exception occurred"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
try:
|
||||
self._error_string = self.msg_fmt % kwargs
|
||||
|
||||
except Exception:
|
||||
if _FATAL_EXCEPTION_FORMAT_ERRORS:
|
||||
raise
|
||||
else:
|
||||
# at least get the core message out if something happened
|
||||
self._error_string = self.msg_fmt
|
||||
|
||||
def __str__(self):
|
||||
return self._error_string
|
||||
|
||||
|
||||
class MalformedRequestBody(OpenstackException):
|
||||
msg_fmt = "Malformed message body: %(reason)s"
|
||||
|
||||
|
||||
class InvalidContentType(OpenstackException):
|
||||
msg_fmt = "Invalid content type %(content_type)s"
|
@ -1,101 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# Copyright 2012, Red Hat, 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.
|
||||
|
||||
"""
|
||||
Exception related utilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import six
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
|
||||
|
||||
class save_and_reraise_exception(object):
|
||||
"""Save current exception, run some code and then re-raise.
|
||||
|
||||
In some cases the exception context can be cleared, resulting in None
|
||||
being attempted to be re-raised after an exception handler is run. This
|
||||
can happen when eventlet switches greenthreads or when running an
|
||||
exception handler, code raises and catches an exception. In both
|
||||
cases the exception context will be cleared.
|
||||
|
||||
To work around this, we save the exception state, run handler code, and
|
||||
then re-raise the original exception. If another exception occurs, the
|
||||
saved exception is logged and the new exception is re-raised.
|
||||
|
||||
In some cases the caller may not want to re-raise the exception, and
|
||||
for those circumstances this context provides a reraise flag that
|
||||
can be used to suppress the exception. For example:
|
||||
|
||||
except Exception:
|
||||
with save_and_reraise_exception() as ctxt:
|
||||
decide_if_need_reraise()
|
||||
if not should_be_reraised:
|
||||
ctxt.reraise = False
|
||||
"""
|
||||
def __init__(self):
|
||||
self.reraise = True
|
||||
|
||||
def __enter__(self):
|
||||
self.type_, self.value, self.tb, = sys.exc_info()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is not None:
|
||||
logging.error(_('Original exception being dropped: %s'),
|
||||
traceback.format_exception(self.type_,
|
||||
self.value,
|
||||
self.tb))
|
||||
return False
|
||||
if self.reraise:
|
||||
six.reraise(self.type_, self.value, self.tb)
|
||||
|
||||
|
||||
def forever_retry_uncaught_exceptions(infunc):
|
||||
def inner_func(*args, **kwargs):
|
||||
last_log_time = 0
|
||||
last_exc_message = None
|
||||
exc_count = 0
|
||||
while True:
|
||||
try:
|
||||
return infunc(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
this_exc_message = six.u(str(exc))
|
||||
if this_exc_message == last_exc_message:
|
||||
exc_count += 1
|
||||
else:
|
||||
exc_count = 1
|
||||
# Do not log any more frequently than once a minute unless
|
||||
# the exception message changes
|
||||
cur_time = int(time.time())
|
||||
if (cur_time - last_log_time > 60 or
|
||||
this_exc_message != last_exc_message):
|
||||
logging.exception(
|
||||
_('Unexpected exception occurred %d time(s)... '
|
||||
'retrying.') % exc_count)
|
||||
last_log_time = cur_time
|
||||
last_exc_message = this_exc_message
|
||||
exc_count = 0
|
||||
# This should be a very rare event. In case it isn't, do
|
||||
# a sleep.
|
||||
time.sleep(1)
|
||||
return inner_func
|
@ -1,139 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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 contextlib
|
||||
import errno
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from billingstack.openstack.common import excutils
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
_FILE_CACHE = {}
|
||||
|
||||
|
||||
def ensure_tree(path):
|
||||
"""Create a directory (and any ancestor directories required)
|
||||
|
||||
:param path: Directory to create
|
||||
"""
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
if not os.path.isdir(path):
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def read_cached_file(filename, force_reload=False):
|
||||
"""Read from a file if it has been modified.
|
||||
|
||||
:param force_reload: Whether to reload the file.
|
||||
:returns: A tuple with a boolean specifying if the data is fresh
|
||||
or not.
|
||||
"""
|
||||
global _FILE_CACHE
|
||||
|
||||
if force_reload and filename in _FILE_CACHE:
|
||||
del _FILE_CACHE[filename]
|
||||
|
||||
reloaded = False
|
||||
mtime = os.path.getmtime(filename)
|
||||
cache_info = _FILE_CACHE.setdefault(filename, {})
|
||||
|
||||
if not cache_info or mtime > cache_info.get('mtime', 0):
|
||||
LOG.debug(_("Reloading cached file %s") % filename)
|
||||
with open(filename) as fap:
|
||||
cache_info['data'] = fap.read()
|
||||
cache_info['mtime'] = mtime
|
||||
reloaded = True
|
||||
return (reloaded, cache_info['data'])
|
||||
|
||||
|
||||
def delete_if_exists(path, remove=os.unlink):
|
||||
"""Delete a file, but ignore file not found error.
|
||||
|
||||
:param path: File to delete
|
||||
:param remove: Optional function to remove passed path
|
||||
"""
|
||||
|
||||
try:
|
||||
remove(path)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def remove_path_on_error(path, remove=delete_if_exists):
|
||||
"""Protect code that wants to operate on PATH atomically.
|
||||
Any exception will cause PATH to be removed.
|
||||
|
||||
:param path: File to work with
|
||||
:param remove: Optional function to remove passed path
|
||||
"""
|
||||
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
remove(path)
|
||||
|
||||
|
||||
def file_open(*args, **kwargs):
|
||||
"""Open file
|
||||
|
||||
see built-in file() documentation for more details
|
||||
|
||||
Note: The reason this is kept in a separate module is to easily
|
||||
be able to provide a stub module that doesn't alter system
|
||||
state at all (for unit tests)
|
||||
"""
|
||||
return file(*args, **kwargs)
|
||||
|
||||
|
||||
def write_to_tempfile(content, path=None, suffix='', prefix='tmp'):
|
||||
"""Create temporary file or use existing file.
|
||||
|
||||
This util is needed for creating temporary file with
|
||||
specified content, suffix and prefix. If path is not None,
|
||||
it will be used for writing content. If the path doesn't
|
||||
exist it'll be created.
|
||||
|
||||
:param content: content for temporary file.
|
||||
:param path: same as parameter 'dir' for mkstemp
|
||||
:param suffix: same as parameter 'suffix' for mkstemp
|
||||
:param prefix: same as parameter 'prefix' for mkstemp
|
||||
|
||||
For example: it can be used in database tests for creating
|
||||
configuration files.
|
||||
"""
|
||||
if path:
|
||||
ensure_tree(path)
|
||||
|
||||
(fd, path) = tempfile.mkstemp(suffix=suffix, dir=path, prefix=prefix)
|
||||
try:
|
||||
os.write(fd, content)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return path
|
@ -1,373 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
# Copyright 2013 IBM Corp.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
gettext for openstack-common modules.
|
||||
|
||||
Usual usage in an openstack.common module:
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _
|
||||
"""
|
||||
|
||||
import copy
|
||||
import gettext
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
import UserString as _userString
|
||||
except ImportError:
|
||||
import collections as _userString
|
||||
|
||||
from babel import localedata
|
||||
import six
|
||||
|
||||
_localedir = os.environ.get('billingstack'.upper() + '_LOCALEDIR')
|
||||
_t = gettext.translation('billingstack', localedir=_localedir, fallback=True)
|
||||
|
||||
_AVAILABLE_LANGUAGES = {}
|
||||
USE_LAZY = False
|
||||
|
||||
|
||||
def enable_lazy():
|
||||
"""Convenience function for configuring _() to use lazy gettext
|
||||
|
||||
Call this at the start of execution to enable the gettextutils._
|
||||
function to use lazy gettext functionality. This is useful if
|
||||
your project is importing _ directly instead of using the
|
||||
gettextutils.install() way of importing the _ function.
|
||||
"""
|
||||
global USE_LAZY
|
||||
USE_LAZY = True
|
||||
|
||||
|
||||
def _(msg):
|
||||
if USE_LAZY:
|
||||
return Message(msg, 'billingstack')
|
||||
else:
|
||||
if six.PY3:
|
||||
return _t.gettext(msg)
|
||||
return _t.ugettext(msg)
|
||||
|
||||
|
||||
def install(domain, lazy=False):
|
||||
"""Install a _() function using the given translation domain.
|
||||
|
||||
Given a translation domain, install a _() function using gettext's
|
||||
install() function.
|
||||
|
||||
The main difference from gettext.install() is that we allow
|
||||
overriding the default localedir (e.g. /usr/share/locale) using
|
||||
a translation-domain-specific environment variable (e.g.
|
||||
NOVA_LOCALEDIR).
|
||||
|
||||
:param domain: the translation domain
|
||||
:param lazy: indicates whether or not to install the lazy _() function.
|
||||
The lazy _() introduces a way to do deferred translation
|
||||
of messages by installing a _ that builds Message objects,
|
||||
instead of strings, which can then be lazily translated into
|
||||
any available locale.
|
||||
"""
|
||||
if lazy:
|
||||
# NOTE(mrodden): Lazy gettext functionality.
|
||||
#
|
||||
# The following introduces a deferred way to do translations on
|
||||
# messages in OpenStack. We override the standard _() function
|
||||
# and % (format string) operation to build Message objects that can
|
||||
# later be translated when we have more information.
|
||||
#
|
||||
# Also included below is an example LocaleHandler that translates
|
||||
# Messages to an associated locale, effectively allowing many logs,
|
||||
# each with their own locale.
|
||||
|
||||
def _lazy_gettext(msg):
|
||||
"""Create and return a Message object.
|
||||
|
||||
Lazy gettext function for a given domain, it is a factory method
|
||||
for a project/module to get a lazy gettext function for its own
|
||||
translation domain (i.e. nova, glance, cinder, etc.)
|
||||
|
||||
Message encapsulates a string so that we can translate
|
||||
it later when needed.
|
||||
"""
|
||||
return Message(msg, domain)
|
||||
|
||||
from six import moves
|
||||
moves.builtins.__dict__['_'] = _lazy_gettext
|
||||
else:
|
||||
localedir = '%s_LOCALEDIR' % domain.upper()
|
||||
if six.PY3:
|
||||
gettext.install(domain,
|
||||
localedir=os.environ.get(localedir))
|
||||
else:
|
||||
gettext.install(domain,
|
||||
localedir=os.environ.get(localedir),
|
||||
unicode=True)
|
||||
|
||||
|
||||
class Message(_userString.UserString, object):
|
||||
"""Class used to encapsulate translatable messages."""
|
||||
def __init__(self, msg, domain):
|
||||
# _msg is the gettext msgid and should never change
|
||||
self._msg = msg
|
||||
self._left_extra_msg = ''
|
||||
self._right_extra_msg = ''
|
||||
self._locale = None
|
||||
self.params = None
|
||||
self.domain = domain
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
# NOTE(mrodden): this should always resolve to a unicode string
|
||||
# that best represents the state of the message currently
|
||||
|
||||
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
|
||||
if self.locale:
|
||||
lang = gettext.translation(self.domain,
|
||||
localedir=localedir,
|
||||
languages=[self.locale],
|
||||
fallback=True)
|
||||
else:
|
||||
# use system locale for translations
|
||||
lang = gettext.translation(self.domain,
|
||||
localedir=localedir,
|
||||
fallback=True)
|
||||
|
||||
if six.PY3:
|
||||
ugettext = lang.gettext
|
||||
else:
|
||||
ugettext = lang.ugettext
|
||||
|
||||
full_msg = (self._left_extra_msg +
|
||||
ugettext(self._msg) +
|
||||
self._right_extra_msg)
|
||||
|
||||
if self.params is not None:
|
||||
full_msg = full_msg % self.params
|
||||
|
||||
return six.text_type(full_msg)
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
return self._locale
|
||||
|
||||
@locale.setter
|
||||
def locale(self, value):
|
||||
self._locale = value
|
||||
if not self.params:
|
||||
return
|
||||
|
||||
# This Message object may have been constructed with one or more
|
||||
# Message objects as substitution parameters, given as a single
|
||||
# Message, or a tuple or Map containing some, so when setting the
|
||||
# locale for this Message we need to set it for those Messages too.
|
||||
if isinstance(self.params, Message):
|
||||
self.params.locale = value
|
||||
return
|
||||
if isinstance(self.params, tuple):
|
||||
for param in self.params:
|
||||
if isinstance(param, Message):
|
||||
param.locale = value
|
||||
return
|
||||
if isinstance(self.params, dict):
|
||||
for param in self.params.values():
|
||||
if isinstance(param, Message):
|
||||
param.locale = value
|
||||
|
||||
def _save_dictionary_parameter(self, dict_param):
|
||||
full_msg = self.data
|
||||
# look for %(blah) fields in string;
|
||||
# ignore %% and deal with the
|
||||
# case where % is first character on the line
|
||||
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
|
||||
|
||||
# if we don't find any %(blah) blocks but have a %s
|
||||
if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
|
||||
# apparently the full dictionary is the parameter
|
||||
params = copy.deepcopy(dict_param)
|
||||
else:
|
||||
params = {}
|
||||
for key in keys:
|
||||
try:
|
||||
params[key] = copy.deepcopy(dict_param[key])
|
||||
except TypeError:
|
||||
# cast uncopyable thing to unicode string
|
||||
params[key] = six.text_type(dict_param[key])
|
||||
|
||||
return params
|
||||
|
||||
def _save_parameters(self, other):
|
||||
# we check for None later to see if
|
||||
# we actually have parameters to inject,
|
||||
# so encapsulate if our parameter is actually None
|
||||
if other is None:
|
||||
self.params = (other, )
|
||||
elif isinstance(other, dict):
|
||||
self.params = self._save_dictionary_parameter(other)
|
||||
else:
|
||||
# fallback to casting to unicode,
|
||||
# this will handle the problematic python code-like
|
||||
# objects that cannot be deep-copied
|
||||
try:
|
||||
self.params = copy.deepcopy(other)
|
||||
except TypeError:
|
||||
self.params = six.text_type(other)
|
||||
|
||||
return self
|
||||
|
||||
# overrides to be more string-like
|
||||
def __unicode__(self):
|
||||
return self.data
|
||||
|
||||
def __str__(self):
|
||||
if six.PY3:
|
||||
return self.__unicode__()
|
||||
return self.data.encode('utf-8')
|
||||
|
||||
def __getstate__(self):
|
||||
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
|
||||
'domain', 'params', '_locale']
|
||||
new_dict = self.__dict__.fromkeys(to_copy)
|
||||
for attr in to_copy:
|
||||
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
|
||||
|
||||
return new_dict
|
||||
|
||||
def __setstate__(self, state):
|
||||
for (k, v) in state.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
# operator overloads
|
||||
def __add__(self, other):
|
||||
copied = copy.deepcopy(self)
|
||||
copied._right_extra_msg += other.__str__()
|
||||
return copied
|
||||
|
||||
def __radd__(self, other):
|
||||
copied = copy.deepcopy(self)
|
||||
copied._left_extra_msg += other.__str__()
|
||||
return copied
|
||||
|
||||
def __mod__(self, other):
|
||||
# do a format string to catch and raise
|
||||
# any possible KeyErrors from missing parameters
|
||||
self.data % other
|
||||
copied = copy.deepcopy(self)
|
||||
return copied._save_parameters(other)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.data * other
|
||||
|
||||
def __rmul__(self, other):
|
||||
return other * self.data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
def __getslice__(self, start, end):
|
||||
return self.data.__getslice__(start, end)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# NOTE(mrodden): handle lossy operations that we can't deal with yet
|
||||
# These override the UserString implementation, since UserString
|
||||
# uses our __class__ attribute to try and build a new message
|
||||
# after running the inner data string through the operation.
|
||||
# At that point, we have lost the gettext message id and can just
|
||||
# safely resolve to a string instead.
|
||||
ops = ['capitalize', 'center', 'decode', 'encode',
|
||||
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
|
||||
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
|
||||
if name in ops:
|
||||
return getattr(self.data, name)
|
||||
else:
|
||||
return _userString.UserString.__getattribute__(self, name)
|
||||
|
||||
|
||||
def get_available_languages(domain):
|
||||
"""Lists the available languages for the given translation domain.
|
||||
|
||||
:param domain: the domain to get languages for
|
||||
"""
|
||||
if domain in _AVAILABLE_LANGUAGES:
|
||||
return copy.copy(_AVAILABLE_LANGUAGES[domain])
|
||||
|
||||
localedir = '%s_LOCALEDIR' % domain.upper()
|
||||
find = lambda x: gettext.find(domain,
|
||||
localedir=os.environ.get(localedir),
|
||||
languages=[x])
|
||||
|
||||
# NOTE(mrodden): en_US should always be available (and first in case
|
||||
# order matters) since our in-line message strings are en_US
|
||||
language_list = ['en_US']
|
||||
# NOTE(luisg): Babel <1.0 used a function called list(), which was
|
||||
# renamed to locale_identifiers() in >=1.0, the requirements master list
|
||||
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
|
||||
# this check when the master list updates to >=1.0, and update all projects
|
||||
list_identifiers = (getattr(localedata, 'list', None) or
|
||||
getattr(localedata, 'locale_identifiers'))
|
||||
locale_identifiers = list_identifiers()
|
||||
for i in locale_identifiers:
|
||||
if find(i) is not None:
|
||||
language_list.append(i)
|
||||
_AVAILABLE_LANGUAGES[domain] = language_list
|
||||
return copy.copy(language_list)
|
||||
|
||||
|
||||
def get_localized_message(message, user_locale):
|
||||
"""Gets a localized version of the given message in the given locale.
|
||||
|
||||
If the message is not a Message object the message is returned as-is.
|
||||
If the locale is None the message is translated to the default locale.
|
||||
|
||||
:returns: the translated message in unicode, or the original message if
|
||||
it could not be translated
|
||||
"""
|
||||
translated = message
|
||||
if isinstance(message, Message):
|
||||
original_locale = message.locale
|
||||
message.locale = user_locale
|
||||
translated = six.text_type(message)
|
||||
message.locale = original_locale
|
||||
return translated
|
||||
|
||||
|
||||
class LocaleHandler(logging.Handler):
|
||||
"""Handler that can have a locale associated to translate Messages.
|
||||
|
||||
A quick example of how to utilize the Message class above.
|
||||
LocaleHandler takes a locale and a target logging.Handler object
|
||||
to forward LogRecord objects to after translating the internal Message.
|
||||
"""
|
||||
|
||||
def __init__(self, locale, target):
|
||||
"""Initialize a LocaleHandler
|
||||
|
||||
:param locale: locale to use for translating messages
|
||||
:param target: logging.Handler object to forward
|
||||
LogRecord objects to after translation
|
||||
"""
|
||||
logging.Handler.__init__(self)
|
||||
self.locale = locale
|
||||
self.target = target
|
||||
|
||||
def emit(self, record):
|
||||
if isinstance(record.msg, Message):
|
||||
# set the locale and resolve to a string
|
||||
record.msg.locale = self.locale
|
||||
|
||||
self.target.emit(record)
|
@ -1,68 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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 related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class."""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ValueError, AttributeError):
|
||||
raise ImportError('Class %s cannot be found (%s)' %
|
||||
(class_str,
|
||||
traceback.format_exception(*sys.exc_info())))
|
||||
|
||||
|
||||
def import_object(import_str, *args, **kwargs):
|
||||
"""Import a class and return an instance of it."""
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||
"""Tries to import object from default namespace.
|
||||
|
||||
Imports a class and return an instance of it, first by trying
|
||||
to find the class in a default namespace, then failing back to
|
||||
a full path if not found in the default namespace.
|
||||
"""
|
||||
import_value = "%s.%s" % (name_space, import_str)
|
||||
try:
|
||||
return import_class(import_value)(*args, **kwargs)
|
||||
except ImportError:
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_module(import_str):
|
||||
"""Import a module."""
|
||||
__import__(import_str)
|
||||
return sys.modules[import_str]
|
||||
|
||||
|
||||
def try_import(import_str, default=None):
|
||||
"""Try to import a module and if it fails return default."""
|
||||
try:
|
||||
return import_module(import_str)
|
||||
except ImportError:
|
||||
return default
|
@ -1,130 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
def __init__(self, message, lineno, line):
|
||||
self.msg = message
|
||||
self.line = line
|
||||
self.lineno = lineno
|
||||
|
||||
def __str__(self):
|
||||
return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line)
|
||||
|
||||
|
||||
class BaseParser(object):
|
||||
lineno = 0
|
||||
parse_exc = ParseError
|
||||
|
||||
def _assignment(self, key, value):
|
||||
self.assignment(key, value)
|
||||
return None, []
|
||||
|
||||
def _get_section(self, line):
|
||||
if line[-1] != ']':
|
||||
return self.error_no_section_end_bracket(line)
|
||||
if len(line) <= 2:
|
||||
return self.error_no_section_name(line)
|
||||
|
||||
return line[1:-1]
|
||||
|
||||
def _split_key_value(self, line):
|
||||
colon = line.find(':')
|
||||
equal = line.find('=')
|
||||
if colon < 0 and equal < 0:
|
||||
return self.error_invalid_assignment(line)
|
||||
|
||||
if colon < 0 or (equal >= 0 and equal < colon):
|
||||
key, value = line[:equal], line[equal + 1:]
|
||||
else:
|
||||
key, value = line[:colon], line[colon + 1:]
|
||||
|
||||
value = value.strip()
|
||||
if ((value and value[0] == value[-1]) and
|
||||
(value[0] == "\"" or value[0] == "'")):
|
||||
value = value[1:-1]
|
||||
return key.strip(), [value]
|
||||
|
||||
def parse(self, lineiter):
|
||||
key = None
|
||||
value = []
|
||||
|
||||
for line in lineiter:
|
||||
self.lineno += 1
|
||||
|
||||
line = line.rstrip()
|
||||
if not line:
|
||||
# Blank line, ends multi-line values
|
||||
if key:
|
||||
key, value = self._assignment(key, value)
|
||||
continue
|
||||
elif line[0] in (' ', '\t'):
|
||||
# Continuation of previous assignment
|
||||
if key is None:
|
||||
self.error_unexpected_continuation(line)
|
||||
else:
|
||||
value.append(line.lstrip())
|
||||
continue
|
||||
|
||||
if key:
|
||||
# Flush previous assignment, if any
|
||||
key, value = self._assignment(key, value)
|
||||
|
||||
if line[0] == '[':
|
||||
# Section start
|
||||
section = self._get_section(line)
|
||||
if section:
|
||||
self.new_section(section)
|
||||
elif line[0] in '#;':
|
||||
self.comment(line[1:].lstrip())
|
||||
else:
|
||||
key, value = self._split_key_value(line)
|
||||
if not key:
|
||||
return self.error_empty_key(line)
|
||||
|
||||
if key:
|
||||
# Flush previous assignment, if any
|
||||
self._assignment(key, value)
|
||||
|
||||
def assignment(self, key, value):
|
||||
"""Called when a full assignment is parsed"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def new_section(self, section):
|
||||
"""Called when a new section is started"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def comment(self, comment):
|
||||
"""Called when a comment is parsed"""
|
||||
pass
|
||||
|
||||
def error_invalid_assignment(self, line):
|
||||
raise self.parse_exc("No ':' or '=' found in assignment",
|
||||
self.lineno, line)
|
||||
|
||||
def error_empty_key(self, line):
|
||||
raise self.parse_exc('Key cannot be empty', self.lineno, line)
|
||||
|
||||
def error_unexpected_continuation(self, line):
|
||||
raise self.parse_exc('Unexpected continuation line',
|
||||
self.lineno, line)
|
||||
|
||||
def error_no_section_end_bracket(self, line):
|
||||
raise self.parse_exc('Invalid section (must end with ])',
|
||||
self.lineno, line)
|
||||
|
||||
def error_no_section_name(self, line):
|
||||
raise self.parse_exc('Empty section name', self.lineno, line)
|
@ -1,180 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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.
|
||||
|
||||
'''
|
||||
JSON related utilities.
|
||||
|
||||
This module provides a few things:
|
||||
|
||||
1) A handy function for getting an object down to something that can be
|
||||
JSON serialized. See to_primitive().
|
||||
|
||||
2) Wrappers around loads() and dumps(). The dumps() wrapper will
|
||||
automatically use to_primitive() for you if needed.
|
||||
|
||||
3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
|
||||
is available.
|
||||
'''
|
||||
|
||||
|
||||
import datetime
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import json
|
||||
try:
|
||||
import xmlrpclib
|
||||
except ImportError:
|
||||
# NOTE(jd): xmlrpclib is not shipped with Python 3
|
||||
xmlrpclib = None
|
||||
|
||||
import six
|
||||
|
||||
from billingstack.openstack.common import gettextutils
|
||||
from billingstack.openstack.common import importutils
|
||||
from billingstack.openstack.common import timeutils
|
||||
|
||||
netaddr = importutils.try_import("netaddr")
|
||||
|
||||
_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod,
|
||||
inspect.isfunction, inspect.isgeneratorfunction,
|
||||
inspect.isgenerator, inspect.istraceback, inspect.isframe,
|
||||
inspect.iscode, inspect.isbuiltin, inspect.isroutine,
|
||||
inspect.isabstract]
|
||||
|
||||
_simple_types = (six.string_types + six.integer_types
|
||||
+ (type(None), bool, float))
|
||||
|
||||
|
||||
def to_primitive(value, convert_instances=False, convert_datetime=True,
|
||||
level=0, max_depth=3):
|
||||
"""Convert a complex object into primitives.
|
||||
|
||||
Handy for JSON serialization. We can optionally handle instances,
|
||||
but since this is a recursive function, we could have cyclical
|
||||
data structures.
|
||||
|
||||
To handle cyclical data structures we could track the actual objects
|
||||
visited in a set, but not all objects are hashable. Instead we just
|
||||
track the depth of the object inspections and don't go too deep.
|
||||
|
||||
Therefore, convert_instances=True is lossy ... be aware.
|
||||
|
||||
"""
|
||||
# handle obvious types first - order of basic types determined by running
|
||||
# full tests on nova project, resulting in the following counts:
|
||||
# 572754 <type 'NoneType'>
|
||||
# 460353 <type 'int'>
|
||||
# 379632 <type 'unicode'>
|
||||
# 274610 <type 'str'>
|
||||
# 199918 <type 'dict'>
|
||||
# 114200 <type 'datetime.datetime'>
|
||||
# 51817 <type 'bool'>
|
||||
# 26164 <type 'list'>
|
||||
# 6491 <type 'float'>
|
||||
# 283 <type 'tuple'>
|
||||
# 19 <type 'long'>
|
||||
if isinstance(value, _simple_types):
|
||||
return value
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
if convert_datetime:
|
||||
return timeutils.strtime(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
# value of itertools.count doesn't get caught by nasty_type_tests
|
||||
# and results in infinite loop when list(value) is called.
|
||||
if type(value) == itertools.count:
|
||||
return six.text_type(value)
|
||||
|
||||
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
||||
# tests that raise an exception in a mocked method that
|
||||
# has a @wrap_exception with a notifier will fail. If
|
||||
# we up the dependency to 0.5.4 (when it is released) we
|
||||
# can remove this workaround.
|
||||
if getattr(value, '__module__', None) == 'mox':
|
||||
return 'mock'
|
||||
|
||||
if level > max_depth:
|
||||
return '?'
|
||||
|
||||
# The try block may not be necessary after the class check above,
|
||||
# but just in case ...
|
||||
try:
|
||||
recursive = functools.partial(to_primitive,
|
||||
convert_instances=convert_instances,
|
||||
convert_datetime=convert_datetime,
|
||||
level=level,
|
||||
max_depth=max_depth)
|
||||
if isinstance(value, dict):
|
||||
return dict((k, recursive(v)) for k, v in value.iteritems())
|
||||
elif isinstance(value, (list, tuple)):
|
||||
return [recursive(lv) for lv in value]
|
||||
|
||||
# It's not clear why xmlrpclib created their own DateTime type, but
|
||||
# for our purposes, make it a datetime type which is explicitly
|
||||
# handled
|
||||
if xmlrpclib and isinstance(value, xmlrpclib.DateTime):
|
||||
value = datetime.datetime(*tuple(value.timetuple())[:6])
|
||||
|
||||
if convert_datetime and isinstance(value, datetime.datetime):
|
||||
return timeutils.strtime(value)
|
||||
elif isinstance(value, gettextutils.Message):
|
||||
return value.data
|
||||
elif hasattr(value, 'iteritems'):
|
||||
return recursive(dict(value.iteritems()), level=level + 1)
|
||||
elif hasattr(value, '__iter__'):
|
||||
return recursive(list(value))
|
||||
elif convert_instances and hasattr(value, '__dict__'):
|
||||
# Likely an instance of something. Watch for cycles.
|
||||
# Ignore class member vars.
|
||||
return recursive(value.__dict__, level=level + 1)
|
||||
elif netaddr and isinstance(value, netaddr.IPAddress):
|
||||
return six.text_type(value)
|
||||
else:
|
||||
if any(test(value) for test in _nasty_type_tests):
|
||||
return six.text_type(value)
|
||||
return value
|
||||
except TypeError:
|
||||
# Class objects are tricky since they may define something like
|
||||
# __iter__ defined but it isn't callable as list().
|
||||
return six.text_type(value)
|
||||
|
||||
|
||||
def dumps(value, default=to_primitive, **kwargs):
|
||||
return json.dumps(value, default=default, **kwargs)
|
||||
|
||||
|
||||
def loads(s):
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def load(s):
|
||||
return json.load(s)
|
||||
|
||||
|
||||
try:
|
||||
import anyjson
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
anyjson._modules.append((__name__, 'dumps', TypeError,
|
||||
'loads', ValueError, 'load'))
|
||||
anyjson.force_implementation(__name__)
|
@ -1,47 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Local storage of variables using weak references"""
|
||||
|
||||
import threading
|
||||
import weakref
|
||||
|
||||
|
||||
class WeakLocal(threading.local):
|
||||
def __getattribute__(self, attr):
|
||||
rval = super(WeakLocal, self).__getattribute__(attr)
|
||||
if rval:
|
||||
# NOTE(mikal): this bit is confusing. What is stored is a weak
|
||||
# reference, not the value itself. We therefore need to lookup
|
||||
# the weak reference and return the inner value here.
|
||||
rval = rval()
|
||||
return rval
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
value = weakref.ref(value)
|
||||
return super(WeakLocal, self).__setattr__(attr, value)
|
||||
|
||||
|
||||
# NOTE(mikal): the name "store" should be deprecated in the future
|
||||
store = WeakLocal()
|
||||
|
||||
# A "weak" store uses weak references and allows an object to fall out of scope
|
||||
# when it falls out of scope in the code that uses the thread local storage. A
|
||||
# "strong" store will hold a reference to the object so that it never falls out
|
||||
# of scope.
|
||||
weak_store = WeakLocal()
|
||||
strong_store = threading.local()
|
@ -1,305 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 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 contextlib
|
||||
import errno
|
||||
import functools
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import fileutils
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import local
|
||||
from billingstack.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
util_opts = [
|
||||
cfg.BoolOpt('disable_process_locking', default=False,
|
||||
help='Whether to disable inter-process locks'),
|
||||
cfg.StrOpt('lock_path',
|
||||
default=os.environ.get("BILLINGSTACK_LOCK_PATH"),
|
||||
help=('Directory to use for lock files.'))
|
||||
]
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(util_opts)
|
||||
|
||||
|
||||
def set_defaults(lock_path):
|
||||
cfg.set_defaults(util_opts, lock_path=lock_path)
|
||||
|
||||
|
||||
class _InterProcessLock(object):
|
||||
"""Lock implementation which allows multiple locks, working around
|
||||
issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
|
||||
not require any cleanup. Since the lock is always held on a file
|
||||
descriptor rather than outside of the process, the lock gets dropped
|
||||
automatically if the process crashes, even if __exit__ is not executed.
|
||||
|
||||
There are no guarantees regarding usage by multiple green threads in a
|
||||
single process here. This lock works only between processes. Exclusive
|
||||
access between local threads should be achieved using the semaphores
|
||||
in the @synchronized decorator.
|
||||
|
||||
Note these locks are released when the descriptor is closed, so it's not
|
||||
safe to close the file descriptor while another green thread holds the
|
||||
lock. Just opening and closing the lock file can break synchronisation,
|
||||
so lock files must be accessed only using this abstraction.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.lockfile = None
|
||||
self.fname = name
|
||||
|
||||
def __enter__(self):
|
||||
self.lockfile = open(self.fname, 'w')
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Using non-blocking locks since green threads are not
|
||||
# patched to deal with blocking locking calls.
|
||||
# Also upon reading the MSDN docs for locking(), it seems
|
||||
# to have a laughable 10 attempts "blocking" mechanism.
|
||||
self.trylock()
|
||||
return self
|
||||
except IOError as e:
|
||||
if e.errno in (errno.EACCES, errno.EAGAIN):
|
||||
# external locks synchronise things like iptables
|
||||
# updates - give it some time to prevent busy spinning
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
raise
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
try:
|
||||
self.unlock()
|
||||
self.lockfile.close()
|
||||
except IOError:
|
||||
LOG.exception(_("Could not release the acquired lock `%s`"),
|
||||
self.fname)
|
||||
|
||||
def trylock(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def unlock(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _WindowsLock(_InterProcessLock):
|
||||
def trylock(self):
|
||||
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
|
||||
def unlock(self):
|
||||
msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
|
||||
|
||||
class _PosixLock(_InterProcessLock):
|
||||
def trylock(self):
|
||||
fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
|
||||
def unlock(self):
|
||||
fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
import msvcrt
|
||||
InterProcessLock = _WindowsLock
|
||||
else:
|
||||
import fcntl
|
||||
InterProcessLock = _PosixLock
|
||||
|
||||
_semaphores = weakref.WeakValueDictionary()
|
||||
_semaphores_lock = threading.Lock()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def lock(name, lock_file_prefix=None, external=False, lock_path=None):
|
||||
"""Context based lock
|
||||
|
||||
This function yields a `threading.Semaphore` instance (if we don't use
|
||||
eventlet.monkey_patch(), else `semaphore.Semaphore`) unless external is
|
||||
True, in which case, it'll yield an InterProcessLock instance.
|
||||
|
||||
:param lock_file_prefix: The lock_file_prefix argument is used to provide
|
||||
lock files on disk with a meaningful prefix.
|
||||
|
||||
:param external: The external keyword argument denotes whether this lock
|
||||
should work across multiple processes. This means that if two different
|
||||
workers both run a a method decorated with @synchronized('mylock',
|
||||
external=True), only one of them will execute at a time.
|
||||
|
||||
:param lock_path: The lock_path keyword argument is used to specify a
|
||||
special location for external lock files to live. If nothing is set, then
|
||||
CONF.lock_path is used as a default.
|
||||
"""
|
||||
with _semaphores_lock:
|
||||
try:
|
||||
sem = _semaphores[name]
|
||||
except KeyError:
|
||||
sem = threading.Semaphore()
|
||||
_semaphores[name] = sem
|
||||
|
||||
with sem:
|
||||
LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
|
||||
|
||||
# NOTE(mikal): I know this looks odd
|
||||
if not hasattr(local.strong_store, 'locks_held'):
|
||||
local.strong_store.locks_held = []
|
||||
local.strong_store.locks_held.append(name)
|
||||
|
||||
try:
|
||||
if external and not CONF.disable_process_locking:
|
||||
LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
|
||||
{'lock': name})
|
||||
|
||||
# We need a copy of lock_path because it is non-local
|
||||
local_lock_path = lock_path or CONF.lock_path
|
||||
if not local_lock_path:
|
||||
raise cfg.RequiredOptError('lock_path')
|
||||
|
||||
if not os.path.exists(local_lock_path):
|
||||
fileutils.ensure_tree(local_lock_path)
|
||||
LOG.info(_('Created lock path: %s'), local_lock_path)
|
||||
|
||||
def add_prefix(name, prefix):
|
||||
if not prefix:
|
||||
return name
|
||||
sep = '' if prefix.endswith('-') else '-'
|
||||
return '%s%s%s' % (prefix, sep, name)
|
||||
|
||||
# NOTE(mikal): the lock name cannot contain directory
|
||||
# separators
|
||||
lock_file_name = add_prefix(name.replace(os.sep, '_'),
|
||||
lock_file_prefix)
|
||||
|
||||
lock_file_path = os.path.join(local_lock_path, lock_file_name)
|
||||
|
||||
try:
|
||||
lock = InterProcessLock(lock_file_path)
|
||||
with lock as lock:
|
||||
LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
|
||||
{'lock': name, 'path': lock_file_path})
|
||||
yield lock
|
||||
finally:
|
||||
LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
|
||||
{'lock': name, 'path': lock_file_path})
|
||||
else:
|
||||
yield sem
|
||||
|
||||
finally:
|
||||
local.strong_store.locks_held.remove(name)
|
||||
|
||||
|
||||
def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
|
||||
"""Synchronization decorator.
|
||||
|
||||
Decorating a method like so::
|
||||
|
||||
@synchronized('mylock')
|
||||
def foo(self, *args):
|
||||
...
|
||||
|
||||
ensures that only one thread will execute the foo method at a time.
|
||||
|
||||
Different methods can share the same lock::
|
||||
|
||||
@synchronized('mylock')
|
||||
def foo(self, *args):
|
||||
...
|
||||
|
||||
@synchronized('mylock')
|
||||
def bar(self, *args):
|
||||
...
|
||||
|
||||
This way only one of either foo or bar can be executing at a time.
|
||||
"""
|
||||
|
||||
def wrap(f):
|
||||
@functools.wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
try:
|
||||
with lock(name, lock_file_prefix, external, lock_path):
|
||||
LOG.debug(_('Got semaphore / lock "%(function)s"'),
|
||||
{'function': f.__name__})
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
LOG.debug(_('Semaphore / lock released "%(function)s"'),
|
||||
{'function': f.__name__})
|
||||
return inner
|
||||
return wrap
|
||||
|
||||
|
||||
def synchronized_with_prefix(lock_file_prefix):
|
||||
"""Partial object generator for the synchronization decorator.
|
||||
|
||||
Redefine @synchronized in each project like so::
|
||||
|
||||
(in nova/utils.py)
|
||||
from nova.openstack.common import lockutils
|
||||
|
||||
synchronized = lockutils.synchronized_with_prefix('nova-')
|
||||
|
||||
|
||||
(in nova/foo.py)
|
||||
from nova import utils
|
||||
|
||||
@utils.synchronized('mylock')
|
||||
def bar(self, *args):
|
||||
...
|
||||
|
||||
The lock_file_prefix argument is used to provide lock files on disk with a
|
||||
meaningful prefix.
|
||||
"""
|
||||
|
||||
return functools.partial(synchronized, lock_file_prefix=lock_file_prefix)
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""Create a dir for locks and pass it to command from arguments
|
||||
|
||||
If you run this:
|
||||
python -m openstack.common.lockutils python setup.py testr <etc>
|
||||
|
||||
a temporary directory will be created for all your locks and passed to all
|
||||
your tests in an environment variable. The temporary dir will be deleted
|
||||
afterwards and the return value will be preserved.
|
||||
"""
|
||||
|
||||
lock_dir = tempfile.mkdtemp()
|
||||
os.environ["BILLINGSTACK_LOCK_PATH"] = lock_dir
|
||||
try:
|
||||
ret_val = subprocess.call(argv[1:])
|
||||
finally:
|
||||
shutil.rmtree(lock_dir, ignore_errors=True)
|
||||
return ret_val
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
@ -1,626 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""Openstack logging handler.
|
||||
|
||||
This module adds to logging functionality by adding the option to specify
|
||||
a context object when calling the various log methods. If the context object
|
||||
is not specified, default formatting is used. Additionally, an instance uuid
|
||||
may be passed as part of the log message, which is intended to make it easier
|
||||
for admins to find messages related to a specific instance.
|
||||
|
||||
It also allows setting of formatting information through conf.
|
||||
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import importutils
|
||||
from billingstack.openstack.common import jsonutils
|
||||
from billingstack.openstack.common import local
|
||||
|
||||
|
||||
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password']
|
||||
|
||||
# NOTE(ldbragst): Let's build a list of regex objects using the list of
|
||||
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
|
||||
# to the list of _SANITIZE_KEYS and we can generate regular expressions
|
||||
# for XML and JSON automatically.
|
||||
_SANITIZE_PATTERNS = []
|
||||
_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
|
||||
r'(<%(key)s>).*?(</%(key)s>)',
|
||||
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
|
||||
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])']
|
||||
|
||||
for key in _SANITIZE_KEYS:
|
||||
for pattern in _FORMAT_PATTERNS:
|
||||
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
|
||||
_SANITIZE_PATTERNS.append(reg_ex)
|
||||
|
||||
|
||||
common_cli_opts = [
|
||||
cfg.BoolOpt('debug',
|
||||
short='d',
|
||||
default=False,
|
||||
help='Print debugging output (set logging level to '
|
||||
'DEBUG instead of default WARNING level).'),
|
||||
cfg.BoolOpt('verbose',
|
||||
short='v',
|
||||
default=False,
|
||||
help='Print more verbose output (set logging level to '
|
||||
'INFO instead of default WARNING level).'),
|
||||
]
|
||||
|
||||
logging_cli_opts = [
|
||||
cfg.StrOpt('log-config-append',
|
||||
metavar='PATH',
|
||||
deprecated_name='log-config',
|
||||
help='The name of logging configuration file. It does not '
|
||||
'disable existing loggers, but just appends specified '
|
||||
'logging configuration to any other existing logging '
|
||||
'options. Please see the Python logging module '
|
||||
'documentation for details on logging configuration '
|
||||
'files.'),
|
||||
cfg.StrOpt('log-format',
|
||||
default=None,
|
||||
metavar='FORMAT',
|
||||
help='DEPRECATED. '
|
||||
'A logging.Formatter log message format string which may '
|
||||
'use any of the available logging.LogRecord attributes. '
|
||||
'This option is deprecated. Please use '
|
||||
'logging_context_format_string and '
|
||||
'logging_default_format_string instead.'),
|
||||
cfg.StrOpt('log-date-format',
|
||||
default=_DEFAULT_LOG_DATE_FORMAT,
|
||||
metavar='DATE_FORMAT',
|
||||
help='Format string for %%(asctime)s in log records. '
|
||||
'Default: %(default)s'),
|
||||
cfg.StrOpt('log-file',
|
||||
metavar='PATH',
|
||||
deprecated_name='logfile',
|
||||
help='(Optional) Name of log file to output to. '
|
||||
'If no default is set, logging will go to stdout.'),
|
||||
cfg.StrOpt('log-dir',
|
||||
deprecated_name='logdir',
|
||||
help='(Optional) The base directory used for relative '
|
||||
'--log-file paths'),
|
||||
cfg.BoolOpt('use-syslog',
|
||||
default=False,
|
||||
help='Use syslog for logging.'),
|
||||
cfg.StrOpt('syslog-log-facility',
|
||||
default='LOG_USER',
|
||||
help='syslog facility to receive log lines')
|
||||
]
|
||||
|
||||
generic_log_opts = [
|
||||
cfg.BoolOpt('use_stderr',
|
||||
default=True,
|
||||
help='Log output to standard error')
|
||||
]
|
||||
|
||||
log_opts = [
|
||||
cfg.StrOpt('logging_context_format_string',
|
||||
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||
'%(name)s [%(request_id)s %(user)s %(tenant)s] '
|
||||
'%(instance)s%(message)s',
|
||||
help='format string to use for log messages with context'),
|
||||
cfg.StrOpt('logging_default_format_string',
|
||||
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||
'%(name)s [-] %(instance)s%(message)s',
|
||||
help='format string to use for log messages without context'),
|
||||
cfg.StrOpt('logging_debug_format_suffix',
|
||||
default='%(funcName)s %(pathname)s:%(lineno)d',
|
||||
help='data to append to log format when level is DEBUG'),
|
||||
cfg.StrOpt('logging_exception_prefix',
|
||||
default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
|
||||
'%(instance)s',
|
||||
help='prefix each line of exception output with this format'),
|
||||
cfg.ListOpt('default_log_levels',
|
||||
default=[
|
||||
'amqp=WARN',
|
||||
'amqplib=WARN',
|
||||
'boto=WARN',
|
||||
'keystone=INFO',
|
||||
'qpid=WARN',
|
||||
'sqlalchemy=WARN',
|
||||
'suds=INFO',
|
||||
'iso8601=WARN',
|
||||
],
|
||||
help='list of logger=LEVEL pairs'),
|
||||
cfg.BoolOpt('publish_errors',
|
||||
default=False,
|
||||
help='publish error events'),
|
||||
cfg.BoolOpt('fatal_deprecations',
|
||||
default=False,
|
||||
help='make deprecations fatal'),
|
||||
|
||||
# NOTE(mikal): there are two options here because sometimes we are handed
|
||||
# a full instance (and could include more information), and other times we
|
||||
# are just handed a UUID for the instance.
|
||||
cfg.StrOpt('instance_format',
|
||||
default='[instance: %(uuid)s] ',
|
||||
help='If an instance is passed with the log message, format '
|
||||
'it like this'),
|
||||
cfg.StrOpt('instance_uuid_format',
|
||||
default='[instance: %(uuid)s] ',
|
||||
help='If an instance UUID is passed with the log message, '
|
||||
'format it like this'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_cli_opts(common_cli_opts)
|
||||
CONF.register_cli_opts(logging_cli_opts)
|
||||
CONF.register_opts(generic_log_opts)
|
||||
CONF.register_opts(log_opts)
|
||||
|
||||
# our new audit level
|
||||
# NOTE(jkoelker) Since we synthesized an audit level, make the logging
|
||||
# module aware of it so it acts like other levels.
|
||||
logging.AUDIT = logging.INFO + 1
|
||||
logging.addLevelName(logging.AUDIT, 'AUDIT')
|
||||
|
||||
|
||||
try:
|
||||
NullHandler = logging.NullHandler
|
||||
except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7
|
||||
class NullHandler(logging.Handler):
|
||||
def handle(self, record):
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
def createLock(self):
|
||||
self.lock = None
|
||||
|
||||
|
||||
def _dictify_context(context):
|
||||
if context is None:
|
||||
return None
|
||||
if not isinstance(context, dict) and getattr(context, 'to_dict', None):
|
||||
context = context.to_dict()
|
||||
return context
|
||||
|
||||
|
||||
def _get_binary_name():
|
||||
return os.path.basename(inspect.stack()[-1][1])
|
||||
|
||||
|
||||
def _get_log_file_path(binary=None):
|
||||
logfile = CONF.log_file
|
||||
logdir = CONF.log_dir
|
||||
|
||||
if logfile and not logdir:
|
||||
return logfile
|
||||
|
||||
if logfile and logdir:
|
||||
return os.path.join(logdir, logfile)
|
||||
|
||||
if logdir:
|
||||
binary = binary or _get_binary_name()
|
||||
return '%s.log' % (os.path.join(logdir, binary),)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def mask_password(message, secret="***"):
|
||||
"""Replace password with 'secret' in message.
|
||||
|
||||
:param message: The string which includes security information.
|
||||
:param secret: value with which to replace passwords, defaults to "***".
|
||||
:returns: The unicode value of message with the password fields masked.
|
||||
|
||||
For example:
|
||||
>>> mask_password("'adminPass' : 'aaaaa'")
|
||||
"'adminPass' : '***'"
|
||||
>>> mask_password("'admin_pass' : 'aaaaa'")
|
||||
"'admin_pass' : '***'"
|
||||
>>> mask_password('"password" : "aaaaa"')
|
||||
'"password" : "***"'
|
||||
>>> mask_password("'original_password' : 'aaaaa'")
|
||||
"'original_password' : '***'"
|
||||
>>> mask_password("u'original_password' : u'aaaaa'")
|
||||
"u'original_password' : u'***'"
|
||||
"""
|
||||
message = six.text_type(message)
|
||||
|
||||
# NOTE(ldbragst): Check to see if anything in message contains any key
|
||||
# specified in _SANITIZE_KEYS, if not then just return the message since
|
||||
# we don't have to mask any passwords.
|
||||
if not any(key in message for key in _SANITIZE_KEYS):
|
||||
return message
|
||||
|
||||
secret = r'\g<1>' + secret + r'\g<2>'
|
||||
for pattern in _SANITIZE_PATTERNS:
|
||||
message = re.sub(pattern, secret, message)
|
||||
return message
|
||||
|
||||
|
||||
class BaseLoggerAdapter(logging.LoggerAdapter):
|
||||
|
||||
def audit(self, msg, *args, **kwargs):
|
||||
self.log(logging.AUDIT, msg, *args, **kwargs)
|
||||
|
||||
|
||||
class LazyAdapter(BaseLoggerAdapter):
|
||||
def __init__(self, name='unknown', version='unknown'):
|
||||
self._logger = None
|
||||
self.extra = {}
|
||||
self.name = name
|
||||
self.version = version
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if not self._logger:
|
||||
self._logger = getLogger(self.name, self.version)
|
||||
return self._logger
|
||||
|
||||
|
||||
class ContextAdapter(BaseLoggerAdapter):
|
||||
warn = logging.LoggerAdapter.warning
|
||||
|
||||
def __init__(self, logger, project_name, version_string):
|
||||
self.logger = logger
|
||||
self.project = project_name
|
||||
self.version = version_string
|
||||
|
||||
@property
|
||||
def handlers(self):
|
||||
return self.logger.handlers
|
||||
|
||||
def deprecated(self, msg, *args, **kwargs):
|
||||
stdmsg = _("Deprecated: %s") % msg
|
||||
if CONF.fatal_deprecations:
|
||||
self.critical(stdmsg, *args, **kwargs)
|
||||
raise DeprecatedConfig(msg=stdmsg)
|
||||
else:
|
||||
self.warn(stdmsg, *args, **kwargs)
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
# NOTE(mrodden): catch any Message/other object and
|
||||
# coerce to unicode before they can get
|
||||
# to the python logging and possibly
|
||||
# cause string encoding trouble
|
||||
if not isinstance(msg, six.string_types):
|
||||
msg = six.text_type(msg)
|
||||
|
||||
if 'extra' not in kwargs:
|
||||
kwargs['extra'] = {}
|
||||
extra = kwargs['extra']
|
||||
|
||||
context = kwargs.pop('context', None)
|
||||
if not context:
|
||||
context = getattr(local.store, 'context', None)
|
||||
if context:
|
||||
extra.update(_dictify_context(context))
|
||||
|
||||
instance = kwargs.pop('instance', None)
|
||||
instance_uuid = (extra.get('instance_uuid', None) or
|
||||
kwargs.pop('instance_uuid', None))
|
||||
instance_extra = ''
|
||||
if instance:
|
||||
instance_extra = CONF.instance_format % instance
|
||||
elif instance_uuid:
|
||||
instance_extra = (CONF.instance_uuid_format
|
||||
% {'uuid': instance_uuid})
|
||||
extra.update({'instance': instance_extra})
|
||||
|
||||
extra.update({"project": self.project})
|
||||
extra.update({"version": self.version})
|
||||
extra['extra'] = extra.copy()
|
||||
return msg, kwargs
|
||||
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def __init__(self, fmt=None, datefmt=None):
|
||||
# NOTE(jkoelker) we ignore the fmt argument, but its still there
|
||||
# since logging.config.fileConfig passes it.
|
||||
self.datefmt = datefmt
|
||||
|
||||
def formatException(self, ei, strip_newlines=True):
|
||||
lines = traceback.format_exception(*ei)
|
||||
if strip_newlines:
|
||||
lines = [itertools.ifilter(
|
||||
lambda x: x,
|
||||
line.rstrip().splitlines()) for line in lines]
|
||||
lines = list(itertools.chain(*lines))
|
||||
return lines
|
||||
|
||||
def format(self, record):
|
||||
message = {'message': record.getMessage(),
|
||||
'asctime': self.formatTime(record, self.datefmt),
|
||||
'name': record.name,
|
||||
'msg': record.msg,
|
||||
'args': record.args,
|
||||
'levelname': record.levelname,
|
||||
'levelno': record.levelno,
|
||||
'pathname': record.pathname,
|
||||
'filename': record.filename,
|
||||
'module': record.module,
|
||||
'lineno': record.lineno,
|
||||
'funcname': record.funcName,
|
||||
'created': record.created,
|
||||
'msecs': record.msecs,
|
||||
'relative_created': record.relativeCreated,
|
||||
'thread': record.thread,
|
||||
'thread_name': record.threadName,
|
||||
'process_name': record.processName,
|
||||
'process': record.process,
|
||||
'traceback': None}
|
||||
|
||||
if hasattr(record, 'extra'):
|
||||
message['extra'] = record.extra
|
||||
|
||||
if record.exc_info:
|
||||
message['traceback'] = self.formatException(record.exc_info)
|
||||
|
||||
return jsonutils.dumps(message)
|
||||
|
||||
|
||||
def _create_logging_excepthook(product_name):
|
||||
def logging_excepthook(type, value, tb):
|
||||
extra = {}
|
||||
if CONF.verbose:
|
||||
extra['exc_info'] = (type, value, tb)
|
||||
getLogger(product_name).critical(str(value), **extra)
|
||||
return logging_excepthook
|
||||
|
||||
|
||||
class LogConfigError(Exception):
|
||||
|
||||
message = _('Error loading logging config %(log_config)s: %(err_msg)s')
|
||||
|
||||
def __init__(self, log_config, err_msg):
|
||||
self.log_config = log_config
|
||||
self.err_msg = err_msg
|
||||
|
||||
def __str__(self):
|
||||
return self.message % dict(log_config=self.log_config,
|
||||
err_msg=self.err_msg)
|
||||
|
||||
|
||||
def _load_log_config(log_config_append):
|
||||
try:
|
||||
logging.config.fileConfig(log_config_append,
|
||||
disable_existing_loggers=False)
|
||||
except moves.configparser.Error as exc:
|
||||
raise LogConfigError(log_config_append, str(exc))
|
||||
|
||||
|
||||
def setup(product_name):
|
||||
"""Setup logging."""
|
||||
if CONF.log_config_append:
|
||||
_load_log_config(CONF.log_config_append)
|
||||
else:
|
||||
_setup_logging_from_conf()
|
||||
sys.excepthook = _create_logging_excepthook(product_name)
|
||||
|
||||
|
||||
def set_defaults(logging_context_format_string):
|
||||
cfg.set_defaults(log_opts,
|
||||
logging_context_format_string=
|
||||
logging_context_format_string)
|
||||
|
||||
|
||||
def _find_facility_from_conf():
|
||||
facility_names = logging.handlers.SysLogHandler.facility_names
|
||||
facility = getattr(logging.handlers.SysLogHandler,
|
||||
CONF.syslog_log_facility,
|
||||
None)
|
||||
|
||||
if facility is None and CONF.syslog_log_facility in facility_names:
|
||||
facility = facility_names.get(CONF.syslog_log_facility)
|
||||
|
||||
if facility is None:
|
||||
valid_facilities = facility_names.keys()
|
||||
consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
|
||||
'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
|
||||
'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
|
||||
'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
|
||||
'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
|
||||
valid_facilities.extend(consts)
|
||||
raise TypeError(_('syslog facility must be one of: %s') %
|
||||
', '.join("'%s'" % fac
|
||||
for fac in valid_facilities))
|
||||
|
||||
return facility
|
||||
|
||||
|
||||
def _setup_logging_from_conf():
|
||||
log_root = getLogger(None).logger
|
||||
for handler in log_root.handlers:
|
||||
log_root.removeHandler(handler)
|
||||
|
||||
if CONF.use_syslog:
|
||||
facility = _find_facility_from_conf()
|
||||
syslog = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=facility)
|
||||
log_root.addHandler(syslog)
|
||||
|
||||
logpath = _get_log_file_path()
|
||||
if logpath:
|
||||
filelog = logging.handlers.WatchedFileHandler(logpath)
|
||||
log_root.addHandler(filelog)
|
||||
|
||||
if CONF.use_stderr:
|
||||
streamlog = ColorHandler()
|
||||
log_root.addHandler(streamlog)
|
||||
|
||||
elif not CONF.log_file:
|
||||
# pass sys.stdout as a positional argument
|
||||
# python2.6 calls the argument strm, in 2.7 it's stream
|
||||
streamlog = logging.StreamHandler(sys.stdout)
|
||||
log_root.addHandler(streamlog)
|
||||
|
||||
if CONF.publish_errors:
|
||||
handler = importutils.import_object(
|
||||
"billingstack.openstack.common.log_handler.PublishErrorsHandler",
|
||||
logging.ERROR)
|
||||
log_root.addHandler(handler)
|
||||
|
||||
datefmt = CONF.log_date_format
|
||||
for handler in log_root.handlers:
|
||||
# NOTE(alaski): CONF.log_format overrides everything currently. This
|
||||
# should be deprecated in favor of context aware formatting.
|
||||
if CONF.log_format:
|
||||
handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
|
||||
datefmt=datefmt))
|
||||
log_root.info('Deprecated: log_format is now deprecated and will '
|
||||
'be removed in the next release')
|
||||
else:
|
||||
handler.setFormatter(ContextFormatter(datefmt=datefmt))
|
||||
|
||||
if CONF.debug:
|
||||
log_root.setLevel(logging.DEBUG)
|
||||
elif CONF.verbose:
|
||||
log_root.setLevel(logging.INFO)
|
||||
else:
|
||||
log_root.setLevel(logging.WARNING)
|
||||
|
||||
for pair in CONF.default_log_levels:
|
||||
mod, _sep, level_name = pair.partition('=')
|
||||
level = logging.getLevelName(level_name)
|
||||
logger = logging.getLogger(mod)
|
||||
logger.setLevel(level)
|
||||
|
||||
_loggers = {}
|
||||
|
||||
|
||||
def getLogger(name='unknown', version='unknown'):
|
||||
if name not in _loggers:
|
||||
_loggers[name] = ContextAdapter(logging.getLogger(name),
|
||||
name,
|
||||
version)
|
||||
return _loggers[name]
|
||||
|
||||
|
||||
def getLazyLogger(name='unknown', version='unknown'):
|
||||
"""Returns lazy logger.
|
||||
|
||||
Creates a pass-through logger that does not create the real logger
|
||||
until it is really needed and delegates all calls to the real logger
|
||||
once it is created.
|
||||
"""
|
||||
return LazyAdapter(name, version)
|
||||
|
||||
|
||||
class WritableLogger(object):
|
||||
"""A thin wrapper that responds to `write` and logs."""
|
||||
|
||||
def __init__(self, logger, level=logging.INFO):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
|
||||
def write(self, msg):
|
||||
self.logger.log(self.level, msg)
|
||||
|
||||
|
||||
class ContextFormatter(logging.Formatter):
|
||||
"""A context.RequestContext aware formatter configured through flags.
|
||||
|
||||
The flags used to set format strings are: logging_context_format_string
|
||||
and logging_default_format_string. You can also specify
|
||||
logging_debug_format_suffix to append extra formatting if the log level is
|
||||
debug.
|
||||
|
||||
For information about what variables are available for the formatter see:
|
||||
http://docs.python.org/library/logging.html#formatter
|
||||
|
||||
"""
|
||||
|
||||
def format(self, record):
|
||||
"""Uses contextstring if request_id is set, otherwise default."""
|
||||
# NOTE(sdague): default the fancier formating params
|
||||
# to an empty string so we don't throw an exception if
|
||||
# they get used
|
||||
for key in ('instance', 'color'):
|
||||
if key not in record.__dict__:
|
||||
record.__dict__[key] = ''
|
||||
|
||||
if record.__dict__.get('request_id', None):
|
||||
self._fmt = CONF.logging_context_format_string
|
||||
else:
|
||||
self._fmt = CONF.logging_default_format_string
|
||||
|
||||
if (record.levelno == logging.DEBUG and
|
||||
CONF.logging_debug_format_suffix):
|
||||
self._fmt += " " + CONF.logging_debug_format_suffix
|
||||
|
||||
# Cache this on the record, Logger will respect our formated copy
|
||||
if record.exc_info:
|
||||
record.exc_text = self.formatException(record.exc_info, record)
|
||||
return logging.Formatter.format(self, record)
|
||||
|
||||
def formatException(self, exc_info, record=None):
|
||||
"""Format exception output with CONF.logging_exception_prefix."""
|
||||
if not record:
|
||||
return logging.Formatter.formatException(self, exc_info)
|
||||
|
||||
stringbuffer = moves.StringIO()
|
||||
traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
|
||||
None, stringbuffer)
|
||||
lines = stringbuffer.getvalue().split('\n')
|
||||
stringbuffer.close()
|
||||
|
||||
if CONF.logging_exception_prefix.find('%(asctime)') != -1:
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
|
||||
formatted_lines = []
|
||||
for line in lines:
|
||||
pl = CONF.logging_exception_prefix % record.__dict__
|
||||
fl = '%s%s' % (pl, line)
|
||||
formatted_lines.append(fl)
|
||||
return '\n'.join(formatted_lines)
|
||||
|
||||
|
||||
class ColorHandler(logging.StreamHandler):
|
||||
LEVEL_COLORS = {
|
||||
logging.DEBUG: '\033[00;32m', # GREEN
|
||||
logging.INFO: '\033[00;36m', # CYAN
|
||||
logging.AUDIT: '\033[01;36m', # BOLD CYAN
|
||||
logging.WARN: '\033[01;33m', # BOLD YELLOW
|
||||
logging.ERROR: '\033[01;31m', # BOLD RED
|
||||
logging.CRITICAL: '\033[01;31m', # BOLD RED
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
record.color = self.LEVEL_COLORS[record.levelno]
|
||||
return logging.StreamHandler.format(self, record)
|
||||
|
||||
|
||||
class DeprecatedConfig(Exception):
|
||||
message = _("Fatal call to deprecated config: %(msg)s")
|
||||
|
||||
def __init__(self, msg):
|
||||
super(Exception, self).__init__(self.message % dict(msg=msg))
|
@ -1,147 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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 sys
|
||||
|
||||
from eventlet import event
|
||||
from eventlet import greenthread
|
||||
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common import timeutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoopingCallDone(Exception):
|
||||
"""Exception to break out and stop a LoopingCall.
|
||||
|
||||
The poll-function passed to LoopingCall can raise this exception to
|
||||
break out of the loop normally. This is somewhat analogous to
|
||||
StopIteration.
|
||||
|
||||
An optional return-value can be included as the argument to the exception;
|
||||
this return-value will be returned by LoopingCall.wait()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, retvalue=True):
|
||||
""":param retvalue: Value that LoopingCall.wait() should return."""
|
||||
self.retvalue = retvalue
|
||||
|
||||
|
||||
class LoopingCallBase(object):
|
||||
def __init__(self, f=None, *args, **kw):
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
self.f = f
|
||||
self._running = False
|
||||
self.done = None
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
def wait(self):
|
||||
return self.done.wait()
|
||||
|
||||
|
||||
class FixedIntervalLoopingCall(LoopingCallBase):
|
||||
"""A fixed interval looping call."""
|
||||
|
||||
def start(self, interval, initial_delay=None):
|
||||
self._running = True
|
||||
done = event.Event()
|
||||
|
||||
def _inner():
|
||||
if initial_delay:
|
||||
greenthread.sleep(initial_delay)
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
start = timeutils.utcnow()
|
||||
self.f(*self.args, **self.kw)
|
||||
end = timeutils.utcnow()
|
||||
if not self._running:
|
||||
break
|
||||
delay = interval - timeutils.delta_seconds(start, end)
|
||||
if delay <= 0:
|
||||
LOG.warn(_('task run outlasted interval by %s sec') %
|
||||
-delay)
|
||||
greenthread.sleep(delay if delay > 0 else 0)
|
||||
except LoopingCallDone as e:
|
||||
self.stop()
|
||||
done.send(e.retvalue)
|
||||
except Exception:
|
||||
LOG.exception(_('in fixed duration looping call'))
|
||||
done.send_exception(*sys.exc_info())
|
||||
return
|
||||
else:
|
||||
done.send(True)
|
||||
|
||||
self.done = done
|
||||
|
||||
greenthread.spawn_n(_inner)
|
||||
return self.done
|
||||
|
||||
|
||||
# TODO(mikal): this class name is deprecated in Havana and should be removed
|
||||
# in the I release
|
||||
LoopingCall = FixedIntervalLoopingCall
|
||||
|
||||
|
||||
class DynamicLoopingCall(LoopingCallBase):
|
||||
"""A looping call which sleeps until the next known event.
|
||||
|
||||
The function called should return how long to sleep for before being
|
||||
called again.
|
||||
"""
|
||||
|
||||
def start(self, initial_delay=None, periodic_interval_max=None):
|
||||
self._running = True
|
||||
done = event.Event()
|
||||
|
||||
def _inner():
|
||||
if initial_delay:
|
||||
greenthread.sleep(initial_delay)
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
idle = self.f(*self.args, **self.kw)
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
if periodic_interval_max is not None:
|
||||
idle = min(idle, periodic_interval_max)
|
||||
LOG.debug(_('Dynamic looping call sleeping for %.02f '
|
||||
'seconds'), idle)
|
||||
greenthread.sleep(idle)
|
||||
except LoopingCallDone as e:
|
||||
self.stop()
|
||||
done.send(e.retvalue)
|
||||
except Exception:
|
||||
LOG.exception(_('in dynamic looping call'))
|
||||
done.send_exception(*sys.exc_info())
|
||||
return
|
||||
else:
|
||||
done.send(True)
|
||||
|
||||
self.done = done
|
||||
|
||||
greenthread.spawn(_inner)
|
||||
return self.done
|
@ -1,81 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Network-related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import urlparse
|
||||
|
||||
|
||||
def parse_host_port(address, default_port=None):
|
||||
"""Interpret a string as a host:port pair.
|
||||
|
||||
An IPv6 address MUST be escaped if accompanied by a port,
|
||||
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
|
||||
means both [2001:db8:85a3::8a2e:370:7334] and
|
||||
[2001:db8:85a3::8a2e:370]:7334.
|
||||
|
||||
>>> parse_host_port('server01:80')
|
||||
('server01', 80)
|
||||
>>> parse_host_port('server01')
|
||||
('server01', None)
|
||||
>>> parse_host_port('server01', default_port=1234)
|
||||
('server01', 1234)
|
||||
>>> parse_host_port('[::1]:80')
|
||||
('::1', 80)
|
||||
>>> parse_host_port('[::1]')
|
||||
('::1', None)
|
||||
>>> parse_host_port('[::1]', default_port=1234)
|
||||
('::1', 1234)
|
||||
>>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234)
|
||||
('2001:db8:85a3::8a2e:370:7334', 1234)
|
||||
|
||||
"""
|
||||
if address[0] == '[':
|
||||
# Escaped ipv6
|
||||
_host, _port = address[1:].split(']')
|
||||
host = _host
|
||||
if ':' in _port:
|
||||
port = _port.split(':')[1]
|
||||
else:
|
||||
port = default_port
|
||||
else:
|
||||
if address.count(':') == 1:
|
||||
host, port = address.split(':')
|
||||
else:
|
||||
# 0 means ipv4, >1 means ipv6.
|
||||
# We prohibit unescaped ipv6 addresses with port.
|
||||
host = address
|
||||
port = default_port
|
||||
|
||||
return (host, None if port is None else int(port))
|
||||
|
||||
|
||||
def urlsplit(url, scheme='', allow_fragments=True):
|
||||
"""Parse a URL using urlparse.urlsplit(), splitting query and fragments.
|
||||
This function papers over Python issue9374 when needed.
|
||||
|
||||
The parameters are the same as urlparse.urlsplit.
|
||||
"""
|
||||
scheme, netloc, path, query, fragment = urlparse.urlsplit(
|
||||
url, scheme, allow_fragments)
|
||||
if allow_fragments and '#' in path:
|
||||
path, fragment = path.split('#', 1)
|
||||
if '?' in path:
|
||||
path, query = path.split('?', 1)
|
||||
return urlparse.SplitResult(scheme, netloc, path, query, fragment)
|
@ -1,14 +0,0 @@
|
||||
# Copyright 2011 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.
|
@ -1,173 +0,0 @@
|
||||
# Copyright 2011 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 socket
|
||||
import uuid
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import context
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import importutils
|
||||
from billingstack.openstack.common import jsonutils
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common import timeutils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notifier_opts = [
|
||||
cfg.MultiStrOpt('notification_driver',
|
||||
default=[],
|
||||
help='Driver or drivers to handle sending notifications'),
|
||||
cfg.StrOpt('default_notification_level',
|
||||
default='INFO',
|
||||
help='Default notification level for outgoing notifications'),
|
||||
cfg.StrOpt('default_publisher_id',
|
||||
default=None,
|
||||
help='Default publisher_id for outgoing notifications'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(notifier_opts)
|
||||
|
||||
WARN = 'WARN'
|
||||
INFO = 'INFO'
|
||||
ERROR = 'ERROR'
|
||||
CRITICAL = 'CRITICAL'
|
||||
DEBUG = 'DEBUG'
|
||||
|
||||
log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
|
||||
|
||||
|
||||
class BadPriorityException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def notify_decorator(name, fn):
|
||||
"""Decorator for notify which is used from utils.monkey_patch().
|
||||
|
||||
:param name: name of the function
|
||||
:param function: - object of the function
|
||||
:returns: function -- decorated function
|
||||
|
||||
"""
|
||||
def wrapped_func(*args, **kwarg):
|
||||
body = {}
|
||||
body['args'] = []
|
||||
body['kwarg'] = {}
|
||||
for arg in args:
|
||||
body['args'].append(arg)
|
||||
for key in kwarg:
|
||||
body['kwarg'][key] = kwarg[key]
|
||||
|
||||
ctxt = context.get_context_from_function_and_args(fn, args, kwarg)
|
||||
notify(ctxt,
|
||||
CONF.default_publisher_id or socket.gethostname(),
|
||||
name,
|
||||
CONF.default_notification_level,
|
||||
body)
|
||||
return fn(*args, **kwarg)
|
||||
return wrapped_func
|
||||
|
||||
|
||||
def publisher_id(service, host=None):
|
||||
if not host:
|
||||
try:
|
||||
host = CONF.host
|
||||
except AttributeError:
|
||||
host = CONF.default_publisher_id or socket.gethostname()
|
||||
return "%s.%s" % (service, host)
|
||||
|
||||
|
||||
def notify(context, publisher_id, event_type, priority, payload):
|
||||
"""Sends a notification using the specified driver
|
||||
|
||||
:param publisher_id: the source worker_type.host of the message
|
||||
:param event_type: the literal type of event (ex. Instance Creation)
|
||||
:param priority: patterned after the enumeration of Python logging
|
||||
levels in the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
|
||||
:param payload: A python dictionary of attributes
|
||||
|
||||
Outgoing message format includes the above parameters, and appends the
|
||||
following:
|
||||
|
||||
message_id
|
||||
a UUID representing the id for this notification
|
||||
|
||||
timestamp
|
||||
the GMT timestamp the notification was sent at
|
||||
|
||||
The composite message will be constructed as a dictionary of the above
|
||||
attributes, which will then be sent via the transport mechanism defined
|
||||
by the driver.
|
||||
|
||||
Message example::
|
||||
|
||||
{'message_id': str(uuid.uuid4()),
|
||||
'publisher_id': 'compute.host1',
|
||||
'timestamp': timeutils.utcnow(),
|
||||
'priority': 'WARN',
|
||||
'event_type': 'compute.create_instance',
|
||||
'payload': {'instance_id': 12, ... }}
|
||||
|
||||
"""
|
||||
if priority not in log_levels:
|
||||
raise BadPriorityException(
|
||||
_('%s not in valid priorities') % priority)
|
||||
|
||||
# Ensure everything is JSON serializable.
|
||||
payload = jsonutils.to_primitive(payload, convert_instances=True)
|
||||
|
||||
msg = dict(message_id=str(uuid.uuid4()),
|
||||
publisher_id=publisher_id,
|
||||
event_type=event_type,
|
||||
priority=priority,
|
||||
payload=payload,
|
||||
timestamp=str(timeutils.utcnow()))
|
||||
|
||||
for driver in _get_drivers():
|
||||
try:
|
||||
driver.notify(context, msg)
|
||||
except Exception as e:
|
||||
LOG.exception(_("Problem '%(e)s' attempting to "
|
||||
"send to notification system. "
|
||||
"Payload=%(payload)s")
|
||||
% dict(e=e, payload=payload))
|
||||
|
||||
|
||||
_drivers = None
|
||||
|
||||
|
||||
def _get_drivers():
|
||||
"""Instantiate, cache, and return drivers based on the CONF."""
|
||||
global _drivers
|
||||
if _drivers is None:
|
||||
_drivers = {}
|
||||
for notification_driver in CONF.notification_driver:
|
||||
try:
|
||||
driver = importutils.import_module(notification_driver)
|
||||
_drivers[notification_driver] = driver
|
||||
except ImportError:
|
||||
LOG.exception(_("Failed to load notifier %s. "
|
||||
"These notifications will not be sent.") %
|
||||
notification_driver)
|
||||
return _drivers.values()
|
||||
|
||||
|
||||
def _reset_drivers():
|
||||
"""Used by unit tests to reset the drivers."""
|
||||
global _drivers
|
||||
_drivers = None
|
@ -1,37 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import jsonutils
|
||||
from billingstack.openstack.common import log as logging
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def notify(_context, message):
|
||||
"""Notifies the recipient of the desired event given the model.
|
||||
|
||||
Log notifications using OpenStack's default logging system.
|
||||
"""
|
||||
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
logger = logging.getLogger(
|
||||
'billingstack.openstack.common.notification.%s' %
|
||||
message['event_type'])
|
||||
getattr(logger, priority)(jsonutils.dumps(message))
|
@ -1,19 +0,0 @@
|
||||
# Copyright 2011 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.
|
||||
|
||||
|
||||
def notify(_context, message):
|
||||
"""Notifies the recipient of the desired event given the model."""
|
||||
pass
|
@ -1,46 +0,0 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from billingstack.openstack.common import cfg
|
||||
from billingstack.openstack.common import context as req_context
|
||||
from billingstack.openstack.common.gettextutils import _
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common import rpc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notification_topic_opt = cfg.ListOpt(
|
||||
'notification_topics', default=['notifications', ],
|
||||
help='AMQP topic used for openstack notifications')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(notification_topic_opt)
|
||||
|
||||
|
||||
def notify(context, message):
|
||||
"""Sends a notification to the RabbitMQ"""
|
||||
if not context:
|
||||
context = req_context.get_admin_context()
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
for topic in CONF.notification_topics:
|
||||
topic = '%s.%s' % (topic, priority)
|
||||
try:
|
||||
rpc.notify(context, topic, message)
|
||||
except Exception, e:
|
||||
LOG.exception(_("Could not send notification to %(topic)s. "
|
||||
"Payload=%(message)s"), locals())
|
@ -1,47 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from billingstack.openstack.common import context as req_context
|
||||
from billingstack.openstack.common.gettextutils import _ # noqa
|
||||
from billingstack.openstack.common import log as logging
|
||||
from billingstack.openstack.common import rpc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notification_topic_opt = cfg.ListOpt(
|
||||
'notification_topics', default=['notifications', ],
|
||||
help='AMQP topic used for OpenStack notifications')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(notification_topic_opt)
|
||||
|
||||
|
||||
def notify(context, message):
|
||||
"""Sends a notification via RPC."""
|
||||
if not context:
|
||||
context = req_context.get_admin_context()
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
for topic in CONF.notification_topics:
|
||||
topic = '%s.%s' % (topic, priority)
|
||||
try:
|
||||
rpc.notify(context, topic, message)
|
||||
except Exception:
|
||||
LOG.exception(_("Could not send notification to %(topic)s. "
|
||||
"Payload=%(message)s"),
|
||||
{"topic": topic, "message": message})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user