diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..5674976 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = synergy +omit = synergy/openstack/* + +[report] +ignore_errors = True diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..6d83b3c --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..a1a7bcb --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,3 @@ +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/synergy-service diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..42cb110 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +Synergy Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c978a52 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a94e95c --- /dev/null +++ b/README.rst @@ -0,0 +1,40 @@ +------------------------------ + SYNERGY +------------------------------ + +Synergy is as a new extensible general purpose management OpenStack service. +Its capabilities are implemented by a collection of managers which are specific +and independent pluggable tasks, executed periodically or interactively. The +managers can interact with each other in a loosely coupled way. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/synergy-service +* Source: http://git.openstack.org/cgit/openstack/synergy-service +* Bugs: http://bugs.launchpad.net/synergy-service + +================ +1. INSTALLATION +================ + +--------------- +1.1 REQUISITES +--------------- + + +---------------------- +1.2 OPTIONAL PACKAGES +---------------------- + +--------------- +1.3 INSTALLING +--------------- + + +------------------ +1.4 CONFIGURATION +------------------ + + +--------------- +1.5 SECURITY +--------------- diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..15cd6cb --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/bin/synergy b/bin/synergy new file mode 100644 index 0000000..d1cae6d --- /dev/null +++ b/bin/synergy @@ -0,0 +1,10 @@ +#!/usr/bin/python +# PBR Generated from u'console_scripts' + +import sys + +from synergy.service import main + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/config/synergy.conf b/config/synergy.conf new file mode 100644 index 0000000..56b1043 --- /dev/null +++ b/config/synergy.conf @@ -0,0 +1,23 @@ +[DEFAULT] + +[Logger] +filename=/var/log/synergy/synergy.log +level=INFO +formatter="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +maxBytes=1048576 +backupCount=100 + + +[WSGI] +host=localhost +port=8051 +threads=2 +use_ssl=False +#ssl_ca_file= +#ssl_cert_file= +#ssl_key_file= +max_header_line=16384 +retry_until_window=30 +tcp_keepidle=600 +backlog=4096 + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 0000000..e2df1ae --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'synergy-service' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..1728a61 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..f0289e4 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. synergy documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to synergy's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..4927ba5 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,5 @@ +============ +Installation +============ + +TODO diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..a6210d3 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..d86ea05 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use synergy in a project:: + + import synergy diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..9d3f568 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,108 @@ +Packaging +========= + +Packaging for Ubuntu and CentOS using Docker +-------------------------------------------- + +We provide Dockerfiles for CentOS 7 and Ubuntu 14.04. A Dockerfile for Ubuntu > +14.04 should work by just changing the "FROM" statement of the Ubuntu 14.04 +Dockerfile. Using these, you can easily build rpm and deb packages for +synergy-service without having to setup the build system on your own system. + +The build process using Docker is made of 3 steps: + +1. Build the docker image + +2. Setup the build variables + +3. Run the build with docker + +If the build is successful, the package will be put in the build directory +inside the synergy-service directory. + + +### Example for CentOS 7 + +- go into the directory that contains the Dockerfile for CentOS 7 + + cd synergy-service/packaging/docker/centos7 + +- build the docker image and tag it + + docker build -t synergy-centos7-builder . + +- edit the file `synergy-service/packaging/docker/build_env.sh` to define environment variables. + +- launch the container + + docker run -i -v /path/to/synergy-service:/tmp/synergy-service \ + --env-file=/path/to/synergy-service/packaging/docker/build_env.sh \ + synergy-centos7-builder + + This actually mount the synergy-service directory to `/tmp/synergy-service` on + the guest. + It also loads environment variables from the `build_env.sh` file. + +- the resulting rpm should be in the build directory if successful + + +### Example for Ubuntu 14.04 + +- go into the directory that contains the Dockerfile for Ubuntu 14.04 + + cd synergy-service/packaging/docker/ubuntu-14.04 + +- build the docker image and tag it + + docker build -t synergy-ubuntu14.04-builder . + +- edit the file `synergy-service/packaging/docker/build_env.sh` to define environment variables. + +- launch the container + + docker run -i -v /path/to/synergy-service:/tmp/synergy-service \ + --env-file=/path/to/synergy-service/packaging/docker/build_env.sh \ + synergy-ubuntu14.04-builder + +- the resulting deb should be in the build directory if successful + + +Packaging for Ubuntu +-------------------- + +1. Install the necessary build packages: + - debhelper + - dh-systemd + - build-essential + - devscripts + - python-all + - python-setuptools + +2. Make a gzip archive of synergy-service named `python-synergy-service_VERSION.orig.tar.gz`. + +3. Copy `synergy-service/packaging/debian` to `synergy-service/debian`. + +4. Go in the `synergy-service` directory and build with `debuild -us -uc`. + + +Packaging for CentOS +-------------------- + +1. Install the necessary build packages: + - rpm-build + - python-devel + - python-setuptools + +2. Setup your rpmbuild environment if not already done. + + mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +3. Move `synergy-service/packaging/rpm/python-synergy.spec` to + `~/rpmbuild/SPECS`. + +4. Create a source archive: + + cp -r /path/to/synergy-service ~/rpmbuild/SOURCES/python-synergy-service + tar cjf python-synergy-service python-synergy-service.tar.bz2 + +5. Go in `~/rpmbuild/SPECS` and buils with `rpmbuild -ba python-synergy.spec`. diff --git a/packaging/debian/changelog b/packaging/debian/changelog new file mode 100644 index 0000000..5844edf --- /dev/null +++ b/packaging/debian/changelog @@ -0,0 +1,5 @@ +python-synergy-service (0.1-1) unstable; urgency=low + + * Initial release + + -- Vincent Llorens Mon, 15 Feb 2016 14:02:30 +0000 diff --git a/packaging/debian/compat b/packaging/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/packaging/debian/compat @@ -0,0 +1 @@ +9 diff --git a/packaging/debian/control b/packaging/debian/control new file mode 100644 index 0000000..efc8cd8 --- /dev/null +++ b/packaging/debian/control @@ -0,0 +1,25 @@ +Source: python-synergy-service +Section: contrib/python +Priority: optional +Maintainer: Vincent Llorens +Build-Depends: debhelper (>= 9), + dh-python, + dh-systemd, + git-core, + python-all, + python-pbr, + python-setuptools +Standards-Version: 3.9.5 +Homepage: https://launchpad.net/synergy-service +Vcs-Git: git://git.launchpad.net/synergy-service +Vcs-Browser: https://git.launchpad.net/synergy-service + +Package: python-synergy-service +Architecture: all +Depends: ${python:Depends}, ${misc:Depends}, adduser +Description: Synergy service for OpenStack. + Synergy is as a new extensible general purpose management OpenStack service. + Its capabilities are implemented by a collection of managers which are + specific and independent pluggable tasks, executed periodically or + interactively. The managers can interact with each other in a loosely + coupled way. diff --git a/packaging/debian/copyright b/packaging/debian/copyright new file mode 100644 index 0000000..593a354 --- /dev/null +++ b/packaging/debian/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: synergy-service +Source: + +Files: * +Copyright: 2015, 2016 Lisa Zangrando +License: Apache-2 + 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. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0' diff --git a/packaging/debian/docs b/packaging/debian/docs new file mode 100644 index 0000000..a1320b1 --- /dev/null +++ b/packaging/debian/docs @@ -0,0 +1 @@ +README.rst diff --git a/packaging/debian/python-synergy-service.dirs b/packaging/debian/python-synergy-service.dirs new file mode 100644 index 0000000..139641d --- /dev/null +++ b/packaging/debian/python-synergy-service.dirs @@ -0,0 +1,5 @@ +etc/synergy +var/lib/synergy +var/log/synergy +var/run/synergy +var/lock/synergy diff --git a/packaging/debian/python-synergy-service.install b/packaging/debian/python-synergy-service.install new file mode 100644 index 0000000..5052746 --- /dev/null +++ b/packaging/debian/python-synergy-service.install @@ -0,0 +1,2 @@ +config/synergy.conf /etc/synergy +scripts/synergy.service /lib/systemd/system diff --git a/packaging/debian/python-synergy-service.lintian-overrides b/packaging/debian/python-synergy-service.lintian-overrides new file mode 100644 index 0000000..5deaf8c --- /dev/null +++ b/packaging/debian/python-synergy-service.lintian-overrides @@ -0,0 +1,5 @@ +# Both systemv init and systemd scripts are provided. +# Systemd script is automatically provided with the name "synergy-service", +# whereas the init script is provided with the name "python-synergy-service". +# Lintian complains because they don't have the same name. +systemd-no-service-for-init-script diff --git a/packaging/debian/python-synergy-service.postinst b/packaging/debian/python-synergy-service.postinst new file mode 100755 index 0000000..428f8b1 --- /dev/null +++ b/packaging/debian/python-synergy-service.postinst @@ -0,0 +1,48 @@ +#!/bin/sh +# postinst script for python-synergy-service +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + configure) + # Add synergy group & user + adduser --quiet --system --group --home /var/lib/synergy synergy > /dev/null 2>&1 + # Change dirs and files permission/ownership + chown -R synergy:synergy /etc/synergy \ + /var/lib/synergy \ + /var/lock/synergy \ + /var/log/synergy \ + /var/run/synergy + chmod 0700 /var/lib/synergy/ + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/packaging/debian/python-synergy-service.postrm b/packaging/debian/python-synergy-service.postrm new file mode 100755 index 0000000..477804e --- /dev/null +++ b/packaging/debian/python-synergy-service.postrm @@ -0,0 +1,41 @@ +#!/bin/sh +# postrm script for python-synergy-service +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `remove' +# * `purge' +# * `upgrade' +# * `failed-upgrade' +# * `abort-install' +# * `abort-install' +# * `abort-upgrade' +# * `disappear' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + + +case "$1" in + remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + purge) + update-rc.d python-synergy-service remove >/dev/null + ;; + + *) + echo "postrm called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/packaging/debian/python-synergy-service.preinst b/packaging/debian/python-synergy-service.preinst new file mode 100755 index 0000000..b4f864f --- /dev/null +++ b/packaging/debian/python-synergy-service.preinst @@ -0,0 +1,34 @@ +#!/bin/sh +# preinst script for python-synergy-service +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `install' +# * `install' +# * `upgrade' +# * `abort-upgrade' +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + install|upgrade) + ;; + + abort-upgrade) + ;; + + *) + echo "preinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/packaging/debian/python-synergy-service.synergy.init b/packaging/debian/python-synergy-service.synergy.init new file mode 100644 index 0000000..4f8e513 --- /dev/null +++ b/packaging/debian/python-synergy-service.synergy.init @@ -0,0 +1,166 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: synergy +# Required-Start: $local_fs $network $remote_fs $syslog +# Required-Stop: $local_fs $network $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: +# Description: +# <...> +# <...> +### END INIT INFO + +# Author: Vincent Llorens + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="python-synergy-service" +NAME=synergy +DAEMON=/usr/sbin/synergy +DAEMON_ARGS="" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + # start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + # || return 1 + # start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + # $DAEMON_ARGS \ + # || return 2 + # The above code will not work for interpreted scripts, use the next + # six lines below instead (Ref: #643337, start-stop-daemon(8) ) + start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \ + --name $NAME --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $DAEMON \ + --name $NAME -- $DAEMON_ARGS \ + || return 2 + + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/packaging/debian/python-synergy-service.synergy.upstart b/packaging/debian/python-synergy-service.synergy.upstart new file mode 100644 index 0000000..597ea73 --- /dev/null +++ b/packaging/debian/python-synergy-service.synergy.upstart @@ -0,0 +1,19 @@ +description "Synergy service" +author "Lisa Zangrando " + +start on runlevel [2345] +stop on runlevel [!2345] + +pre-start script + for i in "lock run log lib"; do + mkdir -p /var/"$i"/synergy + chown synergy:synergy /var/"$i"/synergy + done +end script + +script + DAEMON_ARGS="" + exec start-stop-daemon --start --chdir /var/lib/synergy \ + --chuid synergy:synergy --make-pidfile --pidfile /var/run/synergy/synergy.pid \ + --exec /usr/bin/synergy -- --config-file=/etc/synergy/synergy.conf ${DAEMON_ARGS} +end script diff --git a/packaging/debian/rules b/packaging/debian/rules new file mode 100755 index 0000000..a2cf5f2 --- /dev/null +++ b/packaging/debian/rules @@ -0,0 +1,31 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#DH_VERBOSE = 1 + +# see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* +DPKG_EXPORT_BUILDFLAGS = 1 +include /usr/share/dpkg/default.mk + +# see FEATURE AREAS in dpkg-buildflags(1) +#export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +# see ENVIRONMENT in dpkg-buildflags(1) +# package maintainers to append CFLAGS +#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +# package maintainers to append LDFLAGS +#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed +export PYBUILD_NAME=python-synergy-service + +# main packaging script based on dh7 syntax +%: + dh $@ --with python2,systemd --buildsystem=pybuild + +override_dh_installinit: + dh_installinit --name synergy + +# debmake generated override targets +# This is example for Cmake (See http://bugs.debian.org/641051 ) +#override_dh_auto_configure: +# dh_auto_configure -- \ +# -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) diff --git a/packaging/debian/source/format b/packaging/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packaging/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packaging/docker/build_env.sh b/packaging/docker/build_env.sh new file mode 100644 index 0000000..db714a4 --- /dev/null +++ b/packaging/docker/build_env.sh @@ -0,0 +1 @@ +PKG_VERSION=0.1 diff --git a/packaging/docker/centos7/Dockerfile b/packaging/docker/centos7/Dockerfile new file mode 100644 index 0000000..0f4efb9 --- /dev/null +++ b/packaging/docker/centos7/Dockerfile @@ -0,0 +1,13 @@ +FROM centos:7 +MAINTAINER Vincent Llorens +RUN yum install -y https://rdoproject.org/repos/rdo-release.rpm \ + rpm-build \ + python-devel \ + python-setuptools \ + && yum update -y +RUN mkdir /tmp/synergy-service +RUN useradd -m -p pkger pkger +USER pkger +COPY build.sh /home/pkger/ +WORKDIR /home/pkger/ +CMD bash build.sh diff --git a/packaging/docker/centos7/build.sh b/packaging/docker/centos7/build.sh new file mode 100644 index 0000000..f85d9d4 --- /dev/null +++ b/packaging/docker/centos7/build.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e -x + +RPMBUILD=/home/pkger/rpmbuild +PKG_DIR=/tmp/synergy-service + +function setup() { + mkdir -p /home/pkger/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + cd $RPMBUILD/SOURCES/ + cp -r $PKG_DIR python-synergy-service-$PKG_VERSION + rm -r python-synergy-service-$PKG_VERSION/build || true + tar cjf python-synergy-service-${PKG_VERSION}.tar.bz2 python-synergy-service-$PKG_VERSION + cp $PKG_DIR/packaging/rpm/python-synergy.spec $RPMBUILD/SPECS/python-synergy.spec +} + +function build() { + cd $RPMBUILD/SPECS + export PBR_VERSION=$PKG_VERSION + rpmbuild -ba python-synergy.spec + mkdir -p $PKG_DIR/build/ + cp -i $RPMBUILD/RPMS/noarch/python-synergy-service-*.rpm $PKG_DIR/build/ +} + +function clean() { + rm -rf $RPMBUILD +} + +clean +setup +build diff --git a/packaging/docker/ubuntu-14.04/Dockerfile b/packaging/docker/ubuntu-14.04/Dockerfile new file mode 100644 index 0000000..5263747 --- /dev/null +++ b/packaging/docker/ubuntu-14.04/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:14.04 +MAINTAINER Vincent Llorens +RUN apt-get update \ + && apt-get install -y build-essential \ + debhelper \ + devscripts \ + dh-systemd \ + git-core \ + python-all \ + python-pbr \ + python-setuptools +RUN mkdir /tmp/synergy-service +RUN useradd -m -p pkger pkger +USER pkger +COPY build.sh /home/pkger/build.sh +WORKDIR /home/pkger +CMD bash build.sh diff --git a/packaging/docker/ubuntu-14.04/build.sh b/packaging/docker/ubuntu-14.04/build.sh new file mode 100644 index 0000000..df1b17b --- /dev/null +++ b/packaging/docker/ubuntu-14.04/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -e -x + +PKG_DIR=/tmp/synergy-service + +function setup() { + cd /home/pkger + cp -r $PKG_DIR synergy-service + tar cfz python-synergy-service_${PKG_VERSION}.orig.tar.gz synergy-service + mv synergy-service python-synergy-service + cp -r python-synergy-service/packaging/debian python-synergy-service/debian +} + +function build() { + cd /home/pkger/python-synergy-service + debuild -us -uc + mkdir -p $PKG_DIR/build + cp -i /home/pkger/*.deb $PKG_DIR/build +} + +function clean() { + rm -r /home/pkger/python-synergy-service* +} + +setup +build +clean diff --git a/packaging/rpm/python-synergy.spec b/packaging/rpm/python-synergy.spec new file mode 100644 index 0000000..2986b1e --- /dev/null +++ b/packaging/rpm/python-synergy.spec @@ -0,0 +1,96 @@ +%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} + +Name: python-synergy-service +Version: 0.1 +Release: 1%{?dist} +Summary: Synergy service + +License: ASL 2.0 +URL: https://launchpad.net/synergy-service +Source0: https://launchpad.net/synergy-service/%{name}-%{version}.tar.bz2 + +BuildArch: noarch +BuildRequires: systemd +BuildRequires: python-devel +BuildRequires: python-setuptools +Requires(pre): shadow-utils +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd +Requires: python-eventlet +Requires: python-oslo-config +Requires: python-oslo-messaging +Requires: python-oslo-log +Requires: python-dateutil + + +%description +Synergy is as a new extensible general purpose management OpenStack service. +Its capabilities are implemented by a collection of managers which are +specific and independent pluggable tasks, executed periodically or +interactively. The managers can interact with each other in a loosely coupled +way. + + +%prep +%setup -q + + +%build +%{__python} setup.py build + + +%install +rm -rf $RPM_BUILD_ROOT +%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT + +install -d -m0755 %{buildroot}%{_sysconfdir}/synergy +install -D -m0644 config/synergy.conf %{buildroot}%{_sysconfdir}/synergy/synergy.conf +install -D -m0644 scripts/synergy.service %{buildroot}%{_unitdir}/synergy.service +install -d -m0700 %{buildroot}%{_localstatedir}/lib/synergy +install -d -m0755 %{buildroot}%{_localstatedir}/log/synergy +touch %{buildroot}%{_localstatedir}/log/synergy/synergy.log +install -d -m0755 %{buildroot}%{_localstatedir}/run/synergy +install -d -m0755 %{buildroot}%{_localstatedir}/lock/synergy + + +%files +%doc README.rst +%{python_sitelib}/* +%config(noreplace) %{_sysconfdir}/synergy/synergy.conf +%{_bindir}/synergy +%{_unitdir}/synergy.service +%defattr(-, synergy, root, -) +%{_localstatedir}/lock/synergy/ +%{_localstatedir}/log/synergy/ +%{_localstatedir}/log/synergy/synergy.log +%{_localstatedir}/run/synergy/ +%attr(700, synergy, root) %{_localstatedir}/lib/synergy/ + + +%pre +getent group synergy > /dev/null || groupadd -r synergy +getent passwd synergy > /dev/null || \ + useradd -r -g synergy -s /sbin/nologin synergy +exit 0 + + +%post +%systemd_post synergy.service + + +%preun +%systemd_preun synergy.service + + +%postun +%systemd_postun_with_restart synergy.service +if [ "$1" = 0 ]; then + userdel -r synergy + groupdel synergy + true +fi + +%changelog +* Wed Jan 20 2016 Vincent Llorens +- WIP RPM release diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d0daa7d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +pbr>=1.6 +eventlet +oslo.config +oslo.messaging +python-dateutil diff --git a/scripts/synergy.service b/scripts/synergy.service new file mode 100644 index 0000000..1942345 --- /dev/null +++ b/scripts/synergy.service @@ -0,0 +1,12 @@ +[Unit] +Description=Synergy service +After=mysql.service postgresql.service slapd.service rabbitmq-server.service ntp.service + +[Service] +User=synergy +Group=synergy +WorkingDirectory=/var/lib/synergy +ExecStart=/usr/bin/synergy + +[Install] +WantedBy=multi-user.target diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ba6ce42 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,50 @@ +[metadata] +name = synergy-service +version = 0.1.1 +summary = Synergy is as an extensible general purpose management OpenStack service. +description-file = + README.rst +author = Lisa Zangrando +author-email = lisa.zangrando@pd.infn.it +home-page = https://launchpad.net/synergy-service +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + +[files] +packages = + synergy +scripts = + bin/synergy + +[entry_points] +synergy.managers = + timer = synergy.examples.timer_manager:TimerManager + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = synergy/locale +domain = synergy + +[update_catalog] +domain = synergy +output_dir = synergy/locale +input_file = synergy/locale/synergy.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = synergy/locale/synergy.pot diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..98b93eb --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/synergy/__init__.py b/synergy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synergy/common/__init__.py b/synergy/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synergy/common/command.py b/synergy/common/command.py new file mode 100644 index 0000000..d60f23f --- /dev/null +++ b/synergy/common/command.py @@ -0,0 +1,60 @@ +from synergy.common import serializer + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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 Command(serializer.SynergyObject): + VERSION = "1.0" + + def __init__(self, name): + super(Command, self).__init__(name) + + def getParameters(self): + parameters = self.get("parameters") + + if parameters is None: + self.set("parameters", {}) + + return self.get("parameters") + + def addParameter(self, name, value): + self.getParameters()[name] = value + + def getParameter(self, name): + return self.getParameters().get(name, None) + + def setParameters(self, parameters): + self.set("parameters", parameters) + + def getResults(self): + result = self.get("result") + + if not result: + self.set("result", {}) + + return self.get("result") + + def addResult(self, name, value): + self.getResults()[name] = value + + def getResult(self, name): + return self.getResults().get(name, None) + + def setResults(self, data): + self.set("result", data) diff --git a/synergy/common/config.py b/synergy/common/config.py new file mode 100644 index 0000000..3cfdb67 --- /dev/null +++ b/synergy/common/config.py @@ -0,0 +1,103 @@ +try: + from oslo_config import cfg +except ImportError: + from oslo.config import cfg + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + + +CONF = cfg.CONF + +service_opts = [ + cfg.StrOpt("topic", default="synergy_topic", help="the topic"), + cfg.StrOpt("exchange", default="synergy_exchange", help="the exchange"), +] + +wsgi_opts = [ + cfg.StrOpt("host", default="localhost", + help="Address to bind the server"), + cfg.IntOpt("port", default=8051, + help="The port on which the server will listen"), + cfg.IntOpt("threads", default=1000), + cfg.IntOpt("backlog", default=4096, + help="Number of backlog requests to configure the socket with"), + cfg.IntOpt("tcp_keepidle", default=600, + help="Sets the value of TCP_KEEPIDLE in seconds for each server" + " socket (not supported on OS X)"), + cfg.IntOpt("retry_until_window", default=30, + help="Number of seconds to keep retrying to listen"), + cfg.IntOpt("max_header_line", default=16384, + help="Max header line to accommodate large tokens"), + cfg.BoolOpt("use_ssl", default=False, + help="Enable SSL on the API server"), + cfg.StrOpt("ssl_ca_file", default=None, + help="CA certificate file to use to verify connecting clients"), + cfg.StrOpt("ssl_cert_file", default=None, + help="The certificate file"), + cfg.StrOpt("ssl_key_file", default=None, + help="The private key file") +] + +logger_opts = [ + cfg.StrOpt("filename", default="/var/log/synergy/synergy.log", + required=True), + cfg.StrOpt("level", default="INFO", required=False), + cfg.IntOpt("maxBytes", default=1048576), + cfg.IntOpt("backupCount", default=100), + cfg.StrOpt("formatter", + default="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + required=False) +] + +manager_opts = [ + cfg.BoolOpt("autostart", default=False), + cfg.IntOpt("rate", default=60) +] + +""" +keystone_opts = [ + cfg.StrOpt("admin_user", required=True), + cfg.StrOpt("admin_password", required=True), + cfg.StrOpt("admin_project_name", required=True), + cfg.StrOpt("auth_url", required=True) +] + +mysql_opts = [ + cfg.StrOpt("host", required=True), + cfg.StrOpt("user", default="synergy"), + cfg.StrOpt("password", required=True), + cfg.StrOpt("db", default="synergy", required=True), + cfg.IntOpt("pool_size", default="10", required=False) +] +""" + +cfg.CONF.register_opts(service_opts) +cfg.CONF.register_opts(wsgi_opts, group="WSGI") +cfg.CONF.register_opts(logger_opts, group="Logger") +# cfg.CONF.register_opts(socket_opts) +# cfg.CONF.register_opts(keystone_opts, group="Keystone") +# cfg.CONF.register_opts(mysql_opts, group="MYSQL") + + +def parse_args(args=None, usage=None, default_config_files=None): + cfg.CONF(args=args, + project='synergy', + version="1.0", + usage=usage, + default_config_files=default_config_files) diff --git a/synergy/common/context.py b/synergy/common/context.py new file mode 100644 index 0000000..ceb2276 --- /dev/null +++ b/synergy/common/context.py @@ -0,0 +1,172 @@ +import copy +import datetime +import six +import uuid + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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 generate_request_id(): + return 'req-' + str(uuid.uuid4()) + + +class RequestContext(object): + """Security context and request information. + + Represents the user taking a given action within the system. + + """ + + def __init__(self, user_id, project_id, is_admin=None, read_deleted="no", + roles=None, remote_address=None, timestamp=None, + request_id=None, auth_token=None, overwrite=True, + quota_class=None, user_name=None, project_name=None, + service_catalog=None, instance_lock_checked=False, **kwargs): + self.user_id = user_id + self.project_id = project_id + self.roles = roles or [] + self.read_deleted = read_deleted + self.remote_address = remote_address + if not timestamp: + timestamp = datetime.datetime.utcnow() + if isinstance(timestamp, six.string_types): + timestamp = datetime.datetime.strptime(timestamp, + '%Y-%m-%dT%H:%M:%S.%f') + self.timestamp = timestamp + if not request_id: + request_id = generate_request_id() + self.request_id = request_id + self.auth_token = auth_token + + if service_catalog: + # Only include required parts of service_catalog + self.service_catalog = [ + s for s in service_catalog if s.get('type') in ('volume',)] + else: + # if list is empty or none + self.service_catalog = [] + + self.instance_lock_checked = instance_lock_checked + + # NOTE(markmc): this attribute is currently only used by the + # rs_limits turnstile pre-processor. + # See https://lists.launchpad.net/openstack/msg12200.html + self.quota_class = quota_class + self.user_name = user_name + self.project_name = project_name + self.is_admin = is_admin + # if self.is_admin is None: + # self.is_admin = policy.check_is_admin(self) + + """ + if overwrite or not hasattr(local.store, 'context'): + self.update_store() + """ + def _get_read_deleted(self): + return self._read_deleted + + def _set_read_deleted(self, read_deleted): + if read_deleted not in ('no', 'yes', 'only'): + raise ValueError("read_deleted can only be one of 'no', " + "'yes' or 'only', not %r" % read_deleted) + self._read_deleted = read_deleted + + def _del_read_deleted(self): + del self._read_deleted + + read_deleted = property(_get_read_deleted, _set_read_deleted, + _del_read_deleted) + + def update_store(self): + # local.store.context = self + pass + + def toDict(self): + date_format = "%Y-%m-%dT%H:%M:%S.%f" + + return {'user_id': self.user_id, + 'project_id': self.project_id, + 'is_admin': self.is_admin, + 'read_deleted': self.read_deleted, + 'roles': self.roles, + 'remote_address': self.remote_address, + 'timestamp': datetime.datetime.strptime(self.timestamp, + date_format), + 'request_id': self.request_id, + 'auth_token': self.auth_token, + 'quota_class': self.quota_class, + 'user_name': self.user_name, + 'service_catalog': self.service_catalog, + 'project_name': self.project_name, + 'instance_lock_checked': self.instance_lock_checked, + 'tenant': self.tenant, + 'user': self.user} + + @classmethod + def fromDict(cls, values): + values.pop('user', None) + values.pop('tenant', None) + + return cls(**values) + + def elevated(self, read_deleted=None, overwrite=False): + """Return a version of this context with admin flag set.""" + context = copy.copy(self) + context.is_admin = True + + if 'admin' not in context.roles: + context.roles.append('admin') + + if read_deleted is not None: + context.read_deleted = read_deleted + + return context + + # NOTE(sirp): the openstack/common version of RequestContext uses + # tenant/user whereas the Nova version uses project_id/user_id. We need + # this shim in order to use context-aware code from openstack/common, like + # logging, until we make the switch to using openstack/common's version of + # RequestContext. + @property + def tenant(self): + return self.project_id + + @property + def user(self): + return self.user_id + + +def get_admin_context(read_deleted="no"): + return RequestContext(user_id=None, + project_id=None, + is_admin=True, + read_deleted=read_deleted, + overwrite=False) + + +def is_user_context(context): + """Indicates if the request context is a normal user.""" + if not context: + return False + if context.is_admin: + return False + if not context.user_id or not context.project_id: + return False + return True diff --git a/synergy/common/log.py b/synergy/common/log.py new file mode 100644 index 0000000..e321420 --- /dev/null +++ b/synergy/common/log.py @@ -0,0 +1,68 @@ +import logging +import logging.handlers + +try: + from oslo_config import cfg +except ImportError: + from oslo.config import cfg + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + +CONF = cfg.CONF +loggers = {} + + +def getLogger(name="unknown"): + global loggers + + if loggers.get(name): + return loggers.get(name) + + else: + logger = logging.getLogger(name) + + if CONF.Logger.level == "DEBUG": + logger.setLevel(logging.DEBUG) + elif CONF.Logger.level == "INFO": + logger.setLevel(logging.INFO) + elif CONF.Logger.level == "WARNING": + logger.setLevel(logging.WARNING) + elif CONF.Logger.level == "ERROR": + logger.setLevel(logging.ERROR) + elif CONF.Logger.level == "CRITICAL": + logger.setLevel(logging.CRITICAL) + else: + logger.setLevel(logging.INFO) + + # create a logging format + formatter = logging.Formatter(CONF.Logger.formatter) + + # Add the log message handler to the logger + handler = logging.handlers.RotatingFileHandler( + CONF.Logger.filename, + maxBytes=CONF.Logger.maxBytes, + backupCount=CONF.Logger.backupCount) + + handler.setFormatter(formatter) + + logger.addHandler(handler) + + loggers[name] = logger + + return logger diff --git a/synergy/common/manager.py b/synergy/common/manager.py new file mode 100644 index 0000000..3110c2e --- /dev/null +++ b/synergy/common/manager.py @@ -0,0 +1,147 @@ +from threading import Condition +from threading import Timer + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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 Manager(object): + + def __init__(self, name): + self.config_opts = [] + self.condition = Condition() + self.name = name + self.status = "CREATED" + self.autostart = False + self.rate = -1 + self.timer = None + self.is_running = False + self.managers = {} + + def execute(self, command, *args, **kargs): + pass + + def task(self): + pass + + def doOnEvent(self, event_type, *args, **kargs): + pass + + def getManagers(self): + return self.managers + + def getManager(self, name): + return self.managers.get(name, None) + + def notify(self, event_type="DEFAULT", manager_name=None, *args, **kargs): + if manager_name is not None: + if manager_name in self.managers: + self.managers[manager_name].doOnEvent(event_type, + *args, **kargs) + else: + for manager in self.managers.values(): + if manager.getName() != manager_name: + manager.doOnEvent(event_type, *args, **kargs) + + def getName(self): + return self.name + + def getOptions(self): + return self.config_opts + + def isAutoStart(self): + return self.autostart + + def setAutoStart(self, autostart): + self.autostart = autostart + + def getRate(self): + return self.rate + + def setRate(self, rate): + if rate and rate > 0: + self.rate = rate + + def setup(self): + """Manager initialization + + Hook to do additional manager initialization when one requests + the service be started. This is called before any service record + is created. + Child classes should override this method. + """ + pass + + def destroy(self): + pass + + def getStatus(self): + return self.status + + def setStatus(self, status): + with self.condition: + self.status = status + + self.condition.notifyAll() + # if self.status == "RUNNING": + # self.__task() + + """ + def __task(self): + if self.rate: + if self.status == "RUNNING": + self.task() + self.timer = Timer(self.rate, self.__task) + self.timer.start() + else: + self.timer.cancel() + """ + + def start(self): + if not self.rate: + return + + if not self.is_running and self.rate > 0: + self.timer = Timer(self.rate * 60, self._run) + self.timer.start() + self.is_running = True + + def _run(self): + self.is_running = False + self.start() + + if self.status == "RUNNING": + self.task() + + def stop(self): + self.timer.cancel() + self.is_running = False + + def run(self): + if not self.rate: + return + + with self.condition: + while self.status != "DESTROYED" and self.status != "ERROR": + if self.status == "RUNNING": + self.task() + + self.condition.wait(self.rate) + else: + self.condition.wait() diff --git a/synergy/common/serializer.py b/synergy/common/serializer.py new file mode 100644 index 0000000..d7ca473 --- /dev/null +++ b/synergy/common/serializer.py @@ -0,0 +1,165 @@ +try: + import oslo_messaging +except ImportError: + import oslo.messaging as oslo_messaging + +from synergy.common import context as ctx +from synergy.common import log as logging +from synergy.common import utils + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + + +LOG = logging.getLogger(__name__) + + +class SynergyObject(object): + """Base class and object factory. + + This forms the base of all objects that can be remoted or instantiated + via RPC. Simply defining a class that inherits from this base class + will make it remotely instantiatable. Objects should implement the + necessary "get" classmethod routines as well as "set" object methods + as appropriate. + """ + VERSION = "1.0" + + def __init__(self, name=None): + self.attributes = {} + + if name: + self.attributes["name"] = name + + def getName(self): + return self.attributes["name"] + + def setName(self, name): + self.attributes["name"] = name + + def get(self, field=None): + return self.attributes.get(field, None) + + def set(self, field, value): + self.attributes[field] = value + + def setContext(self, context): + self.context = context + + def setAttributes(self, attributes): + if attributes: + self.attributes = attributes + + @classmethod + def deserialize(cls, context, entity): + if "synergy_object.namespace" not in entity: + raise Exception("synergy_object.namespace nof defined!") + + if "synergy_object.name" not in entity: + raise Exception("synergy_object.name nof defined!") + + if "synergy_object.version" not in entity: + raise Exception("synergy_object.version nof defined!") + + if entity["synergy_object.namespace"] != 'synergy': + raise Exception("unsupported object objtype='%s.%s" + % (entity["synergy_object.namespace"], + entity["synergy_object.name"])) + + objName = entity['synergy_object.name'] + # objVer = entity['synergy_object.version'] + objClass = utils.import_class(objName) + + # objInstance = objClass(context=context, data=entity) + + objInstance = objClass(name=None) + objInstance.setContext(context) + objInstance.setAttributes(entity) + + return objInstance + + def serialize(self): + name = self.__class__.__module__ + "." + self.__class__.__name__ + self.attributes['synergy_object.name'] = name + self.attributes['synergy_object.version'] = self.VERSION + self.attributes['synergy_object.namespace'] = 'synergy' + + return self.attributes + + def log(self): + for key, value in self.attributes.items(): + LOG.info("%s = %s" % (key, value)) + + +class SynergySerializer(oslo_messaging.Serializer): + def __init__(self): + super(oslo_messaging.Serializer, self).__init__() + + def serialize_entity(self, context, entity): + if not entity: + return entity + + if isinstance(entity, SynergyObject): + entity = entity.serialize() + elif isinstance(entity, dict): + result = {} + + for key, value in entity.items(): + result[key] = self.serialize_entity(context, value) + + entity = result + + return entity + + def deserialize_entity(self, context, entity): + if isinstance(entity, dict): + if 'synergy_object.name' in entity: + entity = SynergyObject.deserialize(context, entity) + else: + result = {} + + for key, value in entity.items(): + result[key] = self.deserialize_entity(context, value) + + entity = result + + return entity + + def serialize_context(self, context): + return context.toDict() + + def deserialize_context(self, context): + return ctx.RequestContext.fromDict(context) + + +class RequestContextSerializer(oslo_messaging.Serializer): + def __init__(self): + pass + + def serialize_entity(self, context, entity): + return entity + + def deserialize_entity(self, context, entity): + return entity + + def serialize_context(self, context): + return context.toDict() + + def deserialize_context(self, context): + return ctx.RequestContext.fromDict(context) diff --git a/synergy/common/service.py b/synergy/common/service.py new file mode 100644 index 0000000..5ee03b9 --- /dev/null +++ b/synergy/common/service.py @@ -0,0 +1,68 @@ +import os +import signal +import sys + +try: + from oslo_config import cfg +except ImportError: + from oslo.config import cfg + +from synergy.common import log as logging + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) +SIGTERM_SENT = False + + +class Service(object): + def __init__(self, name): + self.name = name + signal.signal(signal.SIGTERM, self.sigterm_handler) + signal.signal(signal.SIGINT, self.sigterm_handler) + + def sigterm_handler(self, signum, frame): + global SIGTERM_SENT + if not SIGTERM_SENT: + LOG.info("Shutting down %s" % self.name) + SIGTERM_SENT = True + self.stop() + os.killpg(0, signal.SIGTERM) + + sys.exit() + + def getName(self): + return self.name + + def start(self): + pass + + def stop(self): + pass + + def wait(self): + pass + + def restart(self): + # Reload config files and restart service + CONF.reload_config_files() + self.stop() + self.start() diff --git a/synergy/common/utils.py b/synergy/common/utils.py new file mode 100644 index 0000000..2567894 --- /dev/null +++ b/synergy/common/utils.py @@ -0,0 +1,33 @@ +import sys +import traceback + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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 import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + + try: + return getattr(sys.modules[mod_str], class_str) + except AttributeError: + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) diff --git a/synergy/common/wsgi.py b/synergy/common/wsgi.py new file mode 100644 index 0000000..6582728 --- /dev/null +++ b/synergy/common/wsgi.py @@ -0,0 +1,299 @@ +import errno +import eventlet +import os +import re +import socket +import ssl +import time + +from synergy.common import log as logging +from sys import exc_info +from traceback import format_tb + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + +LOG = logging.getLogger(__name__) + + +class Dispatcher(object): + """Dispatcher + + The main WSGI application. Dispatch the current request to + the functions from above and store the regular expression + captures in the WSGI environment as `myapp.url_args` so that + the functions from above can access the url placeholders. + If nothing matches call the `not_found` function. + """ + + def __init__(self): + self.actions = {} + + def register(self, action, callback): + self.actions[action] = callback + + def unregister(self, action): + del self.actions[action] + + def __call__(self, environ, start_response): + """Call the application can catch exceptions.""" + appiter = None + # just call the application and send the output back unchanged + # but catch exceptions + + path = environ.get('PATH_INFO', '').lstrip('/') + application = None + + for regex, callback in self.actions.items(): + match = re.search(regex, path) + if match is not None: + environ['myapp.url_args'] = match.groups() + application = callback + break + + if application is not None: + try: + self.appiter = callback(environ, start_response) + for item in self.appiter: + yield item + # if an exception occours we get the exception information and + # prepare a traceback we can render + except Exception: + e_type, e_value, tb = exc_info() + traceback = ['Traceback (most recent call last):'] + traceback += format_tb(tb) + traceback.append('%s: %s' % (e_type.__name__, e_value)) + # we might have not a stated response by now. + # Try to start one with the status + # code 500 or ignore an raised exception if the application + # already started one. + try: + start_response("500 INTERNAL SERVER ERROR", + [('Content-Type', 'text/plain')]) + except Exception: + pass + yield '\n'.join(traceback) + + # wsgi applications might have a close function. + # If it exists it *must* be called. + if hasattr(appiter, 'close'): + self.appiter.close() + else: + """Called if no applations matches.""" + try: + start_response("404 NOT FOUND", + [('Content-Type', 'text/plain')]) + except Exception: + pass + yield "Not Found" + + +class WSGILog(object): + """A thin wrapper that responds to `write` and logs.""" + + def __init__(self, logger, level=20): + self.logger = logger + self.level = level + + def write(self, msg): + self.logger.log(self.level, msg.rstrip()) + + +class Server(object): + """Server class to manage multiple WSGI sockets and applications.""" + + def __init__(self, name, host_name, host_port=8051, threads=1000, + application=None, use_ssl=False, ssl_ca_file=None, + ssl_cert_file=None, ssl_key_file=None, max_header_line=16384, + retry_until_window=30, tcp_keepidle=600, backlog=4096): + + """Parameters + + name: the server's name + host_name: the host's name + host_port: + application: + backlog: number of backlog requests to configure the socket with + tcp_keepidle: sets the value of TCP_KEEPIDLE in seconds for each server + socket. Not supported on OS X + retry_until_window: number of seconds to keep retrying to listen + max_header_line: max header line to accommodate large tokens + use_ssl: enable SSL on the API server + ssl_ca_file: CA certificate file to use to verify connecting clients + ssl_cert_file: the certificate file + ssl_key_file: the private key file + """ + + # Raise the default from 8192 to accommodate large tokens + eventlet.wsgi.MAX_HEADER_LINE = max_header_line + + self.name = name + self.host_name = host_name + self.host_port = host_port + self.application = application + self.threads = threads + self.socket = None + self.use_ssl = use_ssl + self.tcp_keepidle = tcp_keepidle + self.backlog = backlog + self.retry_until_window = retry_until_window + self.running = False + self.dispatcher = Dispatcher() + + if not application: + self.application = self.dispatcher + + if use_ssl: + if not os.path.exists(ssl_cert_file): + raise RuntimeError("Unable to find ssl_cert_file: %s" + % ssl_cert_file) + + if not os.path.exists(ssl_key_file): + raise RuntimeError("Unable to find ssl_key_file : %s" + % ssl_key_file) + + # ssl_ca_file is optional + if ssl_ca_file and not os.path.exists(ssl_ca_file): + raise RuntimeError("Unable to find ssl_ca_file: %s" + % ssl_ca_file) + + self.ssl_kwargs = { + 'server_side': True, + 'certfile': ssl_cert_file, + 'keyfile': ssl_key_file, + 'cert_reqs': ssl.CERT_NONE, + } + + if ssl_ca_file: + self.ssl_kwargs['ca_certs'] = ssl_ca_file + self.ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED + + def register(self, action, callback): + self.dispatcher.register(action, callback) + + def unregister(self, action): + self.dispatcher.unregister(action) + + def start(self): + """Run a WSGI server with the given application. + + :param application: The application to be run in the WSGI server + :param port: Port to bind to if none is specified in conf + """ + + pgid = os.getpid() + try: + # NOTE(flaper87): Make sure this process + # runs in its own process group. + os.setpgid(pgid, pgid) + except OSError: + pgid = 0 + + try: + info = socket.getaddrinfo(self.host_name, + self.host_port, + socket.AF_UNSPEC, + socket.SOCK_STREAM)[0] + family = info[0] + bind_addr = info[-1] + except Exception as ex: + LOG.error("Unable to listen on %s:%s: %s" + % (self.host_name, self.host_port, ex)) + raise ex + + retry_until = time.time() + self.retry_until_window + exception = None + + while not self.socket and time.time() < retry_until: + try: + self.socket = eventlet.listen(bind_addr, + backlog=self.backlog, + family=family) + if self.use_ssl: + self.socket = ssl.wrap_socket(self.socket, + **self.ssl_kwargs) + + if self.use_ssl: + ssl.wrap_socket(self.sock, **self.ssl_kwarg) + + except socket.error as ex: + exception = ex + LOG.error("Unable to listen on %s:%s: %s" + % (self.host_name, self.host_port, ex)) + + if ex.errno == errno.EADDRINUSE: + retry_until = 0 + eventlet.sleep(0.1) + break + + if exception is not None: + raise exception + + if not self.socket: + raise RuntimeError("Could not bind to %s:%s after trying for %d s" + % (self.host_host, self.host_port, + self.retry_until_window)) + + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # sockets can hang around forever without keepalive + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + # This option isn't available in the OS X version of eventlet + if hasattr(socket, 'TCP_KEEPIDLE'): + self.socket.setsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE, + self.tcp_keepidle) + + os.umask(0o27) # ensure files are created with the correct privileges + + self.pool = eventlet.GreenPool(self.threads) + self.pool.spawn_n(self._single_run, self.application, self.socket) + + self.running = True + + def isRunning(self): + return self.running + + def stop(self): + LOG.info("shutting down: requests left: %s", self.pool.running()) + self.running = False + self.pool.resize(0) + # self.pool.waitall() + + if self.socket: + eventlet.greenio.shutdown_safe(self.socket) + self.socket.close() + + self.running = False + + def wait(self): + """Wait until all servers have completed running""" + try: + self.pool.waitall() + except KeyboardInterrupt: + pass + + def _single_run(self, application, sock): + """Start a WSGI server in a new green thread.""" + LOG.info("Starting single process server") + eventlet.wsgi.server(sock, application, + custom_pool=self.pool, + log=WSGILog(LOG), + debug=False) diff --git a/synergy/examples/__init__.py b/synergy/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synergy/examples/timer_manager.py b/synergy/examples/timer_manager.py new file mode 100644 index 0000000..f6f28d4 --- /dev/null +++ b/synergy/examples/timer_manager.py @@ -0,0 +1,44 @@ +import time + +from synergy.common import log as logging +from synergy.common.manager import Manager + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + +LOG = logging.getLogger(__name__) + + +class TimerManager(Manager): + + def __init__(self): + Manager.__init__(self, name="TimerManager") + + def setup(self): + LOG.info("%s setup invoked!" % (self.name)) + + def execute(self, cmd): + LOG.info("%s execute invoked!" % (self.name)) + LOG.info("command name=%s" % (cmd.getName())) + + def destroy(self): + LOG.info("%s destroy invoked!" % (self.name)) + + def task(self): + localtime = time.asctime(time.localtime(time.time())) + LOG.info("Local current time: %s" % (localtime)) diff --git a/synergy/service.py b/synergy/service.py new file mode 100644 index 0000000..2a9b9f1 --- /dev/null +++ b/synergy/service.py @@ -0,0 +1,476 @@ +import eventlet +import json +import sys + +from cgi import escape +from cgi import parse_qs +from pkg_resources import iter_entry_points +from synergy.common import config +from synergy.common import log as logging +from synergy.common import serializer +from synergy.common import service +from synergy.common import wsgi + +try: + from oslo_config import cfg +except ImportError: + from oslo.config import cfg + + +__author__ = "Lisa Zangrando" +__email__ = "lisa.zangrando[AT]pd.infn.it" +__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud +All Rights Reserved + +Licensed under the Apache License, Version 2.0; +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.""" + + +CONF = cfg.CONF +LOG = None +MANAGER_ENTRY_POINT = "synergy.managers" # used to discover Synergy managers + + +class ManagerRPC(object): + + def __init__(self, managers): + self.managers = managers + + def list(self, ctx, **args): + result = [] + + for name, manager in self.managers.items(): + result.append(name) + + return result + + def start(self, ctx, **args): + manager_name = args.get("arg").get("manager", None) + result = {} + + for name, manager in self.managers.items(): + if manager.getStatus() == "ACTIVE" \ + and (not manager_name or manager_name == name): + LOG.info("starting the %s manager" % (name)) + try: + # self.managers[name].start() + self.managers[name].setStatus("RUNNING") + LOG.info("%s manager started" % (name)) + + result[name] = manager.getStatus() + except Exception as ex: + self.managers[name].setStatus("ERROR") + LOG.error("error occurred during the manager start-up %s" + % (ex)) + + result[name] = manager.getStatus() + pass + + return result + + def stop(self, ctx, **args): + manager_name = args.get("arg").get("manager", None) + result = {} + + for name, manager in self.managers.items(): + if manager.getStatus() == "RUNNING" \ + and (not manager_name or manager_name == name): + LOG.info("stopping the %s manager" % (name)) + try: + # self.managers[name].stop() + self.managers[name].setStatus("ACTIVE") + LOG.info("%s manager stopped" % (name)) + + result[name] = manager.getStatus() + except Exception as ex: + self.managers[name].setStatus("ERROR") + LOG.error("error occurred during the manager stop %s" + % (ex)) + + result[name] = manager.getStatus() + pass + + return result + + def execute(self, ctx, **args): + manager_name = args.get("arg").get("manager", None) + command = args.get("arg").get("command", None) + result = {} + + if not manager_name: + result["error"] = "manager name not defined!" + + if not command: + result["error"] = "command not defined!" + + if manager_name in self.managers: + manager = self.managers[manager_name] + manager.execute(cmd=command) + result["command"] = "OK" + + return result + + def status(self, ctx, **args): + manager_name = args.get("arg").get("manager", None) + result = {} + + for name, manager in self.managers.items(): + if not manager_name or manager_name == name: + result[name] = manager.getStatus() + + return result + + +class Synergy(service.Service): + """Service object for binaries running on hosts. + + A service takes a manager and enables rpc by listening to queues based + on topic. It also periodically runs tasks on the manager and reports + it state to the database services table. + """ + + def __init__(self, *args, **kwargs): + super(Synergy, self).__init__("Synergy") + + self.managers = {} + + for entry in iter_entry_points(MANAGER_ENTRY_POINT): + LOG.info("loading manager %r", entry.name) + + try: + """ + found = False + + try: + CONF.get(entry.name) + found = True + except Exception as ex: + LOG.info("missing section [%s] in synergy.conf for manager" + " %r: using the default values" + % (entry.name, entry.name)) + """ + + CONF.register_opts(config.manager_opts, group=entry.name) + + manager_conf = CONF.get(entry.name) + manager_class = entry.load() + + manager_obj = manager_class(*args, **kwargs) + LOG.info("manager instance %r created!", entry.name) + + manager_obj.setAutoStart(manager_conf.autostart) + manager_obj.setRate(manager_conf.rate) + + self.managers[manager_obj.getName()] = manager_obj + + CONF.register_opts(manager_obj.getOptions(), group=entry.name) + except Exception as ex: + LOG.error("Exception has occured", exc_info=1) + + LOG.error("manager %r instantiation error: %s" + % (entry.name, ex)) + self.managers[manager_obj.getName()].setStatus("ERROR") + + raise Exception("manager %r instantiation error: %s" + % (entry.name, ex)) + + for name, manager in self.managers.items(): + manager.managers = self.managers + + try: + manager.setup() + manager.setStatus("ACTIVE") + + LOG.info("manager '%s' initialized!" % (manager.getName())) + except Exception as ex: + LOG.error("manager '%s' instantiation error: %s" % (name, ex)) + self.managers[manager.getName()].setStatus("ERROR") + raise ex + + self.saved_args, self.saved_kwargs = args, kwargs + + def listManagers(self, environ, start_response): + result = [] + + for name, manager in self.managers.items(): + result.append(name) + + start_response("200 OK", [("Content-Type", "text/html")]) + return ["%s" % json.dumps(result)] + + def getManagerStatus(self, environ, start_response): + manager_list = None + result = {} + + query = environ.get("QUERY_STRING", None) + + if query: + parameters = parse_qs(query) + + if "manager" in parameters: + if isinstance(parameters['manager'], (list, tuple)): + manager_list = parameters['manager'] + else: + manager_list = [parameters['manager']] + + for manager in manager_list: + escape(manager) + + for name, manager in self.managers.items(): + if not manager_list or name in manager_list: + result[name] = manager.getStatus() + + if manager_list and len(manager_list) == 1 and len(result) == 0: + start_response("404 NOT FOUND", [("Content-Type", "text/plain")]) + return ["manager %r not found!" % manager_list[0]] + else: + start_response("200 OK", [("Content-Type", "text/html")]) + return ["%s" % json.dumps(result)] + + def executeCommand(self, environ, start_response): + manager_name = None + command = None + + synergySerializer = serializer.SynergySerializer() + query = environ.get("QUERY_STRING", None) + # LOG.info("QUERY_STRING %s" % query) + if query: + parameters = parse_qs(query) + + if "manager" in parameters: + manager_name = escape(parameters['manager'][0]) + + if "command" in parameters: + command_string = escape(parameters['command'][0]) + command_string = command_string.replace("'", "\"") + entity = json.loads(command_string) + command = synergySerializer.deserialize_entity(context=None, + entity=entity) + + if not query or not manager_name or not command: + start_response("404 NOT FOUND", [("Content-Type", "text/plain")]) + return ["wrong query"] + + if manager_name in self.managers: + manager = self.managers[manager_name] + try: + manager.execute(cmd=command) + result = synergySerializer.serialize_entity(context=None, + entity=command) + # LOG.info("command result %s" % result) + + start_response("200 OK", [("Content-Type", "text/html")]) + return ["%s" % json.dumps(result)] + except Exception as ex: + LOG.info("executeCommand error: %s" % ex) + start_response("404 NOT FOUND", + [("Content-Type", "text/plain")]) + return ["error: %s" % ex] + else: + start_response("404 NOT FOUND", [("Content-Type", "text/plain")]) + return ["manager %r not found!" % manager_name] + + def startManager(self, environ, start_response): + manager_list = None + result = {} + + # synergySerializer = serializer.SynergySerializer() + query = environ.get("QUERY_STRING", None) + + if query: + parameters = parse_qs(query) + + if "manager" in parameters: + if isinstance(parameters['manager'], (list, tuple)): + manager_list = parameters['manager'] + else: + manager_list = [parameters['manager']] + + for manager in manager_list: + escape(manager) + + for name, manager in self.managers.items(): + if not manager_list or name in manager_list: + result[name] = {} + + if manager.getStatus() == "ACTIVE": + LOG.info("starting the %r manager" % (name)) + try: + # self.managers[name].start() + self.managers[name].setStatus("RUNNING") + LOG.info("%r manager started!" % (name)) + + result[name]["message"] = "started successfully" + except Exception as ex: + self.managers[name].setStatus("ERROR") + LOG.error("error occurred during the manager start-up" + "%s" % (ex)) + + result[name]["message"] = "ERROR: %s" % ex + pass + else: + result[name]["message"] = "WARN: already started" + + result[name]["status"] = manager.getStatus() + + if manager_list and len(manager_list) == 1 and len(result) == 0: + start_response("404 NOT FOUND", [("Content-Type", "text/plain")]) + return ["manager %r not found!" % manager_list[0]] + else: + start_response("200 OK", [("Content-Type", "text/html")]) + return ["%s" % json.dumps(result)] + + def stopManager(self, environ, start_response): + manager_list = None + result = {} + + query = environ.get("QUERY_STRING", None) + + if query: + parameters = parse_qs(query) + + if "manager" in parameters: + if isinstance(parameters['manager'], (list, tuple)): + manager_list = parameters['manager'] + else: + manager_list = [parameters['manager']] + + for manager in manager_list: + escape(manager) + + for name, manager in self.managers.items(): + if not manager_list or name in manager_list: + result[name] = {} + + if manager.getStatus() == "RUNNING": + LOG.info("stopping the %r manager" % (name)) + try: + # self.managers[name].stop() + self.managers[name].setStatus("ACTIVE") + LOG.info("%r manager stopped!" % (name)) + + result[name]["message"] = "stopped successfully" + except Exception as ex: + self.managers[name].setStatus("ERROR") + LOG.error("error occurred during the manager stop: %s" + % (ex)) + + result[name]["message"] = "ERROR: %s" % ex + pass + else: + result[name]["message"] = "WARN: already stopped" + + result[name]["status"] = manager.getStatus() + + if manager_list and len(manager_list) == 1 and len(result) == 0: + start_response("404 NOT FOUND", [("Content-Type", "text/plain")]) + return ["manager %r not found!" % manager_list[0]] + else: + start_response("200 OK", [("Content-Type", "text/html")]) + return ["%s" % json.dumps(result)] + + def start(self): + self.model_disconnected = False + + for name, manager in self.managers.items(): + if manager.getStatus() != "ERROR" and manager.isAutoStart(): + try: + LOG.info("starting the %r manager" % (name)) + manager.start() + manager.setStatus("RUNNING") + LOG.info("%r manager started! (rate=%s min)" + % (name, manager.getRate())) + except Exception as ex: + LOG.error("error occurred during the manager start %s" + % (ex)) + manager.setStatus("ERROR") + raise ex + + self.wsgi_server = wsgi.Server( + name="WSGI server", + host_name=CONF.WSGI.host, + host_port=CONF.WSGI.port, + threads=CONF.WSGI.threads, + use_ssl=CONF.WSGI.use_ssl, + ssl_ca_file=CONF.WSGI.ssl_ca_file, + ssl_cert_file=CONF.WSGI.ssl_cert_file, + ssl_key_file=CONF.WSGI.ssl_key_file, + max_header_line=CONF.WSGI.max_header_line, + retry_until_window=CONF.WSGI.retry_until_window, + tcp_keepidle=CONF.WSGI.tcp_keepidle, + backlog=CONF.WSGI.backlog) + + self.wsgi_server.register(r'^$', wsgi.index) + self.wsgi_server.register(r'synergy/list', self.listManagers) + self.wsgi_server.register(r'synergy/status', self.getManagerStatus) + self.wsgi_server.register(r'synergy/execute', self.executeCommand) + self.wsgi_server.register(r'synergy/start', self.startManager) + self.wsgi_server.register(r'synergy/stop', self.stopManager) + self.wsgi_server.start() + + LOG.info("STARTED!") + self.wsgi_server.wait() + + def kill(self): + """Destroy the service object in the datastore.""" + LOG.warn("killing service") + self.stop() + LOG.warn("Service killed") + + def stop(self): + for name, manager in self.managers.items(): + LOG.info("destroying the %s manager" % (name)) + try: + manager.setStatus("DESTROYED") + manager.destroy() + # manager.join() + # LOG.info("%s manager destroyed" % (name)) + except Exception as ex: + manager.setStatus("ERROR") + LOG.error("error occurred during the manager destruction: %s" + % ex) + + if self.wsgi_server: + self.wsgi_server.stop() + + LOG.info("STOPPED!") + + +def main(): + try: + eventlet.monkey_patch(os=False) + + # the configuration will be into the cfg.CONF global data structure + config.parse_args(args=sys.argv[1:], + default_config_files=["/etc/synergy/synergy.conf"]) + + if not cfg.CONF.config_file: + sys.exit("ERROR: Unable to find configuration file via the " + "default search paths (~/.synergy/, ~/, /etc/synergy/" + ", /etc/) and the '--config-file' option!") + + global LOG + # LOG = logging.getLogger(None) + + LOG = logging.getLogger(__name__) + LOG.info("Starting Synergy...") + + # set session ID to this process so we can kill group in sigterm + # os.setsid() + + server = Synergy() + server.start() + + LOG.info("Synergy started") + except Exception as ex: + LOG.error("unrecoverable error: %s" % ex) diff --git a/synergy/tests/__init__.py b/synergy/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synergy/tests/base.py b/synergy/tests/base.py new file mode 100644 index 0000000..bc2d9c8 --- /dev/null +++ b/synergy/tests/base.py @@ -0,0 +1,18 @@ +# 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 oslotest import base + + +class TestCase(base.BaseTestCase): + + """Test case base class for all unit tests.""" diff --git a/synergy/tests/test_manager.py b/synergy/tests/test_manager.py new file mode 100644 index 0000000..e02e79c --- /dev/null +++ b/synergy/tests/test_manager.py @@ -0,0 +1,27 @@ +# coding: utf-8 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from synergy.common.manager import Manager +from synergy.tests import base + + +class TestManager(base.TestCase): + + def setUp(self): + super(TestManager, self).setUp() + self.manager = Manager(name="dummy_manager") + + def test_name(self): + self.assertEqual(self.manager.getName(), "dummy_manager") diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..21a7e3b --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,14 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking<0.11,>=0.10.0 + +coverage>=3.6 +python-subunit>=0.0.18 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 +oslosphinx>=2.5.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 +testscenarios>=0.4 +testtools>=1.4.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a1cec90 --- /dev/null +++ b/tox.ini @@ -0,0 +1,60 @@ +[tox] +minversion = 2.0 +envlist = py27-constraints,pep8-constraints +skipsdist = True + +[testenv] +usedevelop = True +install_command = + constraints: {[testenv:common-constraints]install_command} + pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/test-requirements.txt +commands = python setup.py test --slowest --testr-args='{posargs}' + +[testenv:common-constraints] +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:pep8-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:venv-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:cover-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:docs-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py build_sphinx + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[testenv:debug-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build