diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5728467 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# python +*.py[cod] + +# packaging +*.egg +*.egg-info +build +eggs +parts +var +sdist +dist +develop-eggs +.installed.cfg +lib64 +deb_dist +__pycache__ + +# Installer logs +pip-log.txt + +# spec +.coverage +.tox +.cache +nosetests.xml + +# dev +.idea +*~ +*.swp + +# Packaging +MANIFEST + +# cython +*.c +*.so + +# doc +_build diff --git a/debian/.gitignore b/debian/.gitignore new file mode 100644 index 0000000..c4cc071 --- /dev/null +++ b/debian/.gitignore @@ -0,0 +1,2 @@ +python-ocf-agent* +files diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..709437d --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +ocf-agent (1.4) unstable; urgency=low + + * source package automatically created by stdeb 0.6.0+git + + -- Dmitry Ilyin Tue, 01 Dec 2015 18:01:39 +0300 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..7561087 --- /dev/null +++ b/debian/control @@ -0,0 +1,11 @@ +Source: ocf-agent +Maintainer: Dmitry Ilyin +Section: python +Priority: optional +Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 7) +Standards-Version: 3.9.1 + +Package: python-ocf-agent +Architecture: all +Depends: ${misc:Depends}, ${python:Depends}, python-psutil (>= 1) +Description: Python library for Pacemaker OCF agents diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..9994e18 --- /dev/null +++ b/debian/copyright @@ -0,0 +1 @@ +Mirantis LLC diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..00983ef --- /dev/null +++ b/debian/rules @@ -0,0 +1,9 @@ +#!/usr/bin/make -f + +# This file was automatically generated by stdeb 0.6.0+git at +# Tue, 01 Dec 2015 18:01:39 +0300 + +%: + dh $@ --with python2 --buildsystem=python_distutils + + diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..d3827e7 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +1.0 diff --git a/devel_requirements.txt b/devel_requirements.txt new file mode 100644 index 0000000..dc34c56 --- /dev/null +++ b/devel_requirements.txt @@ -0,0 +1,7 @@ +mock +pytest +flake8 +psutil + +sphinx +cloud_sptheme diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..9391050 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ocf_agent.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ocf_agent.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/ocf_agent" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ocf_agent" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..6c79859 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# +# ocf_agent documentation build configuration file, created by +# sphinx-quickstart on Sun Nov 29 14:53:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('.')) + +from ocf_agent import AUTHOR +from ocf_agent import COPYRIGHT +from ocf_agent import PROJECT +from ocf_agent import VERSION + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# 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.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', +] + +autoclass_content = 'both' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = unicode(PROJECT) +copyright = unicode(COPYRIGHT) +author = unicode(AUTHOR) + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = VERSION +# The full version, including alpha/beta/rc tags. +release = VERSION + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['.tox', 'tests', '_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'cloud' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ocf_agent_doc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + 'papersize': 'a4paper', + + # The font size ('10pt', '11pt' or '12pt'). + 'pointsize': '12pt', + + # Additional stuff for the LaTeX preamble. + 'preamble': '', + + # Latex figure (float) alignment + 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'ocf_agent.tex', u'ocf\\_agent Documentation', + u'Dmitry Ilyin', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'ocf_agent', u'ocf_agent Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'ocf_agent', u'ocf_agent Documentation', + author, 'ocf_agent', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..12d4ccb --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +Welcome to ocf_agent's documentation! +===================================== + +Contents: + +.. toctree:: + :maxdepth: 4 + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..49dee14 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ocf_agent.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ocf_agent.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..de6af3d --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +ocf_agent +========= + +.. toctree:: + :maxdepth: 4 + + ocf_agent diff --git a/docs/ocf_agent b/docs/ocf_agent new file mode 120000 index 0000000..e1067cc --- /dev/null +++ b/docs/ocf_agent @@ -0,0 +1 @@ +../ocf_agent \ No newline at end of file diff --git a/docs/ocf_agent.modules.rst b/docs/ocf_agent.modules.rst new file mode 100644 index 0000000..fe7e678 --- /dev/null +++ b/docs/ocf_agent.modules.rst @@ -0,0 +1,70 @@ +ocf_agent.modules package +========================= + +Submodules +---------- + +ocf_agent.modules.environment module +------------------------------------ + +.. automodule:: ocf_agent.modules.environment + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.exit module +----------------------------- + +.. automodule:: ocf_agent.modules.exit + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.handlers module +--------------------------------- + +.. automodule:: ocf_agent.modules.handlers + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.lock module +----------------------------- + +.. automodule:: ocf_agent.modules.lock + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.log module +---------------------------- + +.. automodule:: ocf_agent.modules.log + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.metadata module +--------------------------------- + +.. automodule:: ocf_agent.modules.metadata + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.modules.parameters module +----------------------------------- + +.. automodule:: ocf_agent.modules.parameters + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ocf_agent.modules + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/ocf_agent.rst b/docs/ocf_agent.rst new file mode 100644 index 0000000..e9cd1cc --- /dev/null +++ b/docs/ocf_agent.rst @@ -0,0 +1,61 @@ +ocf_agent package +================= + +Subpackages +----------- + +.. toctree:: + + ocf_agent.modules + +Submodules +---------- + +ocf_agent.agent module +---------------------- + +.. automodule:: ocf_agent.agent + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.constants module +-------------------------- + +.. automodule:: ocf_agent.constants + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.handler module +------------------------ + +.. automodule:: ocf_agent.handler + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.helpers module +------------------------ + +.. automodule:: ocf_agent.helpers + :members: + :undoc-members: + :show-inheritance: + +ocf_agent.parameter module +-------------------------- + +.. automodule:: ocf_agent.parameter + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ocf_agent + :members: + :undoc-members: + :show-inheritance: diff --git a/examples/compile-dummy.sh b/examples/compile-dummy.sh new file mode 100755 index 0000000..9a7a2c8 --- /dev/null +++ b/examples/compile-dummy.sh @@ -0,0 +1,4 @@ +rm -f dummy.c dummy dummy.o +cython --embed -o dummy.c dummy.py +gcc -pthread -c dummy.c -I/usr/include/python2.7 -I/usr/include/python2.7 +gcc -pthread -o dummy dummy.o -L/usr/lib -L/usr/lib/python2.7/config-x86_64-linux-gnu -lpython2.7 -lpthread -ldl -lutil -lm -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions diff --git a/examples/dummy.py b/examples/dummy.py new file mode 100755 index 0000000..498a925 --- /dev/null +++ b/examples/dummy.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from ocf_agent.agent import Agent +from ocf_agent.parameter import StringParameter +from ocf_agent.handler import Handler +from ocf_agent.handler import MonitorHandler + + +class DummyOCF(Agent): + VERSION = "1.0" + LOCK_DIR = '/tmp' + SHORTDESC = "Dummy OCF agent" + LONGDESC = "This OCF agent does nothing. It can be used for testing in" \ + "both simple and master modes." + LOG_HANDLERS = ['console'] + + # PARAMETERS # + + class OCFParameter_fake(StringParameter): + DEFAULT = 'fake value' + LONGDESC = "A fake parameter that has no effect" + SHORTDESC = "A fake parameter" + + # HANDLERS # + + class OCFHandler_start(Handler): + LONGDESC = "Start the dummy service" + SHORTDESC = "Start service" + + class OCFHandler_stop(Handler): + LONGDESC = "Stop the dummy service" + SHORTDESC = "Stop service" + + class OCFHandler_reload(Handler): + LONGDESC = "Reload the dummy service" + SHORTDESC = "Reload service" + + class OCFHandler_monitor(MonitorHandler): + LONGDESC = "Monitor the dummy service" + SHORTDESC = "Monitor service" + + class OCFHandler_promote(Handler): + LONGDESC = "Promote the dummy service to the master mode" + SHORTDESC = "Promote service" + + class OCFHandler_demote(Handler): + LONGDESC = "Demote the dummy service from the master mode" + SHORTDESC = "Demote service" + + class OCFHandler_notify(Handler): + LONGDESC = "Notify the dummy service if the master status changes" + SHORTDESC = "Notify service" + + class OCFHandler_migrate_to(Handler): + LONGDESC = "Ask the dummy service to migrate to the other node" + SHORTDESC = "Migrate service to node" + + class OCFHandler_migrate_from(Handler): + LONGDESC = "Ask the dummy service to migrate from the other node" + SHORTDESC = "Migrate service from node" + + ########################################################################### + + def handler_promote(self): + self.lock.create_file('master') + self.lock.create() + self.exit.success('The agent was promoted') + + def handler_demote(self): + self.lock.remove_file('master') + self.lock.create() + self.exit.success('The agent was demoted') + + def handler_start(self): + self.lock.create() + self.exit.success('The agent was started') + + def handler_stop(self): + self.lock.remove() + self.exit.success('The agent was stopped') + + def handler_monitor(self): + if self.lock.file_is_present('master'): + self.exit.running_master('The agent is running in the master mode') + if self.lock.is_present: + self.exit.success('The agent is running') + self.exit.not_running('The agent is not running') + + def handler_reload(self): + self.lock.create() + self.exit.success('The agent was reloaded') + + def handler_notify(self): + self.lock.create() + self.exit.success('The agent was notified') + + def handler_migrate_to(self): + self.lock.remove() + self.exit.success('The agent will migrate to') + + def handler_migrate_from(self): + self.lock.create() + self.exit.success('The agent will migrate from') + + +############################################################################### + +if __name__ == "__main__": + ocf = DummyOCF() + ocf.call() diff --git a/examples/ocf-tester-wrapper.sh b/examples/ocf-tester-wrapper.sh new file mode 100755 index 0000000..0448636 --- /dev/null +++ b/examples/ocf-tester-wrapper.sh @@ -0,0 +1,7 @@ +#!/bin/sh +if [ -f "${SCRIPT}" ]; then + python "${SCRIPT}" ${@} +else + echo "There is no such script: '${SCRIPT}'" + exit 1 +fi diff --git a/examples/ocf_agent b/examples/ocf_agent new file mode 120000 index 0000000..e1067cc --- /dev/null +++ b/examples/ocf_agent @@ -0,0 +1 @@ +../ocf_agent \ No newline at end of file diff --git a/examples/run-ocf-tester.sh b/examples/run-ocf-tester.sh new file mode 100755 index 0000000..486cc7f --- /dev/null +++ b/examples/run-ocf-tester.sh @@ -0,0 +1,31 @@ +#!/bin/sh +export OCF_ROOT='/usr/lib/ocf' +export TESTER_OPTS='-v -d' + +check_ocf_tester() { + which ocf-tester 1>/dev/null 2>/dev/null + if [ $? -gt "0" ]; then + echo "OCF Tester is not installed. Skipping tests!" + exit 0 + fi +} + +run_ocf_tester() { + echo "========================================" + echo "Running ocf-tester on script '${SCRIPT}'" + ocf-tester ${TESTER_OPTS} -n 'ocf-tester' ocf-tester-wrapper.sh + if [ $? -gt "0" ]; then + echo "Script: '${SCRIPT}' have FAILED the test!" + exit 1 + else + echo "Script: '${SCRIPT}' have PASSED the test!" + fi + echo "========================================" +} + +check_ocf_tester + +for script in ${@}; do + export SCRIPT="${script}" + run_ocf_tester +done diff --git a/examples/sleep.py b/examples/sleep.py new file mode 100755 index 0000000..9c4fb83 --- /dev/null +++ b/examples/sleep.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from ocf_agent.agent import Agent +from ocf_agent.parameter import StringParameter +from ocf_agent.handler import Handler +from ocf_agent.handler import MonitorHandler + + +class SleepOCF(Agent): + VERSION = "1.0" + LOCK_DIR = '/tmp' + PID_DIR = '/tmp' + SHORTDESC = "Sleep test OCF agent" + LONGDESC = "This OCF agent can run 'sleep' command as a daemon" + LOG_HANDLERS = ['console'] + + # PARAMETERS # + + class OCFParameter_time(StringParameter): + DEFAULT = '10000' + LONGDESC = "How long the sleep process should work" + SHORTDESC = "Sleep time" + + # HANDLERS # + + class OCFHandler_start(Handler): + LONGDESC = "Start the dummy service" + SHORTDESC = "Start service" + + class OCFHandler_stop(Handler): + LONGDESC = "Stop the dummy service" + SHORTDESC = "Stop service" + + class OCFHandler_reload(Handler): + LONGDESC = "Reload the dummy service" + SHORTDESC = "Reload service" + + class OCFHandler_monitor(MonitorHandler): + LONGDESC = "Monitor the dummy service" + SHORTDESC = "Monitor service" + + ########################################################################### + + COMMAND = 'sleep' + + @property + def is_running(self): + return self.process.is_running(self.pid.number) + + def handler_start(self): + if self.is_running: + self.exit.success( + 'Process is already running with pid: "%s"' % ( + self.pid.number, + ) + ) + + process = self.process.daemonize( + self.COMMAND, + self.param('time') + ) + pid = process.pid + self.pid.create(pid) + self.exit.success( + 'Process started with pid: "%s" pid_file "%s"!' % ( + pid, + self.pid.path, + ) + ) + + def handler_stop(self): + if not self.is_running: + self.exit.success('Process is already not running!') + + pid = self.pid.number + result = self.process.ensure_terminate(pid) + + if result: + self.pid.remove() + self.exit.success( + 'Process: "%s" have been stopped!' % ( + pid, + ) + ) + + self.exit.error_generic( + 'Process: "%s" have FAILED to stop!' % ( + pid, + ) + ) + + def handler_monitor(self): + if not self.pid.present: + self.exit.not_running( + 'There is no pid_file: "%s". Service is not running!' % ( + self.pid.path + ) + ) + + if not self.is_running: + self.exit.not_running( + 'Pid_file: "%s" is present with pid: "%s" ' + 'but the service is not running' % ( + self.pid.path, + self.pid.number, + ) + ) + + self.exit.success( + 'Process is running with pid: "%s"' % self.pid.number + ) + + ####################################################################### + + +if __name__ == "__main__": + ocf = SleepOCF() + ocf.call() diff --git a/install_requirements.txt b/install_requirements.txt new file mode 100644 index 0000000..a4d92cc --- /dev/null +++ b/install_requirements.txt @@ -0,0 +1 @@ +psutil diff --git a/ocf_agent/__init__.py b/ocf_agent/__init__.py new file mode 100644 index 0000000..12ba189 --- /dev/null +++ b/ocf_agent/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +PROJECT = 'ocf_agent' +VERSION = '1.5' +AUTHOR = 'Dmitry Ilyin' +EMAIL = 'dilyin@mirantis.com' +COPYRIGHT = 'Mirantis inc.' +LICENSE = 'Apache v2.0' diff --git a/ocf_agent/agent.py b/ocf_agent/agent.py new file mode 100644 index 0000000..9e3b2eb --- /dev/null +++ b/ocf_agent/agent.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- + +import sys +from ocf_agent import constants +from ocf_agent.helpers import docstring_format +from ocf_agent.helpers import memoization +from ocf_agent.modules.environment import Environment +from ocf_agent.modules.exit import Exit +from ocf_agent.modules.handlers import Handlers +from ocf_agent.modules.lock import Lock +from ocf_agent.modules.pid import Pid +from ocf_agent.modules.process import Process +from ocf_agent.modules.log import Log +from ocf_agent.modules.metadata import MetaData +from ocf_agent.modules.parameters import Parameters + + +class Agent(object): + _action = None + + @property + @docstring_format(constants.CONST_NAME) + def name(self): + """ + Name of this OCF agent. It's either taken from the class name + or can be manually set by the *{0}* constant. + + :return: The agent name + :rtype: str + """ + return getattr( + self, + constants.CONST_NAME, + self.__class__.__name__, + ) + + @property + @docstring_format(constants.CONST_VERSION, constants.DEFAULT_VERSION) + def version(self): + """ + The agent's version number. Can be set by the *{0}* constant or will + default to **{1}**. + + :return: The agent version + :rtype: str + """ + return getattr( + self, + constants.CONST_VERSION, + constants.DEFAULT_VERSION, + ) + + @property + @docstring_format(constants.CONST_SHORT_DESCRIPTION) + def short_description(self): + """ + Short description string of this agent. Can be manually set by the + *{0}* constant. Will default to name if not set. + + :return: Agent's short description + :rtype: str + """ + return getattr( + self, + constants.CONST_SHORT_DESCRIPTION, + self.name, + ) + + @property + @docstring_format(constants.CONST_LONG_DESCRIPTION) + def long_description(self): + """ + Long description string of this agent. Can be manually set by + the *{0}* constant. Will default to the short description unless + defined. + + :return: Agent's long description + :rtype: str + """ + return getattr( + self, + constants.CONST_LONG_DESCRIPTION, + self.short_description, + ) + + @property + @docstring_format(constants.CONST_LANGUAGE, constants.DEFAULT_LANGUAGE) + def language(self): + """ + The language reported by this agent in its metadata XML. + It can be manually set by the *{0}* constant and will return the + default value **{1}** if not defined. + + :return: The agent's language + :rtype: str + """ + return getattr( + self, + constants.CONST_LANGUAGE, + constants.DEFAULT_LANGUAGE, + ) + + @property + @docstring_format(constants.CONST_ENCODING, constants.DEFAULT_ENCODING) + def encoding(self): + """ + Returns the encoding used by this agent in its metadata and logging. + Can be manually defined be the *{0}* constant or will use the + default value **{1}**. + + :return: Agent's encoding + :rtype: str + """ + return getattr( + self, + constants.CONST_ENCODING, + constants.DEFAULT_ENCODING, + ) + + @property + def action(self): + """ + Returns the action name the OCF have been called with or the + manually defined action. The action will be used to determine which + handler should be run. Returns None if no action was defined. + + :return: The defined action + :rtype: str or None + """ + if self._action is not None: + return self._action + if len(sys.argv) >= 2: + action = sys.argv[1] + self.action = action + return self._action + + @action.setter + def action(self, value=None): + """ + Manually sets the resource agent's action + + :type value: str + :param value: The action name + """ + if value is None: + self._action = None + return + if value in constants.ALIAS_HANDLERS: + value = constants.ALIAS_HANDLERS[value] + self._action = value + + def call(self, action=None): + """ + Call the agent with defined action. It will try to find a handler + function for this action and run it. + + :param action: Run with this action + :type action: str + """ + if action: + self.action = action + self.validate() + if self.action == 'validate-all': + self.exit.success('Validation successful') + if self.action == "meta-data": + self.metadata.show() + self.exit.success('Metadata output') + if self.action == 'usage': + self.usage() + self.exit.success('Usage output') + self.handlers.current() + + __call__ = call + + def validate(self): + """ + Validate the agent's configuration and the configuration of all + the agent's handlers and parameters. Agent will validate itself first, + and then run the validate methods of other objects. + """ + if self.action is None: + self.usage() + self.exit.error_arguments('No action specified') + if self.action not in self.handlers.actions: + self.usage() + self.exit.error_unimplemented( + "Specified action: '%s' is neither a built-in " + "nor a defined action" % self.action + ) + + self.parameters.validate() + self.handlers.validate() + + def usage(self): + """ + Prints the agent's usage information including all implemented handlers + """ + self.log.output( + "usage: %s {%s}\n" % ( + self.name, + "|".join(self.handlers.actions) + ) + ) + + def param(self, name): + """ + Shortcut to get a parameter value by its name. Returns None + if the parameter is not found. + + :param name: Parameter name + :type name: str + :return: Parameter value + :rtype: str or int or bool or None + """ + return self.parameters.value(name) + + ########################################################################### + + @property + @memoization + def exit(self): + """ + The Exit object handles different exit conditions, exit codes + and logging. + + :return: The Exit object + :rtype: Exit + """ + return Exit(self) + + @property + @memoization + def parameters(self): + """ + Agent's parameters object. It deals with parameter collection, values + defaults, types and validation. + + :return: The parameters object + :rtype: Parameters + """ + return Parameters(self) + + @property + @memoization + def handlers(self): + """ + Returns the handlers object. It's responsible for handlers gathering, + processing their attributes, choosing the right handler for an action + and running the handler function. + + :return: The handlers object + :rtype: Handlers + """ + return Handlers(self) + + @property + @memoization + def environment(self): + """ + The environment object does everything related to the environment + variables passed from the cluster. It can collect and retrieve many + predefined values. + + :return: The environment object + :rtype: Environment + """ + return Environment(self) + + env = environment + + @property + @memoization + def log(self): + """ + The Log object handles all agent's logging and output functions. + It can control where dies the logging go, the log level and + log formats. + + :return: The log object + :rtype: Log + """ + return Log(self) + + @property + @memoization + def metadata(self): + """ + The Metadata object can generate the metadata XML that is required to + report the agents capabilities to the cluster. + + :return: the metadata object + :rtype: MetaData + """ + return MetaData(self) + + @property + @memoization + def lock(self): + """ + The Lock object can work with lock files. It can create lock file + for this agent with or without custom key, check if they are present + and remove them. + + :return: The lock object + :rtype: Lock + """ + return Lock(self) + + @property + @memoization + def pid(self): + """ + The Pid object can work with pid files. It can create pid file + for this agent with or without custom key, check if they are present, + read and remove pid files either created by the service or by the + agent. + + :return: The pid object + :rtype: Pid + """ + return Pid(self) + + @property + @memoization + def process(self): + """ + The Process object can run processes, inspect the process list + and send signals to a process. + + :return: The process object + :rtype: Process + """ + return Process(self) diff --git a/ocf_agent/constants.py b/ocf_agent/constants.py new file mode 100644 index 0000000..8b1912f --- /dev/null +++ b/ocf_agent/constants.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +""" +:var OCF_SUCCESS: The exit code of successful action. + For monitor action it's the exit code of the running agent or + successful test. + For multi-state monitor it's the exit code of the agent running + in the Slave mode. +""" + +OCF_SUCCESS = 0 +OCF_ERR_GENERIC = 1 +OCF_ERR_ARGS = 2 +OCF_ERR_UNIMPLEMENTED = 3 +OCF_ERR_PERM = 4 +OCF_ERR_INSTALLED = 5 +OCF_ERR_CONFIGURED = 6 +OCF_NOT_RUNNING = 7 +OCF_RUNNING_MASTER = 8 +OCF_FAILED_MASTER = 9 + +ENV_MANDATORY = [ + "OCF_ROOT", + "OCF_RA_VERSION_MAJOR", + "OCF_RA_VERSION_MINOR", + "OCF_RESOURCE_INSTANCE", + "OCF_RESOURCE_TYPE", +] + +HANDLERS_MANDATORY = [ + "start", + "stop", + "monitor", +] + +HANDLERS_OPTIONAL = [ + "promote", + "demote", + "migrate_to", + "migrate_from", + "notify", + "recover", + "reload", +] + +BUILTIN_HANDLERS = [ + "usage", + "meta-data", + "validate-all", +] + +ALIAS_HANDLERS = { + 'status': 'monitor', + 'help': 'usage', + 'restart': 'reload', + 'validate': 'validate-all', + 'meta': 'meta-data', +} + +VALID_HANDLERS = HANDLERS_MANDATORY + HANDLERS_OPTIONAL + +VAR_PARAMETER_PREFIX = 'OCF_RESKEY_' +VAR_CRM_META_PREFIX = 'OCF_RESKEY_CRM_meta_' +VAR_CRM_NOTIFY_PREFIX = 'OCF_RESKEY_CRM_meta_notify_' +PARAMETER_CLASS_PREFIX = 'OCFParameter_' +HANDLER_CLASS_PREFIX = 'OCFHandler_' +OCF_HANDLER_METHOD_PREFIX = 'handler_' +CONST_MEMOIZATION = '__memoization__' + +DEFAULT_LANGUAGE = 'en' +DEFAULT_ENCODING = 'utf-8' +DEFAULT_VERSION = '1' +DEFAULT_INTERVAL = '10' +DEFAULT_TIMEOUT = '20' +DEFAULT_DEPTH = '0' +DEFAULT_LOG_FACILITY = 'daemon' +DEFAULT_OCF_ROOT = '/usr/lib/ocf' +DEFAULT_CLUSTER_TYPE = 'corosync' +DEFAULT_QUORUM_TYPE = 'pcmk' + +OCF_VAR_RESOURCE_TYPE = 'OCF_RESOURCE_TYPE' +OCF_VAR_RESOURCE_PROVIDER = 'OCF_RESOURCE_PROVIDER' +OCF_VAR_RESOURCE_INSTANCE = 'OCF_RESOURCE_INSTANCE' +OCF_VAR_CHECK_LEVEL = 'OCF_CHECK_LEVEL' +OCF_VAR_RA_VERSION_MINOR = 'OCF_RA_VERSION_MINOR' +OCF_VAR_RA_VERSION_MAJOR = 'OCF_RA_VERSION_MAJOR' +OCF_VAR_DEBUG = 'HA_debug' +OCF_VAR_LOGD = 'HA_LOGD' +OCF_VAR_LOG_FACILITY = 'HA_LOGFACILITY' +OCF_VAR_ROOT = 'OCF_ROOT' +OCF_VAR_CLUSTER_TYPE = 'HA_cluster_type' +OCF_VAR_QUORUM_TYPE = 'HA_quorum_type' +OCF_VAR_META_MASTER_MAX = 'OCF_RESKEY_CRM_meta_master_max' +OCF_VAR_META_CLONE_MAX = 'OCF_RESKEY_CRM_meta_clone_max' +OCF_VAR_META_MASTER = 'OCF_RESKEY_CRM_meta_master' +OCF_VAR_META_CLONE = 'OCF_RESKEY_CRM_meta_clone' +OCF_VAR_META_MIGRATE_SOURCE = 'OCF_RESKEY_CRM_meta_migrate_source' +OCF_VAR_META_MIGRATE_TARGET = 'OCF_RESKEY_CRM_meta_migrate_target' + +VALID_ROLES = [ + 'Master', + 'Slave', +] + +CONST_ACTION = 'ACTION' +CONST_NAME = 'NAME' +CONST_SHORT_DESCRIPTION = 'SHORTDESC' +CONST_LONG_DESCRIPTION = 'LONGDESC' +CONST_LANGUAGE = 'LANG' +CONST_DEFAULT = 'DEFAULT' +CONST_REQUIRED = 'REQUIRED' +CONST_UNIQUE = 'UNIQUE' +CONST_VERSION = 'VERSION' +CONST_ENCODING = 'ENCODING' +CONST_TIMEOUT = 'TIMEOUT' +CONST_INTERVAL = 'INTERVAL' +CONST_DEPTH = 'DEPTH' +CONST_ROLE = 'ROLE' +CONST_METHOD = 'METHOD' + +VALUES_TRUE = frozenset(("1", "t", "true", "yes", "y", 'on')) +VALUES_FALSE = frozenset(("0", "f", "false", "no", "n", 'off')) + +# lock module +DEFAULT_LOCK_DIR = '/var/lock/pacemaker' +CONST_LOCK_DIR = 'LOCK_DIR' +CONST_LOCK_FILE = 'LOCK_FILE' + +# pid module +DEFAULT_PID_DIR = '/var/run/pacemaker' +CONST_PID_DIR = 'PID_DIR' +CONST_PID_FILE = 'PID_FILE' + +# log module +HA_LOGD_SOCKET = '/var/lib/heartbeat/log_daemon' +SYSLOG_SOCKET = '/dev/log' +LOG_FILE_DIRECTORY = '/var/log/corosync' +DEFAULT_LOG_HANDLERS = ['console', 'syslog'] +CONST_HANDLERS = 'LOG_HANDLERS' + +# process module +KILL_SIGNAL_RETRY = 5 +TERM_SIGNAL_RETRY = 5 diff --git a/ocf_agent/handler.py b/ocf_agent/handler.py new file mode 100644 index 0000000..2b74ffa --- /dev/null +++ b/ocf_agent/handler.py @@ -0,0 +1,331 @@ +from ocf_agent import constants +from ocf_agent.helpers import docstring_format +from ocf_agent.helpers import memoization +from ocf_agent.helpers import string_to_integer + + +class Handler(object): + """ + The Handler object represents an action OCF agent can be called with. + It can process the action's attributes and select the agent's handler + method to run. + """ + _action = None + + def __init__(self, handlers=None): + """ + The Handler object should be created with the parent Handlers object as + the first argument. + + :param handlers: Parent Handlers object + :type handlers: Handlers + """ + self.handlers = handlers + self.agent = self.handlers.agent + + def validate(self): + """ + Validate if this Handler object is configured correctly. + """ + if not self.__class__.__name__.startswith( + constants.HANDLER_CLASS_PREFIX + ): + self.agent.exit.error_configuration( + "ResourceHandler class '%s' name does not start with '%s'" % + (self.__class__.__name__, constants.HANDLER_CLASS_PREFIX) + ) + + @property + @docstring_format(constants.CONST_ACTION) + def action(self): + """ + Returns the action of this handler. Action is determined either by the + handler's class name after the prefix or manually by the *{0}* + constant. + Action is used to select the agent's handler method to run when this + object is called. + + :return: Handler action + :rtype: str + """ + if self._action is not None: + return self._action + if hasattr(self, constants.CONST_ACTION): + action = getattr(self, constants.CONST_ACTION) + else: + action = str( + self.__class__.__name__[len(constants.HANDLER_CLASS_PREFIX):] + ) + if action.startswith('monitor_'): + action = action.split('_')[0] + self._action = action + return self._action + + name = action + full_name = action + + @property + def attribute_names(self): + """ + Returns the list of attributes that are meaningful for this handler. + They are used to assemble actions in the meta-data xml. + :return: list of attribute names + :rtype: list + """ + return ['name', 'timeout'] + + @property + @memoization + def attributes(self): + """ + Returns the dictionary of attribute names and their values. + + :return: Attribute names and values + :rtype: dict + """ + attributes = {} + for attribute_name in self.attribute_names: + attribute_value = getattr(self, attribute_name, None) + if attribute_value is not None: + attributes[attribute_name] = attribute_value + return attributes + + @property + @docstring_format(constants.CONST_SHORT_DESCRIPTION) + def short_description(self): + """ + Short description string of this handler. Can be defined by the *{0}* + constant or will default to the handler's action. + This value is not actually used anywhere but can be used for reference. + + :return: Short description string + :rtype: str + """ + return getattr( + self, + constants.CONST_SHORT_DESCRIPTION, + self.action + ) + + @property + @docstring_format(constants.CONST_LONG_DESCRIPTION) + def long_description(self): + """ + Long description of this handler. Can be defined by the + *{0}* constant or will default to the short description. + This value is not actually used anywhere but can be used for reference. + + :return: Long description + :rtype: str + """ + return getattr( + self, + constants.CONST_LONG_DESCRIPTION, + self.short_description + ) + + @property + @docstring_format(constants.CONST_LANGUAGE, constants.DEFAULT_LANGUAGE) + def language(self): + """ + The handler's description language. Can be set by the *{0}* constant + or will default to the default value **{1}**. + :return: + """ + return getattr( + self, + constants.CONST_LANGUAGE, + constants.DEFAULT_LANGUAGE + ) + + @property + @docstring_format(constants.CONST_METHOD) + def default_method_name(self): + """ + This is the name of the agent's handler method called by this handler + if no other method name is defined by the *{0}* constant. + + :return: Default handler method name + :rtype: str + """ + return constants.OCF_HANDLER_METHOD_PREFIX + str(self.full_name) + + @property + @docstring_format(constants.CONST_METHOD) + def method_name(self): + """ + The handler will try to find this method in the Agent object and + call it when the Handler object is called. Can be defined by the *{0}* + constant or will be set to the default method name. + + :return: Handler's method name + :rtype: str + """ + return getattr( + self, + constants.CONST_METHOD, + self.default_method_name + ) + + @property + def method(self): + """ + Returns the Agent's method object associated with this handler. + Will return None if the method is not found. + + :return: The Agent's handler method. + :rtype: func or None + """ + return getattr(self.handlers.agent, self.method_name, None) + + def call(self): + """ + When the Handler object is called it will try to find Agent's handler + method and call it. If there is no such method the agent will + exit with error message. + """ + if self.method is not None and hasattr(self.method, '__call__'): + self.method() + else: + self.agent.exit.error_unimplemented( + "Agent does not have method: '%s'" % self.method_name + ) + + __call__ = call + + @property + @memoization + @docstring_format(constants.CONST_TIMEOUT, constants.DEFAULT_TIMEOUT) + def timeout(self): + """ + Returns the handler's timeout value. It will be used in the meta-data + XML to advise the user what is the minimal number of seconds required + to execute this agent's action. + The value can be set by the *{0}* constant and will default to the + default value **{1}** if not set. + + :return: The timeout value in seconds + :rtype: int + """ + return string_to_integer( + getattr( + self, + constants.CONST_TIMEOUT, + constants.DEFAULT_TIMEOUT + ) + ) + + +############################################################################### + + +class MonitorHandler(Handler): + """ + MonitorHandler extends the Handler object with several properties that + only monitor actions have. + """ + + @property + def attribute_names(self): + """ + MonitorHandler has all the properties of the Handler object and + additional ones. + + :return: List of attributes + :rtype: list + """ + return super(MonitorHandler, self).attribute_names + [ + 'interval', 'depth', 'role'] + + @property + @memoization + @docstring_format(constants.CONST_INTERVAL, constants.DEFAULT_INTERVAL) + def interval(self): + """ + Interval is the number of seconds between the monitor actions calls. + This value is used in the meta-data XML to advise the user what is the + minimum interval for this monitor? or for this monitor type if there + are several monitor actions defined. + Interval can be set by the *{0}* constant and will default to **{1}** + if unset. + + :return: The interval value in seconds + :rtype: int + """ + return string_to_integer( + getattr( + self, + constants.CONST_INTERVAL, + constants.DEFAULT_INTERVAL, + ) + ) + + @property + @memoization + @docstring_format(constants.CONST_DEPTH, constants.DEFAULT_DEPTH) + def depth(self): + """ + Depth, or the check_level, is used to define several monitor actions. + For example, the short monitor action with a small depth number and + the long and expensive one with a large depth. Small monitor can have + a small interval value and be called very often and the large monitor + can perform many additional checks, have a large monitor value and be + called infrequently. The depth value will be used to distinguish these + actions. + Depth can be set by the *{0}* constant and will default to **{1}** + if unset. + + :return: The depth value + :rtype: int + """ + return string_to_integer( + getattr( + self, + constants.CONST_DEPTH, + constants.DEFAULT_DEPTH, + ) + ) + + @property + @memoization + @docstring_format(constants.CONST_ROLE) + def role(self): + """ + Role can be used to tell if this monitor action should be run only on + the master instance in a multi-state configuration, only on a slave one + or or any instance. + The value can be Master, Slave or None. + Role can be set by the *{0}* constant and will default to **None**. + + :return: The role value + :rtype: str or None + """ + role = getattr( + self, + constants.CONST_ROLE, + None + ) + if role is None: + return None + else: + role = str(role).lower().capitalize() + return role + + @property + @memoization + def full_name(self): + """ + Full name of this handler. + It will ne equal to the name for a non-monitor handler and will + contain depth and role for a monitor action. + Full name is used to find the Agent's handler method. + + :return: Handler's full name + :rtype: str + """ + full_name = self.action + if self.role is not None: + full_name += '_' + self.role.lower() + if self.depth is not None and self.depth != 0: + full_name += '_' + str(self.depth) + return full_name diff --git a/ocf_agent/helpers.py b/ocf_agent/helpers.py new file mode 100644 index 0000000..b4bf022 --- /dev/null +++ b/ocf_agent/helpers.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +from ocf_agent import constants + + +def string_to_bool(value, default=None): + """ + Convert a string value to boolean with + different possible true and false variants and + the default value option if conversion was not successful. + + :param value: Input value + :type value: str + :param default: Optional default value + :type default: object + :return: true, false, or the default value + :rtype: True or False or None + """ + if value is None or isinstance(value, bool): + return value + if str(value).lower() in constants.VALUES_TRUE: + return True + if str(value).lower() in constants.VALUES_FALSE: + return False + return default + + +def string_to_integer(value, default=None): + """ + Convert a string value to an integer value stripping non-numeric + letters and with optional default value if conversion was not successful. + + :param value: Input value + :type value: str + :param default: Optional default value + :type default: object + :return: Integer value or the default value + :rtype: int or None + """ + if value is None: + return None + try: + return abs(int(value)) + except (ValueError, TypeError): + pass + try: + value = ''.join([letter for letter in str(value) if letter.isdigit()]) + return abs(int(value)) + except (ValueError, TypeError): + return default + + +def memoization_prepare(self): + """ + Prepare the memoization structure in the class + """ + if not isinstance( + getattr(self, constants.CONST_MEMOIZATION, None), + dict, + ): + setattr(self, constants.CONST_MEMOIZATION, {}) + + +def memoization_set(self, key, value): + """ + Set the new value to the memoization structure + + :param self: Class self object + :param key: Property name + :type key: str + :param value: Property value + :type value: object + """ + memoization_prepare(self) + getattr(self, constants.CONST_MEMOIZATION, {})[key] = value + return value + + +def memoization_get(self, key): + """ + Retrieve the stored memoization value + + :param self: Class self object + :param key: Property name + :type key: str + """ + memoization_prepare(self) + return getattr(self, constants.CONST_MEMOIZATION, {}).get(key, None) + + +def memoization(function): + """ + Property memoization decorator. + + Saves the first property function output value and returns the saved + value when the property function is called again. Should be used only + for a property method or a method without arguments. Property decorator + should be applied after this one. + + :param function: Property function + :type function: func + :return: Decorated property function + :rtype: func + """ + + def _decorator_(self): + key = function.__name__ + value = memoization_get(self, key) + if value is not None: + return value + else: + value = function(self) + memoization_set(self, key, value) + return value + + _decorator_.__doc__ = function.__doc__ + return _decorator_ + + +def docstring_format(*values): + """ + This decorator can be used to replace placeholders in a method docstring + with variable or constant values. It allows using variables in a + method docstring. It also screen all underscore characters to be + processed correctly by Sphinx. + + :param values A list of substitute values + :type values: list + :return: Method with formatted docstring + :rtype: func + """ + + def _decorator_(function): + function.__doc__ = function.__doc__.format(*values).replace('_', '\_') + return function + + return _decorator_ diff --git a/ocf_agent/modules/__init__.py b/ocf_agent/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ocf_agent/modules/environment.py b/ocf_agent/modules/environment.py new file mode 100644 index 0000000..79ed921 --- /dev/null +++ b/ocf_agent/modules/environment.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +import os +from ocf_agent import constants +from ocf_agent.helpers import memoization +from ocf_agent.helpers import string_to_bool +from ocf_agent.helpers import string_to_integer + + +class Environment(object): + def __init__(self, agent): + self.agent = agent + + @property + @memoization + def environment(self): + """ + Returns the dictionary of all relevant environment variables and + their values + @rtype: dict + @return: Dictionary of environment variables and their values + """ + environment = {} + for variable in os.environ.keys(): + if variable.startswith('HA_') or \ + variable.startswith('OCF_') or \ + variable.startswith('PCMK_'): + environment[variable] = os.environ[variable] + return environment + + all = environment + + get = os.getenv + + @property + @memoization + def meta(self): + meta = {} + for variable in os.environ.keys(): + if variable.startswith(constants.VAR_CRM_META_PREFIX): + meta_variable_name = \ + variable[len(constants.VAR_CRM_META_PREFIX):] + meta[meta_variable_name] = \ + os.environ[variable] + return meta + + @property + @memoization + def notify(self): + notify = {} + for variable in os.environ.keys(): + if variable.startswith(constants.VAR_CRM_NOTIFY_PREFIX): + notify_variable_name = \ + variable[len(constants.VAR_CRM_NOTIFY_PREFIX):] + notify[notify_variable_name] = \ + os.environ[variable] + return notify + + @property + def res_class(self): + return 'ocf' + + @property + def res_type(self): + return os.getenv( + constants.OCF_VAR_RESOURCE_TYPE, + None + ) + + @property + def res_provider(self): + return os.getenv( + constants.OCF_VAR_RESOURCE_PROVIDER, + None + ) + + @property + def res_instance(self): + return os.getenv( + constants.OCF_VAR_RESOURCE_INSTANCE, + self.agent.name, + ) + + @property + def instance_name(self): + if self.res_instance is None: + return None + instance = self.res_instance.split(':') + return instance[0] + + instance = instance_name + + @property + def instance_suffix(self): + if self.res_instance is None: + return None + instance = self.res_instance.split(':') + if len(instance) < 2: + return None + return instance[1] + + @property + @memoization + def check_level(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_CHECK_LEVEL, + constants.DEFAULT_DEPTH, + ) + ) + + depth = check_level + + @property + @memoization + def ra_version_major(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_RA_VERSION_MAJOR, + 1, + ) + ) + + @property + @memoization + def ra_version_minor(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_RA_VERSION_MINOR, + 0, + ) + ) + + @property + @memoization + def is_debug(self): + return string_to_bool( + os.getenv( + constants.OCF_VAR_DEBUG, + False, + ), + False, + ) + + @property + @memoization + def is_logd(self): + return string_to_bool( + os.getenv( + constants.OCF_VAR_LOGD, + False, + ), + False, + ) + + @property + def log_facility(self): + return os.getenv( + constants.OCF_VAR_LOG_FACILITY, + constants.DEFAULT_LOG_FACILITY, + ) + + @property + def ocf_root(self): + return os.getenv( + constants.OCF_VAR_ROOT, + constants.DEFAULT_OCF_ROOT, + ) + + @property + def cluster_type(self): + return os.getenv( + constants.OCF_VAR_CLUSTER_TYPE, + constants.DEFAULT_CLUSTER_TYPE, + ) + + @property + def quorum_type(self): + return os.getenv( + constants.OCF_VAR_QUORUM_TYPE, + constants.DEFAULT_QUORUM_TYPE, + ) + + @property + @memoization + def meta_master_max(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_META_MASTER_MAX, + None + ) + ) + + @property + @memoization + def meta_clone_max(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_META_CLONE_MAX, + None + ) + ) + + @property + @memoization + def meta_master(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_META_MASTER, + None + ) + ) + + @property + @memoization + def meta_clone(self): + return string_to_integer( + os.getenv( + constants.OCF_VAR_META_CLONE, + None + ) + ) + + @property + @memoization + def is_clone(self): + return self.meta_clone_max is not None and self.meta_clone_max > 0 + + @property + @memoization + def is_ms(self): + return self.meta_master_max is not None and self.meta_master_max > 0 diff --git a/ocf_agent/modules/exit.py b/ocf_agent/modules/exit.py new file mode 100644 index 0000000..6587dd5 --- /dev/null +++ b/ocf_agent/modules/exit.py @@ -0,0 +1,237 @@ +import sys +from ocf_agent import constants +from ocf_agent.helpers import docstring_format + + +class Exit(object): + """ + The exit object handlers the Agent's exit conditions, exit codes + and logging. + """ + + def __init__(self, agent): + """ + The Exit object should be created with the Agent parent object as + the first argument. + + :param agent: Parent Agent + :type agent: Agent + """ + self.agent = agent + + def output(self, event, message, code): + """ + Output the exit message to the Agent's logger + + :param event: Exit event name + :type event: str + :param message: Exit event message + :type message: str + :param code: Exit code + :type code: int + """ + self.agent.log.info( + '%s: %s - exit code: %d' % (event, message, code) + ) + + @docstring_format(constants.OCF_SUCCESS) + def success(self, message): + """ + There was no error and the action have completed successfully. + It is the expected result of any action like start, stop, promote, + and demote. The agent will exit with code **{0}**. + + For the monitor action of a simple or cloned service this exit code + means that the service is running and all checks have passed. + + For the multi-state service this exit code means that the service is + running in the Slave mode. + If the service is running in the Master mode is should return return + OCFRunningMaster instead. + + :param message: Event message + :type message: str + """ + self.output( + 'success', + message, + constants.OCF_SUCCESS, + ) + sys.exit(constants.OCF_SUCCESS) + + running = success + running_slave = success + + @docstring_format(constants.OCF_ERR_GENERIC) + def error_generic(self, message): + """ + Generic or unspecified error. This return code **{0}** should be used + if other error codes cannot describe the problem. The monitor action + should return this for a crashed, hang, or otherwise failed service. + + The cluster manager will try to recover from this error by restarting + the agent service on the same node unless configured not to. + + :param message: Event message + :type message: str + """ + self.output( + 'success', + message, + constants.OCF_ERR_GENERIC, + ) + sys.exit(constants.OCF_ERR_GENERIC) + + @docstring_format(constants.OCF_ERR_ARGS) + def error_arguments(self, message): + """ + Arguments error should be raise if the agent have been called + without an action or validation have failed. Any action can return + the code **{0}** in similar cases. + + :param message: Event message + :type message: str + """ + self.output( + 'error_arguments', + message, + constants.OCF_ERR_ARGS, + ) + sys.exit(constants.OCF_ERR_ARGS) + + @docstring_format(constants.OCF_ERR_UNIMPLEMENTED) + def error_unimplemented(self, message): + """ + The agent have been executed with an action that is not implemented + or the handler method is missing. For example, if an agent does not + support multi-state configuration but is asked to do promote or demote + it should return exit code **{0}**. + + :param message: Event message + :type message: str + """ + self.output( + 'error_unimplemented', + message, + constants.OCF_ERR_UNIMPLEMENTED, + ) + sys.exit(constants.OCF_ERR_UNIMPLEMENTED) + + @docstring_format(constants.OCF_ERR_PERM) + def error_permissions(self, message): + """ + THe agent was not able to perform an action due to a permission + problem. It could not open a file, socket or write to a directory. + The return code should be **{0}**. + + The cluster manager will try to start the failed resource on the + other node because this error is counted as an unrecoverable. + + :param message: Event message + :type message: str + """ + self.output( + 'error_permissions', + message, + constants.OCF_ERR_PERM, + ) + sys.exit(constants.OCF_ERR_PERM) + + @docstring_format(constants.OCF_ERR_INSTALLED) + def error_installation(self, message): + """ + The executable binary is not installed or the resource is + misconfigured and cannot find its binary. The return code should + be **{0}**. + + This error is treated as fatal and the cluster will not try to recover + the resource neither on the same node nor on any other until the + problem is fixed and resource is is cleaned up. + + :param message: Event message + :type message: str + """ + self.output( + 'error_installation', + message, + constants.OCF_ERR_INSTALLED, + ) + sys.exit(constants.OCF_ERR_INSTALLED) + + @docstring_format(constants.OCF_ERR_CONFIGURED) + def error_configuration(self, message): + """ + The resource is misconfigured by a user. For example, parameter have + been given an incorrect value type. The return code should + be **{0}**. + + This error is treated as fatal and the cluster will not try to + restart the resource and will wait for a user intervention. + + :param message: Event message + :type message: str + """ + self.output( + 'error_configuration', + message, + constants.OCF_ERR_CONFIGURED, + ) + sys.exit(constants.OCF_ERR_CONFIGURED) + + @docstring_format(constants.OCF_NOT_RUNNING) + def not_running(self, message): + """ + The exit code **{0}** should be returned only by a monitor action if + the service is not running. It should be *cleanly* stopped service, + not a crashed one. + + If the service is not running due to some error, other exit codes + should be used and the stop action should return 'success' code if + completed successfully. + + :param message: Event message + :type message: str + """ + self.output( + 'not_running', + message, + constants.OCF_NOT_RUNNING, + ) + sys.exit(constants.OCF_NOT_RUNNING) + + @docstring_format(constants.OCF_RUNNING_MASTER) + def running_master(self, message): + """ + The exit code **{0}** should only be returned by a monitor action in + the master-slave configuration when it have determined that the + resource is currently running in the Master mode. IF the resource + is running in the Slave mode it should return the 'success' exit code. + + :param message: Event message + :type message: str + """ + self.output( + 'running_master', + message, + constants.OCF_RUNNING_MASTER, + ) + sys.exit(constants.OCF_RUNNING_MASTER) + + @docstring_format(constants.OCF_FAILED_MASTER) + def master_failed(self, message): + """ + In the multi-state configuration exit code **{0}** should be returned + by a monitor action if the resource have failed in the master mode. + + The cluster will try to recover the resource in-place by stopping, + starting and promoting it again. + + :param message: Event message + :type message: str + """ + self.output( + 'master_failed', + message, + constants.OCF_FAILED_MASTER, + ) + sys.exit(constants.OCF_FAILED_MASTER) diff --git a/ocf_agent/modules/handlers.py b/ocf_agent/modules/handlers.py new file mode 100644 index 0000000..c29e0f1 --- /dev/null +++ b/ocf_agent/modules/handlers.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +from ocf_agent import constants +from ocf_agent.helpers import memoization + + +class Handlers(object): + """ + The Handlers object is a collection of handler objects. + It can collect then and work with their attributes. + """ + + def __init__(self, agent): + """ + The Handlers object should be created with the parent Agent object + as the first argument. + + :param agent: Parent Agent object + :type agent: Agent + """ + self.agent = agent + + @property + @memoization + def handlers(self): + """ + Returns the list of defined Handler instances + + :rtype: list + :return: The list of Handler objects + """ + handlers = [] + for entry in dir(self.agent): + if not entry.startswith(constants.HANDLER_CLASS_PREFIX): + continue + handler_class = getattr(self.agent, entry) + handler_class_instance = handler_class(self) + handlers.append(handler_class_instance) + return handlers + + all = handlers + __call__ = handlers + + @property + @memoization + def actions(self): + """ + Returns a set of all implemented Handler actions + + :rtype: set + :return: Set of implemented action names + """ + defined_handlers = set( + [ + handler.action for handler in self.handlers + ] + ) + defined_handlers.update(constants.BUILTIN_HANDLERS) + defined_handlers.update(constants.ALIAS_HANDLERS.keys()) + return defined_handlers + + @property + def current(self): + """ + Find the current Handler instance using the Agent's action value and + check\_level for a monitor action. Raises error if the handler is + not found. + + :return: The current Handler + :rtype: Handler + """ + handler = self.get( + action=self.agent.action, + check_level=self.agent.environment.check_level, + ) + if handler is not None: + return handler + self.agent.exit.error_unimplemented( + "Handler for action '%s' is not found" % self.agent.action + ) + + def get(self, action, check_level=None): + """ + Try to get a Handler instance by its name and, for a monitor action, + by its check level. Returns None if the instance is not found. + + :param action: Action name + :type action: str + :param check_level: Monitor check level + :type check_level: int or None + :return: The Handler instance + :rtype: Handler of None + """ + if action == 'monitor' and check_level is not None: + matcher_handlers = [ + handler for handler in self.handlers if + handler.action == action and + handler.depth == check_level + ] + if matcher_handlers: + return matcher_handlers[0] + else: + return self.get(action) + else: + matched_handlers = [ + handler for handler in self.handlers + if handler.action == action + ] + if matched_handlers: + return matched_handlers[0] + return None + + @property + @memoization + def attributes(self): + """ + Return the dictionary of all Handler full names and their + attributes. + + :return: Dictionary of handler full names and attributes. + :rtype: dict + """ + attributes = {} + for handler in self.handlers: + attributes[handler.full_name] = handler.attributes + return attributes + + def validate(self): + """ + Validate if the handlers are configured correctly. + """ + for handler in self.handlers: + handler.validate() diff --git a/ocf_agent/modules/lock.py b/ocf_agent/modules/lock.py new file mode 100644 index 0000000..f3a40d0 --- /dev/null +++ b/ocf_agent/modules/lock.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +import os +from ocf_agent import constants +from ocf_agent.helpers import docstring_format + + +class Lock(object): + """ + The Lock object can create, check and remove lock files. + """ + + def __init__(self, agent): + """ + The Lock object should have the Agent object as the first argument. + + :param agent: The parent Agent + :type agent: Agent + """ + self.agent = agent + + @property + @docstring_format(constants.CONST_LOCK_DIR, constants.DEFAULT_LOCK_DIR) + def directory(self): + """ + The directory where all the lock files will be placed. + Can be set by the *{0}* constant in the Agent class + and will default to **{1}**. + + :return: Lock directory path + :rtype: str + """ + return getattr( + self.agent, + constants.CONST_LOCK_DIR, + constants.DEFAULT_LOCK_DIR, + ) + + def file_name(self, key=None): + """ + The lock file name for this agent with the custom key if the key is + provided. + + :param key: Custom lock file suffix + :type key: str or None + :return: Lock file name + :rtype: str + """ + file_name = self.agent.name + if self.agent.environment.res_instance is not None: + file_name += '-' + self.agent.environment.res_instance + if key is not None: + file_name += '-' + key + return file_name + '.lock' + + @docstring_format(constants.CONST_LOCK_FILE) + def file_path(self, key=None): + """ + The full path to the agent's lock file. If the key is provided it's + added to the agent name as a suffix. If key is not used the default + lock file path can be set by the **{0}** constant in the Agent class. + + :param key: Custom lock file suffix + :type key: str or None + :return: Lock file path + :rtype: str + """ + if key is not None: + return os.path.join(self.directory, self.file_name(key)) + return getattr( + self.agent, + constants.CONST_LOCK_FILE, + os.path.join(self.directory, self.file_name()), + ) + + @property + def path(self): + """ + Alias for 'file_path' with the default key. + + :return: Lock file path + :rtype: str + """ + return self.file_path() + + def make_directory(self): + """ + Create the lock file directory if it's not present. + """ + if not os.path.isdir(self.directory): + os.mkdir(self.directory) + + def file_is_present(self, key=None): + """ + Check if the specified lock file is present or not. + + :param key: Custom lock file suffix + :type key: str or None + :return: Boolean value + :rtype: bool + """ + return os.path.isfile(self.file_path(key)) + + @property + def is_present(self): + """ + Alias for 'file_is_present' for the default lock file + + :return: Boolean value + :rtype: bool + """ + return self.file_is_present() + + present = is_present + + def create_file(self, key=None): + """ + Create the specified lock file + + :param key: Custom lock file suffix + :type key: str or None + """ + self.make_directory() + open(self.file_path(key), 'w').close() + + def create(self): + """ + Alias for 'create_file' for the default lock file. + """ + self.create_file() + + def remove_file(self, key=None): + """ + Remove the specified lock file + + :param key: Custom lock file suffix + :type key: str or None + """ + if self.file_is_present(key): + os.remove(self.file_path(key)) + + def remove(self): + """ + Alias for 'remove_file' for the default lock file. + """ + self.remove_file() diff --git a/ocf_agent/modules/log.py b/ocf_agent/modules/log.py new file mode 100644 index 0000000..d59d4cd --- /dev/null +++ b/ocf_agent/modules/log.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +import logging +import os +import sys +from logging.handlers import SysLogHandler +from ocf_agent import constants + + +class Log(object): + """ + The Logger object is a wrapper of the Python's logger class. + It can create a configured Logger object and use it to log messages. + """ + def __init__(self, agent): + """ + The Log object requires the Agent object as the first argument. + + :param agent: Parent Agent. + :type agent: Agent + """ + self.agent = agent + + @property + def logger(self): + """ + Returns the configured Logger class instance. It can be used by + logging methods or can be used directly. + + :return: Logger object + :rtype: Logger + """ + logger = logging.getLogger(self.tag) + logger.level = self.level + logger.handlers = [] + if 'console' in self.enabled_handlers: + logger.handlers.append(self.handler_console) + if 'syslog' in self.enabled_handlers: + logger.handlers.append(self.handler_syslog) + if 'file' in self.enabled_handlers: + logger.handlers.append(self.handler_file) + return logger + + @property + def enabled_handlers(self): + """ + Get the list of enabled handler types + + :return: List of handler names + :rtype: list + """ + return getattr( + self.agent, + constants.CONST_HANDLERS, + constants.DEFAULT_LOG_HANDLERS, + ) + + @property + def level(self): + """ + Returns the current maximum log level. Debug mode can be enabled + if the pacemaker sends the debug environment variable. + + :return: Debug level + :rtype: int + """ + if self.agent.environment.is_debug: + return logging.DEBUG + else: + return logging.INFO + + @property + def tag(self): + """ + Returns the log tag. It's used as the Logger object name to mark + the process which have emitted the message. + + :return: Log Tag + :rtype: str + """ + tag = self.agent.environment.res_instance + if self.agent.action: + tag += '[%s]' % self.agent.action + return tag + + @property + def log_file_path(self): + """ + Path to the log file. Used by the 'file' handler. + + :return: Path to log file + :rtype: str + """ + log_file = self.agent.environment.instance_name + if self.agent.environment.instance_suffix: + log_file += '_' + self.agent.environment.instance_suffix + log_file += '.log' + return os.path.join(constants.LOG_FILE_DIRECTORY, log_file) + + # log formats # + + @property + def format_date(self): + """ + Date format string + + :rtype: str + """ + return '%Y-%m-%d %H:%M:%S' + + @property + def format_date_prefix(self): + """ + Date prefix of the log message. It will be used by console and + log handlers to mark the message time and date. + + :type: str + """ + return '%(asctime)s.%(msecs)03d' + + @property + def format_suffix(self): + """ + Log message suffix. The body part of the log message. + + :rtype: str + """ + return '%(name)s %(levelname)s %(message)s' + + @property + def formatter_file(self): + """ + The formatter with the date prefix. Used for file and console logger. + + :return: Formatter object + :rtype: Formatter + """ + file_format = '%s %s' % ( + self.format_date_prefix, + self.format_suffix, + ) + return logging.Formatter(file_format, self.format_date) + + formatter_console = formatter_file + + @property + def formatter_syslog(self): + """ + The Syslog formatter does not send the date prefix. + + :return: Formatter object + :rtype: Formatter + """ + return logging.Formatter(self.format_suffix) + + # log handlers # + + @property + def handler_console(self): + """ + The Console handler sends the messages to the standard error output. + + :return: Console Handler + :rtype: Handler + """ + handler = logging.StreamHandler( + stream=sys.stderr, + ) + handler.setFormatter(self.formatter_console) + return handler + + @property + def handler_file(self): + """ + The File handler sends the messages directly to a log file. + + :return: File Handler + :rtype: Handler + """ + handler = logging.FileHandler( + filename=self.log_file_path, + encoding=self.agent.encoding, + ) + handler.setFormatter(self.formatter_file) + return handler + + @property + def handler_syslog(self): + """ + The Syslog handler sends the messages to the syslog service. + + :return: Syslog Handler + :rtype: Handler + """ + handler = SysLogHandler( + address=constants.SYSLOG_SOCKET, + facility=self.agent.environment.log_facility, + ) + handler.setFormatter(self.formatter_syslog) + return handler + + # logging methods # + + def log(self, level, msg, *args, **kwargs): + self.logger.log(level, msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + self.logger.debug(msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + self.logger.info(msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + self.logger.warning(msg, *args, **kwargs) + + warn = warning + + def error(self, msg, *args, **kwargs): + self.logger.error(msg, *args, **kwargs) + + err = error + + def critical(self, msg, *args, **kwargs): + self.logger.critical(msg, *args, **kwargs) + + crit = critical + + def exception(self, msg, *args, **kwargs): + self.logger.exception(msg, *args, **kwargs) + + @staticmethod + def output(msg): + """ + Directly write the message to the standard output. + + :param msg: Text + :type: msg: str + """ + sys.stdout.write(msg) diff --git a/ocf_agent/modules/metadata.py b/ocf_agent/modules/metadata.py new file mode 100644 index 0000000..80d8cb2 --- /dev/null +++ b/ocf_agent/modules/metadata.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +try: + from html import escape as meta_escape +except ImportError: + from cgi import escape as meta_escape + + +class MetaData(object): + """ + The MetaData object is used to generate the meta-data XML. It will be + used to describe this agent's capabilities to the cluster. + """ + + def __init__(self, agent): + """ + The MetaData object required the parent Agent obeject as the first + argument. + + :param agent: Parent Agent + :type agent: Agent + """ + self.agent = agent + + def show(self): + """ + Show the meta-data XML text using the Log's output method. + """ + self.agent.log.output(self.xml) + + @staticmethod + def escape_string(value): + """ + Uses html or cgi module's escape method to mask all dangerous + characters before they are inserted to the XML. + + :param value: Input string + :type value: str + :return: Output string + :rtype: str + """ + if hasattr(value, 'replace'): + return meta_escape(value) + return value + + def format_line(self, offset, line, *arguments): + """ + Format a string with offset and interpolation + + :param offset: offset width + :type offset: int + :param line: line template + :type line: str + :param arguments: template arguments + :type arguments: list + :return: formatted string + :rtype: str + """ + arguments = tuple(map(self.escape_string, arguments)) + tab = ' ' + return tab * int(offset) + line % arguments + "\n" + + @property + def xml(self): + """ + Generate the meta-data XML text + + :rtype: str + :return: XML meta-data text + """ + xml = '' + xml += self.format_line( + 0, + '', + self.agent.encoding + ) + xml += self.format_line( + 0, + '' + ) + xml += self.format_line( + 0, + '', + self.agent.name, + self.agent.version + ) + xml += self.format_line( + 1, + '%s', + self.agent.version + ) + xml += self.format_line( + 1, + '%s', + self.agent.language, + self.agent.long_description + ) + xml += self.format_line( + 1, + '%s', + self.agent.language, + self.agent.short_description + ) + + xml += self.format_line( + 1, + '' + ) + + for parameter in self.agent.parameters.all.values(): + xml += self.format_line( + 2, + '', + parameter.name, + int(parameter.unique), + int(parameter.required) + ) + xml += self.format_line( + 3, + '%s', + parameter.language, + parameter.long_description + ) + xml += self.format_line( + 3, + '%s', + parameter.language, + parameter.short_description + ) + xml += self.format_line( + 3, + '', + parameter.type_name, + parameter.default + ) + xml += self.format_line( + 2, + '' + ) + + xml += self.format_line( + 1, + '' + ) + + xml += self.format_line( + 1, + '' + ) + + for handler in self.agent.handlers.all: + line = '') + xml += self.format_line( + 0, + '' + ) + return xml diff --git a/ocf_agent/modules/parameters.py b/ocf_agent/modules/parameters.py new file mode 100644 index 0000000..a13670e --- /dev/null +++ b/ocf_agent/modules/parameters.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from ocf_agent import constants +from ocf_agent.helpers import memoization + + +class Parameters(object): + """ + The Parameters object is a collection of Parameter objects. + It can collect Parameters and work with their values. + """ + + def __init__(self, agent): + """ + The Parameters object should be created with the parent Agent + object as the first argument. + + :param agent: Parent Agent object + :type agent: Agent + """ + self.agent = agent + + @property + @memoization + def parameters(self): + """ + Returns the dictionary of all defined parameter names + and their object instances. + + :rtype: dict + :return: Dictionary of parameter names and objects + """ + parameters = {} + for entry in dir(self.agent): + if not entry.startswith(constants.PARAMETER_CLASS_PREFIX): + continue + parameter_class = getattr(self.agent, entry) + parameter_class_instance = parameter_class(self) + parameters[parameter_class_instance.name] = \ + parameter_class_instance + return parameters + + all = parameters + __call__ = parameters + + def get(self, name): + """ + Get the parameter instance by its name. + + :param name: Parameter name + :type name: str + :return: Parameter instance + :type: Parameter or None + """ + if name in self.parameters: + return self.parameters[name] + return None + + def value(self, name): + """ + Get the parameter value by the parameter name. + + :param name: Parameter name + :type name: str + :return: Parameter value + :rtype: int or boot or str or None + """ + parameter = self.get(name) + if parameter is None: + return None + return parameter.value + + @property + def values(self): + """ + Get a dictionary of parameter names and their values. + + :return: Parameter name and values + :rtype: dict + """ + values = {} + for parameter_name, parameter in self.parameters.items(): + values[parameter_name] = parameter.value + return values + + def validate(self): + """ + Validate if the parameters are configured correctly. + """ + for parameter in self.parameters.values(): + parameter.validate() diff --git a/ocf_agent/modules/pid.py b/ocf_agent/modules/pid.py new file mode 100644 index 0000000..84dff1a --- /dev/null +++ b/ocf_agent/modules/pid.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +import os +from ocf_agent import constants +from ocf_agent.helpers import docstring_format +from ocf_agent.helpers import string_to_integer + + +class Pid(object): + """ + The Pid object can create, check, read and remove pid files. + """ + + def __init__(self, agent): + """ + The Pid object should have the Agent object as the first argument. + + :param agent: The parent Agent + :type agent: Agent + """ + self.agent = agent + + @property + @docstring_format(constants.CONST_PID_DIR, constants.DEFAULT_PID_DIR) + def directory(self): + """ + The directory where all the pid files will be placed. + Can be set by the *{0}* constant in the Agent class + and will default to **{1}**. + + :return: Pid file directory + :rtype: str + """ + return getattr( + self.agent, + constants.CONST_PID_DIR, + constants.DEFAULT_PID_DIR, + ) + + def file_name(self, key=None): + """ + The pid file name for this agent. with the custom key if the key is + provided. + + :param key: Custom pid file suffix + :type key: str or None + :return: Pid file name + :rtype: str + """ + file_name = self.agent.name + if self.agent.environment.res_instance is not None: + file_name += '-' + self.agent.environment.res_instance + if key is not None: + file_name += '-' + key + return file_name + '.pid' + + @docstring_format(constants.CONST_PID_FILE) + def file_path(self, key=None): + """ + The full path to the agent's pid file. If the key is provided it's + added to the agent name as a suffix. If key is not used the default + pid file path can be set by the **{0}** constant in the Agent class. + + If the pid file is created by the service, the agent can still work + with this file if the path is defined as the constant. + + :param key: Custom pid file suffix + :type key: str or None + :return: Pid file path + :rtype: str + """ + if key is not None: + return os.path.join(self.directory, self.file_name(key)) + return getattr( + self.agent, + constants.CONST_PID_FILE, + os.path.join(self.directory, self.file_name()), + ) + + @property + def path(self): + """ + Alias for 'file_path' with the default key + + :return: Pid file path + :rtype: str + """ + return self.file_path() + + def make_directory(self): + """ + Create the pid file directory if it's not present. + """ + if not os.path.isdir(self.directory): + os.mkdir(self.directory) + + def file_is_present(self, key=None): + """ + Check if the specified pid file is present or not. + + :param key: Custom pid file suffix + :type key: str or None + :return: Boolean value + :rtype: bool + """ + return os.path.isfile(self.file_path(key)) + + @property + def is_present(self): + """ + Alias for 'file_is_present' for the default pid file + + :return: Boolean value + :rtype: bool + """ + return self.file_is_present() + + present = is_present + + def create_file(self, number, key=None): + """ + Create the specified pid file and + write the pid number to it. + + :param number: Pid file number + :type number: int + :param key: Custom pid file suffix + :type key: str or None + """ + self.make_directory() + with open(self.file_path(key), 'w') as pid_file: + pid_file.write("%d\n" % number) + + def create(self, number): + """ + Alias for 'create_file' for the default pid file. + + :param number: Pid number + :param number: int + """ + self.create_file(number) + + def read_file(self, key=None): + """ + Read the pid file and return the recorded number or + None if the file cannot be read. + + :param key: Custom pid file suffix + :type key: str or None + :return: The pid number + :rtype: int or None + """ + if not self.file_is_present(key): + return None + with open(self.file_path(key), 'r') as pid_file: + number = pid_file.read() + return string_to_integer(number) + + @property + def read(self): + """ + Alias for 'read_file' for the default pid file. + """ + return self.read_file() + + number = read + + def remove_file(self, key=None): + """ + Remove the specified pid file + + :param key: Custom pid file suffix + :type key: str or None + """ + if self.file_is_present(key): + os.remove(self.file_path(key)) + + def remove(self): + """ + Alias for 'remove_file' for the default pid file. + """ + self.remove_file() diff --git a/ocf_agent/modules/process.py b/ocf_agent/modules/process.py new file mode 100644 index 0000000..d821808 --- /dev/null +++ b/ocf_agent/modules/process.py @@ -0,0 +1,125 @@ +import psutil +from ocf_agent.helpers import string_to_integer +from subprocess import PIPE +from time import sleep +from ocf_agent import constants + + +class Process(object): + def __init__(self, agent): + self.agent = agent + + @property + def iterator(self): + return psutil.process_iter() + + @property + def processes(self): + return list(self.iterator) + + all = processes + + def find(self, name): + return [ + process for process in self.iterator + if name in process.name or name in ' '.join(process.cmdline) + ] + + @staticmethod + def is_running(pid): + pid = string_to_integer(pid) + return psutil.pid_exists(pid) + + @staticmethod + def get(pid): + pid = string_to_integer(pid) + try: + return psutil.Process(pid) + except (TypeError, psutil.NoSuchProcess): + return None + + def kill(self, pid): + process = self.get(pid) + if process is not None: + process.kill() + + def terminate(self, pid): + process = self.get(pid) + if process is None: + return + process.terminate() + + def ensure_terminate(self, pid): + process = self.get(pid) + if process is None: + return True + for i in range(constants.TERM_SIGNAL_RETRY): + if not process.is_running(): + return True + try: + process.terminate() + except psutil.NoSuchProcess: + return True + sleep(1) + return self.ensure_kill(pid) + + def ensure_kill(self, pid): + process = self.get(pid) + if process is None: + return True + for i in range(constants.KILL_SIGNAL_RETRY): + if not process.is_running(): + return True + try: + process.kill() + except psutil.NoSuchProcess: + return True + sleep(1) + return False + + @staticmethod + def sub(*args, **kwargs): + process = psutil.Popen(args, stdout=PIPE, stderr=PIPE, **kwargs) + stdout, stderr = process.communicate() + process.wait() + return { + 'stdout': stdout, + 'stderr': stderr, + 'code': process.returncode, + 'process': process, + } + + @staticmethod + def run(*args, **kwargs): + process = psutil.Popen(args, **kwargs) + process.communicate() + process.wait() + return process.returncode + + def run_shell(self, command, **kwargs): + return self.run(command, shell=True, **kwargs) + + def sub_shell(self, command, **kwargs): + return self.sub(command, shell=True, **kwargs) + + @staticmethod + def daemonize(*args, **kwargs): + """ + :param args: + :param kwargs: + :return: + """ + # TODO: need to fork twice to prevent children from becoming zombies + process = psutil.Popen( + args, + shell=False, + stdin=None, + stdout=None, + stderr=None, + close_fds=True, + cwd='/', + **kwargs + ) + return process + +# TODO: a method to kill a process and all of its children diff --git a/ocf_agent/parameter.py b/ocf_agent/parameter.py new file mode 100644 index 0000000..b088e8a --- /dev/null +++ b/ocf_agent/parameter.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- + +import os +from ocf_agent import constants +from ocf_agent.helpers import docstring_format +from ocf_agent.helpers import memoization +from ocf_agent.helpers import string_to_bool +from ocf_agent.helpers import string_to_integer + + +class BaseParameter(object): + def __init__(self, parameters=None): + """ + A parameter should be created with its parent Parameters + object provided ans the first argument. + + :param parameters: The parent parameters object + :type parameters: Parameters + """ + self.parameters = parameters + self.agent = self.parameters.agent + self._value = None + + def validate(self): + """ + Validate if this Parameter object is configured correctly + """ + if not self.__class__.__name__.startswith( + constants.PARAMETER_CLASS_PREFIX + ): + self.agent.exit.error_configuration( + "ResourceParameter class name does not start with '%s'" % + constants.PARAMETER_CLASS_PREFIX + ) + + @property + @docstring_format(constants.CONST_NAME) + def name(self): + """ + The parameter's name can be taken from the parameter class' name + or manually defined in the parameter class by the *{0}* constant. + + :return: Parameter's name + :rtype: str + """ + if hasattr(self, constants.CONST_NAME): + name = getattr( + self, + constants.CONST_NAME + ) + else: + name = str( + self.__class__.__name__[len(constants.PARAMETER_CLASS_PREFIX):] + ) + return name + + @property + @docstring_format(constants.CONST_SHORT_DESCRIPTION) + def short_description(self): + """ + Short description of this parameter. Will default to the parameter's + name unless defined by the *{0}* constant. + + :return: Short description + :rtype: str + """ + return getattr( + self, + constants.CONST_SHORT_DESCRIPTION, + self.name + ) + + @property + @docstring_format(constants.CONST_LONG_DESCRIPTION) + def long_description(self): + """ + Long description of this parameter. It will be taken from the constant + *{0}* in the parameter's class or will default to the short + description. + + :return: Long description + :rtype: str + """ + return getattr( + self, + constants.CONST_LONG_DESCRIPTION, + self.short_description + ) + + @property + @docstring_format(constants.CONST_LANGUAGE, constants.DEFAULT_LANGUAGE) + def language(self): + """ + The description language reported by this parameter. Will use the + value defined by the *{0}* constant or the default value **{1}**. + + :return: Parameter language + :rtype: str + """ + return getattr( + self, + constants.CONST_LANGUAGE, + constants.DEFAULT_LANGUAGE + ) + + @property + def type(self): + """ + Returns the Python type which the value of this parameter should + belong to. This method should be redefined by the inherited classes. + + :return: Expected type of the value + """ + self.agent.exit.error_unimplemented( + '%s is an abstract class and cannot be used directly' % + self.__class__.__name__ + ) + + @property + def type_name(self): + """ + Returns the string name of the expected type + + :rtype: str + :return: Type name + """ + if self.type is int: + return "integer" + if self.type is str: + return "string" + if self.type is bool: + return "boolean" + + @property + @memoization + @docstring_format(constants.CONST_DEFAULT) + def default(self): + """ + The default value of this parameter. Can be defined by the *{0}* + constant inside the parameter class definition. + + :return: The default value + :rtype: bool or str or int + """ + return self.process_value( + getattr( + self, + constants.CONST_DEFAULT, + None) + ) + + @property + def value(self): + """ + Returns this parameter's value. It will try to take the already + defined value. Then the value will be taken from the + corresponding environment variable, and, finally, the + default value will be returned. + + :return: The parameter's value of the default value + :rtype: bool or str or int + """ + if self._value is not None: + return self._value + if self.env_variable_name in os.environ: + self.value = os.environ[self.env_variable_name] + if self._value is not None: + return self._value + return self.default + + @value.setter + def value(self, new_value): + """ + Manually set the current parameter value and run the value processing. + + :param new_value: The new value + :type new_value: object + """ + self._value = self.process_value(new_value) + + @property + @memoization + @docstring_format(constants.CONST_UNIQUE) + def unique(self): + """ + Indicates that this value is unique across the cluster. + If there are several resources of this type, a single value + can be assigned only to a single instance and other instances + should have different values. + If disabled, many similar resources may have the same value of + this parameter. + Can be set by the *{0}* constant in the parameter definition + and can be either True or False. Defaults to **False**. + + :rtype: bool + :return: true or false + """ + return string_to_bool( + getattr(self, constants.CONST_UNIQUE, False) + ) + + @property + @memoization + @docstring_format(constants.CONST_REQUIRED) + def required(self): + """ + Indicates that this value is required to use this resource and + a user should provide an explicit value for this parameter. + Default value will nod be used. + Can ne set by the *{0}* constant in the parameter definition + and can be either true or False. Defaults to **False**. + + :rtype: bool + :return: true or false + """ + return string_to_bool( + getattr(self, constants.CONST_REQUIRED, False) + ) + + @property + def env_variable_name(self): + """ + The environment variable name used to pass the value + of this parameter from the cluster configuration. + + :rtype: str + :return: The environment variable name + """ + return constants.VAR_PARAMETER_PREFIX + self.name + + def process_value(self, value): + """ + Called modify and validate function to import a new data + either for the default of for the current parameter value. + + :type value: object + :param value: a new value + :rtype: object + :return: processed value + """ + value = self.modify_value(value) + if not self.validate_value(value): + self.parameters.agent.exit.error_arguments( + "The value: '%s' of the parameter: '%s' is not correct!" % ( + value, self.name)) + return value + + def validate_value(self, value): + """ + Validates the value of this parameter. This function + can be redefined by a child class if you need to validate + a specific value type. Returns true if value passes the test. + + :type value: object + :param value: a new value + :rtype: bool + :return: true or false + """ + if value is None: + return True + return isinstance(value, self.type) + + def modify_value(self, value): + """ + This function is used to somehow modify the new value. + It should be redefined by a child class for a specific + action. Does nothing by default. + + :type value: object + :param value: the new value + :rtype: object + :return: the modified value + """ + return value + + +############################################################################### + + +class StringParameter(BaseParameter): + @property + def type(self): + """ + Type of the String property + + :return: string type + """ + return str + + def modify_value(self, value): + """ + String provider does not chencge the input value keeping + it as a string. + + :param value: input value + :type value: object + :return: modified value + :rtype: str + """ + if value is None: + return value + return str(value) + + +############################################################################### + + +class IntegerParameter(BaseParameter): + @property + def type(self): + """ + Type of the Integer property + + :return: integer type + """ + return int + + def modify_value(self, value): + """ + Integer property converts ins value to an integer + or to None if input value cannot be converted. + + :param value: input value + :type value: str + :return: integer value + """ + return string_to_integer(value) + + +############################################################################### + + +class BooleanParameter(BaseParameter): + @property + def type(self): + """ + Type of the Boolean property + + :return: boolean type + """ + return bool + + def modify_value(self, value): + """ + Boolean property converts its value to boolean or to None + if the value cannot be converted. + + :param value: input value + :type value: str + :return: boolean value + :rtype: bool + """ + return string_to_bool(value) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..3615abe --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from ocf_agent import AUTHOR, LICENSE, VERSION, EMAIL, PROJECT +from distutils.core import setup + +# from Cython.Build import cythonize +# ext_modules=cythonize(['ocf_agent/*.py', 'dummy.py']) + +setup( + name=PROJECT, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + py_modules=['ocf_agent'], + packages=['ocf_agent', 'ocf_agent/modules'], + license=LICENSE, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/agent/__init__.py b/tests/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/agent/test_agent.py b/tests/agent/test_agent.py new file mode 100644 index 0000000..3ec1a1f --- /dev/null +++ b/tests/agent/test_agent.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from mock import patch +from tests.fixtures.agents import UnitTestAgent +from tests.fixtures.agents import UnitTestEmptyAgent + + +class AgentConfiguredBasicPropertiesTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + + def tearDown(self): + del self.agent + + def test_has_name(self): + self.assertEqual(self.agent.name, 'configured_ocf_agent') + + def test_has_short_description(self): + self.assertEqual(self.agent.short_description, + 'Test OCF agent') + + def test_has_long_description(self): + self.assertEqual(self.agent.long_description, 'OCF Agent for tests') + + def test_has_language(self): + self.assertEqual(self.agent.language, 'en_US') + + def test_has_encoding(self): + self.assertEqual(self.agent.encoding, 'UTF-8') + + def test_has_version(self): + self.assertEqual(self.agent.version, '0.0.1') + + @patch('sys.argv', ['test', 'monitor']) + def test_can_get_action(self): + self.assertEqual(self.agent.action, 'monitor') + + def test_can_set_action(self): + self.agent.action = 'monitor' + self.assertEqual(self.agent.action, 'monitor') + + def test_can_use_action_aliases(self): + self.agent.action = 'status' + self.assertEqual(self.agent.action, 'monitor') + + @patch('sys.argv', ['test']) + @patch('ocf_agent.agent.Agent.usage') + @patch('ocf_agent.modules.exit.Exit.output') + def test_fails_if_no_action_set(self, mock1, mock2): + with self.assertRaises(SystemExit): + self.agent.validate() + self.assertTrue(mock1.called) + self.assertTrue(mock2.called) + + @patch('sys.argv', ['test', 'test']) + @patch('ocf_agent.agent.Agent.usage') + @patch('ocf_agent.modules.exit.Exit.output') + def test_fails_if_bad_action(self, mock1, mock2): + with self.assertRaises(SystemExit): + self.agent.validate() + self.assertTrue(mock1.called) + self.assertTrue(mock2.called) + + +class AgentEmptyBasicPropertiesTest(TestCase): + def setUp(self): + self.agent = UnitTestEmptyAgent() + + def tearDown(self): + del self.agent + + def test_has_name(self): + self.assertEqual(self.agent.name, 'UnitTestEmptyAgent') + + def test_has_short_description(self): + self.assertEqual(self.agent.short_description, 'UnitTestEmptyAgent') + + def test_has_long_description(self): + self.assertEqual(self.agent.long_description, 'UnitTestEmptyAgent') + + def test_has_language(self): + self.assertEqual(self.agent.language, 'en') + + def test_has_encoding(self): + self.assertEqual(self.agent.encoding, 'utf-8') + + def test_has_version(self): + self.assertEqual(self.agent.version, '1') diff --git a/tests/agent/test_agent_environment.py b/tests/agent/test_agent_environment.py new file mode 100644 index 0000000..5dd0409 --- /dev/null +++ b/tests/agent/test_agent_environment.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from mock import patch +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.agent import Agent +import os + + +class AgentEnvTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.environment = self.agent.environment + os.environ = {} + + def tearDown(self): + del self.agent + del self.environment + + def test_has_agent(self): + self.assertEquals(self.environment.agent, self.agent) + self.assertIsInstance(self.environment.agent, Agent) + + @patch('os.environ', {'OCF_RESOURCE_TYPE': 'test_type'}) + def test_can_get_resource_type(self): + self.assertEqual(self.environment.res_type, 'test_type') + + def test_default_resource_type(self): + self.assertEqual(self.environment.res_type, None) + + @patch('os.environ', {'OCF_RESOURCE_PROVIDER': 'test_provider'}) + def test_can_get_resource_provider(self): + self.assertEqual(self.environment.res_provider, 'test_provider') + + def test_default_resource_provider(self): + self.assertEqual(self.environment.res_provider, None) + + def test_can_get_resource_class(self): + self.assertEqual(self.environment.res_class, 'ocf') + + @patch('os.environ', {'OCF_RESOURCE_INSTANCE': 'test_instance'}) + def test_can_get_resource_instance(self): + self.assertEqual(self.environment.res_instance, 'test_instance') + self.assertEqual(self.environment.instance_name, 'test_instance') + self.assertEqual(self.environment.instance, 'test_instance') + self.assertEqual(self.environment.instance_suffix, None) + + @patch('os.environ', {'OCF_RESOURCE_INSTANCE': 'test_instance:1'}) + def test_can_get_resource_instance_with_number(self): + self.assertEqual(self.environment.res_instance, 'test_instance:1') + self.assertEqual(self.environment.instance_name, 'test_instance') + self.assertEqual(self.environment.instance, 'test_instance') + self.assertEqual(self.environment.instance_suffix, '1') + + def test_default_resource_instance(self): + self.assertEqual(self.environment.res_instance, self.agent.name) + self.assertEqual(self.environment.instance_name, self.agent.name) + self.assertEqual(self.environment.instance, self.agent.name) + self.assertEqual(self.environment.instance_suffix, None) + + @patch('os.environ', {'OCF_CHECK_LEVEL': '20'}) + def test_can_get_resource_check_level(self): + self.assertEqual(self.environment.check_level, 20) + self.assertEqual(self.environment.depth, self.environment.check_level) + + def test_default_check_level(self): + self.assertEqual(self.environment.check_level, 0) + self.assertEqual(self.environment.depth, 0) + + @patch('os.environ', {'OCF_RA_VERSION_MAJOR': '2'}) + def test_can_get_ra_version_major(self): + self.assertEqual(self.environment.ra_version_major, 2) + + def test_can_get_default_ra_version_major(self): + self.assertEqual(self.environment.ra_version_major, 1) + + @patch('os.environ', {'OCF_RA_VERSION_MINOR': '1'}) + def test_can_get_ra_version_minor(self): + self.assertEqual(self.environment.ra_version_minor, 1) + + def test_can_get_default_ra_version_minor(self): + self.assertEqual(self.environment.ra_version_minor, 0) + + @patch('os.environ', {'HA_debug': '1'}) + def test_can_get_ha_debug(self): + self.assertEqual(self.environment.is_debug, True) + + @patch('os.environ', {'HA_debug': 'bad_value'}) + def test_can_get_invalid_ha_debug(self): + self.assertEquals(self.environment.is_debug, False) + + def test_can_get_default_ha_debug(self): + self.assertEqual(self.environment.is_debug, False) + + @patch('os.environ', {'HA_LOGD': 'yes'}) + def test_can_get_ha_logd(self): + self.assertEqual(self.environment.is_logd, True) + + @patch('os.environ', {'HA_LOGD': 'bad_value'}) + def test_can_get_invalid_ha_logd(self): + self.assertEqual(self.environment.is_logd, False) + + def test_can_get_default_ha_logd(self): + self.assertEqual(self.environment.is_logd, False) + + @patch('os.environ', {'HA_LOGFACILITY': 'user'}) + def test_can_get_ha_log_facility(self): + self.assertEqual(self.environment.log_facility, 'user') + + def test_can_get_default_ha_log_facility(self): + self.assertEqual(self.environment.log_facility, 'daemon') + + @patch('os.environ', {'OCF_ROOT': '/usr/local/lib/ocf'}) + def test_can_get_ocf_root(self): + self.assertEqual(self.environment.ocf_root, '/usr/local/lib/ocf') + + def test_can_get_default_ocf_root(self): + self.assertEqual(self.environment.ocf_root, '/usr/lib/ocf') + + @patch('os.environ', {'HA_cluster_type': 'heartbeat'}) + def test_can_get_cluster_type(self): + self.assertEqual(self.environment.cluster_type, 'heartbeat') + + def test_can_get_default_cluster_type(self): + self.assertEqual(self.environment.cluster_type, 'corosync') + + @patch('os.environ', {'HA_quorum_type': 'heartbeat'}) + def test_can_get_quorum_type(self): + self.assertEqual(self.environment.quorum_type, 'heartbeat') + + def test_can_get_default_quorum_type(self): + self.assertEqual(self.environment.quorum_type, 'pcmk') + + @patch('os.environ', {'OCF_RESKEY_CRM_meta_master_max': '1'}) + def test_can_get_meta_master_max(self): + self.assertEqual(self.environment.meta_master_max, 1) + self.assertEqual(self.environment.is_ms, True) + + def test_can_get_default_meta_master_max(self): + self.assertEqual(self.environment.meta_master_max, None) + self.assertEqual(self.environment.is_ms, False) + + @patch('os.environ', {'OCF_RESKEY_CRM_meta_clone_max': '1'}) + def test_can_get_meta_clone_max(self): + self.assertEqual(self.environment.meta_clone_max, 1) + self.assertEqual(self.environment.is_clone, True) + + def test_can_get_default_meta_clone_max(self): + self.assertEqual(self.environment.meta_clone_max, None) + self.assertEqual(self.environment.is_clone, False) + + @patch('os.environ', {'OCF_RESKEY_CRM_meta_master': '1'}) + def test_can_get_meta_master(self): + self.assertEqual(self.environment.meta_master, 1) + + def test_can_get_default_meta_master(self): + self.assertEqual(self.environment.meta_master, None) + + @patch('os.environ', {'OCF_RESKEY_CRM_meta_clone': '1'}) + def test_can_get_meta_clone(self): + self.assertEqual(self.environment.meta_clone, 1) + + def test_can_get_default_meta_clone(self): + self.assertEqual(self.environment.meta_clone, None) + + @patch('os.environ', { + 'OCF_a': '1', + 'HA_b': '2', + 'C': '3'}) + def test_can_collect_environment(self): + self.assertDictEqual(self.environment.all, + {'HA_b': '2', 'OCF_a': '1'}) + + def test_default_environment(self): + self.assertDictEqual(self.environment.all, {}) + + @patch('os.environ', { + 'OCF_RESKEY_CRM_meta_a': '1', + 'OCF_RESKEY_CRM_meta_b': '2', + 'C': '3'}) + def test_can_collect_meta_environment(self): + self.assertDictEqual(self.environment.meta, + {'b': '2', 'a': '1'}) + + def test_default_meta_environment(self): + self.assertDictEqual(self.environment.meta, {}) + + @patch('os.environ', { + 'OCF_RESKEY_CRM_meta_notify_a': '1', + 'OCF_RESKEY_CRM_meta_notify_b': '2', + 'C': '3'}) + def test_can_collect_notify_environment(self): + self.assertDictEqual(self.environment.notify, + {'b': '2', 'a': '1'}) + + def test_default_notify_environment(self): + self.assertDictEqual(self.environment.notify, {}) diff --git a/tests/agent/test_agent_exit.py b/tests/agent/test_agent_exit.py new file mode 100644 index 0000000..21e8e13 --- /dev/null +++ b/tests/agent/test_agent_exit.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from mock import patch +from ocf_agent.agent import Agent +from tests.fixtures.agents import UnitTestAgent + + +class TestAgentExit(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.exit = self.agent.exit + + def tearDown(self): + del self.agent + del self.exit + + def test_has_agent(self): + self.assertEquals(self.exit.agent, self.agent) + self.assertIsInstance(self.exit.agent, Agent) + + @patch('ocf_agent.modules.log.Log.info') + def test_can_send_a_string_to_the_log(self, mock1): + self.exit.output('test event', 'test message', 0) + mock1.assert_called_once_with( + 'test event: test message - exit code: 0' + ) + + +exit_events = { + 'success': 0, + 'error_generic': 1, + 'error_arguments': 2, + 'error_unimplemented': 3, + 'error_permissions': 4, + 'error_installation': 5, + 'error_configuration': 6, + 'not_running': 7, + 'running_master': 8, + 'master_failed': 9, +} + +for event, return_code in exit_events.items(): + @patch('ocf_agent.modules.exit.Exit.output') + def function_test(self, mock1): + message = '%s message' % event + with self.assertRaises(SystemExit) as context: + method = getattr(self.exit, event) + method(message) + mock1.assert_called_once_with( + event, + message, + return_code, + ) + self.assertEquals(context.exception.code, return_code) + + function_name = 'test_can_exit_with_%s' % event + function_test.__name__ = function_name + setattr(TestAgentExit, function_name, function_test) diff --git a/tests/agent/test_agent_handlers.py b/tests/agent/test_agent_handlers.py new file mode 100644 index 0000000..69b2b75 --- /dev/null +++ b/tests/agent/test_agent_handlers.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from mock import patch +from mock import PropertyMock +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.handler import MonitorHandler +from ocf_agent.handler import Handler +from ocf_agent.agent import Agent + + +class AgentHandlersTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.handlers = self.agent.handlers + + def tearDown(self): + del self.agent + del self.handlers + + def test_has_agent(self): + self.assertEquals(self.handlers.agent, self.agent) + self.assertIsInstance(self.handlers.agent, Agent) + + def test_can_get_all_handlers(self): + handlers = self.handlers.all + self.assertIsInstance(handlers, list) + self.assertEquals(len(handlers), 4) + + def test_can_get_a_handler(self): + action = 'start' + handler = self.handlers.get(action=action) + self.assertIsInstance(handler, Handler) + self.assertEquals(handler.action, action) + + def test_can_get_the_monitor_handler_with_a_specific_depth(self): + action = 'monitor' + got_handler = self.handlers.get(action=action, check_level=10) + self.assertIsInstance(got_handler, MonitorHandler) + self.assertEquals(got_handler.depth, 10) + got_handler = self.handlers.get(action=action, check_level=0) + self.assertIsInstance(got_handler, MonitorHandler) + self.assertEquals(got_handler.depth, 0) + + def test_can_get_any_monitor_action_as_a_fallback(self): + action = 'monitor' + depth = 100 + got_handler = self.handlers.get(action=action, check_level=depth) + self.assertIsInstance(got_handler, MonitorHandler) + self.assertIn(got_handler.depth, [0, 10]) + + def test_can_get_all_defined_handler_action(self): + defined_actions = self.handlers.actions + expected_actions = self.agent.expected_defined_handler_actions + self.assertEquals(defined_actions, expected_actions) + self.assertIsInstance(defined_actions, set) + + def test_can_get_the_current_handler(self): + self.agent.action = 'start' + self.assertIsInstance( + self.handlers.current, + Handler, + ) + self.assertEquals( + self.handlers.current.action, + self.agent.action, + ) + + def test_can_get_the_correct_current_monitor_handler(self): + with patch('ocf_agent.modules.environment.Environment.check_level', + new_callable=PropertyMock) as check_level_mock: + check_level_mock.return_value = 10 + self.agent.action = 'monitor' + self.assertIsInstance( + self.handlers.current, + MonitorHandler, + ) + self.assertEquals( + self.handlers.current.action, + self.agent.action, + ) + self.assertEquals(self.handlers.current.depth, 10) + check_level_mock.return_value = 0 + self.assertEquals(self.handlers.current.depth, 0) + + def test_can_get_the_current_handler_attributes(self): + self.agent.action = 'monitor' + attributes = self.handlers.current.attributes + self.assertIsInstance(attributes, dict) + self.assertEquals(attributes, self.agent.expected_monitor_attributes) + + def test_can_get_all_handlers_attributes(self): + attributes = self.handlers.attributes + self.assertIsInstance(attributes, dict) + self.assertDictEqual( + attributes, + self.agent.expected_handlers_attributes, + ) diff --git a/tests/agent/test_agent_lock.py b/tests/agent/test_agent_lock.py new file mode 100644 index 0000000..a454ed5 --- /dev/null +++ b/tests/agent/test_agent_lock.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.agent import Agent +from mock import patch + + +class TestLockAgent(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.lock = self.agent.lock + + def tearDown(self): + del self.agent + del self.lock + + def test_has_agent(self): + self.assertEquals(self.lock.agent, self.agent) + self.assertIsInstance(self.lock.agent, Agent) + + def test_hash_directory(self): + self.assertEqual(self.lock.directory, '/var/lock/pacemaker') + + @patch( + 'ocf_agent.modules.environment.Environment.res_instance', + 'unit_test_agent') + def test_can_make_file_name(self): + self.assertEqual( + self.lock.file_name(), + 'configured_ocf_agent-unit_test_agent.lock', + ) + self.assertEqual( + self.lock.file_name('test'), + 'configured_ocf_agent-unit_test_agent-test.lock', + ) + + @patch( + 'ocf_agent.modules.environment.Environment.res_instance', + 'unit_test_agent') + def test_can_make_file_path(self): + self.assertEqual( + self.lock.file_path(), + '/var/lock/pacemaker/' + 'configured_ocf_agent-unit_test_agent.lock', + ) + self.assertEqual( + self.lock.file_path('test'), + '/var/lock/pacemaker/' + 'configured_ocf_agent-unit_test_agent-test.lock', + ) + + @patch('os.mkdir') + @patch('os.path') + def test_can_make_directory(self, mock1, mock2): + mock2.return_value = None + mock1.isdir.return_value = False + self.lock.make_directory() + mock2.assert_called_once_with('/var/lock/pacemaker') + mock2.reset_mock() + mock1.isdir.return_value = True + self.lock.make_directory() + self.assertFalse(mock2.called) + + @patch('ocf_agent.modules.lock.Lock.file_path', + return_value='/path/to/file') + @patch('os.path') + def test_can_check_that_file_is_present(self, mock1, mock2): + mock1.isfile.return_value = True + self.assertTrue(self.lock.file_is_present()) + mock1.isfile.assert_called_once_with('/path/to/file') + mock1.isfile.return_value = False + self.assertFalse(self.lock.file_is_present()) + self.assertTrue(mock2.called) + + # @patch('ocf_agent.modules.lock.Lock.file_path', + # return_value='/path/to/file') + # @patch('ocf_agent.modules.lock.Lock.make_directory', return_value=None) + # def test_can_create_file(self, mock1, mock2): + # with patch('ocf_agent.modules.lock.Lock.open', mock_open()) as mock3: + # self.lock.create_file() + # self.assertTrue(mock1.called) + # self.assertTrue(mock2.called) + # mock3.assert_called_once_with('/path/to/file', 'w') + # self.assertTrue(mock3().close.called) + + @patch('os.remove') + @patch('ocf_agent.modules.lock.Lock.file_path', + return_value='/path/to/file') + @patch( + 'ocf_agent.modules.lock.Lock.file_is_present', + return_value=True) + def test_can_remove_file(self, mock1, mock2, mock3): + self.lock.remove_file() + self.assertTrue(mock1.called) + self.assertTrue(mock2.called) + self.assertTrue(mock3.called) + mock3.assert_called_once_with('/path/to/file') diff --git a/tests/agent/test_agent_log.py b/tests/agent/test_agent_log.py new file mode 100644 index 0000000..4a5ee5c --- /dev/null +++ b/tests/agent/test_agent_log.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +import logging +import logging.handlers +import sys +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent +from tests.fixtures.agents import UnitTestEmptyAgent +from ocf_agent.agent import Agent +from mock import patch + + +class TestLogAgent(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.log = self.agent.log + + def tearDown(self): + del self.agent + del self.log + + def test_has_agent(self): + self.assertEquals(self.log.agent, self.agent) + self.assertIsInstance(self.log.agent, Agent) + + def test_can_return_the_logger_object(self): + self.assertIsInstance(self.log.logger, logging.Logger) + + def logger_object_hash_configured_handlers(self): + with patch('ocf_agent.modules.log.Log.enabled_handlers') as mock1: + mock1.return_value = ['console'] + self.assertEqual( + self.log.logger.handlers, + [self.log.handler_console], + ) + mock1.return_value = ['file'] + self.assertEqual( + self.log.logger.handlers, + [self.log.handler_file], + ) + mock1.return_value = ['syslog'] + self.assertEqual( + self.log.logger.handlers, + [self.log.handler_syslog], + ) + + def test_can_get_console_handler(self): + self.assertIsInstance(self.log.handler_console, logging.StreamHandler) + self.assertEqual(self.log.handler_console.stream, sys.stderr) + + def test_can_get_file_handler(self): + self.assertIsInstance(self.log.handler_file, logging.FileHandler) + self.assertEqual( + self.log.handler_file.baseFilename, + self.log.log_file_path + ) + + def test_can_get_all_formatters(self): + self.assertIsInstance(self.log.formatter_file, logging.Formatter) + self.assertIsInstance(self.log.formatter_console, logging.Formatter) + self.assertIsInstance(self.log.formatter_syslog, logging.Formatter) + + @patch('ocf_agent.modules.environment.Environment.log_facility', 'test') + def test_can_get_syslog_handler(self): + self.assertIsInstance( + self.log.handler_syslog, + logging.handlers.SysLogHandler, + ) + self.assertEqual( + self.log.handler_syslog.facility, + 'test', + ) + + def test_has_format_properties(self): + self.assertTrue(hasattr(self.log, 'format_date')) + self.assertTrue(hasattr(self.log, 'format_date_prefix')) + self.assertTrue(hasattr(self.log, 'format_suffix')) + + def test_can_get_enabled_handlers(self): + self.assertEqual(self.log.enabled_handlers, ['console']) + + def test_can_get_default_enabled_handlers(self): + empty_agent = UnitTestEmptyAgent() + self.assertEqual( + empty_agent.log.enabled_handlers, ['console', 'syslog'] + ) + del empty_agent + + @patch('ocf_agent.modules.environment.Environment.is_debug', False) + def test_can_get_normal_log_level(self): + self.assertEqual(self.log.level, logging.INFO) + + @patch('ocf_agent.modules.environment.Environment.is_debug', True) + def test_can_get_debug_log_level(self): + self.assertEqual(self.log.level, logging.DEBUG) + + @patch('sys.stdout') + def test_can_write_to_output(self, mock1): + mock1.write.return_value = None + self.log.output('test') + mock1.write.assert_called_once_with('test') + + def test_has_log_methods(self): + methods = [ + 'log', 'debug', 'info', 'warning', 'warn', 'error', 'err', + 'critical', 'crit', 'exception', + ] + for method in methods: + self.assertTrue(hasattr(self.log, method)) diff --git a/tests/agent/test_agent_metadata.py b/tests/agent/test_agent_metadata.py new file mode 100644 index 0000000..6ea670c --- /dev/null +++ b/tests/agent/test_agent_metadata.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.agent import Agent + + +class AgentMetaDataTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.metadata = self.agent.metadata + + def tearDown(self): + del self.agent + del self.metadata + + def test_has_agent(self): + self.assertEquals(self.metadata.agent, self.agent) + self.assertIsInstance(self.metadata.agent, Agent) + + def test_can_format_a_line(self): + line_template = 'a: %s, b: %s, c: %d' + line_parameters = ('test', '', 1) + line_expected = " a: test, b: <test/>, c: 1\n" + line = self.metadata.format_line( + 2, + line_template, + *line_parameters + ) + self.assertEquals(line, line_expected) + + def test_can_generate_meta_data_xml(self): + self.maxDiff = None + self.assertEquals(self.metadata.xml, self.agent.expected_meta_data) diff --git a/tests/agent/test_agent_parameters.py b/tests/agent/test_agent_parameters.py new file mode 100644 index 0000000..bf77d17 --- /dev/null +++ b/tests/agent/test_agent_parameters.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from ocf_agent.parameter import StringParameter +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.agent import Agent +import os + + +class AgentParametersTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + os.environ = {} + + def tearDown(self): + del self.agent + del self.parameters + + def test_has_agent(self): + self.assertEquals(self.parameters.agent, self.agent) + self.assertIsInstance(self.parameters.agent, Agent) + + def test_can_get_all_parameters(self): + self.assertIsInstance(self.parameters.all, dict) + self.assertEquals(len(self.parameters.all.keys()), 1) + + def test_can_get_a_parameter(self): + self.assertIsInstance( + self.parameters.get('test'), + StringParameter, + ) + + def test_can_get_parameter_value(self): + self.assertEqual(self.parameters.value('test'), + 'test default value') + + def test_can_collect_all_parameters_values(self): + self.assertDictEqual( + self.parameters.values, {'test': 'test default value'} + ) diff --git a/tests/agent/test_agent_pid.py b/tests/agent/test_agent_pid.py new file mode 100644 index 0000000..6ba1e99 --- /dev/null +++ b/tests/agent/test_agent_pid.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.agent import Agent +from mock import patch + + +class TestPidAgent(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.pid = self.agent.pid + + def tearDown(self): + del self.agent + del self.pid + + def test_has_agent(self): + self.assertEquals(self.pid.agent, self.agent) + self.assertIsInstance(self.pid.agent, Agent) + + def test_hash_directory(self): + self.assertEqual(self.pid.directory, '/var/run/pacemaker') + + @patch( + 'ocf_agent.modules.environment.Environment.res_instance', + 'unit_test_agent') + def test_can_make_file_name(self): + self.assertEqual( + self.pid.file_name(), + 'configured_ocf_agent-unit_test_agent.pid', + ) + self.assertEqual( + self.pid.file_name('test'), + 'configured_ocf_agent-unit_test_agent-test.pid', + ) + + @patch( + 'ocf_agent.modules.environment.Environment.res_instance', + 'unit_test_agent') + def test_can_make_file_path(self): + self.assertEqual( + self.pid.file_path(), + '/var/run/pacemaker/' + 'configured_ocf_agent-unit_test_agent.pid', + ) + self.assertEqual( + self.pid.file_path('test'), + '/var/run/pacemaker/' + 'configured_ocf_agent-unit_test_agent-test.pid', + ) + + @patch('os.mkdir') + @patch('os.path') + def test_can_make_directory(self, mock1, mock2): + mock2.return_value = None + mock1.isdir.return_value = False + self.pid.make_directory() + mock2.assert_called_once_with('/var/run/pacemaker') + mock2.reset_mock() + mock1.isdir.return_value = True + self.pid.make_directory() + self.assertFalse(mock2.called) + + @patch('ocf_agent.modules.pid.Pid.file_path', + return_value='/path/to/file') + @patch('os.path') + def test_can_check_that_file_is_present(self, mock1, mock2): + mock1.isfile.return_value = True + self.assertTrue(self.pid.is_present) + mock1.isfile.assert_called_once_with('/path/to/file') + mock1.isfile.return_value = False + self.assertFalse(self.pid.is_present) + self.assertTrue(mock2.called) + + # @patch('ocf_agent.modules.pid.Pid.file_path', + # return_value='/path/to/file') + # @patch('ocf_agent.modules.pid.Pid.make_directory', return_value=None) + # def test_can_create_file(self, mock1, mock2): + # with patch('ocf_agent.modules.pid.Pid.open', mock_open()) as mock3: + # self.pid.create_file(1) + # self.assertTrue(mock1.called) + # self.assertTrue(mock2.called) + # mock3.assert_called_once_with('/path/to/file', 'w') + # mock3().write.assert_called_once_with("1\n") + + @patch('os.remove') + @patch('ocf_agent.modules.pid.Pid.file_path', + return_value='/path/to/file') + @patch( + 'ocf_agent.modules.pid.Pid.file_is_present', + return_value=True) + def test_can_remove_file(self, mock1, mock2, mock3): + self.pid.remove_file() + self.assertTrue(mock1.called) + self.assertTrue(mock2.called) + self.assertTrue(mock3.called) + mock3.assert_called_once_with('/path/to/file') + + # @patch('ocf_agent.modules.pid.Pid.file_path', + # return_value='/path/to/file') + # @patch( + # 'ocf_agent.modules.pid.Pid.file_is_present', + # return_value=True) + # def test_can_read_file(self, mock1, mock2): + # open_name = '%s.open' % __name__ + # with patch(open_name, mock_open(read_data="1\n")) as mock3: + # pid = self.pid.read_file() + # self.assertTrue(mock1.called) + # self.assertTrue(mock2.called) + # mock3.assert_called_once_with('/path/to/file', 'r') + # self.assertEqual(pid, 1) diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/agents.py b/tests/fixtures/agents.py new file mode 100644 index 0000000..d0fa9d6 --- /dev/null +++ b/tests/fixtures/agents.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +from ocf_agent.agent import Agent +from ocf_agent.parameter import StringParameter +from ocf_agent.handler import MonitorHandler +from ocf_agent.handler import Handler + + +class UnitTestAgent(Agent): + VERSION = "0.0.1" + SHORTDESC = "Test OCF agent" + LONGDESC = "OCF Agent for tests" + LANG = 'en_US' + ENCODING = 'UTF-8' + NAME = 'configured_ocf_agent' + LOG_HANDLERS = ['console'] + + class OCFParameter_test(StringParameter): + DEFAULT = 'test default value' + LONGDESC = "Test parameter description" + SHORTDESC = "Test parameter" + + class OCFHandler_start(Handler): + pass + + class OCFHandler_StopHandler(Handler): + ACTION = 'stop' + LONGDESC = "The configured stop handler" + SHORTDESC = "Stop handler" + LANG = 'en_US' + TIMEOUT = '30' + METHOD = 'stop_agent' + + class OCFHandler_monitor(MonitorHandler): + pass + + class OCFHandler_monitor_long(MonitorHandler): + DEPTH = '10' + INTERVAL = '60' + ROLE = 'master' + METHOD = 'handler_monitor_long' + + ########## + + def handler_start(self): + pass + + def stop_agent(self): + pass + + def handler_monitor(self): + pass + + def handler_monitor_long(self): + pass + + @property + def expected_handlers_attributes(self): + return { + 'monitor': { + 'name': 'monitor', + 'timeout': 20, + 'depth': 0, + 'interval': 10, + }, + 'monitor_master_10': { + 'name': 'monitor', + 'timeout': 20, + 'depth': 10, + 'interval': 60, + 'role': 'Master', + }, + 'stop': { + 'name': 'stop', + 'timeout': 30, + }, + 'start': { + 'name': 'start', + 'timeout': 20. + }, + } + + @property + def expected_meta_data(self): + return ''' + + + 0.0.1 + OCF Agent for tests + Test OCF agent + + + Test parameter description + Test parameter + + + + + + + + + + +''' + + @property + def expected_start_attributes(self): + return self.expected_handlers_attributes['start'] + + @property + def expected_monitor_attributes(self): + return self.expected_handlers_attributes['monitor'] + + @property + def expected_monitor_long_attributes(self): + return self.expected_handlers_attributes['monitor_10'] + + @property + def expected_defined_handler_actions(self): + return { + 'status', 'monitor', 'meta-data', + 'validate-all', 'usage', 'help', + 'restart', 'validate', 'meta', + 'start', 'stop' + } + + +class UnitTestEmptyAgent(Agent): + pass diff --git a/tests/handler/__init__.py b/tests/handler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/handler/test_handler_basic.py b/tests/handler/test_handler_basic.py new file mode 100644 index 0000000..9291168 --- /dev/null +++ b/tests/handler/test_handler_basic.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from mock import patch +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.modules.handlers import Handlers +from ocf_agent.handler import Handler +from ocf_agent.agent import Agent + + +class MisnamedHandler(Handler): + pass + + +class HandlerStartPropertyTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.handlers = self.agent.handlers + self.handler = UnitTestAgent.OCFHandler_start(self.handlers) + + def tearDown(self): + del self.handler + del self.agent + del self.handlers + + def test_has_handlers(self): + self.assertEquals(self.handler.handlers, self.handlers) + self.assertIsInstance(self.handler.handlers, Handlers) + + def test_has_agent(self): + self.assertEquals(self.handler.agent, self.agent) + self.assertIsInstance(self.handler.agent, Agent) + + def test_has_name(self): + self.assertEquals(self.handler.name, 'start') + self.assertEquals(self.handler.action, 'start') + + @patch('ocf_agent.modules.exit.Exit.output') + def test_fails_if_handler_is_misnamed(self, mock1): + with self.assertRaises(SystemExit): + handler = MisnamedHandler(self.handlers) + handler.validate() + self.assertTrue(mock1.called) + + def test_has_short_description(self): + self.assertEquals(self.handler.short_description, + 'start') + + def test_has_long_description(self): + self.assertEquals(self.handler.long_description, + 'start') + + def test_has_language(self): + self.assertEquals(self.handler.language, 'en') + + def test_has_timeout(self): + self.assertEquals(self.handler.timeout, 20) + + def test_has_default_method_name(self): + self.assertEqual(self.handler.default_method_name, 'handler_start') + + def test_has_method_name(self): + self.assertEquals(self.handler.method_name, 'handler_start') + + def test_has_method(self): + self.assertEquals(self.agent.handler_start, self.handler.method) + + @patch('tests.fixtures.agents.UnitTestAgent.handler_start') + def test_can_call_the_method(self, mock1): + self.handler.call() + self.assertTrue(mock1.called) + + +class HandlerStopPropertyTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.handlers = self.agent.handlers + self.handler = UnitTestAgent.OCFHandler_StopHandler(self.handlers) + + def tearDown(self): + del self.handler + del self.agent + del self.handlers + + def test_has_name(self): + self.assertEquals(self.handler.name, 'stop') + self.assertEquals(self.handler.action, 'stop') + self.assertEquals(self.handler.full_name, 'stop') + + def test_has_short_description(self): + self.assertEquals(self.handler.short_description, + 'Stop handler') + + def test_has_long_description(self): + self.assertEquals(self.handler.long_description, + 'The configured stop handler') + + def test_has_language(self): + self.assertEquals(self.handler.language, 'en_US') + + def test_has_timeout(self): + self.assertEquals(self.handler.timeout, 30) + + def test_has_default_method_name(self): + self.assertEqual(self.handler.default_method_name, 'handler_stop') + + def test_has_method_name(self): + self.assertEquals(self.handler.method_name, 'stop_agent') + + def test_has_method(self): + self.assertEquals(self.agent.stop_agent, self.handler.method) + + @patch('tests.fixtures.agents.UnitTestAgent.stop_agent') + def test_can_call_the_method(self, mock1): + self.handler.call() + self.assertTrue(mock1.called) diff --git a/tests/handler/test_handler_monitor.py b/tests/handler/test_handler_monitor.py new file mode 100644 index 0000000..f792334 --- /dev/null +++ b/tests/handler/test_handler_monitor.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent + + +class HandlerMonitorBasicTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.handlers = self.agent.handlers + self.handler = UnitTestAgent.OCFHandler_monitor(self.handlers) + + def tearDown(self): + del self.handler + del self.agent + del self.handlers + + def test_has_full_name(self): + self.assertEquals(self.handler.name, 'monitor') + self.assertEquals(self.handler.action, 'monitor') + self.assertEquals(self.handler.full_name, 'monitor') + + def test_has_monitor_method_name(self): + self.assertEquals( + self.handler.method_name, + 'handler_monitor', + ) + + def test_can_get_monitor_method(self): + self.assertEquals( + self.handler.method, + self.agent.handler_monitor, + ) + + def test_has_interval(self): + self.assertEquals(self.handler.interval, 10) + + def test_has_depth(self): + self.assertEquals(self.handler.depth, 0) + + def test_has_role(self): + self.assertEquals(self.handler.role, None) + + +class HandlerMonitorConfiguredTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.handlers = self.agent.handlers + self.handler = UnitTestAgent.OCFHandler_monitor_long(self.handlers) + + def tearDown(self): + del self.handler + del self.agent + del self.handlers + + def test_has_full_name(self): + self.assertEquals(self.handler.name, 'monitor') + self.assertEquals(self.handler.action, 'monitor') + self.assertEquals(self.handler.full_name, 'monitor_master_10') + + def test_has_monitor_method_name(self): + self.assertEquals( + self.handler.method_name, + 'handler_monitor_long', + ) + + def test_can_get_monitor_method(self): + self.assertEquals( + self.handler.method, + self.agent.handler_monitor_long, + ) + + def test_has_interval(self): + self.assertEquals(self.handler.interval, 60) + + def test_has_depth(self): + self.assertEquals(self.handler.depth, 10) + + def test_has_role(self): + self.assertEquals(self.handler.role, 'Master') diff --git a/tests/parameter/__init__.py b/tests/parameter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/parameter/test_parameter_basic.py b/tests/parameter/test_parameter_basic.py new file mode 100644 index 0000000..9da57de --- /dev/null +++ b/tests/parameter/test_parameter_basic.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from tests.fixtures.agents import UnitTestAgent +from ocf_agent.modules.parameters import Parameters +from ocf_agent.parameter import StringParameter +from ocf_agent.agent import Agent +from mock import patch +import os + + +class OCFParameter_configured_parameter(StringParameter): + DEFAULT = 'default test value' + LONGDESC = "Test long description" + SHORTDESC = "Test short description" + LANG = 'en_US' + UNIQUE = True + REQUIRED = True + + +class OCFParameter_empty_parameter(StringParameter): + pass + + +class MisnamedParameter(StringParameter): + pass + + +class ParameterConfiguredBasicPropertiesTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = OCFParameter_configured_parameter(self.parameters) + os.environ = {} + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + def test_has_parameters(self): + self.assertEquals(self.parameter.parameters, self.parameters) + self.assertIsInstance(self.parameter.parameters, Parameters) + + def test_has_agent(self): + self.assertEquals(self.parameter.agent, self.agent) + self.assertIsInstance(self.parameter.agent, Agent) + + def test_has_name(self): + self.assertEquals(self.parameter.name, 'configured_parameter') + + def test_has_short_description(self): + self.assertEquals(self.parameter.short_description, + 'Test short description') + + def test_has_long_description(self): + self.assertEquals(self.parameter.long_description, + 'Test long description') + + def test_has_language(self): + self.assertEquals(self.parameter.language, 'en_US') + + def test_has_type(self): + self.assertIs(self.parameter.type, str) + + def test_has_type_name(self): + self.assertEquals(self.parameter.type_name, 'string') + + def test_has_default(self): + self.assertEquals(self.parameter.default, 'default test value') + + def test_has_unique(self): + self.assertEquals(self.parameter.unique, True) + + def test_has_required(self): + self.assertEquals(self.parameter.required, True) + + +class ParameterEmptyBasicPropertiesTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = OCFParameter_empty_parameter(self.parameters) + os.environ = {} + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + def test_has_name(self): + self.assertEquals(self.parameter.name, 'empty_parameter') + + def test_has_short_description(self): + self.assertEquals(self.parameter.short_description, 'empty_parameter') + + def test_has_long_description(self): + self.assertEquals(self.parameter.long_description, 'empty_parameter') + + def test_has_language(self): + self.assertEquals(self.parameter.language, 'en') + + def test_has_type(self): + self.assertIs(self.parameter.type, str) + + def test_has_type_name(self): + self.assertEquals(self.parameter.type_name, 'string') + + def test_has_default(self): + self.assertEquals(self.parameter.default, None) + + def test_has_unique(self): + self.assertEquals(self.parameter.unique, False) + + def test_has_required(self): + self.assertEquals(self.parameter.required, False) + + +class ParameterLogicTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = UnitTestAgent.OCFParameter_test(self.parameters) + os.environ = {} + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + @patch('ocf_agent.modules.exit.Exit.output') + def test_fails_if_parameter_is_misnamed(self, mock1): + with self.assertRaises(SystemExit): + parameter = MisnamedParameter(self.parameters) + parameter.validate() + self.assertTrue(mock1.called) + + def test_knows_its_variable_name(self): + self.assertEqual( + self.parameter.env_variable_name, + 'OCF_RESKEY_test', + ) + + def test_uses_the_default_if_no_value(self): + self.parameter.value = None + self.assertEqual(self.parameter.value, self.parameter.default) + self.assertEqual(self.parameter.value, 'test default value') + + @patch('os.environ', {'OCF_RESKEY_test': 'env value'}) + def test_can_get_value_from_environment(self): + self.parameter.value = None + self.assertEqual(self.parameter.value, 'env value') + + @patch('os.environ', {'OCF_RESKEY_test': 'env value'}) + def test_will_use_manually_set_value(self): + self.parameter.value = 'manual value' + self.assertEqual(self.parameter.value, 'manual value') diff --git a/tests/parameter/test_parameter_types.py b/tests/parameter/test_parameter_types.py new file mode 100644 index 0000000..9c06bc9 --- /dev/null +++ b/tests/parameter/test_parameter_types.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +from unittest import TestCase +from ocf_agent.parameter import BooleanParameter +from ocf_agent.parameter import IntegerParameter +from ocf_agent.parameter import StringParameter +from tests.fixtures.agents import UnitTestAgent + + +class OCFParameter_string_parameter(StringParameter): + pass + + +class OCFParameter_integer_parameter(IntegerParameter): + pass + + +class OCFParameter_boolean_parameter(BooleanParameter): + pass + + +class StringParameterTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = OCFParameter_string_parameter(self.parameters) + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + def test_accepts_none_values(self): + self.parameter.value = None + self.assertIsNone(self.parameter.value) + + def test_accepts_str_values(self): + self.parameter.value = 'test' + self.assertEquals(self.parameter.value, 'test') + + +class IntParameterTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = OCFParameter_integer_parameter(self.parameters) + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + def test_has_type(self): + self.assertIs(self.parameter.type, int) + + def test_has_type_name(self): + self.assertEquals(self.parameter.type_name, 'integer') + + def test_accepts_none_values(self): + self.parameter.value = None + self.assertIsNone(self.parameter.value) + + def test_accepts_int_values(self): + self.parameter.value = 1 + self.assertEquals(self.parameter.value, 1) + + def test_accepts_int_values_as_string(self): + self.parameter.value = '2' + self.assertEquals(self.parameter.value, 2) + + def test_does_not_accept_bad_values(self): + self.parameter.value = 'bad value' + self.assertIsNone(self.parameter.value) + + +class BoolParameterTest(TestCase): + def setUp(self): + self.agent = UnitTestAgent() + self.parameters = self.agent.parameters + self.parameter = OCFParameter_boolean_parameter(self.parameters) + + def tearDown(self): + del self.parameter + del self.parameters + del self.agent + + def test_has_type(self): + self.assertIs(self.parameter.type, bool) + + def test_has_type_name(self): + self.assertEquals(self.parameter.type_name, 'boolean') + + def test_accepts_none_values(self): + self.parameter.value = None + self.assertIsNone(self.parameter.value) + + def test_accepts_bool_values(self): + self.parameter.value = True + self.assertEquals(self.parameter.value, True) + self.parameter.value = False + self.assertEquals(self.parameter.value, False) + + def test_accepts_bool_values_as_string(self): + self.parameter.value = 'off' + self.assertEquals(self.parameter.value, False) + self.parameter.value = 'on' + self.assertEquals(self.parameter.value, True) + + def test_does_not_accept_bad_values(self): + self.parameter.value = 'bad value' + self.assertIsNone(self.parameter.value) diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..141199e --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +from ocf_agent import helpers +from unittest import TestCase +from mock import patch + + +class HelpersTest(TestCase): + def test_string_to_bool(self): + true_values = [True, 'YES', 'on', 'Y', 1, '1'] + false_values = [False, 'NO', 'off', 'N', 0, '0'] + none_values = [None, 'none', 10, 'bad value', '', 1.1, [1], {'a': 1}] + for value in true_values: + self.assertEquals(helpers.string_to_bool(value), True) + for value in false_values: + self.assertEquals(helpers.string_to_bool(value), False) + for value in none_values: + self.assertIsNone(helpers.string_to_bool(value)) + self.assertEquals( + helpers.string_to_bool('bad value', True), + True, + ) + self.assertEquals( + helpers.string_to_bool('bad value', False), + False, + ) + self.assertEquals( + helpers.string_to_bool('bad value', 'default'), + 'default', + ) + self.assertEquals( + helpers.string_to_bool('YES', 'default'), + True + ) + + def test_string_to_integer(self): + values = { + 1: 1, 0: 0, 10: 10, -1: 1, 1.2: 1, + '1': 1, '0': 0, '10': 10, '-10': 10, + '2a': 2, 'a2': 2, 'a2a': 2, + None: None, '': None, 'test': None, + } + for value_in, value_out in values.items(): + self.assertEquals(helpers.string_to_integer(value_in), value_out) + self.assertEquals( + helpers.string_to_integer('bad_value', 10), + 10, + ) + self.assertEquals( + helpers.string_to_integer('bad_value', 'default'), + 'default', + ) + self.assertEquals( + helpers.string_to_integer(1, 'default'), + 1, + ) + + def internal_function(self): + return 'value' + + @helpers.memoization + def memoised_function(self): + return self.internal_function() + + def test_memoisation(self): + with patch('tests.test_helpers.HelpersTest.internal_function') as mock: + mock.return_value = 'value' + self.memoised_function() + self.memoised_function() + self.assertEquals(mock.call_count, 1) + self.assertEquals(self.memoised_function(), 'value') + + @helpers.docstring_format('one', 2) + def documented_method(): + """ + A = {0} + B = {1} + C_ + """ + pass + + def test_docstring_format(self): + self.assertIn('A = one', self.documented_method.__doc__) + self.assertIn('B = 2', self.documented_method.__doc__) + self.assertIn('C\_', self.documented_method.__doc__) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ac50bcd --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +envlist = py27, py34, pep8 +skip_missing_interpreters = True + +[testenv] +basepython = + py27: python2.7 + py34: python3.4 + pep8: python2.7 + docs: python2.7 + ocft: python2.7 + +envdir = + py27: {toxworkdir}/2.7 + py34: {toxworkdir}/3.4 + pep8: {toxworkdir}/2.7 + docs: {toxworkdir}/2.7 + ocft: {toxworkdir}/2.7 + +changedir = + docs: {toxinidir}/docs/ + ocft: {toxinidir}/examples/ + +deps = + -r{toxinidir}/devel_requirements.txt + +commands = + py27: py.test -v -rw -s {posargs} + py34: py.test -v -rw -s {posargs} + pep8: flake8 -v {posargs} + docs: sphinx-build -W -b html . {toxinidir}/docs/_build/html + ocft: {toxinidir}/examples/run-ocf-tester.sh dummy.py sleep.py