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