├── rabbitmq_admin ├── tests │ ├── __init__.py │ ├── base_tests.py │ └── api_tests.py ├── version.py ├── __init__.py ├── base.py └── api.py ├── CONTRIBUTORS.txt ├── MANIFEST.in ├── docs ├── toc.rst ├── release_notes.rst ├── ref │ └── rabbitmq_admin.rst ├── index.rst ├── contributing.rst ├── conf.py └── Makefile ├── publish.py ├── .gitignore ├── .travis.yml ├── setup.cfg ├── LICENSE ├── setup.py └── README.rst /rabbitmq_admin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rabbitmq_admin/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2' 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Micah Hausler (micah.hausler@ambition.com) 2 | -------------------------------------------------------------------------------- /rabbitmq_admin/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from rabbitmq_admin.api import AdminAPI 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CONTRIBUTORS.txt 3 | include LICENSE 4 | prune */tests 5 | -------------------------------------------------------------------------------- /docs/toc.rst: -------------------------------------------------------------------------------- 1 | Table of Contents 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | index 8 | ref/rabbitmq_admin 9 | contributing 10 | release_notes 11 | -------------------------------------------------------------------------------- /docs/release_notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | v0.2 5 | ---- 6 | 7 | * Added support for policies and definitions 8 | 9 | v0.1 10 | ---- 11 | 12 | * This is the initial release of rabbitmq-admin. 13 | -------------------------------------------------------------------------------- /docs/ref/rabbitmq_admin.rst: -------------------------------------------------------------------------------- 1 | .. _ref-rabbitmq_admin: 2 | 3 | Code documentation 4 | ================== 5 | 6 | rabbitmq_admin 7 | -------------- 8 | 9 | .. automodule:: rabbitmq_admin.api 10 | .. autoclass:: rabbitmq_admin.api.AdminAPI 11 | :members: 12 | 13 | .. automethod:: __init__ 14 | -------------------------------------------------------------------------------- /publish.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | subprocess.call('pip install wheel'.split()) 4 | subprocess.call('python setup.py clean --all'.split()) 5 | subprocess.call('python setup.py sdist'.split()) 6 | subprocess.call('pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz'.split()) 7 | subprocess.call('python setup.py register sdist bdist_wheel upload'.split()) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python files 2 | *.pyc 3 | 4 | # Vim files 5 | *.swp 6 | *.swo 7 | 8 | # Coverage files 9 | .coverage 10 | htmlcov/ 11 | 12 | # Setuptools distribution folder. 13 | /dist/ 14 | /build/ 15 | 16 | # Python egg metadata, regenerated from source files by setuptools. 17 | /*.egg-info 18 | /*.egg 19 | /*.eggs 20 | 21 | # Virtualenv 22 | env/ 23 | venv/ 24 | 25 | # OSX 26 | .DS_Store 27 | 28 | # Pycharm 29 | .idea/ 30 | 31 | # Documentation artifacts 32 | docs/_build/ 33 | 34 | # IPython Notebook 35 | .ipynb_checkpoints/ 36 | *.ipynb 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - '2.7' 5 | - '3.4' 6 | - '3.5' 7 | services: 8 | - docker 9 | before_install: 10 | - docker pull rabbitmq:3-management 11 | - sudo iptables -N DOCKER || true 12 | - "docker run -d -h rabbit1 -p 5673:5672 -p 15673:15672 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest --name rabbit1 rabbitmq:3-management" 13 | - docker ps -a 14 | install: 15 | - pip install flake8 nose>=1.3.0 coverage coveralls 16 | - pip install -e .[all] 17 | script: 18 | - flake8 . 19 | - python setup.py nosetests 20 | - coverage report 21 | - python setup.py build_sphinx 22 | after_success: 23 | coveralls 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-coverage=1 3 | cover-branches=1 4 | cover-min-percentage=100 5 | cover-package=rabbitmq_admin 6 | 7 | [coverage:report] 8 | exclude_lines = 9 | # Have to re-enable the standard pragma 10 | pragma: no cover 11 | # Don't complain if tests don't hit defensive assertion code: 12 | raise NotImplementedError 13 | [coverage:run] 14 | omit = 15 | rabbitmq_admin/version.py 16 | 17 | [flake8] 18 | max-line-length = 100 19 | exclude = docs,venv,env,*.egg 20 | max-complexity = 10 21 | ignore = E402 22 | 23 | [build_sphinx] 24 | source-dir = docs/ 25 | build-dir = docs/_build 26 | all_files = 1 27 | 28 | [upload_sphinx] 29 | upload-dir = docs/_build/html 30 | 31 | [bdist_wheel] 32 | universal = 1 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ambition 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | rabbitmq-admin Documentation 2 | ============================= 3 | This project is a python wrapper around the RabbitMQ Management HTTP API. 4 | Not all API calls are supported, and the list is maintained in the project 5 | ``README.rst``. 6 | 7 | Example 8 | ------- 9 | 10 | .. code-block:: python 11 | 12 | >>> from rabbitmq_admin import AdminAPI 13 | >>> api = AdminAPI(url='http://192.168.99.101:15672', auth=('guest', 'guest')) 14 | >>> api.create_vhost('second_vhost') 15 | >>> api.create_user('second_user', 'password') 16 | >>> api.create_user_permission('second_user', 'second_vhost') 17 | >>> api.list_permission() 18 | [{'configure': '.*', 19 | 'read': '.*', 20 | 'user': 'guest', 21 | 'vhost': '/', 22 | 'write': '.*'}, 23 | {'configure': '.*', 24 | 'read': '.*', 25 | 'user': 'second_user', 26 | 'vhost': 'second_vhost', 27 | 'write': '.*'}] 28 | 29 | 30 | Installation 31 | ------------ 32 | 33 | To install the latest release, type:: 34 | 35 | pip install rabbitmq-admin 36 | 37 | To install the latest code directly from source, type:: 38 | 39 | pip install git+git://github.com/ambitioninc/rabbitmq-admin.git 40 | -------------------------------------------------------------------------------- /rabbitmq_admin/tests/base_tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from mock import patch, Mock 4 | import requests 5 | 6 | from rabbitmq_admin.base import Resource 7 | 8 | 9 | class ResourceTests(TestCase): 10 | 11 | def setUp(self): 12 | self.url = 'http://127.0.0.1:15672' 13 | self.auth = ('guest', 'guest') 14 | 15 | self.resource = Resource(self.url, self.auth) 16 | 17 | def test_init(self): 18 | resource = Resource(self.url, self.auth) 19 | 20 | self.assertEqual(resource.url, self.url) 21 | self.assertEqual(resource.auth, self.auth) 22 | self.assertEqual(resource.headers, {'Content-type': 'application/json'}) 23 | 24 | @patch.object(requests, 'put', autospec=True) 25 | def test_put_no_data(self, mock_put): 26 | 27 | mock_response = Mock() 28 | mock_put.return_value = mock_response 29 | 30 | self.resource._put(self.url, auth=self.auth) 31 | 32 | mock_response.raise_for_status.assert_called_once_with() 33 | 34 | @patch.object(Resource, '_post') 35 | def test_api_post(self, mock_post): 36 | url = '/api/definitions' 37 | headers = {'k1': 'v1'} 38 | 39 | self.resource._api_post(url, headers=headers) 40 | mock_post.assert_called_once_with( 41 | url=self.url + url, 42 | auth=self.auth, 43 | headers={ 44 | 'k1': 'v1', 45 | 'Content-type': 'application/json' 46 | } 47 | ) 48 | 49 | @patch.object(requests, 'post', autospec=True) 50 | def test_post_no_data(self, mock_post): 51 | 52 | mock_response = Mock() 53 | mock_post.return_value = mock_response 54 | 55 | self.resource._post(self.url, auth=self.auth) 56 | 57 | mock_response.raise_for_status.assert_called_once_with() 58 | 59 | @patch.object(requests, 'post', autospec=True) 60 | def test_post(self, mock_post): 61 | 62 | mock_response = Mock() 63 | mock_post.return_value = mock_response 64 | 65 | self.resource._post(self.url, auth=self.auth, data={'hello': 'world'}) 66 | 67 | mock_response.raise_for_status.assert_called_once_with() 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # import multiprocessing to avoid this bug (http://bugs.python.org/issue15881#msg170215) 2 | import multiprocessing 3 | assert multiprocessing 4 | import re 5 | from setuptools import setup, find_packages 6 | 7 | 8 | def get_version(): 9 | """ 10 | Extracts the version number from the version.py file. 11 | """ 12 | VERSION_FILE = 'rabbitmq_admin/version.py' 13 | mo = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', open(VERSION_FILE, 'rt').read(), re.M) 14 | if mo: 15 | return mo.group(1) 16 | else: 17 | raise RuntimeError('Unable to find version string in {0}.'.format(VERSION_FILE)) 18 | 19 | install_requires = [ 20 | 'requests>=2.7.0', 21 | 'six>=1.8.0' 22 | ] 23 | tests_require = [ 24 | 'coverage>=4.0', 25 | 'flake8>=2.2.0', 26 | 'pika>=0.10.0', 27 | 'mock>=1.0.1', 28 | 'nose>=1.3.0'] 29 | docs_require = [ 30 | 'Sphinx>=1.2.2', 31 | 'sphinx_rtd_theme'] 32 | 33 | extras_require = { 34 | 'test': tests_require, 35 | 'packaging': ['wheel'], 36 | 'docs': docs_require, 37 | } 38 | 39 | everything = set(install_requires) 40 | for deps in extras_require.values(): 41 | everything.update(deps) 42 | extras_require['all'] = list(everything) 43 | 44 | setup( 45 | name='rabbitmq-admin', 46 | version=get_version(), 47 | description='A python interface for the RabbitMQ Admin HTTP API', 48 | long_description=open('README.rst').read(), 49 | url='https://github.com/ambitioninc/rabbitmq-admin', 50 | author='Micah Hausler', 51 | author_email='opensource@ambition.com', 52 | keywords='RabbitMQ, AMQP, admin', 53 | packages=find_packages(), 54 | classifiers=[ 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Intended Audience :: Developers', 59 | 'License :: OSI Approved :: MIT License', 60 | 'Operating System :: OS Independent', 61 | ], 62 | license='MIT', 63 | include_package_data=True, 64 | test_suite='nose.collector', 65 | install_requires=install_requires, 66 | tests_require=tests_require, 67 | extras_require=extras_require, 68 | zip_safe=False, 69 | ) 70 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/ambitioninc/rabbitmq-admin.svg?branch=master 2 | :target: https://travis-ci.org/ambitioninc/rabbitmq-admin 3 | 4 | .. image:: https://coveralls.io/repos/ambitioninc/rabbitmq-admin/badge.png?branch=master 5 | :target: https://coveralls.io/r/ambitioninc/rabbitmq-admin?branch=master 6 | 7 | 8 | rabbitmq-admin 9 | ============== 10 | This project is a python wrapper around the RabbitMQ Management HTTP API. 11 | 12 | 13 | Example 14 | ------- 15 | :: 16 | 17 | >>> from rabbitmq_admin import AdminAPI 18 | >>> api = AdminAPI(url='http://192.168.99.100:15672', auth=('guest', 'guest')) 19 | >>> api.create_vhost('second_vhost') 20 | >>> api.create_user('second_user', 'password') 21 | >>> api.create_user_permission('second_user', 'second_vhost') 22 | >>> api.list_permission() 23 | [{'configure': '.*', 24 | 'read': '.*', 25 | 'user': 'guest', 26 | 'vhost': '/', 27 | 'write': '.*'}, 28 | {'configure': '.*', 29 | 'read': '.*', 30 | 'user': 'second_user', 31 | 'vhost': 'second_vhost', 32 | 'write': '.*'}] 33 | 34 | Unsupported Management API endpoints 35 | ------------------------------------ 36 | This is a list of unsupported API endpoints. Please do not make issues for 37 | these, but pull requests implementing them are welcome. 38 | 39 | - ``/api/exchanges/vhost/name/bindings/source [GET]`` 40 | - ``/api/exchanges/vhost/name/bindings/destination [GET]`` 41 | - ``/api/exchanges/vhost/name/publish [POST]`` 42 | - ``/api/queues/vhost/name/bindings [GET]`` 43 | - ``/api/queues/vhost/name/contents [DELETE]`` 44 | - ``/api/queues/vhost/name/actions [POST]`` 45 | - ``/api/queues/vhost/name/get [POST]`` 46 | - ``/api/bindings/vhost/e/exchange/q/queue [GET, POST]`` 47 | - ``/api/bindings/vhost/e/exchange/q/queue/props [GET, DELETE]`` 48 | - ``/api/bindings/vhost/e/source/e/destination [GET, POST]`` 49 | - ``/api/bindings/vhost/e/source/e/destination/props [GET, DELETE]`` 50 | - ``/api/parameters [GET]`` 51 | - ``/api/parameters/component [GET]`` 52 | - ``/api/parameters/component/vhost [GET]`` 53 | - ``/api/parameters/component/vhost/name [GET, PUT, DELETE]`` 54 | 55 | Installation 56 | ------------ 57 | To install the latest release, type:: 58 | 59 | pip install rabbitmq-admin 60 | 61 | To install the latest code directly from source, type:: 62 | 63 | pip install git+git://github.com/ambitioninc/rabbitmq-admin.git 64 | 65 | Documentation 66 | ------------- 67 | Full documentation is available at http://rabbitmq-admin.readthedocs.org 68 | 69 | License 70 | ------- 71 | MIT License (see LICENSE) 72 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions and issues are most welcome! All issues and pull requests are 5 | handled through github on the `ambitioninc repository`_. Also, please check for 6 | any existing issues before filing a new one. If you have a great idea but it 7 | involves big changes, please file a ticket before making a pull request! We 8 | want to make sure you don't spend your time coding something that might not fit 9 | the scope of the project. 10 | 11 | .. _ambitioninc repository: https://github.com/ambitioninc/rabbitmq-admin/issues 12 | 13 | Running the tests 14 | ----------------- 15 | 16 | To get the source source code and run the unit tests, you'll first need to have 17 | docker installed, and run a container like this :: 18 | 19 | $ docker run -d \ 20 | -h rabbit1 \ 21 | -p 5672:5672 \ 22 | -p 15672:15672 \ 23 | -e RABBITMQ_DEFAULT_USER=guest \ 24 | -e RABBITMQ_DEFAULT_PASS=guest \ 25 | --name rabbit1 \ 26 | rabbitmq:3-management 27 | 28 | Then set the environment variable ``RABBITMQ_HOST`` to the IP of your docker 29 | server. (This assumes you're using `Docker Toolbox`_ and your vm's name is 30 | ``default``:: 31 | 32 | $ export RABBITMQ_HOST=$(machine ip default) 33 | 34 | Then run:: 35 | 36 | $ git clone git://github.com/ambitioninc/rabbitmq-admin.git 37 | $ cd rabbitmq-admin 38 | $ virtualenv env 39 | $ . env/bin/activate 40 | $ pip install -e .[all] 41 | $ python setup.py nosetests 42 | 43 | While 100% code coverage does not make a project bug-free, it significantly 44 | reduces the number of easily caught bugs! Please make sure coverage is at 100% 45 | before submitting a pull request! 46 | 47 | .. _Docker Toolbox: https://www.docker.com/toolbox 48 | 49 | Code Quality 50 | ------------ 51 | 52 | For code quality, please run flake8:: 53 | 54 | $ pip install flake8 55 | $ flake8 . 56 | 57 | Code Styling 58 | ------------ 59 | Please arrange imports with the following style 60 | 61 | .. code-block:: python 62 | 63 | # Standard library imports 64 | import os 65 | 66 | # Third party package imports 67 | from mock import patch 68 | 69 | # Local package imports 70 | from rabbitmq_admin.version import __version__ 71 | 72 | Please follow `Google's python style`_ guide wherever possible. 73 | 74 | .. _Google's python style: http://google-styleguide.googlecode.com/svn/trunk/pyguide.html 75 | 76 | Building the docs 77 | ----------------- 78 | 79 | When in the project directory:: 80 | 81 | $ pip install -e .[docs] 82 | $ python setup.py build_sphinx 83 | $ open docs/_build/html/index.html 84 | 85 | Release Checklist 86 | ----------------- 87 | 88 | Before a new release, please go through the following checklist: 89 | 90 | * Bump version in rabbitmq_admin/version.py 91 | * Add a release note in docs/release_notes.rst 92 | * Git tag the version 93 | * Upload to pypi:: 94 | 95 | pip install -e .[packaging] 96 | python publish.py 97 | 98 | Publishing to pypi can be accomplished by running python publish.py 99 | 100 | Vulnerability Reporting 101 | ----------------------- 102 | 103 | For any security issues, please do NOT file an issue or pull request on github! 104 | Please contact `security@ambition.com`_ with the GPG key provided on `Ambition's 105 | website`_. 106 | 107 | .. _security@ambition.com: mailto:security@ambition.com 108 | .. _Ambition's website: http://ambition.com/security/ 109 | 110 | -------------------------------------------------------------------------------- /rabbitmq_admin/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from copy import deepcopy 4 | 5 | 6 | class Resource(object): 7 | """ 8 | A base class for API resources 9 | """ 10 | 11 | # """List of allowed methods, allowed values are 12 | # ```['GET', 'PUT', 'POST', 'DELETE']``""" 13 | # ALLOWED_METHODS = [] 14 | 15 | def __init__(self, url, auth): 16 | """ 17 | :param url: The RabbitMQ API url to connect to. This should include the 18 | protocol and port number. 19 | :type url: str 20 | 21 | :param auth: The authentication to pass to the request. See 22 | `Requests' authentication`_ documentation. For the simplest case of 23 | a username and password, simply pass in a tuple of 24 | ``('username', 'password')`` 25 | :type auth: Requests auth 26 | 27 | .. _Requests' authentication: http://docs.python-requests.org/en/latest/user/authentication/ 28 | """ 29 | self.url = url.rstrip('/') 30 | self.auth = auth 31 | 32 | self.headers = { 33 | 'Content-type': 'application/json', 34 | } 35 | 36 | def _api_get(self, url, **kwargs): 37 | """ 38 | A convenience wrapper for _get. Adds headers, auth and base url by 39 | default 40 | """ 41 | kwargs['url'] = self.url + url 42 | kwargs['auth'] = self.auth 43 | 44 | headers = deepcopy(self.headers) 45 | headers.update(kwargs.get('headers', {})) 46 | kwargs['headers'] = headers 47 | return self._get(**kwargs) 48 | 49 | def _get(self, *args, **kwargs): 50 | """ 51 | A wrapper for getting things 52 | 53 | :returns: The response of your get 54 | :rtype: dict 55 | """ 56 | response = requests.get(*args, **kwargs) 57 | 58 | response.raise_for_status() 59 | 60 | return response.json() 61 | 62 | def _api_put(self, url, **kwargs): 63 | """ 64 | A convenience wrapper for _put. Adds headers, auth and base url by 65 | default 66 | """ 67 | kwargs['url'] = self.url + url 68 | kwargs['auth'] = self.auth 69 | 70 | headers = deepcopy(self.headers) 71 | headers.update(kwargs.get('headers', {})) 72 | kwargs['headers'] = headers 73 | self._put(**kwargs) 74 | 75 | def _put(self, *args, **kwargs): 76 | """ 77 | A wrapper for putting things. It will also json encode your 'data' parameter 78 | 79 | :returns: The response of your put 80 | :rtype: dict 81 | """ 82 | if 'data' in kwargs: 83 | kwargs['data'] = json.dumps(kwargs['data']) 84 | response = requests.put(*args, **kwargs) 85 | response.raise_for_status() 86 | 87 | def _api_post(self, url, **kwargs): 88 | """ 89 | A convenience wrapper for _post. Adds headers, auth and base url by 90 | default 91 | """ 92 | kwargs['url'] = self.url + url 93 | kwargs['auth'] = self.auth 94 | 95 | headers = deepcopy(self.headers) 96 | headers.update(kwargs.get('headers', {})) 97 | kwargs['headers'] = headers 98 | self._post(**kwargs) 99 | 100 | def _post(self, *args, **kwargs): 101 | """ 102 | A wrapper for posting things. It will also json encode your 'data' parameter 103 | 104 | :returns: The response of your post 105 | :rtype: dict 106 | """ 107 | if 'data' in kwargs: 108 | kwargs['data'] = json.dumps(kwargs['data']) 109 | response = requests.post(*args, **kwargs) 110 | response.raise_for_status() 111 | 112 | def _api_delete(self, url, **kwargs): 113 | """ 114 | A convenience wrapper for _delete. Adds headers, auth and base url by 115 | default 116 | """ 117 | kwargs['url'] = self.url + url 118 | kwargs['auth'] = self.auth 119 | 120 | headers = deepcopy(self.headers) 121 | headers.update(kwargs.get('headers', {})) 122 | kwargs['headers'] = headers 123 | self._delete(**kwargs) 124 | 125 | def _delete(self, *args, **kwargs): 126 | """ 127 | A wrapper for deleting things 128 | 129 | :returns: The response of your delete 130 | :rtype: dict 131 | """ 132 | response = requests.delete(*args, **kwargs) 133 | response.raise_for_status() 134 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # rabbitmq-admin documentation build configuration file 4 | 5 | import os 6 | import re 7 | 8 | 9 | def get_version(): 10 | """ 11 | Extracts the version number from the version.py file. 12 | """ 13 | VERSION_FILE = '../rabbitmq_admin/version.py' 14 | mo = re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', open(VERSION_FILE, 'rt').read(), re.M) 15 | if mo: 16 | return mo.group(1) 17 | else: 18 | raise RuntimeError('Unable to find version string in {0}.'.format(VERSION_FILE)) 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | extensions = [ 28 | 'sphinx.ext.autodoc', 29 | 'sphinx.ext.intersphinx', 30 | #'sphinx.ext.viewcode', 31 | ] 32 | 33 | autodoc_member_order = 'bysource' 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The master toctree document. 42 | master_doc = 'toc' 43 | 44 | # General information about the project. 45 | project = u'rabbitmq_admin' 46 | copyright = u'2015, Ambition Inc.' 47 | 48 | # The short X.Y version. 49 | version = get_version() 50 | # The full version, including alpha/beta/rc tags. 51 | release = version 52 | 53 | exclude_patterns = ['_build'] 54 | 55 | # The name of the Pygments (syntax highlighting) style to use. 56 | pygments_style = 'sphinx' 57 | 58 | intersphinx_mapping = { 59 | 'python': ('http://python.readthedocs.org/en/v2.7.2/', None), 60 | } 61 | 62 | # -- Options for HTML output ---------------------------------------------- 63 | 64 | html_theme = 'default' 65 | 66 | # Add any paths that contain custom themes here, relative to this directory. 67 | #html_theme_path = [] 68 | 69 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 70 | if not on_rtd: # only import and set the theme if we're building docs locally 71 | import sphinx_rtd_theme 72 | html_theme = 'sphinx_rtd_theme' 73 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 74 | 75 | # Add any paths that contain custom static files (such as style sheets) here, 76 | # relative to this directory. They are copied after the builtin static files, 77 | # so a file named "default.css" will overwrite the builtin "default.css". 78 | #html_static_path = ['_static'] 79 | 80 | # Custom sidebar templates, maps document names to template names. 81 | #html_sidebars = {} 82 | 83 | # Additional templates that should be rendered to pages, maps page names to 84 | # template names. 85 | #html_additional_pages = {} 86 | 87 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 88 | html_show_sphinx = False 89 | 90 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 91 | html_show_copyright = True 92 | 93 | # Output file base name for HTML help builder. 94 | htmlhelp_basename = 'rabbitmq-admindoc' 95 | 96 | 97 | # -- Options for LaTeX output --------------------------------------------- 98 | 99 | latex_elements = { 100 | # The paper size ('letterpaper' or 'a4paper'). 101 | #'papersize': 'letterpaper', 102 | 103 | # The font size ('10pt', '11pt' or '12pt'). 104 | #'pointsize': '10pt', 105 | 106 | # Additional stuff for the LaTeX preamble. 107 | #'preamble': '', 108 | } 109 | 110 | # Grouping the document tree into LaTeX files. List of tuples 111 | # (source start file, target name, title, 112 | # author, documentclass [howto, manual, or own class]). 113 | latex_documents = [ 114 | ('index', 'rabbitmq-admin.tex', u'rabbitmq-admin Documentation', 115 | u'Micah Hausler', 'manual'), 116 | ] 117 | 118 | # -- Options for manual page output --------------------------------------- 119 | 120 | # One entry per manual page. List of tuples 121 | # (source start file, name, description, authors, manual section). 122 | man_pages = [ 123 | ('index', 'rabbitmq-admin', u'rabbitmq-admin Documentation', 124 | [u'Micah Hausler'], 1) 125 | ] 126 | 127 | # -- Options for Texinfo output ------------------------------------------- 128 | 129 | # Grouping the document tree into Texinfo files. List of tuples 130 | # (source start file, target name, title, author, 131 | # dir menu entry, description, category) 132 | texinfo_documents = [ 133 | ('index', 'rabbitmq-admin', u'rabbitmq-admin Documentation', 134 | u'Micah Hausler', 'rabbitmq-admin', 'A short description', 135 | 'Miscellaneous'), 136 | ] 137 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " epub to make an epub" 33 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 34 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 35 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 36 | @echo " text to make text files" 37 | @echo " man to make manual pages" 38 | @echo " texinfo to make Texinfo files" 39 | @echo " info to make Texinfo files and run them through makeinfo" 40 | @echo " gettext to make PO message catalogs" 41 | @echo " changes to make an overview of all changed/added/deprecated items" 42 | @echo " xml to make Docutils-native XML files" 43 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 44 | @echo " linkcheck to check all external links for integrity" 45 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 46 | 47 | clean: 48 | rm -rf $(BUILDDIR)/* 49 | 50 | html: 51 | $(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 54 | 55 | dirhtml: 56 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 59 | 60 | singlehtml: 61 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 62 | @echo 63 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 64 | 65 | pickle: 66 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 67 | @echo 68 | @echo "Build finished; now you can process the pickle files." 69 | 70 | json: 71 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 72 | @echo 73 | @echo "Build finished; now you can process the JSON files." 74 | 75 | htmlhelp: 76 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 77 | @echo 78 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 79 | ".hhp project file in $(BUILDDIR)/htmlhelp." 80 | 81 | epub: 82 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 83 | @echo 84 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 85 | 86 | latex: 87 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 88 | @echo 89 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 90 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 91 | "(use \`make latexpdf' here to do that automatically)." 92 | 93 | latexpdf: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo "Running LaTeX files through pdflatex..." 96 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 97 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 98 | 99 | latexpdfja: 100 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 101 | @echo "Running LaTeX files through platex and dvipdfmx..." 102 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 103 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 104 | 105 | text: 106 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 107 | @echo 108 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 109 | 110 | man: 111 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 112 | @echo 113 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 114 | 115 | texinfo: 116 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 117 | @echo 118 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 119 | @echo "Run \`make' in that directory to run these through makeinfo" \ 120 | "(use \`make info' here to do that automatically)." 121 | 122 | info: 123 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 124 | @echo "Running Texinfo files through makeinfo..." 125 | make -C $(BUILDDIR)/texinfo info 126 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 127 | 128 | gettext: 129 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 130 | @echo 131 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 132 | 133 | changes: 134 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 135 | @echo 136 | @echo "The overview file is in $(BUILDDIR)/changes." 137 | 138 | linkcheck: 139 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 140 | @echo 141 | @echo "Link check complete; look for any errors in the above output " \ 142 | "or in $(BUILDDIR)/linkcheck/output.txt." 143 | 144 | doctest: 145 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 146 | @echo "Testing of doctests in the sources finished, look at the " \ 147 | "results in $(BUILDDIR)/doctest/output.txt." 148 | 149 | xml: 150 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 151 | @echo 152 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 153 | 154 | pseudoxml: 155 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 156 | @echo 157 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 158 | -------------------------------------------------------------------------------- /rabbitmq_admin/tests/api_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | import pika 5 | import requests 6 | from requests import HTTPError 7 | 8 | from rabbitmq_admin.api import AdminAPI 9 | 10 | 11 | class AdminAPITests(TestCase): 12 | """ 13 | These test cases require a docker container up and running 14 | :: 15 | 16 | docker run -d \ 17 | -h rabbit1 \ 18 | -p 5672:5672 \ 19 | -p 15672:15672 \ 20 | -e RABBITMQ_DEFAULT_USER=guest \ 21 | -e RABBITMQ_DEFAULT_PASS=guest \ 22 | -name rabbit1 \ 23 | rabbitmq:3-management 24 | 25 | """ 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | """ 30 | One-time set up that connects as 'guest', creates a 'test_queue' and 31 | sends one message 32 | 33 | TravisCI sometimes turns on RabbitMQ when we don't want it, so we use 34 | alternative ports 5673 and 15673 35 | """ 36 | if os.environ.get('TRAVIS'): # pragma: no cover 37 | cls.host = '127.0.0.1' 38 | cls.amqp_port = 5673 39 | cls.admin_port = 15673 40 | else: # pragma: no cover 41 | cls.host = os.environ.get('RABBITMQ_HOST', '192.168.99.100') 42 | cls.amqp_port = 5672 43 | cls.admin_port = 15672 44 | 45 | credentials = pika.PlainCredentials('guest', 'guest') 46 | cls.connection = pika.BlockingConnection( 47 | pika.ConnectionParameters( 48 | cls.host, 49 | port=cls.amqp_port, 50 | credentials=credentials 51 | ), 52 | ) 53 | channel = cls.connection.channel() 54 | channel.queue_declare(queue='test_queue') 55 | channel.basic_publish( 56 | exchange='', 57 | routing_key='test_queue', 58 | body='Test Message') 59 | 60 | @classmethod 61 | def tearDownClass(cls): 62 | cls.connection.close() 63 | 64 | def setUp(self): 65 | super(AdminAPITests, self).setUp() 66 | 67 | url = 'http://{host}:{port}'.format(host=self.host, port=self.admin_port) 68 | 69 | self.api = AdminAPI(url, auth=('guest', 'guest')) 70 | self.node_name = 'rabbit@rabbit1' 71 | 72 | def test_overview(self): 73 | response = self.api.overview() 74 | self.assertIsInstance(response, dict) 75 | 76 | def test_get_cluster_name(self): 77 | self.assertDictEqual( 78 | self.api.get_cluster_name(), 79 | {'name': 'rabbit@rabbit1'} 80 | ) 81 | 82 | def test_list_nodes(self): 83 | self.assertEqual( 84 | len(self.api.list_nodes()), 85 | 1 86 | ) 87 | 88 | def test_get_node(self): 89 | response = self.api.get_node(self.node_name) 90 | self.assertIsInstance(response, dict) 91 | self.assertEqual(response['name'], self.node_name) 92 | 93 | def test_list_extensions(self): 94 | self.assertEqual( 95 | self.api.list_extensions(), 96 | [{'javascript': 'dispatcher.js'}] 97 | ) 98 | 99 | def test_get_definitions(self): 100 | response = self.api.get_definitions() 101 | self.assertEqual(len(response['users']), 1) 102 | self.assertEqual(len(response['vhosts']), 1) 103 | 104 | def test_post_definitions(self): 105 | response = self.api.get_definitions() 106 | self.api.post_definitions(response) 107 | 108 | def test_list_connections(self): 109 | self.assertEqual( 110 | len(self.api.list_connections()), 111 | 1 112 | ) 113 | 114 | def test_get_connection(self): 115 | cname = self.api.list_connections()[0].get('name') 116 | self.assertIsInstance( 117 | self.api.get_connection(cname), 118 | dict 119 | ) 120 | 121 | def test_delete_connection(self): 122 | with self.assertRaises(requests.HTTPError): 123 | self.api.delete_connection('not-a-connection') 124 | 125 | def test_delete_connection_with_reson(self): 126 | with self.assertRaises(requests.HTTPError): 127 | self.api.delete_connection('not-a-connection', 'I don\'t like you') 128 | 129 | def test_list_connection_channels(self): 130 | cname = self.api.list_connections()[0].get('name') 131 | response = self.api.list_connection_channels(cname) 132 | 133 | self.assertEqual( 134 | response[0].get('name'), 135 | cname + ' (1)' 136 | ) 137 | 138 | def test_list_channels(self): 139 | self.assertEqual( 140 | len(self.api.list_channels()), 141 | 1 142 | ) 143 | 144 | def test_get_channel(self): 145 | cname = self.api.list_channels()[0].get('name') 146 | self.assertIsInstance( 147 | self.api.get_channel(cname), 148 | dict 149 | ) 150 | 151 | def test_list_consumers(self): 152 | self.assertEqual( 153 | self.api.list_consumers(), 154 | [] 155 | ) 156 | 157 | def test_list_consumers_for_vhost(self): 158 | self.assertEqual( 159 | self.api.list_consumers_for_vhost('/'), 160 | [] 161 | ) 162 | 163 | def test_list_exchanges(self): 164 | self.assertEqual( 165 | len(self.api.list_exchanges()), 166 | 8 167 | ) 168 | 169 | def test_list_exchanges_for_vhost(self): 170 | self.assertEqual( 171 | len(self.api.list_exchanges_for_vhost('/')), 172 | 8 173 | ) 174 | 175 | def test_get_create_delete_exchange_for_vhost(self): 176 | name = 'myexchange' 177 | body = { 178 | "type": "direct", 179 | "auto_delete": False, 180 | "durable": False, 181 | "internal": False, 182 | "arguments": {} 183 | } 184 | 185 | self.api.create_exchange_for_vhost(name, '/', body) 186 | self.assertEqual( 187 | len(self.api.list_exchanges_for_vhost('/')), 188 | 9 189 | ) 190 | self.assertEqual( 191 | self.api.get_exchange_for_vhost(name, '/').get('name'), 192 | name 193 | ) 194 | 195 | self.api.delete_exchange_for_vhost(name, '/') 196 | self.assertEqual( 197 | len(self.api.list_exchanges_for_vhost('/')), 198 | 8 199 | ) 200 | 201 | def test_list_bindings(self): 202 | self.assertEqual( 203 | self.api.list_bindings(), 204 | [{'arguments': {}, 205 | 'destination': 'aliveness-test', 206 | 'destination_type': 'queue', 207 | 'properties_key': 'aliveness-test', 208 | 'routing_key': 'aliveness-test', 209 | 'source': '', 210 | 'vhost': '/'}, 211 | {'arguments': {}, 212 | 'destination': 'test_queue', 213 | 'destination_type': 'queue', 214 | 'properties_key': 'test_queue', 215 | 'routing_key': 'test_queue', 216 | 'source': '', 217 | 'vhost': '/'}] 218 | ) 219 | 220 | def test_list_bindings_for_vhost(self): 221 | self.assertEqual( 222 | self.api.list_bindings_for_vhost('/'), 223 | [{'arguments': {}, 224 | 'destination': 'aliveness-test', 225 | 'destination_type': 'queue', 226 | 'properties_key': 'aliveness-test', 227 | 'routing_key': 'aliveness-test', 228 | 'source': '', 229 | 'vhost': '/'}, 230 | {'arguments': {}, 231 | 'destination': 'test_queue', 232 | 'destination_type': 'queue', 233 | 'properties_key': 'test_queue', 234 | 'routing_key': 'test_queue', 235 | 'source': '', 236 | 'vhost': '/'}] 237 | ) 238 | 239 | def test_list_vhosts(self): 240 | response = self.api.list_vhosts() 241 | self.assertEqual( 242 | len(response), 243 | 1 244 | ) 245 | self.assertEqual(response[0].get('name'), '/') 246 | 247 | def test_get_vhosts(self): 248 | response = self.api.get_vhost('/') 249 | self.assertEqual(response.get('name'), '/') 250 | 251 | def test_create_delete_vhost(self): 252 | name = 'vhost2' 253 | 254 | self.api.create_vhost(name) 255 | self.assertEqual( 256 | len(self.api.list_vhosts()), 257 | 2 258 | ) 259 | 260 | self.api.delete_vhost(name) 261 | self.assertEqual( 262 | len(self.api.list_vhosts()), 263 | 1 264 | ) 265 | 266 | def test_create_delete_vhost_tracing(self): 267 | name = 'vhost2' 268 | 269 | self.api.create_vhost(name, tracing=True) 270 | self.assertEqual( 271 | len(self.api.list_vhosts()), 272 | 2 273 | ) 274 | 275 | self.api.delete_vhost(name) 276 | self.assertEqual( 277 | len(self.api.list_vhosts()), 278 | 1 279 | ) 280 | 281 | def test_list_users(self): 282 | self.assertEqual( 283 | len(self.api.list_users()), 284 | 1 285 | ) 286 | 287 | def test_get_user(self): 288 | response = self.api.get_user('guest') 289 | self.assertEqual(response.get('name'), 'guest') 290 | self.assertEqual(response.get('tags'), 'administrator') 291 | 292 | def test_create_delete_user(self): 293 | name = 'user2' 294 | password_hash = '5f4dcc3b5aa765d61d8327deb882cf99' # md5 of 'password' 295 | 296 | self.api.create_user(name, password='', password_hash=password_hash) 297 | self.assertEqual( 298 | len(self.api.list_users()), 299 | 2 300 | ) 301 | 302 | self.api.delete_user(name) 303 | self.assertEqual( 304 | len(self.api.list_users()), 305 | 1 306 | ) 307 | 308 | def test_create_delete_user_password(self): 309 | name = 'user2' 310 | password = 'password' 311 | 312 | self.api.create_user(name, password=password) 313 | self.assertEqual( 314 | len(self.api.list_users()), 315 | 2 316 | ) 317 | 318 | self.api.delete_user(name) 319 | self.assertEqual( 320 | len(self.api.list_users()), 321 | 1 322 | ) 323 | 324 | def test_create_delete_user_no_password(self): 325 | name = 'user2' 326 | password = '' 327 | 328 | self.api.create_user(name, password=password) 329 | self.assertEqual( 330 | len(self.api.list_users()), 331 | 2 332 | ) 333 | 334 | self.api.delete_user(name) 335 | self.assertEqual( 336 | len(self.api.list_users()), 337 | 1 338 | ) 339 | 340 | def test_list_user_permissions(self): 341 | self.assertEqual( 342 | self.api.list_user_permissions('guest'), 343 | [{'configure': '.*', 344 | 'read': '.*', 345 | 'user': 'guest', 346 | 'vhost': '/', 347 | 'write': '.*'}] 348 | ) 349 | 350 | def test_whoami(self): 351 | self.assertEqual( 352 | self.api.whoami(), 353 | {'name': 'guest', 'tags': 'administrator'} 354 | ) 355 | 356 | def test_list_permissions(self): 357 | self.assertEqual( 358 | self.api.list_permissions(), 359 | [{'configure': '.*', 360 | 'read': '.*', 361 | 'user': 'guest', 362 | 'vhost': '/', 363 | 'write': '.*'}] 364 | ) 365 | 366 | def test_get_user_permission(self): 367 | self.assertEqual( 368 | self.api.get_user_permission('/', 'guest'), 369 | { 370 | 'configure': '.*', 371 | 'read': '.*', 372 | 'user': 'guest', 373 | 'vhost': '/', 374 | 'write': '.*' 375 | } 376 | ) 377 | 378 | def test_create_delete_user_permission(self): 379 | uname = 'test_user' 380 | vname = 'test_vhost' 381 | password_hash = '5f4dcc3b5aa765d61d8327deb882cf99' # md5 of 'password' 382 | 383 | # Create test user/vhost 384 | self.api.create_user(uname, password='', password_hash=password_hash) 385 | self.api.create_vhost(vname) 386 | 387 | self.assertEqual(len(self.api.list_user_permissions(uname)), 0) 388 | 389 | # Create the permission 390 | self.api.create_user_permission(uname, vname) 391 | 392 | self.assertEqual(len(self.api.list_user_permissions(uname)), 1) 393 | 394 | # Delete the permission 395 | self.api.delete_user_permission(uname, vname) 396 | 397 | self.assertEqual(len(self.api.list_user_permissions(uname)), 0) 398 | # delete test user/vhost 399 | self.api.delete_user(uname) 400 | self.api.delete_vhost(vname) 401 | 402 | def test_policies(self): 403 | # Create a policy 404 | self.api.create_policy_for_vhost( 405 | vhost="/", 406 | name="ha-all", 407 | definition={"ha-mode": "all"}, 408 | pattern="", 409 | apply_to="all", 410 | ) 411 | 412 | list_all_response = self.api.list_policies() 413 | list_default_response = self.api.list_policies_for_vhost("/") 414 | 415 | self.assertEqual(list_all_response, list_default_response) 416 | self.assertEqual(len(list_default_response), 1) 417 | 418 | with self.assertRaises(HTTPError): 419 | self.api.get_policy_for_vhost("/", "not-a-policy") 420 | 421 | get_response = self.api.get_policy_for_vhost("/", "ha-all") 422 | self.assertEqual( 423 | get_response, 424 | list_all_response[0] 425 | ) 426 | 427 | self.api.delete_policy_for_vhost("/", "ha-all") 428 | self.assertEqual( 429 | len(self.api.list_policies()), 430 | 0 431 | ) 432 | 433 | def test_is_vhost_alive(self): 434 | self.assertDictEqual( 435 | self.api.is_vhost_alive('/'), 436 | {'status': 'ok'} 437 | ) 438 | -------------------------------------------------------------------------------- /rabbitmq_admin/api.py: -------------------------------------------------------------------------------- 1 | from rabbitmq_admin.base import Resource 2 | from six.moves import urllib 3 | 4 | 5 | class AdminAPI(Resource): 6 | """ 7 | The entrypoint for interacting with the RabbitMQ Management HTTP API 8 | """ 9 | 10 | def overview(self): 11 | """ 12 | Various random bits of information that describe the whole system 13 | """ 14 | return self._api_get('/api/overview') 15 | 16 | def get_cluster_name(self): 17 | """ 18 | Name identifying this RabbitMQ cluster. 19 | """ 20 | return self._get( 21 | url=self.url + '/api/cluster-name', 22 | headers=self.headers, 23 | auth=self.auth 24 | ) 25 | 26 | def list_nodes(self): 27 | """ 28 | A list of nodes in the RabbitMQ cluster. 29 | """ 30 | return self._api_get('/api/nodes') 31 | 32 | def get_node(self, name, memory=False, binary=False): 33 | """ 34 | An individual node in the RabbitMQ cluster. Set "memory=true" to get 35 | memory statistics, and "binary=true" to get a breakdown of binary 36 | memory use (may be expensive if there are many small binaries in the 37 | system). 38 | """ 39 | return self._api_get( 40 | url='/api/nodes/{0}'.format(name), 41 | params=dict( 42 | binary=binary, 43 | memory=memory, 44 | ), 45 | ) 46 | 47 | def list_extensions(self): 48 | """ 49 | A list of extensions to the management plugin. 50 | """ 51 | return self._api_get('/api/extensions') 52 | 53 | def get_definitions(self): 54 | """ 55 | The server definitions - exchanges, queues, bindings, users, virtual 56 | hosts, permissions and parameters. Everything apart from messages. 57 | 58 | This method can be used for backing up the configuration of a server 59 | or cluster. 60 | """ 61 | return self._api_get('/api/definitions') 62 | 63 | def post_definitions(self, data): 64 | """ 65 | The server definitions - exchanges, queues, bindings, users, virtual 66 | hosts, permissions and parameters. Everything apart from messages. 67 | POST to upload an existing set of definitions. Note that: 68 | 69 | - The definitions are merged. Anything already existing on the 70 | server but not in the uploaded definitions is untouched. 71 | - Conflicting definitions on immutable objects (exchanges, queues 72 | and bindings) will cause an error. 73 | - Conflicting definitions on mutable objects will cause the object 74 | in the server to be overwritten with the object from the 75 | definitions. 76 | - In the event of an error you will be left with a part-applied set 77 | of definitions. 78 | 79 | This method can be used for restoring the configuration of a server 80 | or cluster. 81 | 82 | :param data: The definitions for a RabbitMQ server 83 | :type data: dict 84 | """ 85 | self._api_post('/api/definitions', data=data) 86 | 87 | def list_connections(self): 88 | """ 89 | A list of all open connections. 90 | """ 91 | return self._api_get('/api/connections') 92 | 93 | def get_connection(self, name): 94 | """ 95 | An individual connection. 96 | 97 | :param name: The connection name 98 | :type name: str 99 | """ 100 | return self._api_get('/api/connections/{0}'.format( 101 | urllib.parse.quote_plus(name) 102 | )) 103 | 104 | def delete_connection(self, name, reason=None): 105 | """ 106 | Closes an individual connection. Give an optional reason 107 | 108 | :param name: The connection name 109 | :type name: str 110 | 111 | :param reason: An option reason why the connection was deleted 112 | :type reason: str 113 | """ 114 | headers = {'X-Reason': reason} if reason else {} 115 | 116 | self._api_delete( 117 | '/api/connections/{0}'.format( 118 | urllib.parse.quote_plus(name) 119 | ), 120 | headers=headers, 121 | ) 122 | 123 | def list_connection_channels(self, name): 124 | """ 125 | List of all channels for a given connection. 126 | 127 | :param name: The connection name 128 | :type name: str 129 | """ 130 | return self._api_get('/api/connections/{0}/channels'.format( 131 | urllib.parse.quote_plus(name) 132 | )) 133 | 134 | def list_channels(self): 135 | """ 136 | A list of all open channels. 137 | """ 138 | return self._api_get('/api/channels') 139 | 140 | def get_channel(self, name): 141 | """ 142 | Details about an individual channel. 143 | 144 | :param name: The channel name 145 | :type name: str 146 | """ 147 | return self._api_get('/api/channels/{0}'.format( 148 | urllib.parse.quote_plus(name) 149 | )) 150 | 151 | def list_consumers(self): 152 | """ 153 | A list of all consumers. 154 | """ 155 | return self._api_get('/api/consumers') 156 | 157 | def list_consumers_for_vhost(self, vhost): 158 | """ 159 | A list of all consumers in a given virtual host. 160 | 161 | :param vhost: The vhost name 162 | :type vhost: str 163 | """ 164 | return self._api_get('/api/consumers/{0}'.format( 165 | urllib.parse.quote_plus(vhost) 166 | )) 167 | 168 | def list_exchanges(self): 169 | """ 170 | A list of all exchanges. 171 | """ 172 | return self._api_get('/api/exchanges') 173 | 174 | def list_exchanges_for_vhost(self, vhost): 175 | """ 176 | A list of all exchanges in a given virtual host. 177 | 178 | :param vhost: The vhost name 179 | :type vhost: str 180 | """ 181 | return self._api_get('/api/exchanges/{0}'.format( 182 | urllib.parse.quote_plus(vhost) 183 | )) 184 | 185 | def get_exchange_for_vhost(self, exchange, vhost): 186 | """ 187 | An individual exchange 188 | 189 | :param exchange: The exchange name 190 | :type exchange: str 191 | 192 | :param vhost: The vhost name 193 | :type vhost: str 194 | """ 195 | return self._api_get('/api/exchanges/{0}/{1}'.format( 196 | urllib.parse.quote_plus(vhost), 197 | urllib.parse.quote_plus(exchange) 198 | )) 199 | 200 | def create_exchange_for_vhost(self, exchange, vhost, body): 201 | """ 202 | Create an individual exchange. 203 | The body should look like: 204 | :: 205 | 206 | { 207 | "type": "direct", 208 | "auto_delete": false, 209 | "durable": true, 210 | "internal": false, 211 | "arguments": {} 212 | } 213 | 214 | The type key is mandatory; other keys are optional. 215 | 216 | :param exchange: The exchange name 217 | :type exchange: str 218 | 219 | :param vhost: The vhost name 220 | :type vhost: str 221 | 222 | :param body: A body for the exchange. 223 | :type body: dict 224 | """ 225 | self._api_put( 226 | '/api/exchanges/{0}/{1}'.format( 227 | urllib.parse.quote_plus(vhost), 228 | urllib.parse.quote_plus(exchange)), 229 | data=body 230 | ) 231 | 232 | def delete_exchange_for_vhost(self, exchange, vhost, if_unused=False): 233 | """ 234 | Delete an individual exchange. You can add the parameter 235 | ``if_unused=True``. This prevents the delete from succeeding if the 236 | exchange is bound to a queue or as a source to another exchange. 237 | 238 | :param exchange: The exchange name 239 | :type exchange: str 240 | 241 | :param vhost: The vhost name 242 | :type vhost: str 243 | 244 | :param if_unused: Set to ``True`` to only delete if it is unused 245 | :type if_unused: bool 246 | """ 247 | self._api_delete( 248 | '/api/exchanges/{0}/{1}'.format( 249 | urllib.parse.quote_plus(vhost), 250 | urllib.parse.quote_plus(exchange)), 251 | params={ 252 | 'if-unused': if_unused 253 | }, 254 | ) 255 | 256 | def list_bindings(self): 257 | """ 258 | A list of all bindings. 259 | """ 260 | return self._api_get('/api/bindings') 261 | 262 | def list_bindings_for_vhost(self, vhost): 263 | """ 264 | A list of all bindings in a given virtual host. 265 | 266 | :param vhost: The vhost name 267 | :type vhost: str 268 | """ 269 | return self._api_get('/api/bindings/{}'.format( 270 | urllib.parse.quote_plus(vhost) 271 | )) 272 | 273 | def list_vhosts(self): 274 | """ 275 | A list of all vhosts. 276 | """ 277 | return self._api_get('/api/vhosts') 278 | 279 | def get_vhost(self, name): 280 | """ 281 | Details about an individual vhost. 282 | 283 | :param name: The vhost name 284 | :type name: str 285 | """ 286 | return self._api_get('/api/vhosts/{0}'.format( 287 | urllib.parse.quote_plus(name) 288 | )) 289 | 290 | def delete_vhost(self, name): 291 | """ 292 | Delete a vhost. 293 | 294 | :param name: The vhost name 295 | :type name: str 296 | """ 297 | self._api_delete('/api/vhosts/{0}'.format( 298 | urllib.parse.quote_plus(name) 299 | )) 300 | 301 | def create_vhost(self, name, tracing=False): 302 | """ 303 | Create an individual vhost. 304 | 305 | :param name: The vhost name 306 | :type name: str 307 | 308 | :param tracing: Set to ``True`` to enable tracing 309 | :type tracing: bool 310 | """ 311 | data = {'tracing': True} if tracing else {} 312 | self._api_put( 313 | '/api/vhosts/{0}'.format(urllib.parse.quote_plus(name)), 314 | data=data, 315 | ) 316 | 317 | def list_users(self): 318 | """ 319 | A list of all users. 320 | """ 321 | return self._api_get('/api/users') 322 | 323 | def get_user(self, name): 324 | """ 325 | Details about an individual user. 326 | 327 | :param name: The user's name 328 | :type name: str 329 | """ 330 | return self._api_get('/api/users/{0}'.format( 331 | urllib.parse.quote_plus(name) 332 | )) 333 | 334 | def delete_user(self, name): 335 | """ 336 | Delete a user. 337 | 338 | :param name: The user's name 339 | :type name: str 340 | """ 341 | self._api_delete('/api/users/{0}'.format( 342 | urllib.parse.quote_plus(name) 343 | )) 344 | 345 | def create_user(self, name, password, password_hash=None, tags=None): 346 | """ 347 | Create a user 348 | 349 | :param name: The user's name 350 | :type name: str 351 | :param password: The user's password. Set to "" if no password is 352 | desired. Takes precedence if ``password_hash`` is also set. 353 | :type password: str 354 | :param password_hash: An optional password hash for the user. 355 | :type password_hash: str 356 | :param tags: A list of tags for the user. Currently recognised tags are 357 | "administrator", "monitoring" and "management". If no tags are 358 | supplied, the user will have no permissions. 359 | :type tags: list of str 360 | """ 361 | data = { 362 | 'tags': ', '.join(tags or []) 363 | } 364 | if password: 365 | data['password'] = password 366 | elif password_hash: 367 | data['password_hash'] = password_hash 368 | else: 369 | data['password_hash'] = "" 370 | 371 | self._api_put( 372 | '/api/users/{0}'.format(urllib.parse.quote_plus(name)), 373 | data=data, 374 | ) 375 | 376 | def list_user_permissions(self, name): 377 | """ 378 | A list of all permissions for a given user. 379 | 380 | :param name: The user's name 381 | :type name: str 382 | """ 383 | return self._api_get('/api/users/{0}/permissions'.format( 384 | urllib.parse.quote_plus(name) 385 | )) 386 | 387 | def whoami(self): 388 | """ 389 | Details of the currently authenticated user. 390 | """ 391 | return self._api_get('/api/whoami') 392 | 393 | def list_permissions(self): 394 | """ 395 | A list of all permissions for all users. 396 | """ 397 | return self._api_get('/api/permissions') 398 | 399 | def get_user_permission(self, vhost, name): 400 | """ 401 | An individual permission of a user and virtual host. 402 | 403 | :param vhost: The vhost name 404 | :type vhost: str 405 | 406 | :param name: The user's name 407 | :type name: str 408 | """ 409 | return self._api_get('/api/permissions/{0}/{1}'.format( 410 | urllib.parse.quote_plus(vhost), 411 | urllib.parse.quote_plus(name) 412 | )) 413 | 414 | def delete_user_permission(self, name, vhost): 415 | """ 416 | Delete an individual permission of a user and virtual host. 417 | 418 | :param name: The user's name 419 | :type name: str 420 | 421 | :param vhost: The vhost name 422 | :type vhost: str 423 | """ 424 | self._api_delete('/api/permissions/{0}/{1}'.format( 425 | urllib.parse.quote_plus(vhost), 426 | urllib.parse.quote_plus(name) 427 | )) 428 | 429 | def create_user_permission(self, 430 | name, 431 | vhost, 432 | configure=None, 433 | write=None, 434 | read=None): 435 | """ 436 | Create a user permission 437 | :param name: The user's name 438 | :type name: str 439 | :param vhost: The vhost to assign the permission to 440 | :type vhost: str 441 | 442 | :param configure: A regex for the user permission. Default is ``.*`` 443 | :type configure: str 444 | :param write: A regex for the user permission. Default is ``.*`` 445 | :type write: str 446 | :param read: A regex for the user permission. Default is ``.*`` 447 | :type read: str 448 | """ 449 | data = { 450 | 'configure': configure or '.*', 451 | 'write': write or '.*', 452 | 'read': read or '.*', 453 | } 454 | self._api_put( 455 | '/api/permissions/{0}/{1}'.format( 456 | urllib.parse.quote_plus(vhost), 457 | urllib.parse.quote_plus(name) 458 | ), 459 | data=data 460 | ) 461 | 462 | def list_policies(self): 463 | """ 464 | A list of all policies 465 | """ 466 | return self._api_get('/api/policies') 467 | 468 | def list_policies_for_vhost(self, vhost): 469 | """ 470 | A list of all policies for a vhost. 471 | """ 472 | return self._api_get('/api/policies/{0}'.format( 473 | urllib.parse.quote_plus(vhost) 474 | )) 475 | 476 | def get_policy_for_vhost(self, vhost, name): 477 | """ 478 | Get a specific policy for a vhost. 479 | 480 | :param vhost: The virtual host the policy is for 481 | :type vhost: str 482 | :param name: The name of the policy 483 | :type name: str 484 | """ 485 | return self._api_get('/api/policies/{0}/{1}'.format( 486 | urllib.parse.quote_plus(vhost), 487 | urllib.parse.quote_plus(name), 488 | )) 489 | 490 | def create_policy_for_vhost( 491 | self, vhost, name, 492 | definition, 493 | pattern=None, 494 | priority=0, 495 | apply_to='all'): 496 | """ 497 | Create a policy for a vhost. 498 | 499 | :param vhost: The virtual host the policy is for 500 | :type vhost: str 501 | :param name: The name of the policy 502 | :type name: str 503 | 504 | :param definition: The definition of the policy. Required 505 | :type definition: dict 506 | :param priority: The priority of the policy. Defaults to 0 507 | :param pattern: The pattern of resource names to apply the policy to 508 | :type pattern: str 509 | :type priority: int 510 | :param apply_to: What resource type to apply the policy to. 511 | Usually "exchanges", "queues", or "all". Defaults to "all" 512 | :type apply_to: str 513 | 514 | Example :: 515 | 516 | # Makes all queues and exchanges on vhost "/" highly available 517 | >>> api.create_policy_for_vhost( 518 | ... vhost="/", 519 | ... name="ha-all", 520 | ... definition={"ha-mode": "all"}, 521 | ... pattern="", 522 | ... apply_to="all") 523 | 524 | """ 525 | data = { 526 | "pattern": pattern, 527 | "definition": definition, 528 | "priority": priority, 529 | "apply-to": apply_to 530 | } 531 | self._api_put( 532 | '/api/policies/{0}/{1}'.format( 533 | urllib.parse.quote_plus(vhost), 534 | urllib.parse.quote_plus(name), 535 | ), 536 | data=data, 537 | ) 538 | 539 | def delete_policy_for_vhost(self, vhost, name): 540 | """ 541 | Delete a specific policy for a vhost. 542 | 543 | :param vhost: The virtual host of the policy 544 | :type vhost: str 545 | :param name: The name of the policy 546 | :type name: str 547 | """ 548 | self._api_delete('/api/policies/{0}/{1}/'.format( 549 | urllib.parse.quote_plus(vhost), 550 | urllib.parse.quote_plus(name), 551 | )) 552 | 553 | def is_vhost_alive(self, vhost): 554 | """ 555 | Declares a test queue, then publishes and consumes a message. 556 | Intended for use by monitoring tools. 557 | 558 | :param vhost: The vhost name to check 559 | :type vhost: str 560 | """ 561 | return self._api_get('/api/aliveness-test/{0}'.format( 562 | urllib.parse.quote_plus(vhost) 563 | )) 564 | --------------------------------------------------------------------------------