mirror of
https://github.com/ceph/ceph
synced 2025-03-06 08:20:12 +00:00
mgr/dashboard_v2: Updated README.rst, added HACKING.rst
Moved developer-related documentation from `README.rst` to a new file `HACKING.rst`. Converted `frontend/README.md` to ReST and merged it into `HACKING.rst`. Added updates to the README provided by @rjfd in PR#83 (updated the whole unit tests content to match the latest changes on unit test execution, updated the development notes about controller development) Signed-off-by: Lenz Grimmer <lgrimmer@suse.com>
This commit is contained in:
parent
8bfdb073ac
commit
efd339cdde
464
src/pybind/mgr/dashboard_v2/HACKING.rst
Normal file
464
src/pybind/mgr/dashboard_v2/HACKING.rst
Normal file
@ -0,0 +1,464 @@
|
||||
Dashboard v2 Developer Documentation
|
||||
====================================
|
||||
|
||||
Frontend Development
|
||||
--------------------
|
||||
|
||||
Before you can start the dashboard from within a development environment, you
|
||||
will need to generate the frontend code and either use a compiled and running
|
||||
Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
|
||||
server.
|
||||
|
||||
The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
|
||||
`Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
|
||||
|
||||
Prerequisites
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Run ``npm install`` in directory ``src/pybind/mgr/dashboard_v2/frontend`` to
|
||||
install the required packages locally.
|
||||
|
||||
.. note::
|
||||
|
||||
If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
|
||||
installed globally, then you need to execute ``ng`` commands with an
|
||||
additional ``npm run`` before it.
|
||||
|
||||
Setting up a Development Server
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
|
||||
|
||||
Run ``npm start -- --proxy-config proxy.conf.json`` for a dev server.
|
||||
Navigate to ``http://localhost:4200/``. The app will automatically
|
||||
reload if you change any of the source files.
|
||||
|
||||
Code Scaffolding
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Run ``ng generate component component-name`` to generate a new
|
||||
component. You can also use
|
||||
``ng generate directive|pipe|service|class|guard|interface|enum|module``.
|
||||
|
||||
Build the Project
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run ``npm run build`` to build the project. The build artifacts will be
|
||||
stored in the ``dist/`` directory. Use the ``-prod`` flag for a
|
||||
production build. Navigate to ``http://localhost:8080``.
|
||||
|
||||
Running Unit Tests
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run ``npm run test`` to execute the unit tests via `Karma
|
||||
<https://karma-runner.github.io>`_.
|
||||
|
||||
Running End-to-End Tests
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run ``npm run e2e`` to execute the end-to-end tests via
|
||||
`Protractor <http://www.protractortest.org/>`__.
|
||||
|
||||
Further Help
|
||||
~~~~~~~~~~~~
|
||||
|
||||
To get more help on the Angular CLI use ``ng help`` or go check out the
|
||||
`Angular CLI
|
||||
README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
|
||||
|
||||
Example of a Generator
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
# Create module 'Core'
|
||||
src/app> ng generate module core -m=app --routing
|
||||
|
||||
# Create module 'Auth' under module 'Core'
|
||||
src/app/core> ng generate module auth -m=core --routing
|
||||
or, alternatively:
|
||||
src/app> ng generate module core/auth -m=core --routing
|
||||
|
||||
# Create component 'Login' under module 'Auth'
|
||||
src/app/core/auth> ng generate component login -m=core/auth
|
||||
or, alternatively:
|
||||
src/app> ng generate component core/auth/login -m=core/auth
|
||||
|
||||
Frontend Typescript Code Style Guide Recommendations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Group the imports based on its source and separate them with a blank
|
||||
line.
|
||||
|
||||
The source groups can be either from Angular, external or internal.
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToastsManager } from 'ng2-toastr';
|
||||
|
||||
import { Credentials } from '../../../shared/models/credentials.model';
|
||||
import { HostService } from './services/host.service';
|
||||
|
||||
|
||||
Backend Development
|
||||
-------------------
|
||||
|
||||
The Python backend code of this module requires a number of Python modules to be
|
||||
installed. They are listed in file ``requirements.txt``. Using `pip
|
||||
<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
|
||||
by issuing ``pip install -r requirements.txt`` in directory
|
||||
``src/pybind/mgr/dashboard_v2``.
|
||||
|
||||
If you're using the `ceph-dev-docker development environment
|
||||
<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
|
||||
``./install_deps.sh`` from the toplevel directory to install them.
|
||||
|
||||
Unit Testing and Linting
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We included a ``tox`` configuration file that will run the unit tests under
|
||||
Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
|
||||
|
||||
You need to install ``tox`` and ``coverage`` before running it. To install the
|
||||
packages in your system, either install it via your operating system's package
|
||||
management tools, e.g. by running ``dnf install python-tox python-coverage`` on
|
||||
Fedora Linux.
|
||||
|
||||
Alternatively, you can use Python's native package installation method::
|
||||
|
||||
$ pip install tox
|
||||
$ pip install coverage
|
||||
|
||||
The unit tests must run against a real Ceph cluster (no mocks are used). This
|
||||
has the advantage of catching bugs originated from changes in the internal Ceph
|
||||
code.
|
||||
|
||||
Our ``tox.ini`` script will start a ``vstart`` Ceph cluster before running the
|
||||
python unit tests, and then it stops the cluster after the tests are run. Of
|
||||
course this implies that you have built/compiled Ceph previously.
|
||||
|
||||
To run tox, run the following command in the root directory (where ``tox.ini``
|
||||
is located)::
|
||||
|
||||
$ PATH=../../../../build/bin:$PATH tox
|
||||
|
||||
We also collect coverage information from the backend code. You can check the
|
||||
coverage information provided by the tox output, or by running the following
|
||||
command after tox has finished successfully::
|
||||
|
||||
$ coverage html
|
||||
|
||||
This command will create a directory ``htmlcov`` with an HTML representation of
|
||||
the code coverage of the backend.
|
||||
|
||||
You can also run a single step of the tox script (aka tox environment), for
|
||||
instance if you only want to run the linting tools, do::
|
||||
|
||||
$ PATH=../../../../build/bin:$PATH tox -e lint
|
||||
|
||||
How to run a single unit test without using ``tox``?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When developing the code of a controller and respective test code, it's useful
|
||||
to be able to run that single test file without going through the whole ``tox``
|
||||
workflow.
|
||||
|
||||
Since the tests must run against a real Ceph cluster, the first thing is to have
|
||||
a Ceph cluster running. For that we can leverage the tox environment that starts
|
||||
a Ceph cluster::
|
||||
|
||||
$ PATH=../../../../build/bin:$PATH tox -e ceph-cluster-start
|
||||
|
||||
The command above uses ``vstart.sh`` script to start a Ceph cluster and
|
||||
automatically enables the ``dashboard_v2`` module, and configures its cherrypy
|
||||
web server to listen in port ``9865``.
|
||||
|
||||
After starting the Ceph cluster we can run our test file using ``py.test`` like
|
||||
this::
|
||||
|
||||
DASHBOARD_V2_PORT=9865 UNITTEST=true py.test -s tests/test_mycontroller.py
|
||||
|
||||
You can run tests multiple times without having to start and stop the Ceph
|
||||
cluster.
|
||||
|
||||
After you finish your tests, you can stop the Ceph cluster using another tox
|
||||
environment::
|
||||
|
||||
$ tox -e ceph-cluster-stop
|
||||
|
||||
How to add a new controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to add a new endpoint to the backend, you just need to add a
|
||||
class derived from ``BaseController`` decorated with ``ApiController`` in a
|
||||
Python file located under the ``controllers`` directory. The Dashboard module
|
||||
will automatically load your new controller upon start.
|
||||
|
||||
For example create a file ``ping2.py`` under ``controllers`` directory with the
|
||||
following code::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, BaseController
|
||||
|
||||
@ApiController('ping2')
|
||||
class Ping2(BaseController):
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
return "Hello"
|
||||
|
||||
Every path given in the ``ApiController`` decorator will automatically be
|
||||
prefixed with ``api``. After reloading the Dashboard module you can access the
|
||||
above mentioned controller by pointing your browser to
|
||||
http://mgr_hostname:8080/api/ping2.
|
||||
|
||||
It is also possible to have nested controllers. The ``RgwController`` uses
|
||||
this technique to make the daemons available through the URL
|
||||
http://mgr_hostname:8080/api/rgw/daemon::
|
||||
|
||||
@ApiController('rgw')
|
||||
@AuthRequired()
|
||||
class Rgw(RESTController):
|
||||
pass
|
||||
|
||||
|
||||
@ApiController('rgw/daemon')
|
||||
@AuthRequired()
|
||||
class RgwDaemon(RESTController):
|
||||
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
|
||||
Note that paths must be unique and that a path like ``rgw/daemon`` has to have
|
||||
a parent ``rgw``. Otherwise it won't work.
|
||||
|
||||
How does the RESTController work?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We also provide a simple mechanism to create REST based controllers using the
|
||||
``RESTController`` class. Any class which inherits from ``RESTController`` will,
|
||||
by default, return JSON.
|
||||
|
||||
The ``RESTController`` is basically an additional abstraction layer which eases
|
||||
and unifies the work with collections. A collection is just an array of objects
|
||||
with a specific type. ``RESTController`` enables some default mappings of
|
||||
request types and given parameters to specific method names. This may sound
|
||||
complicated at first, but it's fairly easy. Lets have look at the following
|
||||
example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
@ApiController('ping2')
|
||||
class Ping2(RESTController):
|
||||
def list(self):
|
||||
return {"msg": "Hello"}
|
||||
|
||||
def get(self, id):
|
||||
return self.objects[id]
|
||||
|
||||
In this case, the ``list`` method is automatically used for all requests to
|
||||
``api/ping2`` where no additional argument is given and where the request type
|
||||
is ``GET``. If the request is given an additional argument, the ID in our
|
||||
case, it won't map to ``list`` anymore but to ``get`` and return the element
|
||||
with the given ID (assuming that ``self.objects`` has been filled before). The
|
||||
same applies to other request types:
|
||||
|
||||
+--------------+------------+----------------+-------------+
|
||||
| Request type | Arguments | Method | Status Code |
|
||||
+==============+============+================+=============+
|
||||
| GET | No | list | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PUT | No | bulk_set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PATCH | No | bulk_set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| POST | No | create | 201 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| DELETE | No | bulk_delete | 204 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| GET | Yes | get | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PUT | Yes | set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PATCH | Yes | set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| DELETE | Yes | delete | 204 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
|
||||
How to restrict access to a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you require that only authenticated users can access you controller, just
|
||||
add the ``AuthRequired`` decorator to your controller class.
|
||||
|
||||
Example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, AuthRequired, RESTController
|
||||
|
||||
|
||||
@ApiController('ping2')
|
||||
@AuthRequired()
|
||||
class Ping2(RESTController):
|
||||
def list(self):
|
||||
return {"msg": "Hello"}
|
||||
|
||||
Now only authenticated users will be able to "ping" your controller.
|
||||
|
||||
|
||||
How to access the manager module instance from a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Each controller class derived from ``BaseController``has a class property that
|
||||
points to the manager module global instance. The property is named ``mgr``.
|
||||
There is another class property called ``logger`` to easily add log messages.
|
||||
|
||||
Example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
|
||||
@ApiController('servers')
|
||||
class Servers(RESTController):
|
||||
def list(self):
|
||||
self.logger.debug('Listing available servers')
|
||||
return {'servers': self.mgr.list_servers()}
|
||||
|
||||
|
||||
How to write a unit test for a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We provide a test helper class called ``ControllerTestCase`` to easily create
|
||||
unit tests for your controller.
|
||||
|
||||
If we want to write a unit test for the above ``Ping2`` controller, create a
|
||||
``test_ping2.py`` file under the ``tests`` directory with the following code::
|
||||
|
||||
from .helper import ControllerTestCase
|
||||
from .controllers.ping2 import Ping2
|
||||
|
||||
|
||||
class Ping2Test(ControllerTestCase):
|
||||
@classmethod
|
||||
def setup_test(cls):
|
||||
Ping2._cp_config['tools.authentica.on'] = False
|
||||
|
||||
def test_ping2(self):
|
||||
self._get("/api/ping2")
|
||||
self.assertStatus(200)
|
||||
self.assertJsonBody({'msg': 'Hello'})
|
||||
|
||||
The ``ControllerTestCase`` class will call the dashboard module code that loads
|
||||
the controllers and initializes the CherryPy webserver. Then it will call the
|
||||
``setup_test()`` class method to execute additional instructions that each test
|
||||
case needs to add to the test.
|
||||
In the example above we use the ``setup_test()`` method to disable the
|
||||
authentication handler for the ``Ping2`` controller.
|
||||
|
||||
|
||||
How to listen for manager notifications in a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The manager notifies the modules of several types of cluster events, such
|
||||
as cluster logging event, etc...
|
||||
|
||||
Each module has a "global" handler function called ``notify`` that the manager
|
||||
calls to notify the module. But this handler function must not block or spend
|
||||
too much time processing the event notification.
|
||||
For this reason we provide a notification queue that controllers can register
|
||||
themselves with to receive cluster notifications.
|
||||
|
||||
The example below represents a controller that implements a very simple live
|
||||
log viewer page::
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
import cherrypy
|
||||
|
||||
from ..tools import ApiController, BaseController, NotificationQueue
|
||||
|
||||
|
||||
@ApiController('livelog')
|
||||
class LiveLog(BaseController):
|
||||
log_buffer = collections.deque(maxlen=1000)
|
||||
|
||||
def __init__(self):
|
||||
super(LiveLog, self).__init__()
|
||||
NotificationQueue.register(self.log, 'clog')
|
||||
|
||||
def log(self, log_struct):
|
||||
self.log_buffer.appendleft(log_struct)
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self):
|
||||
ret = '<html><meta http-equiv="refresh" content="2" /><body>'
|
||||
for l in self.log_buffer:
|
||||
ret += "{}<br>".format(l)
|
||||
ret += "</body></html>"
|
||||
return ret
|
||||
|
||||
As you can see above, the ``NotificationQueue`` class provides a register
|
||||
method that receives the function as its first argument, and receives the
|
||||
"notification type" as the second argument.
|
||||
You can omit the second argument of the ``register`` method, and in that case
|
||||
you are registering to listen all notifications of any type.
|
||||
|
||||
Here is an list of notification types (these might change in the future) that
|
||||
can be used:
|
||||
|
||||
* ``clog``: cluster log notifications
|
||||
* ``command``: notification when a command issued by ``MgrModule.send_command``
|
||||
completes
|
||||
* ``perf_schema_update``: perf counters schema update
|
||||
* ``mon_map``: monitor map update
|
||||
* ``fs_map``: cephfs map update
|
||||
* ``osd_map``: OSD map update
|
||||
* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
|
||||
* ``mon_status``: monitor status regular update
|
||||
* ``health``: health status regular update
|
||||
* ``pg_summary``: regular update of PG status information
|
||||
|
||||
|
||||
How to write a unit test when a controller accesses a Ceph module?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Consider the following example that implements a controller that retrieves the
|
||||
list of RBD images of the ``rbd`` pool::
|
||||
|
||||
import rbd
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
|
||||
@ApiController('rbdimages')
|
||||
class RbdImages(RESTController):
|
||||
def __init__(self):
|
||||
self.ioctx = self.mgr.rados.open_ioctx('rbd')
|
||||
self.rbd = rbd.RBD()
|
||||
|
||||
def list(self):
|
||||
return [{'name': n} for n in self.rbd.list(self.ioctx)]
|
||||
|
||||
In the example above, we want to mock the return value of the ``rbd.list``
|
||||
function, so that we can test the JSON response of the controller.
|
||||
|
||||
The unit test code will look like the following::
|
||||
|
||||
import mock
|
||||
from .helper import ControllerTestCase
|
||||
|
||||
|
||||
class RbdImagesTest(ControllerTestCase):
|
||||
@mock.patch('rbd.RBD.list')
|
||||
def test_list(self, rbd_list_mock):
|
||||
rbd_list_mock.return_value = ['img1', 'img2']
|
||||
self._get('/api/rbdimages')
|
||||
self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
|
@ -4,8 +4,9 @@ Dashboard and Administration Module for Ceph Manager (aka "Dashboard v2")
|
||||
Overview
|
||||
--------
|
||||
|
||||
The original Ceph Manager Dashboard started out as a simple read-only view into
|
||||
various run-time information and performance data of a Ceph cluster.
|
||||
The original Ceph Manager Dashboard that was shipped with Ceph "Luminous"
|
||||
started out as a simple read-only view into various run-time information and
|
||||
performance data of a Ceph cluster.
|
||||
|
||||
However, there is a `growing demand <http://pad.ceph.com/p/mimic-dashboard>`_
|
||||
for adding more web-based management capabilities, to make it easier for
|
||||
@ -38,26 +39,27 @@ JIRA instance <https://tracker.openattic.org/browse/OP-3039>`_.
|
||||
Enabling and Starting the Dashboard
|
||||
-----------------------------------
|
||||
|
||||
The Python backend code of this module requires a number of Python modules to be
|
||||
installed. They are listed in file ``requirements.txt``. Using `pip
|
||||
<https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
|
||||
by issuing ``pip -r requirements.txt``.
|
||||
If you have installed Ceph from distribution packages, the package management
|
||||
system should have taken care of installing all the required dependencies.
|
||||
|
||||
If you're using the `ceph-dev-docker development environment
|
||||
<https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
|
||||
``./install_deps.sh`` from the current directory to install them.
|
||||
If you want to start the dashboard from within a development environment, you
|
||||
need to have built Ceph (see the toplevel ``README.md`` file and the `developer
|
||||
documentation <http://docs.ceph.com/docs/master/dev/>`_ for details on how to
|
||||
accomplish this.
|
||||
|
||||
Start the Dashboard module by running::
|
||||
Finally, you need to build the dashboard frontend code. See the file
|
||||
``HACKING.rst`` in this directory for instructions on setting up the necessary
|
||||
development environment.
|
||||
|
||||
From within a running Ceph cluster, you can start the Dashboard module by
|
||||
running the following command::
|
||||
|
||||
$ ceph mgr module enable dashboard_v2
|
||||
|
||||
You can see currently enabled modules with::
|
||||
You can see currently enabled Manager modules with::
|
||||
|
||||
$ ceph mgr module ls
|
||||
|
||||
Currently you will need to manually generate the frontend code.
|
||||
Instructions can be found in `./frontend/README.md`.
|
||||
|
||||
In order to be able to log in, you need to define a username and password, which
|
||||
will be stored in the MON's configuration database::
|
||||
|
||||
@ -65,300 +67,11 @@ will be stored in the MON's configuration database::
|
||||
|
||||
The password will be stored as a hash using ``bcrypt``.
|
||||
|
||||
The WebUI should then be reachable on TCP port 8080.
|
||||
The Dashboard's WebUI should then be reachable on TCP port 8080.
|
||||
|
||||
Unit Testing and Linting
|
||||
------------------------
|
||||
|
||||
We included a ``tox`` configuration file that will run the unit tests under
|
||||
Python 2 and 3, as well as linting tools to guarantee the uniformity of code.
|
||||
|
||||
You need to install ``tox`` before running it. To install ``tox`` in your
|
||||
system, either install it via your operating system's package management
|
||||
tools, e.g. by running ``dnf install python3-tox`` on Fedora Linux.
|
||||
|
||||
Alternatively, you can use Python's native package installation method::
|
||||
|
||||
$ pip install tox
|
||||
|
||||
To run tox, run the following command in the root directory (where ``tox.ini``
|
||||
is located)::
|
||||
|
||||
$ tox
|
||||
|
||||
|
||||
If you just want to run a single tox environment, for instance only run the
|
||||
linting tools::
|
||||
|
||||
$ tox -e lint
|
||||
|
||||
Developer Notes
|
||||
---------------
|
||||
|
||||
How to add a new controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to add a new endpoint to the backend, you just need to add a
|
||||
class derived from ``BaseController`` decorated with ``ApiController`` in a
|
||||
Python file located under the ``controllers`` directory. The Dashboard module
|
||||
will automatically load your new controller upon start.
|
||||
|
||||
For example create a file ``ping2.py`` under ``controllers`` directory with the
|
||||
following code::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, BaseController
|
||||
|
||||
@ApiController('ping2')
|
||||
class Ping2(BaseController):
|
||||
@cherrypy.expose
|
||||
def default(self, *args):
|
||||
return "Hello"
|
||||
|
||||
Every path given in the ``ApiController`` decorator will automatically be
|
||||
prefixed with ``api``. After reloading the Dashboard module you can access the
|
||||
above mentioned controller by pointing your browser to
|
||||
http://mgr_hostname:8080/api/ping2.
|
||||
|
||||
It is also possible to have nested controllers. The ``RgwController`` uses
|
||||
this technique to make the daemons available through the URL
|
||||
http://mgr_hostname:8080/api/rgw/daemon::
|
||||
|
||||
@ApiController('rgw')
|
||||
@AuthRequired()
|
||||
class Rgw(RESTController):
|
||||
pass
|
||||
|
||||
|
||||
@ApiController('rgw/daemon')
|
||||
@AuthRequired()
|
||||
class RgwDaemon(RESTController):
|
||||
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
|
||||
Note that paths must be unique and that a path like ``rgw/daemon`` has to have
|
||||
a parent ``rgw``. Otherwise it won't work.
|
||||
|
||||
How does the RESTController work?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We also provide a simple mechanism to create REST based controllers using the
|
||||
``RESTController`` class. Any class which inherits from ``RESTController``
|
||||
will, by default, return JSON.
|
||||
|
||||
The ``RESTController`` is basically an additional abstraction layer which eases
|
||||
and unifies the work with collections. A collection is just an array of
|
||||
objects with a specific type. ``RestController`` enable some default mappings
|
||||
of request type and given parameters to specific method names. This may sound
|
||||
complicated at first, but it's fairly easy. Lets have look at the following
|
||||
example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
@ApiController('ping2')
|
||||
class Ping2(RESTController):
|
||||
def list(self):
|
||||
return {"msg": "Hello"}
|
||||
|
||||
def get(self, id):
|
||||
return self.objects[id]
|
||||
|
||||
In this case, the ``list`` method is automatically used for all requests to
|
||||
``api/ping2`` where no additional argument is given and where the request type
|
||||
is ``GET``. If the request is given an additional argument, the ID in our
|
||||
case, it won't map to ``list`` anymore but to ``get`` and return the element
|
||||
with the given ID (assuming that ``self.objects`` has been filled before). The
|
||||
same applies to other request types:
|
||||
|
||||
+--------------+------------+----------------+-------------+
|
||||
| Request type | Arguments | Method | Status Code |
|
||||
+==============+============+================+=============+
|
||||
| GET | No | list | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PUT | No | bulk_set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PATCH | No | bulk_set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| POST | No | create | 201 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| DELETE | No | bulk_delete | 204 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| GET | Yes | get | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PUT | Yes | set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| PATCH | Yes | set | 200 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
| DELETE | Yes | delete | 204 |
|
||||
+--------------+------------+----------------+-------------+
|
||||
|
||||
How to restrict access to a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you require that only authenticated users can access you controller, just
|
||||
add the ``AuthRequired`` decorator to your controller class.
|
||||
|
||||
Example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, AuthRequired, RESTController
|
||||
|
||||
@ApiController('ping2')
|
||||
@AuthRequired()
|
||||
class Ping2(RESTController):
|
||||
def list(self):
|
||||
return {"msg": "Hello"}
|
||||
|
||||
Now only authenticated users will be able to "ping" your controller.
|
||||
|
||||
|
||||
How to access the manager module instance from a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Each controller class derived from ``BaseController``has a class property that
|
||||
points to the manager module global instance. The property is named ``mgr``.
|
||||
There is another class property called ``logger`` to easily add log messages.
|
||||
|
||||
Example::
|
||||
|
||||
import cherrypy
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
@ApiController('servers')
|
||||
class Servers(RESTController):
|
||||
def list(self):
|
||||
self.logger.debug('Listing available servers')
|
||||
return {'servers': self.mgr.list_servers()}
|
||||
|
||||
|
||||
How to write a unit test for a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We provide a test helper class called ``ControllerTestCase`` to easily create
|
||||
unit tests for your controller.
|
||||
|
||||
If we want to write a unit test for the above ``Ping2`` controller, create a
|
||||
``test_ping2.py`` file under the ``tests`` directory with the following code::
|
||||
|
||||
from .helper import ControllerTestCase
|
||||
from .controllers.ping2 import Ping2
|
||||
|
||||
class Ping2Test(ControllerTestCase):
|
||||
@classmethod
|
||||
def setup_test(cls):
|
||||
Ping2._cp_config['tools.authentica.on'] = False
|
||||
|
||||
def test_ping2(self):
|
||||
self._get("/api/ping2")
|
||||
self.assertStatus(200)
|
||||
self.assertJsonBody({'msg': 'Hello'})
|
||||
|
||||
The ``ControllerTestCase`` class will call the dashboard module code that loads
|
||||
the controllers and initializes the CherryPy webserver. Then it will call the
|
||||
``setup_test()`` class method to execute additional instructions that each test
|
||||
case needs to add to the test.
|
||||
In the example above we use the ``setup_test()`` method to disable the
|
||||
authentication handler for the ``Ping2`` controller.
|
||||
|
||||
|
||||
How to listen for manager notifications in a controller?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The manager notifies the modules of several types of cluster events, such
|
||||
as cluster logging event, etc...
|
||||
|
||||
Each module has a "global" handler function called ``notify`` that the manager
|
||||
calls to notify the module. But this handler function must not block or spend
|
||||
too much time processing the event notification.
|
||||
For this reason we provide a notification queue that controllers can register
|
||||
themselves with to receive cluster notifications.
|
||||
|
||||
The example below represents a controller that implements a very simple live
|
||||
log viewer page::
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import collections
|
||||
|
||||
import cherrypy
|
||||
|
||||
from ..tools import ApiController, BaseController, NotificationQueue
|
||||
|
||||
|
||||
@ApiController('livelog')
|
||||
class LiveLog(BaseController):
|
||||
log_buffer = collections.deque(maxlen=1000)
|
||||
|
||||
def __init__(self):
|
||||
super(LiveLog, self).__init__()
|
||||
NotificationQueue.register(self.log, 'clog')
|
||||
|
||||
def log(self, log_struct):
|
||||
self.log_buffer.appendleft(log_struct)
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self):
|
||||
ret = '<html><meta http-equiv="refresh" content="2" /><body>'
|
||||
for l in self.log_buffer:
|
||||
ret += "{}<br>".format(l)
|
||||
ret += "</body></html>"
|
||||
return ret
|
||||
|
||||
As you can see above, the ``NotificationQueue`` class provides a register
|
||||
method that receives the function as its first argument, and receives the
|
||||
"notification type" as the second argument.
|
||||
You can omit the second argument of the ``register`` method, and in that case
|
||||
you are registering to listen all notifications of any type.
|
||||
|
||||
Here is an list of notification types (these might change in the future) that
|
||||
can be used:
|
||||
|
||||
* ``clog``: cluster log notifications
|
||||
* ``command``: notification when a command issued by ``MgrModule.send_command``
|
||||
completes
|
||||
* ``perf_schema_update``: perf counters schema update
|
||||
* ``mon_map``: monitor map update
|
||||
* ``fs_map``: cephfs map update
|
||||
* ``osd_map``: OSD map update
|
||||
* ``service_map``: services (RGW, RBD-Mirror, etc.) map update
|
||||
* ``mon_status``: monitor status regular update
|
||||
* ``health``: health status regular update
|
||||
* ``pg_summary``: regular update of PG status information
|
||||
|
||||
|
||||
How to write a unit test when a controller accesses a Ceph module?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Consider the following example that implements a controller that retrieves the
|
||||
list of RBD images of the ``rbd`` pool::
|
||||
|
||||
import rbd
|
||||
from ..tools import ApiController, RESTController
|
||||
|
||||
@ApiController('rbdimages')
|
||||
class RbdImages(RESTController):
|
||||
def __init__(self):
|
||||
self.ioctx = self.mgr.rados.open_ioctx('rbd')
|
||||
self.rbd = rbd.RBD()
|
||||
|
||||
def list(self):
|
||||
return [{'name': n} for n in self.rbd.list(self.ioctx)]
|
||||
|
||||
In the example above, we want to mock the return value of the ``rbd.list``
|
||||
function, so that we can test the JSON response of the controller.
|
||||
|
||||
The unit test code will look like the following::
|
||||
|
||||
import mock
|
||||
from .helper import ControllerTestCase
|
||||
|
||||
class RbdImagesTest(ControllerTestCase):
|
||||
@mock.patch('rbd.RBD.list')
|
||||
def test_list(self, rbd_list_mock):
|
||||
rbd_list_mock.return_value = ['img1', 'img2']
|
||||
self._get('/api/rbdimages')
|
||||
self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
|
||||
Working on the Dashboard Code
|
||||
-----------------------------
|
||||
|
||||
If you're interested in helping with the development of the dashboard, please
|
||||
see the file ``HACKING.rst`` for details on how to set up a development
|
||||
environment and some other development-related topics.
|
||||
|
@ -1,75 +0,0 @@
|
||||
# Ceph Dashboard
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.3.
|
||||
|
||||
## Installation
|
||||
|
||||
Run `npm install` to install the required packages locally.
|
||||
|
||||
**Note**
|
||||
|
||||
If you do not have installed [Angular CLI](https://github.com/angular/angular-cli) globally, then you need to execute ``ng`` commands with an additional ``npm run`` before it.
|
||||
|
||||
## Development server
|
||||
|
||||
Create the `proxy.conf.json` file based on `proxy.conf.json.sample`.
|
||||
|
||||
Run `npm start -- --proxy-config proxy.conf.json` for a dev server.
|
||||
Navigate to `http://localhost:4200/`.
|
||||
The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. Navigate to `http://localhost:8080`.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `npm run test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `npm run e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
|
||||
## Examples of generator
|
||||
|
||||
```
|
||||
# Create module 'Core'
|
||||
src/app> ng generate module core -m=app --routing
|
||||
|
||||
# Create module 'Auth' under module 'Core'
|
||||
src/app/core> ng generate module auth -m=core --routing
|
||||
or, alternatively:
|
||||
src/app> ng generate module core/auth -m=core --routing
|
||||
|
||||
# Create component 'Login' under module 'Auth'
|
||||
src/app/core/auth> ng generate component login -m=core/auth
|
||||
or, alternatively:
|
||||
src/app> ng generate component core/auth/login -m=core/auth
|
||||
```
|
||||
|
||||
## Recommended style guide
|
||||
|
||||
### Typescript
|
||||
|
||||
Group the imports based on its source and separate them with a blank line.
|
||||
|
||||
The source groups can be either from angular, external or internal.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToastsManager } from 'ng2-toastr';
|
||||
|
||||
import { Credentials } from '../../../shared/models/credentials.model';
|
||||
import { HostService } from './services/host.service';
|
||||
```
|
Loading…
Reference in New Issue
Block a user