]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/HACKING.rst
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / HACKING.rst
1 Ceph Dashboard Developer Documentation
2 ======================================
3
4 .. contents:: Table of Contents
5
6 Frontend Development
7 --------------------
8
9 Before you can start the dashboard from within a development environment, you
10 will need to generate the frontend code and either use a compiled and running
11 Ceph cluster (e.g. started by ``vstart.sh``) or the standalone development web
12 server.
13
14 The build process is based on `Node.js <https://nodejs.org/>`_ and requires the
15 `Node Package Manager <https://www.npmjs.com/>`_ ``npm`` to be installed.
16
17 Prerequisites
18 ~~~~~~~~~~~~~
19
20 * Node 8.9.0 or higher
21 * NPM 5.7.0 or higher
22
23 nodeenv:
24 During Ceph's build we create a virtualenv with ``node`` and ``npm``
25 installed, which can be used as an alternative to installing node/npm in your
26 system.
27
28 If you want to use the node installed in the virtualenv you just need to
29 activate the virtualenv before you run any npm commands. To activate it run
30 ``. build/src/pybind/mgr/dashboard/node-env/bin/activate``.
31
32 Once you finish, you can simply run ``deactivate`` and exit the virtualenv.
33
34 Angular CLI:
35 If you do not have the `Angular CLI <https://github.com/angular/angular-cli>`_
36 installed globally, then you need to execute ``ng`` commands with an
37 additional ``npm run`` before it.
38
39 Package installation
40 ~~~~~~~~~~~~~~~~~~~~
41
42 Run ``npm install`` in directory ``src/pybind/mgr/dashboard/frontend`` to
43 install the required packages locally.
44
45 Setting up a Development Server
46 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
47
48 Create the ``proxy.conf.json`` file based on ``proxy.conf.json.sample``.
49
50 Run ``npm start`` for a dev server.
51 Navigate to ``http://localhost:4200/``. The app will automatically
52 reload if you change any of the source files.
53
54 Code Scaffolding
55 ~~~~~~~~~~~~~~~~
56
57 Run ``ng generate component component-name`` to generate a new
58 component. You can also use
59 ``ng generate directive|pipe|service|class|guard|interface|enum|module``.
60
61 Build the Project
62 ~~~~~~~~~~~~~~~~~
63
64 Run ``npm run build`` to build the project. The build artifacts will be
65 stored in the ``dist/`` directory. Use the ``--prod`` flag for a
66 production build (``npm run build -- --prod``). Navigate to ``https://localhost:8443``.
67
68 Build the Code Documentation
69 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
70
71 Run ``npm run doc-build`` to generate code docs in the ``documentation/``
72 directory. To make them accesible locally for a web browser, run
73 ``npm run doc-serve`` and they will become available at ``http://localhost:8444``.
74 With ``npm run compodoc -- <opts>`` you may
75 `fully configure it <https://compodoc.app/guides/usage.html>`_.
76
77 Code linting and formatting
78 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
79
80 We use the following tools to lint and format the code in all our TS, SCSS and
81 HTML files:
82
83 - `codelyzer <http://codelyzer.com/>`_
84 - `html-linter <https://github.com/chinchiheather/html-linter>`_
85 - `Prettier <https://prettier.io/>`_
86 - `TSLint <https://palantir.github.io/tslint/>`_
87
88 We added 2 npm scripts to help run these tools:
89
90 - ``npm run lint``, will check frontend files against all linters
91 - ``npm run fix``, will try to fix all the detected linting errors
92
93 Writing Unit Tests
94 ~~~~~~~~~~~~~~~~~~
95
96 To write unit tests most efficient we have a small collection of tools,
97 we use within test suites.
98
99 Those tools can be found under
100 ``src/pybind/mgr/dashboard/frontend/src/testing/``, especially take
101 a look at ``unit-test-helper.ts``.
102
103 There you will be able to find:
104
105 ``configureTestBed`` that replaces the initial ``TestBed``
106 methods. It takes the same arguments as ``TestBed.configureTestingModule``.
107 Using it will run your tests a lot faster in development, as it doesn't
108 recreate everything from scratch on every test. To use the default behaviour
109 pass ``true`` as the second argument.
110
111 ``PermissionHelper`` to help determine if
112 the correct actions are shown based on the current permissions and selection
113 in a list.
114
115 ``FormHelper`` which makes testing a form a lot easier
116 with a few simple methods. It allows you to set a control or multiple
117 controls, expect if a control is valid or has an error or just do both with
118 one method. Additional you can expect a template element or multiple elements
119 to be visible in the rendered template.
120
121 Running Unit Tests
122 ~~~~~~~~~~~~~~~~~~
123
124 Create ``unit-test-configuration.ts`` file based on
125 ``unit-test-configuration.ts.sample`` in directory
126 ``src/pybind/mgr/dashboard/frontend/src``.
127
128 Run ``npm run test`` to execute the unit tests via `Jest
129 <https://facebook.github.io/jest/>`_.
130
131 If you get errors on all tests, it could be because `Jest
132 <https://facebook.github.io/jest/>`__ or something else was updated.
133 There are a few ways how you can try to resolve this:
134
135 - Remove all modules with ``rm -rf dist node_modules`` and run ``npm install``
136 again in order to reinstall them
137 - Clear the cache of jest by running ``npx jest --clearCache``
138
139 Running End-to-End (E2E) Tests
140 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
141
142 We use `Protractor <http://www.protractortest.org/>`__ to run our frontend E2E
143 tests.
144
145 Our ``run-frontend-e2e-tests.sh`` script will check if Chrome or Docker is
146 installed and run the tests if either is found.
147
148 Start all frontend E2E tests by running::
149
150 $ ./run-frontend-e2e-tests.sh
151
152 Report:
153 After running the tests you can find the corresponding report as well as screenshots
154 of failed test cases by opening the following file in your browser:
155
156 src/pybind/mgr/dashboard/frontend/.protractor-report/index.html
157
158 Device:
159 You can force the script to use a specific device with the ``-d`` flag::
160
161 $ ./run-frontend-e2e-tests.sh -d <chrome|docker>
162
163 Remote:
164 If you want to run the tests outside the ceph environment, you will need to
165 manually define the dashboard url using ``-r`` and, optionally, credentials (``-u``, ``-p``)::
166
167 $ ./run-frontend-e2e-tests.sh -r <DASHBOARD_URL> -u <E2E_LOGIN_USER> -p <E2E_LOGIN_PWD>
168
169 Note:
170 When using docker, as your device, you might need to run the script with sudo
171 permissions.
172
173 When developing E2E tests, it is not necessary to compile the frontend code
174 on each change of the test files. When your development environment is
175 running (``npm start``), you can point Protractor to just use this
176 environment. To attach `Protractor <http://www.protractortest.org/>`__ to
177 this process, run ``npm run e2e:ci``.
178
179 Note::
180
181 In case you have a somewhat particular environment, you might need to adapt
182 `protractor.conf.js` to point to the appropriate destination.
183
184 Writing End-to-End Tests
185 ~~~~~~~~~~~~~~~~~~~~~~~~
186
187 To be used methods
188 ..................
189
190 For clicking checkboxes, the ``clickCheckbox`` method is supposed to be used.
191 Due an adaption of the ``<input type="checkbox">`` tag, the original checkbox
192 is hidden and unclickable. Instead, a fancier replacement is shown. When the
193 developer tries to use `ElementFinder::click()` on such a checkbox, it will
194 raise an error. The ``clickCheckbox`` method prevents that by clicking the
195 label of the checkbox, like a regular user would do.
196
197 The PagerHelper class
198 .....................
199
200 The ``PageHelper`` class is supposed to be used for general purpose code that
201 can be used on various pages or suites.
202
203 Examples are
204
205 - ``getTableCellByContent()`` - returns a table cell by its content
206 - ``getTabsCount()`` - returns the amount of tabs
207 - ``clickCheckbox()`` - clicks a checkbox
208
209 Every method that could be useful on several pages belongs there. Also, methods
210 which enhance the derived classes of the PageHelper belong there. A good
211 example for such a case is the ``restrictTo()`` decorator. It ensures that a
212 method implemented in a subclass of PageHelper is called on the correct page.
213 It will also show a developer-friendly warning if this is not the case.
214
215 Subclasses of PageHelper
216 ........................
217
218 Helper Methods
219 """"""""""""""
220
221 In order to make code reusable which is specific for a particular suite, make
222 sure to put it in a derived class of the ``PageHelper``. For instance, when
223 talking about the pool suite, such methods would be ``create()``, ``exist()``
224 and ``delete()``. These methods are specific to a pool but are useful for other
225 suites.
226
227 Methods that return HTML elements (for instance of type ``ElementFinder`` or
228 ``ElementArrayFinder``, but also ``Promise<ElementFinder>``) which can only
229 be found on a specific page, should be either implemented in the helper
230 methods of the subclass of PageHelper or as own methods of the subclass of
231 PageHelper.
232
233 Registering a new PageHelper
234 """"""""""""""""""""""""""""
235
236 If you have to create a new Helper class derived from the ``PageHelper``,
237 please also ensure that it is instantiated in the constructor of the
238 ``Helper`` class. That way it can automatically be used by all other suites.
239
240 .. code:: TypeScript
241
242 class Helper {
243 // ...
244 pools: PoolPageHelper;
245
246 constructor() {
247 this.pools = new PoolPageHelper();
248 }
249
250 // ...
251 }
252
253 Using PageHelpers
254 """""""""""""""""
255
256 In any suite, an instance of the ``Helper`` class should be used to call
257 various ``PageHelper`` objects and their methods. This makes all methods of all
258 PageHelpers available to all suites.
259
260 .. code:: TypeScript
261
262 it('should create a pool', () => {
263 helper.pools.exist(poolName, false).then(() => {
264 helper.pools.navigateTo('create');
265 helper.pools.create(poolName).then(() => {
266 helper.pools.navigateTo();
267 helper.pools.exist(poolName, true);
268 });
269 });
270 });
271
272 Code Style
273 ..........
274
275 Please refer to the official `Protractor style-guide
276 <https://www.protractortest.org/#/style-guide>`__ for a better insight on how
277 to write and structure tests as well as what exactly should be covered by
278 end-to-end tests.
279
280 ``describe()`` vs ``it()``
281 """"""""""""""""""""""""""
282
283 Both ``describe()`` and ``it()`` are function blocks, meaning that any executable
284 code necessary for the test can be contained in either block. However, Typescript
285 scoping rules still apply, therefore any variables declared in a ``describe`` are available
286 to the ``it()`` blocks inside of it.
287
288 ``describe()`` typically are containers for tests, allowing you to break tests into
289 multiple parts. Likewise, any setup that must be made before your tests are run can be
290 initialized within the ``describe()`` block. Here is an example:
291
292 .. code:: TypeScript
293
294 describe('create, edit & delete image test', () => {
295 const poolName = 'e2e_images_pool';
296
297 beforeAll(() => {
298 pools.navigateTo('create'); // Need pool for image testing
299 pools.create(poolName, 8, 'rbd').then(() => {
300 pools.navigateTo();
301 pools.exist(poolName, true);
302 });
303 images.navigateTo();
304 });
305
306 As shown, we can initiate the variable ``poolName`` as well as run commands
307 before our test suite begins (creating a pool). ``describe()`` block messages should
308 include what the test suite is.
309
310 ``it()`` blocks typically are parts of an overarching test. They contain the functionality of
311 the test suite, each performing individual roles. Here is an example:
312
313 .. code:: TypeScript
314
315 describe('create, edit & delete image test', () => {
316 it('should create image', () => {
317 images.createImage(imageName, poolName, '1');
318 expect(images.getTableCell(imageName).isPresent()).toBe(true);
319 });
320 it('should edit image', () => {
321 images.editImage(imageName, poolName, newImageName, '2');
322 expect(images.getTableCell(newImageName).isPresent()).toBe(true);
323 });
324 //...
325 });
326
327 As shown from the previous example, our ``describe()`` test suite is to create, edit
328 and delete an image. Therefore, each ``it()`` completes one of these steps, one for creating,
329 one for editing, and so on. Likewise, every ``it()`` blocks message should be in lowercase
330 and written so long as "it" can be the prefix of the message. For example, ``it('edits the test image' () => ...)``
331 vs. ``it('image edit test' () => ...)``. As shown, the first example makes grammatical sense with ``it()`` as the
332 prefix whereas the second message does not.``it()`` should describe what the individual test is doing and
333 what it expects to happen.
334
335 Differences between Frontend Unit Tests and End-to-End (E2E) Tests / FAQ
336 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
337
338 General introduction about testing and E2E/unit tests
339
340
341 What are E2E/unit tests designed for?
342 .....................................
343
344 E2E test:
345
346 "Protractor is an end-to-end test framework for Angular and AngularJS applications.
347 Protractor runs tests against your application running in a real browser,
348 interacting with it as a user would." `(src) <http://www.protractortest.org/#/>`__
349
350 It requires a fully functional system and tests the interaction of all components
351 of the application (Ceph, back-end, front-end).
352 E2E tests are designed to mimic the behavior of the user when interacting with the application
353 - for example when it comes to workflows like creating/editing/deleting an item.
354 Also the tests should verify that certain items are displayed as a user would see them
355 when clicking through the UI (for example a menu entry or a pool that has been
356 created during a test and the pool and its properties should be displayed in the table).
357
358 Angular Unit Tests:
359
360 Unit tests, as the name suggests, are tests for smaller units of the code.
361 Those tests are designed for testing all kinds of Angulars' components (e.g. services, pipes etc.).
362 They do not require a connection to the backend, hence those tests are independent of it.
363 The expected data of the backend is mocked in the frontend and by using this data
364 the functionality of the frontend can be tested without having to have real data from the backend.
365 As previously mentioned, data is either mocked or, in a simple case, contains a static input,
366 a function call and an expected static output.
367 More complex examples include the state of a component (attributes of the component class),
368 that define how the output changes according to the given input.
369
370 Which E2E/unit tests are considered to be valid?
371 ................................................
372
373 This is not easy to answer, but new tests that are written in the same way as already existing
374 dashboard tests should generally be considered valid.
375 Unit tests should focus on the component to be tested.
376 This is either an Angular component, directive, service, pipe, etc.
377
378 E2E tests should focus on testing the functionality of the whole application.
379 Approximately a third of the overall E2E tests should verify the correctness
380 of user visible elements.
381
382 How should an E2E/unit test look like?
383 ......................................
384
385 Unit tests should focus on the described purpose
386 and shouldn't try to test other things in the same `it` block.
387
388 E2E tests should contain a description that either verifies
389 the correctness of a user visible element or a complete process
390 like for example the creation/validation/deletion of a pool.
391
392 What should an E2E/unit test cover?
393 ...................................
394
395 E2E tests should mostly, but not exclusively, cover interaction with the backend.
396 This way the interaction with the backend is utilized to write integration tests.
397
398 A unit test should mostly cover critical or complex functionality
399 of a component (Angular Components, Services, Pipes, Directives, etc).
400
401 What should an E2E/unit test NOT cover?
402 .......................................
403
404 Avoid duplicate testing: do not write E2E tests for what's already
405 been covered as frontend-unit tests and vice versa.
406 It may not be possible to completely avoid an overlap.
407
408 Unit tests should not be used to extensively click through components and E2E tests
409 shouldn't be used to extensively test a single component of Angular.
410
411 Best practices/guideline
412 ........................
413
414 As a general guideline we try to follow the 70/20/10 approach - 70% unit tests,
415 20% integration tests and 10% end-to-end tests.
416 For further information please refer to `this document
417 <https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html>`__
418 and the included "Testing Pyramid".
419
420 Further Help
421 ~~~~~~~~~~~~
422
423 To get more help on the Angular CLI use ``ng help`` or go check out the
424 `Angular CLI
425 README <https://github.com/angular/angular-cli/blob/master/README.md>`__.
426
427 Example of a Generator
428 ~~~~~~~~~~~~~~~~~~~~~~
429
430 ::
431
432 # Create module 'Core'
433 src/app> ng generate module core -m=app --routing
434
435 # Create module 'Auth' under module 'Core'
436 src/app/core> ng generate module auth -m=core --routing
437 or, alternatively:
438 src/app> ng generate module core/auth -m=core --routing
439
440 # Create component 'Login' under module 'Auth'
441 src/app/core/auth> ng generate component login -m=core/auth
442 or, alternatively:
443 src/app> ng generate component core/auth/login -m=core/auth
444
445 Frontend Typescript Code Style Guide Recommendations
446 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
447
448 Group the imports based on its source and separate them with a blank
449 line.
450
451 The source groups can be either from Angular, external or internal.
452
453 Example:
454
455 .. code:: javascript
456
457 import { Component } from '@angular/core';
458 import { Router } from '@angular/router';
459
460 import { ToastrManager } from 'ngx-toastr';
461
462 import { Credentials } from '../../../shared/models/credentials.model';
463 import { HostService } from './services/host.service';
464
465 Frontend components
466 ~~~~~~~~~~~~~~~~~~~
467
468 There are several components that can be reused on different pages.
469 This components are declared on the components module:
470 `src/pybind/mgr/dashboard/frontend/src/app/shared/components`.
471
472 Helper
473 ~~~~~~
474
475 This component should be used to provide additional information to the user.
476
477 Example:
478
479 .. code:: html
480
481 <cd-helper>
482 Some <strong>helper</strong> html text
483 </cd-helper>
484
485 Terminology and wording
486 ~~~~~~~~~~~~~~~~~~~~~~~
487
488 Instead of using the Ceph component names, the approach
489 suggested is to use the logical/generic names (Block over RBD, Filesystem over
490 CephFS, Object over RGW). Nevertheless, as Ceph-Dashboard cannot completely hide
491 the Ceph internals, some Ceph-specific names might remain visible.
492
493 Regarding the wording for action labels and other textual elements (form titles,
494 buttons, etc.), the chosen approach is to follow `these guidelines
495 <https://www.patternfly.org/styles/terminology-and-wording/#terminology-and-wording-for-action-labels>`_.
496 As a rule of thumb, 'Create' and 'Delete' are the proper wording for most forms,
497 instead of 'Add' and 'Remove', unless some already created item is either added
498 or removed to/from a set of items (e.g.: 'Add permission' to a user vs. 'Create
499 (new) permission').
500
501 In order to enforce the use of this wording, a service ``ActionLabelsI18n`` has
502 been created, which provides translated labels for use in UI elements.
503
504 Frontend branding
505 ~~~~~~~~~~~~~~~~~
506
507 Every vendor can customize the 'Ceph dashboard' to his needs. No matter if
508 logo, HTML-Template or TypeScript, every file inside the frontend folder can be
509 replaced.
510
511 To replace files, open ``./frontend/angular.json`` and scroll to the section
512 ``fileReplacements`` inside the production configuration. Here you can add the
513 files you wish to brand. We recommend to place the branded version of a file in
514 the same directory as the original one and to add a ``.brand`` to the file
515 name, right in front of the file extension. A ``fileReplacement`` could for
516 example look like this:
517
518 .. code:: javascript
519
520 {
521 "replace": "src/app/core/auth/login/login.component.html",
522 "with": "src/app/core/auth/login/login.component.brand.html"
523 }
524
525 To serve or build the branded user interface run:
526
527 $ npm run start -- --prod
528
529 or
530
531 $ npm run build -- --prod
532
533 Unfortunately it's currently not possible to use multiple configurations when
534 serving or building the UI at the same time. That means a configuration just
535 for the branding ``fileReplacements`` is not an option, because you want to use
536 the production configuration anyway
537 (https://github.com/angular/angular-cli/issues/10612).
538 Furthermore it's also not possible to use glob expressions for
539 ``fileReplacements``. As long as the feature hasn't been implemented, you have
540 to add the file replacements manually to the angular.json file
541 (https://github.com/angular/angular-cli/issues/12354).
542
543 Nevertheless you should stick to the suggested naming scheme because it makes
544 it easier for you to use glob expressions once it's supported in the future.
545
546 To change the variable defaults you can overwrite them in the file
547 ``./frontend/src/vendor.variables.scss``. Just reassign the variable you want
548 to change, for example ``$color-primary: teal;``
549 To overwrite or extend the default CSS, you can add your own styles in
550 ``./frontend/src/vendor.overrides.scss``.
551
552 I18N
553 ----
554
555 How to extract messages from source code?
556 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
557
558 To extract the I18N messages from the templates and the TypeScript files just
559 run the following command in ``src/pybind/mgr/dashboard/frontend``::
560
561 $ npm run i18n:extract
562
563 This will extract all marked messages from the HTML templates first and then
564 add all marked strings from the TypeScript files to the translation template.
565 Since the extraction from TypeScript files is still not supported by Angular
566 itself, we are using the
567 `ngx-translator <https://github.com/ngx-translate/i18n-polyfill>`_ extractor to
568 parse the TypeScript files.
569
570 When the command ran successfully, it should have created or updated the file
571 ``src/locale/messages.xlf``.
572
573 The file isn't tracked by git, you can just use it to start with the
574 translation offline or add/update the resource files on transifex.
575
576 Supported languages
577 ~~~~~~~~~~~~~~~~~~~
578
579 All our supported languages should be registered in both exports in
580 ``supported-languages.enum.ts`` and have a corresponding test in
581 ``language-selector.component.spec.ts``.
582
583 The ``SupportedLanguages`` enum will provide the list for the default language selection.
584
585 The ``languageBootstrapMapping`` variable will provide the
586 `language support <https://github.com/valor-software/ngx-bootstrap/tree/development/src/chronos/i18n>`_
587 for ngx-bootstrap components like the
588 `date picker <https://valor-software.com/ngx-bootstrap/#/datepicker#locales>`_.
589
590 Translating process
591 ~~~~~~~~~~~~~~~~~~~
592
593 To facilitate the translation process of the dashboard we are using a web tool
594 called `transifex <https://www.transifex.com/>`_.
595
596 If you wish to help translating to any language just go to our `transifex
597 project page <https://www.transifex.com/ceph/ceph-dashboard/>`_, join the
598 project and you can start translating immediately.
599
600 All translations will then be reviewed and later pushed upstream.
601
602 Updating translated messages
603 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
604
605 Any time there are new messages translated and reviewed in a specific language
606 we should update the translation file upstream.
607
608 To do that, check the settings in the i18n config file
609 ``src/pybind/mgr/dashboard/frontend/i18n.config.json``:: and make sure that the
610 organization is *ceph*, the project is *ceph-dashboard* and the resource is
611 the one you want to pull from and push to e.g. *Master:master*. To find a list
612 of avaiable resources visit `<https://www.transifex.com/ceph/ceph-dashboard/content/>`_.
613
614 After you checked the config go to the directory ``src/pybind/mgr/dashboard/frontend`` and run::
615
616 $ npm run i18n
617
618 This command will extract all marked messages from the HTML templates and
619 TypeScript files. Once the source file has been created it will push it to
620 transifex and pull the latest translations. It will also fill all the
621 untranslated strings with the source string.
622 The tool will ask you for an api token, unless you added it by running:
623
624 $ npm run i18n:token
625
626 To create a transifex api token visit `<https://www.transifex.com/user/settings/api/>`_.
627
628 After the command ran successfully, build the UI and check if everything is
629 working as expected. You also might want to run the frontend tests.
630
631 Suggestions
632 ~~~~~~~~~~~
633
634 Strings need to start and end in the same line as the element:
635
636 .. code-block:: html
637
638 <!-- avoid -->
639 <span i18n>
640 Foo
641 </span>
642
643 <!-- recommended -->
644 <span i18n>Foo</span>
645
646
647 <!-- avoid -->
648 <span i18n>
649 Foo bar baz.
650 Foo bar baz.
651 </span>
652
653 <!-- recommended -->
654 <span i18n>Foo bar baz.
655 Foo bar baz.</span>
656
657 Isolated interpolations should not be translated:
658
659 .. code-block:: html
660
661 <!-- avoid -->
662 <span i18n>{{ foo }}</span>
663
664 <!-- recommended -->
665 <span>{{ foo }}</span>
666
667 Interpolations used in a sentence should be kept in the translation:
668
669 .. code-block:: html
670
671 <!-- recommended -->
672 <span i18n>There are {{ x }} OSDs.</span>
673
674 Remove elements that are outside the context of the translation:
675
676 .. code-block:: html
677
678 <!-- avoid -->
679 <label i18n>
680 Profile
681 <span class="required"></span>
682 </label>
683
684 <!-- recommended -->
685 <label>
686 <ng-container i18n>Profile<ng-container>
687 <span class="required"></span>
688 </label>
689
690 Keep elements that affect the sentence:
691
692 .. code-block:: html
693
694 <!-- recommended -->
695 <span i18n>Profile <b>foo</b> will be removed.</span>
696
697 Backend Development
698 -------------------
699
700 The Python backend code of this module requires a number of Python modules to be
701 installed. They are listed in file ``requirements.txt``. Using `pip
702 <https://pypi.python.org/pypi/pip>`_ you may install all required dependencies
703 by issuing ``pip install -r requirements.txt`` in directory
704 ``src/pybind/mgr/dashboard``.
705
706 If you're using the `ceph-dev-docker development environment
707 <https://github.com/ricardoasmarques/ceph-dev-docker/>`_, simply run
708 ``./install_deps.sh`` from the toplevel directory to install them.
709
710 Unit Testing
711 ~~~~~~~~~~~~
712
713 In dashboard we have two different kinds of backend tests:
714
715 1. Unit tests based on ``tox``
716 2. API tests based on Teuthology.
717
718 Unit tests based on tox
719 ~~~~~~~~~~~~~~~~~~~~~~~~
720
721 We included a ``tox`` configuration file that will run the unit tests under
722 Python 2 or 3, as well as linting tools to guarantee the uniformity of code.
723
724 You need to install ``tox`` and ``coverage`` before running it. To install the
725 packages in your system, either install it via your operating system's package
726 management tools, e.g. by running ``dnf install python-tox python-coverage`` on
727 Fedora Linux.
728
729 Alternatively, you can use Python's native package installation method::
730
731 $ pip install tox
732 $ pip install coverage
733
734 To run the tests, run ``src/script/run_tox.sh`` in the dashboard directory (where
735 ``tox.ini`` is located)::
736
737 ## Run Python 2+3 tests+lint commands:
738 $ ../../../script/run_tox.sh --tox-env py27,py3,lint,check
739
740 ## Run Python 3 tests+lint commands:
741 $ ../../../script/run_tox.sh --tox-env py3,lint,check
742
743 ## Run Python 3 arbitrary command (e.g. 1 single test):
744 $ ../../../script/run_tox.sh --tox-env py3 "" tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
745
746 You can also run tox instead of ``run_tox.sh``::
747
748 ## Run Python 3 tests command:
749 $ tox -e py3
750
751 ## Run Python 3 arbitrary command (e.g. 1 single test):
752 $ tox -e py3 tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
753
754 Python files can be automatically fixed and formatted according to PEP8
755 standards by using ``run_tox.sh --tox-env fix`` or ``tox -e fix``.
756
757 We also collect coverage information from the backend code when you run tests. You can check the
758 coverage information provided by the tox output, or by running the following
759 command after tox has finished successfully::
760
761 $ coverage html
762
763 This command will create a directory ``htmlcov`` with an HTML representation of
764 the code coverage of the backend.
765
766 API tests based on Teuthology
767 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
768
769 How to run existing API tests:
770 To run the API tests against a real Ceph cluster, we leverage the Teuthology
771 framework. This has the advantage of catching bugs originated from changes in
772 the internal Ceph code.
773
774 Our ``run-backend-api-tests.sh`` script will start a ``vstart`` Ceph cluster
775 before running the Teuthology tests, and then it stops the cluster after the
776 tests are run. Of course this implies that you have built/compiled Ceph
777 previously.
778
779 Start all dashboard tests by running::
780
781 $ ./run-backend-api-tests.sh
782
783 Or, start one or multiple specific tests by specifying the test name::
784
785 $ ./run-backend-api-tests.sh tasks.mgr.dashboard.test_pool.PoolTest
786
787 Or, ``source`` the script and run the tests manually::
788
789 $ source run-backend-api-tests.sh
790 $ run_teuthology_tests [tests]...
791 $ cleanup_teuthology
792
793 How to write your own tests:
794 There are two possible ways to write your own API tests:
795
796 The first is by extending one of the existing test classes in the
797 ``qa/tasks/mgr/dashboard`` directory.
798
799 The second way is by adding your own API test module if you're creating a new
800 controller for example. To do so you'll just need to add the file containing
801 your new test class to the ``qa/tasks/mgr/dashboard`` directory and implement
802 all your tests here.
803
804 .. note:: Don't forget to add the path of the newly created module to
805 ``modules`` section in ``qa/suites/rados/mgr/tasks/dashboard.yaml``.
806
807 Short example: Let's assume you created a new controller called
808 ``my_new_controller.py`` and the related test module
809 ``test_my_new_controller.py``. You'll need to add
810 ``tasks.mgr.dashboard.test_my_new_controller`` to the ``modules`` section in
811 the ``dashboard.yaml`` file.
812
813 Also, if you're removing test modules please keep in mind to remove the
814 related section. Otherwise the Teuthology test run will fail.
815
816 Please run your API tests on your dev environment (as explained above)
817 before submitting a pull request. Also make sure that a full QA run in
818 Teuthology/sepia lab (based on your changes) has completed successfully
819 before it gets merged. You don't need to schedule the QA run yourself, just
820 add the 'needs-qa' label to your pull request as soon as you think it's ready
821 for merging (e.g. make check was successful, the pull request is approved and
822 all comments have been addressed). One of the developers who has access to
823 Teuthology/the sepia lab will take care of it and report the result back to
824 you.
825
826
827 How to add a new controller?
828 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
829
830 A controller is a Python class that extends from the ``BaseController`` class
831 and is decorated with either the ``@Controller``, ``@ApiController`` or
832 ``@UiApiController`` decorators. The Python class must be stored inside a Python
833 file located under the ``controllers`` directory. The Dashboard module will
834 automatically load your new controller upon start.
835
836 ``@ApiController`` and ``@UiApiController`` are both specializations of the
837 ``@Controller`` decorator.
838
839 The ``@ApiController`` should be used for controllers that provide an API-like
840 REST interface and the ``@UiApiController`` should be used for endpoints consumed
841 by the UI but that are not part of the 'public' API. For any other kinds of
842 controllers the ``@Controller`` decorator should be used.
843
844 A controller has a URL prefix path associated that is specified in the
845 controller decorator, and all endpoints exposed by the controller will share
846 the same URL prefix path.
847
848 A controller's endpoint is exposed by implementing a method on the controller
849 class decorated with the ``@Endpoint`` decorator.
850
851 For example create a file ``ping.py`` under ``controllers`` directory with the
852 following code:
853
854 .. code-block:: python
855
856 from ..tools import Controller, ApiController, UiApiController, BaseController, Endpoint
857
858 @Controller('/ping')
859 class Ping(BaseController):
860 @Endpoint()
861 def hello(self):
862 return {'msg': "Hello"}
863
864 @ApiController('/ping')
865 class ApiPing(BaseController):
866 @Endpoint()
867 def hello(self):
868 return {'msg': "Hello"}
869
870 @UiApiController('/ping')
871 class UiApiPing(BaseController):
872 @Endpoint()
873 def hello(self):
874 return {'msg': "Hello"}
875
876 The ``hello`` endpoint of the ``Ping`` controller can be reached by the
877 following URL: https://mgr_hostname:8443/ping/hello using HTTP GET requests.
878 As you can see the controller URL path ``/ping`` is concatenated to the
879 method name ``hello`` to generate the endpoint's URL.
880
881 In the case of the ``ApiPing`` controller, the ``hello`` endpoint can be
882 reached by the following URL: https://mgr_hostname:8443/api/ping/hello using a
883 HTTP GET request.
884 The API controller URL path ``/ping`` is prefixed by the ``/api`` path and then
885 concatenated to the method name ``hello`` to generate the endpoint's URL.
886 Internally, the ``@ApiController`` is actually calling the ``@Controller``
887 decorator by passing an additional decorator parameter called ``base_url``::
888
889 @ApiController('/ping') <=> @Controller('/ping', base_url="/api")
890
891 ``UiApiPing`` works in a similar way than the ``ApiPing``, but the URL will be
892 prefixed by ``/ui-api``: https://mgr_hostname:8443/ui-api/ping/hello. ``UiApiPing`` is
893 also a ``@Controller`` extension::
894
895 @UiApiController('/ping') <=> @Controller('/ping', base_url="/ui-api")
896
897 The ``@Endpoint`` decorator also supports many parameters to customize the
898 endpoint:
899
900 * ``method="GET"``: the HTTP method allowed to access this endpoint.
901 * ``path="/<method_name>"``: the URL path of the endpoint, excluding the
902 controller URL path prefix.
903 * ``path_params=[]``: list of method parameter names that correspond to URL
904 path parameters. Can only be used when ``method in ['POST', 'PUT']``.
905 * ``query_params=[]``: list of method parameter names that correspond to URL
906 query parameters.
907 * ``json_response=True``: indicates if the endpoint response should be
908 serialized in JSON format.
909 * ``proxy=False``: indicates if the endpoint should be used as a proxy.
910
911 An endpoint method may have parameters declared. Depending on the HTTP method
912 defined for the endpoint the method parameters might be considered either
913 path parameters, query parameters, or body parameters.
914
915 For ``GET`` and ``DELETE`` methods, the method's non-optional parameters are
916 considered path parameters by default. Optional parameters are considered
917 query parameters. By specifying the ``query_parameters`` in the endpoint
918 decorator it is possible to make a non-optional parameter to be a query
919 parameter.
920
921 For ``POST`` and ``PUT`` methods, all method parameters are considered
922 body parameters by default. To override this default, one can use the
923 ``path_params`` and ``query_params`` to specify which method parameters are
924 path and query parameters respectively.
925 Body parameters are decoded from the request body, either from a form format, or
926 from a dictionary in JSON format.
927
928 Let's use an example to better understand the possible ways to customize an
929 endpoint:
930
931 .. code-block:: python
932
933 from ..tools import Controller, BaseController, Endpoint
934
935 @Controller('/ping')
936 class Ping(BaseController):
937
938 # URL: /ping/{key}?opt1=...&opt2=...
939 @Endpoint(path="/", query_params=['opt1'])
940 def index(self, key, opt1, opt2=None):
941 """..."""
942
943 # URL: /ping/{key}?opt1=...&opt2=...
944 @Endpoint(query_params=['opt1'])
945 def __call__(self, key, opt1, opt2=None):
946 """..."""
947
948 # URL: /ping/post/{key1}/{key2}
949 @Endpoint('POST', path_params=['key1', 'key2'])
950 def post(self, key1, key2, data1, data2=None):
951 """..."""
952
953
954 In the above example we see how the ``path`` option can be used to override the
955 generated endpoint URL in order to not use the method's name in the URL. In the
956 ``index`` method we set the ``path`` to ``"/"`` to generate an endpoint that is
957 accessible by the root URL of the controller.
958
959 An alternative approach to generate an endpoint that is accessible through just
960 the controller's path URL is by using the ``__call__`` method, as we show in
961 the above example.
962
963 From the third method you can see that the path parameters are collected from
964 the URL by parsing the list of values separated by slashes ``/`` that come
965 after the URL path ``/ping`` for ``index`` method case, and ``/ping/post`` for
966 the ``post`` method case.
967
968 Defining path parameters in endpoints's URLs using python methods's parameters
969 is very easy but it is still a bit strict with respect to the position of these
970 parameters in the URL structure.
971 Sometimes we may want to explicitly define a URL scheme that
972 contains path parameters mixed with static parts of the URL.
973 Our controller infrastructure also supports the declaration of URL paths with
974 explicit path parameters at both the controller level and method level.
975
976 Consider the following example:
977
978 .. code-block:: python
979
980 from ..tools import Controller, BaseController, Endpoint
981
982 @Controller('/ping/{node}/stats')
983 class Ping(BaseController):
984
985 # URL: /ping/{node}/stats/{date}/latency?unit=...
986 @Endpoint(path="/{date}/latency")
987 def latency(self, node, date, unit="ms"):
988 """ ..."""
989
990 In this example we explicitly declare a path parameter ``{node}`` in the
991 controller URL path, and a path parameter ``{date}`` in the ``latency``
992 method. The endpoint for the ``latency`` method is then accessible through
993 the URL: https://mgr_hostname:8443/ping/{node}/stats/{date}/latency .
994
995 For a full set of examples on how to use the ``@Endpoint``
996 decorator please check the unit test file: ``tests/test_controllers.py``.
997 There you will find many examples of how to customize endpoint methods.
998
999
1000 Implementing Proxy Controller
1001 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1002
1003 Sometimes you might need to relay some requests from the Dashboard frontend
1004 directly to an external service.
1005 For that purpose we provide a decorator called ``@Proxy``.
1006 (As a concrete example, check the ``controllers/rgw.py`` file where we
1007 implemented an RGW Admin Ops proxy.)
1008
1009
1010 The ``@Proxy`` decorator is a wrapper of the ``@Endpoint`` decorator that
1011 already customizes the endpoint for working as a proxy.
1012 A proxy endpoint works by capturing the URL path that follows the controller
1013 URL prefix path, and does not do any decoding of the request body.
1014
1015 Example:
1016
1017 .. code-block:: python
1018
1019 from ..tools import Controller, BaseController, Proxy
1020
1021 @Controller('/foo/proxy')
1022 class FooServiceProxy(BaseController):
1023
1024 @Proxy()
1025 def proxy(self, path, **params):
1026 """
1027 if requested URL is "/foo/proxy/access/service?opt=1"
1028 then path is "access/service" and params is {'opt': '1'}
1029 """
1030
1031
1032 How does the RESTController work?
1033 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1034
1035 We also provide a simple mechanism to create REST based controllers using the
1036 ``RESTController`` class. Any class which inherits from ``RESTController`` will,
1037 by default, return JSON.
1038
1039 The ``RESTController`` is basically an additional abstraction layer which eases
1040 and unifies the work with collections. A collection is just an array of objects
1041 with a specific type. ``RESTController`` enables some default mappings of
1042 request types and given parameters to specific method names. This may sound
1043 complicated at first, but it's fairly easy. Lets have look at the following
1044 example:
1045
1046 .. code-block:: python
1047
1048 import cherrypy
1049 from ..tools import ApiController, RESTController
1050
1051 @ApiController('ping')
1052 class Ping(RESTController):
1053 def list(self):
1054 return {"msg": "Hello"}
1055
1056 def get(self, id):
1057 return self.objects[id]
1058
1059 In this case, the ``list`` method is automatically used for all requests to
1060 ``api/ping`` where no additional argument is given and where the request type
1061 is ``GET``. If the request is given an additional argument, the ID in our
1062 case, it won't map to ``list`` anymore but to ``get`` and return the element
1063 with the given ID (assuming that ``self.objects`` has been filled before). The
1064 same applies to other request types:
1065
1066 +--------------+------------+----------------+-------------+
1067 | Request type | Arguments | Method | Status Code |
1068 +==============+============+================+=============+
1069 | GET | No | list | 200 |
1070 +--------------+------------+----------------+-------------+
1071 | PUT | No | bulk_set | 200 |
1072 +--------------+------------+----------------+-------------+
1073 | POST | No | create | 201 |
1074 +--------------+------------+----------------+-------------+
1075 | DELETE | No | bulk_delete | 204 |
1076 +--------------+------------+----------------+-------------+
1077 | GET | Yes | get | 200 |
1078 +--------------+------------+----------------+-------------+
1079 | PUT | Yes | set | 200 |
1080 +--------------+------------+----------------+-------------+
1081 | DELETE | Yes | delete | 204 |
1082 +--------------+------------+----------------+-------------+
1083
1084 How to use a custom API endpoint in a RESTController?
1085 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1086
1087 If you don't have any access restriction you can use ``@Endpoint``. If you
1088 have set a permission scope to restrict access to your endpoints,
1089 ``@Endpoint`` will fail, as it doesn't know which permission property should be
1090 used. To use a custom endpoint inside a restricted ``RESTController`` use
1091 ``@RESTController.Collection`` instead. You can also choose
1092 ``@RESTController.Resource`` if you have set a ``RESOURCE_ID`` in your
1093 ``RESTController`` class.
1094
1095 .. code-block:: python
1096
1097 import cherrypy
1098 from ..tools import ApiController, RESTController
1099
1100 @ApiController('ping', Scope.Ping)
1101 class Ping(RESTController):
1102 RESOURCE_ID = 'ping'
1103
1104 @RESTController.Resource('GET')
1105 def some_get_endpoint(self):
1106 return {"msg": "Hello"}
1107
1108 @RESTController.Collection('POST')
1109 def some_post_endpoint(self, **data):
1110 return {"msg": data}
1111
1112 Both decorators also support four parameters to customize the
1113 endpoint:
1114
1115 * ``method="GET"``: the HTTP method allowed to access this endpoint.
1116 * ``path="/<method_name>"``: the URL path of the endpoint, excluding the
1117 controller URL path prefix.
1118 * ``status=200``: set the HTTP status response code
1119 * ``query_params=[]``: list of method parameter names that correspond to URL
1120 query parameters.
1121
1122 How to restrict access to a controller?
1123 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1124
1125 All controllers require authentication by default.
1126 If you require that the controller can be accessed without authentication,
1127 then you can add the parameter ``secure=False`` to the controller decorator.
1128
1129 Example:
1130
1131 .. code-block:: python
1132
1133 import cherrypy
1134 from . import ApiController, RESTController
1135
1136
1137 @ApiController('ping', secure=False)
1138 class Ping(RESTController):
1139 def list(self):
1140 return {"msg": "Hello"}
1141
1142 How to create a dedicated UI endpoint which uses the 'public' API?
1143 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1144
1145 Sometimes we want to combine multiple calls into one single call
1146 to save bandwidth or for other performance reasons.
1147 In order to achieve that, we first have to create an ``@UiApiController`` which
1148 is used for endpoints consumed by the UI but that are not part of the
1149 'public' API. Let the ui class inherit from the REST controller class.
1150 Now you can use all methods from the api controller.
1151
1152 Example:
1153
1154 .. code-block:: python
1155
1156 import cherrypy
1157 from . import UiApiController, ApiController, RESTController
1158
1159
1160 @ApiController('ping', secure=False) # /api/ping
1161 class Ping(RESTController):
1162 def list(self):
1163 return self._list()
1164
1165 def _list(self): # To not get in conflict with the JSON wrapper
1166 return [1,2,3]
1167
1168
1169 @UiApiController('ping', secure=False) # /ui-api/ping
1170 class PingUi(Ping):
1171 def list(self):
1172 return self._list() + [4, 5, 6]
1173
1174 How to access the manager module instance from a controller?
1175 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1176
1177 We provide the manager module instance as a global variable that can be
1178 imported in any module.
1179
1180 Example:
1181
1182 .. code-block:: python
1183
1184 import logging
1185 import cherrypy
1186 from .. import mgr
1187 from ..tools import ApiController, RESTController
1188
1189 logger = logging.getLogger(__name__)
1190
1191 @ApiController('servers')
1192 class Servers(RESTController):
1193 def list(self):
1194 logger.debug('Listing available servers')
1195 return {'servers': mgr.list_servers()}
1196
1197
1198 How to write a unit test for a controller?
1199 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1200
1201 We provide a test helper class called ``ControllerTestCase`` to easily create
1202 unit tests for your controller.
1203
1204 If we want to write a unit test for the above ``Ping`` controller, create a
1205 ``test_ping.py`` file under the ``tests`` directory with the following code:
1206
1207 .. code-block:: python
1208
1209 from .helper import ControllerTestCase
1210 from .controllers.ping import Ping
1211
1212
1213 class PingTest(ControllerTestCase):
1214 @classmethod
1215 def setup_test(cls):
1216 Ping._cp_config['tools.authenticate.on'] = False
1217 cls.setup_controllers([Ping])
1218
1219 def test_ping(self):
1220 self._get("/api/ping")
1221 self.assertStatus(200)
1222 self.assertJsonBody({'msg': 'Hello'})
1223
1224 The ``ControllerTestCase`` class starts by initializing a CherryPy webserver.
1225 Then it will call the ``setup_test()`` class method where we can explicitly
1226 load the controllers that we want to test. In the above example we are only
1227 loading the ``Ping`` controller. We can also disable authentication of a
1228 controller at this stage, as depicted in the example.
1229
1230
1231 How to listen for manager notifications in a controller?
1232 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1233
1234 The manager notifies the modules of several types of cluster events, such
1235 as cluster logging event, etc...
1236
1237 Each module has a "global" handler function called ``notify`` that the manager
1238 calls to notify the module. But this handler function must not block or spend
1239 too much time processing the event notification.
1240 For this reason we provide a notification queue that controllers can register
1241 themselves with to receive cluster notifications.
1242
1243 The example below represents a controller that implements a very simple live
1244 log viewer page:
1245
1246 .. code-block:: python
1247
1248 from __future__ import absolute_import
1249
1250 import collections
1251
1252 import cherrypy
1253
1254 from ..tools import ApiController, BaseController, NotificationQueue
1255
1256
1257 @ApiController('livelog')
1258 class LiveLog(BaseController):
1259 log_buffer = collections.deque(maxlen=1000)
1260
1261 def __init__(self):
1262 super(LiveLog, self).__init__()
1263 NotificationQueue.register(self.log, 'clog')
1264
1265 def log(self, log_struct):
1266 self.log_buffer.appendleft(log_struct)
1267
1268 @cherrypy.expose
1269 def default(self):
1270 ret = '<html><meta http-equiv="refresh" content="2" /><body>'
1271 for l in self.log_buffer:
1272 ret += "{}<br>".format(l)
1273 ret += "</body></html>"
1274 return ret
1275
1276 As you can see above, the ``NotificationQueue`` class provides a register
1277 method that receives the function as its first argument, and receives the
1278 "notification type" as the second argument.
1279 You can omit the second argument of the ``register`` method, and in that case
1280 you are registering to listen all notifications of any type.
1281
1282 Here is an list of notification types (these might change in the future) that
1283 can be used:
1284
1285 * ``clog``: cluster log notifications
1286 * ``command``: notification when a command issued by ``MgrModule.send_command``
1287 completes
1288 * ``perf_schema_update``: perf counters schema update
1289 * ``mon_map``: monitor map update
1290 * ``fs_map``: cephfs map update
1291 * ``osd_map``: OSD map update
1292 * ``service_map``: services (RGW, RBD-Mirror, etc.) map update
1293 * ``mon_status``: monitor status regular update
1294 * ``health``: health status regular update
1295 * ``pg_summary``: regular update of PG status information
1296
1297
1298 How to write a unit test when a controller accesses a Ceph module?
1299 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1300
1301 Consider the following example that implements a controller that retrieves the
1302 list of RBD images of the ``rbd`` pool:
1303
1304 .. code-block:: python
1305
1306 import rbd
1307 from .. import mgr
1308 from ..tools import ApiController, RESTController
1309
1310
1311 @ApiController('rbdimages')
1312 class RbdImages(RESTController):
1313 def __init__(self):
1314 self.ioctx = mgr.rados.open_ioctx('rbd')
1315 self.rbd = rbd.RBD()
1316
1317 def list(self):
1318 return [{'name': n} for n in self.rbd.list(self.ioctx)]
1319
1320 In the example above, we want to mock the return value of the ``rbd.list``
1321 function, so that we can test the JSON response of the controller.
1322
1323 The unit test code will look like the following:
1324
1325 .. code-block:: python
1326
1327 import mock
1328 from .helper import ControllerTestCase
1329
1330
1331 class RbdImagesTest(ControllerTestCase):
1332 @mock.patch('rbd.RBD.list')
1333 def test_list(self, rbd_list_mock):
1334 rbd_list_mock.return_value = ['img1', 'img2']
1335 self._get('/api/rbdimages')
1336 self.assertJsonBody([{'name': 'img1'}, {'name': 'img2'}])
1337
1338
1339
1340 How to add a new configuration setting?
1341 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1342
1343 If you need to store some configuration setting for a new feature, we already
1344 provide an easy mechanism for you to specify/use the new config setting.
1345
1346 For instance, if you want to add a new configuration setting to hold the
1347 email address of the dashboard admin, just add a setting name as a class
1348 attribute to the ``Options`` class in the ``settings.py`` file::
1349
1350 # ...
1351 class Options(object):
1352 # ...
1353
1354 ADMIN_EMAIL_ADDRESS = ('admin@admin.com', str)
1355
1356 The value of the class attribute is a pair composed by the default value for that
1357 setting, and the python type of the value.
1358
1359 By declaring the ``ADMIN_EMAIL_ADDRESS`` class attribute, when you restart the
1360 dashboard module, you will automatically gain two additional CLI commands to
1361 get and set that setting::
1362
1363 $ ceph dashboard get-admin-email-address
1364 $ ceph dashboard set-admin-email-address <value>
1365
1366 To access, or modify the config setting value from your Python code, either
1367 inside a controller or anywhere else, you just need to import the ``Settings``
1368 class and access it like this:
1369
1370 .. code-block:: python
1371
1372 from settings import Settings
1373
1374 # ...
1375 tmp_var = Settings.ADMIN_EMAIL_ADDRESS
1376
1377 # ....
1378 Settings.ADMIN_EMAIL_ADDRESS = 'myemail@admin.com'
1379
1380 The settings management implementation will make sure that if you change a
1381 setting value from the Python code you will see that change when accessing
1382 that setting from the CLI and vice-versa.
1383
1384
1385 How to run a controller read-write operation asynchronously?
1386 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1387
1388 Some controllers might need to execute operations that alter the state of the
1389 Ceph cluster. These operations might take some time to execute and to maintain
1390 a good user experience in the Web UI, we need to run those operations
1391 asynchronously and return immediately to frontend some information that the
1392 operations are running in the background.
1393
1394 To help in the development of the above scenario we added the support for
1395 asynchronous tasks. To trigger the execution of an asynchronous task we must
1396 use the following class method of the ``TaskManager`` class::
1397
1398 from ..tools import TaskManager
1399 # ...
1400 TaskManager.run(name, metadata, func, args, kwargs)
1401
1402 * ``name`` is a string that can be used to group tasks. For instance
1403 for RBD image creation tasks we could specify ``"rbd/create"`` as the
1404 name, or similarly ``"rbd/remove"`` for RBD image removal tasks.
1405
1406 * ``metadata`` is a dictionary where we can store key-value pairs that
1407 characterize the task. For instance, when creating a task for creating
1408 RBD images we can specify the metadata argument as
1409 ``{'pool_name': "rbd", image_name': "test-img"}``.
1410
1411 * ``func`` is the python function that implements the operation code, which
1412 will be executed asynchronously.
1413
1414 * ``args`` and ``kwargs`` are the positional and named arguments that will be
1415 passed to ``func`` when the task manager starts its execution.
1416
1417 The ``TaskManager.run`` method triggers the asynchronous execution of function
1418 ``func`` and returns a ``Task`` object.
1419 The ``Task`` provides the public method ``Task.wait(timeout)``, which can be
1420 used to wait for the task to complete up to a timeout defined in seconds and
1421 provided as an argument. If no argument is provided the ``wait`` method
1422 blocks until the task is finished.
1423
1424 The ``Task.wait`` is very useful for tasks that usually are fast to execute but
1425 that sometimes may take a long time to run.
1426 The return value of the ``Task.wait`` method is a pair ``(state, value)``
1427 where ``state`` is a string with following possible values:
1428
1429 * ``VALUE_DONE = "done"``
1430 * ``VALUE_EXECUTING = "executing"``
1431
1432 The ``value`` will store the result of the execution of function ``func`` if
1433 ``state == VALUE_DONE``. If ``state == VALUE_EXECUTING`` then
1434 ``value == None``.
1435
1436 The pair ``(name, metadata)`` should unequivocally identify the task being
1437 run, which means that if you try to trigger a new task that matches the same
1438 ``(name, metadata)`` pair of the currently running task, then the new task
1439 is not created and you get the task object of the current running task.
1440
1441 For instance, consider the following example:
1442
1443 .. code-block:: python
1444
1445 task1 = TaskManager.run("dummy/task", {'attr': 2}, func)
1446 task2 = TaskManager.run("dummy/task", {'attr': 2}, func)
1447
1448 If the second call to ``TaskManager.run`` executes while the first task is
1449 still executing then it will return the same task object:
1450 ``assert task1 == task2``.
1451
1452
1453 How to get the list of executing and finished asynchronous tasks?
1454 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1455
1456 The list of executing and finished tasks is included in the ``Summary``
1457 controller, which is already polled every 5 seconds by the dashboard frontend.
1458 But we also provide a dedicated controller to get the same list of executing
1459 and finished tasks.
1460
1461 The ``Task`` controller exposes the ``/api/task`` endpoint that returns the
1462 list of executing and finished tasks. This endpoint accepts the ``name``
1463 parameter that accepts a glob expression as its value.
1464 For instance, an HTTP GET request of the URL ``/api/task?name=rbd/*``
1465 will return all executing and finished tasks which name starts with ``rbd/``.
1466
1467 To prevent the finished tasks list from growing unbounded, we will always
1468 maintain the 10 most recent finished tasks, and the remaining older finished
1469 tasks will be removed when reaching a TTL of 1 minute. The TTL is calculated
1470 using the timestamp when the task finished its execution. After a minute, when
1471 the finished task information is retrieved, either by the summary controller or
1472 by the task controller, it is automatically deleted from the list and it will
1473 not be included in further task queries.
1474
1475 Each executing task is represented by the following dictionary::
1476
1477 {
1478 'name': "name", # str
1479 'metadata': { }, # dict
1480 'begin_time': "2018-03-14T15:31:38.423605Z", # str (ISO 8601 format)
1481 'progress': 0 # int (percentage)
1482 }
1483
1484 Each finished task is represented by the following dictionary::
1485
1486 {
1487 'name': "name", # str
1488 'metadata': { }, # dict
1489 'begin_time': "2018-03-14T15:31:38.423605Z", # str (ISO 8601 format)
1490 'end_time': "2018-03-14T15:31:39.423605Z", # str (ISO 8601 format)
1491 'duration': 0.0, # float
1492 'progress': 0 # int (percentage)
1493 'success': True, # bool
1494 'ret_value': None, # object, populated only if 'success' == True
1495 'exception': None, # str, populated only if 'success' == False
1496 }
1497
1498
1499 How to use asynchronous APIs with asynchronous tasks?
1500 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1501
1502 The ``TaskManager.run`` method as described in a previous section, is well
1503 suited for calling blocking functions, as it runs the function inside a newly
1504 created thread. But sometimes we want to call some function of an API that is
1505 already asynchronous by nature.
1506
1507 For these cases we want to avoid creating a new thread for just running a
1508 non-blocking function, and want to leverage the asynchronous nature of the
1509 function. The ``TaskManager.run`` is already prepared to be used with
1510 non-blocking functions by passing an object of the type ``TaskExecutor`` as an
1511 additional parameter called ``executor``. The full method signature of
1512 ``TaskManager.run``::
1513
1514 TaskManager.run(name, metadata, func, args=None, kwargs=None, executor=None)
1515
1516
1517 The ``TaskExecutor`` class is responsible for code that executes a given task
1518 function, and defines three methods that can be overridden by
1519 subclasses::
1520
1521 def init(self, task)
1522 def start(self)
1523 def finish(self, ret_value, exception)
1524
1525 The ``init`` method is called before the running the task function, and
1526 receives the task object (of class ``Task``).
1527
1528 The ``start`` method runs the task function. The default implementation is to
1529 run the task function in the current thread context.
1530
1531 The ``finish`` method should be called when the task function finishes with
1532 either the ``ret_value`` populated with the result of the execution, or with
1533 an exception object in the case that execution raised an exception.
1534
1535 To leverage the asynchronous nature of a non-blocking function, the developer
1536 should implement a custom executor by creating a subclass of the
1537 ``TaskExecutor`` class, and provide an instance of the custom executor class
1538 as the ``executor`` parameter of the ``TaskManager.run``.
1539
1540 To better understand the expressive power of executors, we write a full example
1541 of use a custom executor to execute the ``MgrModule.send_command`` asynchronous
1542 function:
1543
1544 .. code-block:: python
1545
1546 import json
1547 from mgr_module import CommandResult
1548 from .. import mgr
1549 from ..tools import ApiController, RESTController, NotificationQueue, \
1550 TaskManager, TaskExecutor
1551
1552
1553 class SendCommandExecutor(TaskExecutor):
1554 def __init__(self):
1555 super(SendCommandExecutor, self).__init__()
1556 self.tag = None
1557 self.result = None
1558
1559 def init(self, task):
1560 super(SendCommandExecutor, self).init(task)
1561
1562 # we need to listen for 'command' events to know when the command
1563 # finishes
1564 NotificationQueue.register(self._handler, 'command')
1565
1566 # store the CommandResult object to retrieve the results
1567 self.result = self.task.fn_args[0]
1568 if len(self.task.fn_args) > 4:
1569 # the user specified a tag for the command, so let's use it
1570 self.tag = self.task.fn_args[4]
1571 else:
1572 # let's generate a unique tag for the command
1573 self.tag = 'send_command_{}'.format(id(self))
1574 self.task.fn_args.append(self.tag)
1575
1576 def _handler(self, data):
1577 if data == self.tag:
1578 # the command has finished, notifying the task with the result
1579 self.finish(self.result.wait(), None)
1580 # deregister listener to avoid memory leaks
1581 NotificationQueue.deregister(self._handler, 'command')
1582
1583
1584 @ApiController('test')
1585 class Test(RESTController):
1586
1587 def _run_task(self, osd_id):
1588 task = TaskManager.run("test/task", {}, mgr.send_command,
1589 [CommandResult(''), 'osd', osd_id,
1590 json.dumps({'prefix': 'perf histogram dump'})],
1591 executor=SendCommandExecutor())
1592 return task.wait(1.0)
1593
1594 def get(self, osd_id):
1595 status, value = self._run_task(osd_id)
1596 return {'status': status, 'value': value}
1597
1598
1599 The above ``SendCommandExecutor`` executor class can be used for any call to
1600 ``MgrModule.send_command``. This means that we should need just one custom
1601 executor class implementation for each non-blocking API that we use in our
1602 controllers.
1603
1604 The default executor, used when no executor object is passed to
1605 ``TaskManager.run``, is the ``ThreadedExecutor``. You can check its
1606 implementation in the ``tools.py`` file.
1607
1608
1609 How to update the execution progress of an asynchronous task?
1610 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1611
1612 The asynchronous tasks infrastructure provides support for updating the
1613 execution progress of an executing task.
1614 The progress can be updated from within the code the task is executing, which
1615 usually is the place where we have the progress information available.
1616
1617 To update the progress from within the task code, the ``TaskManager`` class
1618 provides a method to retrieve the current task object::
1619
1620 TaskManager.current_task()
1621
1622 The above method is only available when using the default executor
1623 ``ThreadedExecutor`` for executing the task.
1624 The ``current_task()`` method returns the current ``Task`` object. The
1625 ``Task`` object provides two public methods to update the execution progress
1626 value: the ``set_progress(percentage)``, and the ``inc_progress(delta)``
1627 methods.
1628
1629 The ``set_progress`` method receives as argument an integer value representing
1630 the absolute percentage that we want to set to the task.
1631
1632 The ``inc_progress`` method receives as argument an integer value representing
1633 the delta we want to increment to the current execution progress percentage.
1634
1635 Take the following example of a controller that triggers a new task and
1636 updates its progress:
1637
1638 .. code-block:: python
1639
1640 from __future__ import absolute_import
1641 import random
1642 import time
1643 import cherrypy
1644 from ..tools import TaskManager, ApiController, BaseController
1645
1646
1647 @ApiController('dummy_task')
1648 class DummyTask(BaseController):
1649 def _dummy(self):
1650 top = random.randrange(100)
1651 for i in range(top):
1652 TaskManager.current_task().set_progress(i*100/top)
1653 # or TaskManager.current_task().inc_progress(100/top)
1654 time.sleep(1)
1655 return "finished"
1656
1657 @cherrypy.expose
1658 @cherrypy.tools.json_out()
1659 def default(self):
1660 task = TaskManager.run("dummy/task", {}, self._dummy)
1661 return task.wait(5) # wait for five seconds
1662
1663
1664 How to deal with asynchronous tasks in the front-end?
1665 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1666
1667 All executing and most recently finished asynchronous tasks are displayed on
1668 "Background-Tasks" and if finished on "Recent-Notifications" in the menu bar.
1669 For each task a operation name for three states (running, success and failure),
1670 a function that tells who is involved and error descriptions, if any, have to
1671 be provided. This can be achieved by appending
1672 ``TaskManagerMessageService.messages``. This has to be done to achieve
1673 consistency among all tasks and states.
1674
1675 Operation Object
1676 Ensures consistency among all tasks. It consists of three verbs for each
1677 different state f.e.
1678 ``{running: 'Creating', failure: 'create', success: 'Created'}``.
1679
1680 #. Put running operations in present participle f.e. ``'Updating'``.
1681 #. Failed messages always start with ``'Failed to '`` and should be continued
1682 with the operation in present tense f.e. ``'update'``.
1683 #. Put successful operations in past tense f.e. ``'Updated'``.
1684
1685 Involves Function
1686 Ensures consistency among all messages of a task, it resembles who's
1687 involved by the operation. It's a function that returns a string which
1688 takes the metadata from the task to return f.e.
1689 ``"RBD 'somePool/someImage'"``.
1690
1691 Both combined create the following messages:
1692
1693 * Failure => ``"Failed to create RBD 'somePool/someImage'"``
1694 * Running => ``"Creating RBD 'somePool/someImage'"``
1695 * Success => ``"Created RBD 'somePool/someImage'"``
1696
1697 For automatic task handling use ``TaskWrapperService.wrapTaskAroundCall``.
1698
1699 If for some reason ``wrapTaskAroundCall`` is not working for you,
1700 you have to subscribe to your asynchronous task manually through
1701 ``TaskManagerService.subscribe``, and provide it with a callback,
1702 in case of a success to notify the user. A notification can
1703 be triggered with ``NotificationService.notifyTask``. It will use
1704 ``TaskManagerMessageService.messages`` to display a message based on the state
1705 of a task.
1706
1707 Notifications of API errors are handled by ``ApiInterceptorService``.
1708
1709 Usage example:
1710
1711 .. code-block:: javascript
1712
1713 export class TaskManagerMessageService {
1714 // ...
1715 messages = {
1716 // Messages for task 'rbd/create'
1717 'rbd/create': new TaskManagerMessage(
1718 // Message prefixes
1719 ['create', 'Creating', 'Created'],
1720 // Message suffix
1721 (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'`,
1722 (metadata) => ({
1723 // Error code and description
1724 '17': `Name is already used by RBD '${metadata.pool_name}/${
1725 metadata.image_name}'.`
1726 })
1727 ),
1728 // ...
1729 };
1730 // ...
1731 }
1732
1733 export class RBDFormComponent {
1734 // ...
1735 createAction() {
1736 const request = this.createRequest();
1737 // Subscribes to 'call' with submitted 'task' and handles notifications
1738 return this.taskWrapper.wrapTaskAroundCall({
1739 task: new FinishedTask('rbd/create', {
1740 pool_name: request.pool_name,
1741 image_name: request.name
1742 }),
1743 call: this.rbdService.create(request)
1744 });
1745 }
1746 // ...
1747 }
1748
1749
1750 REST API documentation
1751 ~~~~~~~~~~~~~~~~~~~~~~
1752 There is an automatically generated Swagger UI page for documentation of the REST
1753 API endpoints.However, by default it is not very detailed. There are two
1754 decorators that can be used to add more information:
1755
1756 * ``@EndpointDoc()`` for documentation of endpoints. It has four optional arguments
1757 (explained below): ``description``, ``group``, ``parameters`` and
1758 ``responses``.
1759 * ``@ControllerDoc()`` for documentation of controller or group associated with
1760 the endpoints. It only takes the two first arguments: ``description`` and
1761 ``group``.
1762
1763
1764 ``description``: A a string with a short (1-2 sentences) description of the object.
1765
1766
1767 ``group``: By default, an endpoint is grouped together with other endpoints
1768 within the same controller class. ``group`` is a string that can be used to
1769 assign an endpoint or all endpoints in a class to another controller or a
1770 conceived group name.
1771
1772
1773 ``parameters``: A dict used to describe path, query or request body parameters.
1774 By default, all parameters for an endpoint are listed on the Swagger UI page,
1775 including information of whether the parameter is optional/required and default
1776 values. However, there will be no description of the parameter and the parameter
1777 type will only be displayed in some cases.
1778 When adding information, each parameters should be described as in the example
1779 below. Note that the parameter type should be expressed as a built-in python
1780 type and not as a string. Allowed values are ``str``, ``int``, ``bool``, ``float``.
1781
1782 .. code-block:: python
1783
1784 @EndpointDoc(parameters={'my_string': (str, 'Description of my_string')})
1785 def method(my_string): pass
1786
1787 For body parameters, more complex cases are possible. If the parameter is a
1788 dictionary, the type should be replaced with a ``dict`` containing its nested
1789 parameters. When describing nested parameters, the same format as other
1790 parameters is used. However, all nested parameters are set as required by default.
1791 If the nested parameter is optional this must be specified as for ``item2`` in
1792 the example below. If a nested parameters is set to optional, it is also
1793 possible to specify the default value (this will not be provided automatically
1794 for nested parameters).
1795
1796 .. code-block:: python
1797
1798 @EndpointDoc(parameters={
1799 'my_dictionary': ({
1800 'item1': (str, 'Description of item1'),
1801 'item2': (str, 'Description of item2', True), # item2 is optional
1802 'item3': (str, 'Description of item3', True, 'foo'), # item3 is optional with 'foo' as default value
1803 }, 'Description of my_dictionary')})
1804 def method(my_dictionary): pass
1805
1806 If the parameter is a ``list`` of primitive types, the type should be
1807 surrounded with square brackets.
1808
1809 .. code-block:: python
1810
1811 @EndpointDoc(parameters={'my_list': ([int], 'Description of my_list')})
1812 def method(my_list): pass
1813
1814 If the parameter is a ``list`` with nested parameters, the nested parameters
1815 should be placed in a dictionary and surrounded with square brackets.
1816
1817 .. code-block:: python
1818
1819 @EndpointDoc(parameters={
1820 'my_list': ([{
1821 'list_item': (str, 'Description of list_item'),
1822 'list_item2': (str, 'Description of list_item2')
1823 }], 'Description of my_list')})
1824 def method(my_list): pass
1825
1826
1827 ``responses``: A dict used for describing responses. Rules for describing
1828 responses are the same as for request body parameters, with one difference:
1829 responses also needs to be assigned to the related response code as in the
1830 example below:
1831
1832 .. code-block:: python
1833
1834 @EndpointDoc(responses={
1835 '400':{'my_response': (str, 'Description of my_response')}})
1836 def method(): pass
1837
1838
1839 Error Handling in Python
1840 ~~~~~~~~~~~~~~~~~~~~~~~~
1841
1842 Good error handling is a key requirement in creating a good user experience
1843 and providing a good API.
1844
1845 Dashboard code should not duplicate C++ code. Thus, if error handling in C++
1846 is sufficient to provide good feedback, a new wrapper to catch these errors
1847 is not necessary. On the other hand, input validation is the best place to
1848 catch errors and generate the best error messages. If required, generate
1849 errors as soon as possible.
1850
1851 The backend provides few standard ways of returning errors.
1852
1853 First, there is a generic Internal Server Error::
1854
1855 Status Code: 500
1856 {
1857 "version": <cherrypy version, e.g. 13.1.0>,
1858 "detail": "The server encountered an unexpected condition which prevented it from fulfilling the request.",
1859 }
1860
1861
1862 For errors generated by the backend, we provide a standard error
1863 format::
1864
1865 Status Code: 400
1866 {
1867 "detail": str(e), # E.g. "[errno -42] <some error message>"
1868 "component": "rbd", # this can be null to represent a global error code
1869 "code": "3", # Or a error name, e.g. "code": "some_error_key"
1870 }
1871
1872
1873 In case, the API Endpoints uses @ViewCache to temporarily cache results,
1874 the error looks like so::
1875
1876 Status Code 400
1877 {
1878 "detail": str(e), # E.g. "[errno -42] <some error message>"
1879 "component": "rbd", # this can be null to represent a global error code
1880 "code": "3", # Or a error name, e.g. "code": "some_error_key"
1881 'status': 3, # Indicating the @ViewCache error status
1882 }
1883
1884 In case, the API Endpoints uses a task the error looks like so::
1885
1886 Status Code 400
1887 {
1888 "detail": str(e), # E.g. "[errno -42] <some error message>"
1889 "component": "rbd", # this can be null to represent a global error code
1890 "code": "3", # Or a error name, e.g. "code": "some_error_key"
1891 "task": { # Information about the task itself
1892 "name": "taskname",
1893 "metadata": {...}
1894 }
1895 }
1896
1897
1898 Our WebUI should show errors generated by the API to the user. Especially
1899 field-related errors in wizards and dialogs or show non-intrusive notifications.
1900
1901 Handling exceptions in Python should be an exception. In general, we
1902 should have few exception handlers in our project. Per default, propagate
1903 errors to the API, as it will take care of all exceptions anyway. In general,
1904 log the exception by adding ``logger.exception()`` with a description to the
1905 handler.
1906
1907 We need to distinguish between user errors from internal errors and
1908 programming errors. Using different exception types will ease the
1909 task for the API layer and for the user interface:
1910
1911 Standard Python errors, like ``SystemError``, ``ValueError`` or ``KeyError``
1912 will end up as internal server errors in the API.
1913
1914 In general, do not ``return`` error responses in the REST API. They will be
1915 returned by the error handler. Instead, raise the appropriate exception.
1916
1917 Plug-ins
1918 ~~~~~~~~
1919
1920 New functionality can be provided by means of a plug-in architecture. Among the
1921 benefits this approach brings in, loosely coupled development is one of the most
1922 notable. As the Ceph Dashboard grows in feature richness, its code-base becomes
1923 more and more complex. The hook-based nature of a plug-in architecture allows to
1924 extend functionality in a controlled manner, and isolate the scope of the
1925 changes.
1926
1927 Ceph Dashboard relies on `Pluggy <https://pluggy.readthedocs.io>`_ to provide
1928 for plug-ing support. On top of pluggy, an interface-based approach has been
1929 implemented, with some safety checks (method override and abstract method
1930 checks).
1931
1932 In order to create a new plugin, the following steps are required:
1933
1934 #. Add a new file under ``src/pybind/mgr/dashboard/plugins``.
1935 #. Import the ``PLUGIN_MANAGER`` instance and the ``Interfaces``.
1936 #. Create a class extending the desired interfaces. The plug-in library will
1937 check if all the methods of the interfaces have been properly overridden.
1938 #. Register the plugin in the ``PLUGIN_MANAGER`` instance.
1939 #. Import the plug-in from within the Ceph Dashboard ``module.py`` (currently no
1940 dynamic loading is implemented).
1941
1942 The available Mixins (helpers) are:
1943
1944 - ``CanMgr``: provides the plug-in with access to the ``mgr`` instance under ``self.mgr``.
1945
1946 The available Interfaces are:
1947
1948 - ``Initializable``: requires overriding ``init()`` hook. This method is run at
1949 the very beginning of the dashboard module, right after all imports have been
1950 performed.
1951 - ``Setupable``: requires overriding ``setup()`` hook. This method is run in the
1952 Ceph Dashboard ``serve()`` method, right after CherryPy has been configured,
1953 but before it is started. It's a placeholder for the plug-in initialization
1954 logic.
1955 - ``HasOptions``: requires overriding ``get_options()`` hook by returning a list
1956 of ``Options()``. The options returned here are added to the
1957 ``MODULE_OPTIONS``.
1958 - ``HasCommands``: requires overriding ``register_commands()`` hook by defining
1959 the commands the plug-in can handle and decorating them with ``@CLICommand``.
1960 The commands can be optionally returned, so that they can be invoked
1961 externally (which makes unit testing easier).
1962 - ``HasControllers``: requires overriding ``get_controllers()`` hook by defining
1963 and returning the controllers as usual.
1964 - ``FilterRequest.BeforeHandler``: requires overriding
1965 ``filter_request_before_handler()`` hook. This method receives a
1966 ``cherrypy.request`` object for processing. A usual implementation of this
1967 method will allow some requests to pass or will raise a ``cherrypy.HTTPError``
1968 based on the ``request`` metadata and other conditions.
1969
1970 New interfaces and hooks should be added as soon as they are required to
1971 implement new functionality. The above list only comprises the hooks needed for
1972 the existing plugins.
1973
1974 A sample plugin implementation would look like this:
1975
1976 .. code-block:: python
1977
1978 # src/pybind/mgr/dashboard/plugins/mute.py
1979
1980 from . import PLUGIN_MANAGER as PM
1981 from . import interfaces as I
1982
1983 from mgr_module import CLICommand, Option
1984 import cherrypy
1985
1986 @PM.add_plugin
1987 class Mute(I.CanMgr, I.Setupable, I.HasOptions, I.HasCommands,
1988 I.FilterRequest.BeforeHandler, I.HasControllers):
1989 @PM.add_hook
1990 def get_options(self):
1991 return [Option('mute', default=False, type='bool')]
1992
1993 @PM.add_hook
1994 def setup(self):
1995 self.mute = self.mgr.get_module_option('mute')
1996
1997 @PM.add_hook
1998 def register_commands(self):
1999 @CLICommand("dashboard mute")
2000 def _(mgr):
2001 self.mute = True
2002 self.mgr.set_module_option('mute', True)
2003 return 0
2004
2005 @PM.add_hook
2006 def filter_request_before_handler(self, request):
2007 if self.mute:
2008 raise cherrypy.HTTPError(500, "I'm muted :-x")
2009
2010 @PM.add_hook
2011 def get_controllers(self):
2012 from ..controllers import ApiController, RESTController
2013
2014 @ApiController('/mute')
2015 class MuteController(RESTController):
2016 def get(_):
2017 return self.mute
2018
2019 return [MuteController]
2020
2021
2022 Additionally, a helper for creating plugins ``SimplePlugin`` is provided. It
2023 facilitates the basic tasks (Options, Commands, and common Mixins). The previous
2024 plugin could be rewritten like this:
2025
2026 .. code-block:: python
2027
2028 from . import PLUGIN_MANAGER as PM
2029 from . import interfaces as I
2030 from .plugin import SimplePlugin as SP
2031
2032 import cherrypy
2033
2034 @PM.add_plugin
2035 class Mute(SP, I.Setupable, I.FilterRequest.BeforeHandler, I.HasControllers):
2036 OPTIONS = [
2037 SP.Option('mute', default=False, type='bool')
2038 ]
2039
2040 def shut_up(self):
2041 self.set_option('mute', True)
2042 self.mute = True
2043 return 0
2044
2045 COMMANDS = [
2046 SP.Command("dashboard mute", handler=shut_up)
2047 ]
2048
2049 @PM.add_hook
2050 def setup(self):
2051 self.mute = self.get_option('mute')
2052
2053 @PM.add_hook
2054 def filter_request_before_handler(self, request):
2055 if self.mute:
2056 raise cherrypy.HTTPError(500, "I'm muted :-x")
2057
2058 @PM.add_hook
2059 def get_controllers(self):
2060 from ..controllers import ApiController, RESTController
2061
2062 @ApiController('/mute')
2063 class MuteController(RESTController):
2064 def get(_):
2065 return self.mute
2066
2067 return [MuteController]
2068
2069