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