From e22571b894c9c693b2ad97d33fe5eaf09637a3fa Mon Sep 17 00:00:00 2001 From: GHA Date: Wed, 23 Oct 2024 00:24:20 +0000 Subject: [PATCH] update f573c0519883ad90270ac7a56955be70cf115808 --- .buildinfo | 4 + .nojekyll | 0 _images/summary.png | Bin 0 -> 56562 bytes _sources/api.rst.txt | 98 + _sources/index.md.txt | 18 + _sources/introduction.md.txt | 16 + _sources/rdfm_android_device_client.md.txt | 172 + _sources/rdfm_artifact.md.txt | 150 + _sources/rdfm_frontend.md.txt | 81 + _sources/rdfm_linux_device_client.md.txt | 190 ++ _sources/rdfm_manager.md.txt | 201 ++ _sources/rdfm_mcumgr_device_client.md.txt | 475 +++ _sources/rdfm_mgmt_server.md.txt | 438 +++ _sources/rdfm_ota_manual.md.txt | 113 + _sources/server_operation.md.txt | 111 + _sources/system_overview.md.txt | 44 + .../0053ba6958e79f26751eabb555bd73d0.woff2 | Bin 0 -> 4728 bytes .../029e176ad602329b4434892101db9cf3.woff2 | Bin 0 -> 6044 bytes .../07ff82964967feebb9c96288e0e0df05.woff2 | Bin 0 -> 13588 bytes .../0948409a22b5979aa7e1ec20da9e61f1.woff2 | Bin 0 -> 5604 bytes .../0a0ad0eae50e549ecd713b9ad417f1a1.woff2 | Bin 0 -> 4888 bytes .../0b68e8634c96265eb32a0c769416b5b0.woff2 | Bin 0 -> 5928 bytes .../0d1b73eee266eabb2cff35dfa4ce25a3.woff2 | Bin 0 -> 16812 bytes .../0e1f73c6737cdf273efb4b79504e4c0a.woff2 | Bin 0 -> 13076 bytes .../0e326670106c8eb6a11a8c30734ecfc8.ttf | Bin 0 -> 23124 bytes .../0ec3cc19652785204ea2e322330f0f1b.woff2 | Bin 0 -> 16164 bytes .../0f303f31706d39866cced9dcc17b61fb.woff2 | Bin 0 -> 15764 bytes .../101522bafe9c61c68698ecc784607772.woff2 | Bin 0 -> 9712 bytes .../10b31f4cad9ea78d43449886bfbb88ac.woff2 | Bin 0 -> 11804 bytes .../1181a8e619707033241139715eca64c6.woff2 | Bin 0 -> 9672 bytes .../122802d03aed4bf8cd6a03997a97aca4.woff2 | Bin 0 -> 6020 bytes .../1383417807f7965daaf94e7c497dcddb.woff2 | Bin 0 -> 7704 bytes .../144860ed1e48e186f08997e6388a9c3f.woff2 | Bin 0 -> 1512 bytes .../1488146d8b2e9859d6c90e6c2b48f7ef.woff2 | Bin 0 -> 6340 bytes .../1512b579343c6b61c7523cdd838d8328.ttf | Bin 0 -> 23416 bytes .../1c9cc76fd52238330f0aabac35acd2ca.woff2 | Bin 0 -> 6936 bytes .../1f1481679a64a39f3427547aa1b13f0f.woff2 | Bin 0 -> 5032 bytes .../2096d27efc16cbdd79183bf295c8ebde.ttf | Bin 0 -> 21352 bytes .../20dc200cc43ab904876fb0c1697ebe39.woff2 | Bin 0 -> 1480 bytes .../214adfc289a2f2af8b0008c59ed0c7f2.woff2 | Bin 0 -> 4652 bytes .../21953b998bab09c1f60c599caee56378.woff2 | Bin 0 -> 7696 bytes .../22aadc77cafa07b2db9ed560d0320616.woff2 | Bin 0 -> 13200 bytes .../2325b97b584755067ea4f7f56ee05430.woff2 | Bin 0 -> 8348 bytes .../2550c2e2d8495c3ed2d4d52f824374f1.woff2 | Bin 0 -> 7040 bytes .../255cf41e0317d95e3992683a76ef28a8.woff2 | Bin 0 -> 4976 bytes .../25c52b9af13f0d1b10719f5289e8c803.woff2 | Bin 0 -> 7476 bytes .../2781e9e7c3f369b8fc7965e679b17b60.woff2 | Bin 0 -> 11756 bytes .../28e6b81b1bc1964707edd4179e4268f5.ttf | Bin 0 -> 23416 bytes .../2a8c422bef4a7099e99dbf0e61ed5e49.woff2 | Bin 0 -> 7460 bytes .../2aadfad5aee7ceeaf4eb0924efabe5b4.ttf | Bin 0 -> 21772 bytes .../2c0f74be498d2da814c0a84dd6833f70.woff2 | Bin 0 -> 15092 bytes .../2e10480d4154762bc7c8fbb40877e104.woff2 | Bin 0 -> 5928 bytes .../2ea7a97b7c976b121112a088eb398561.woff2 | Bin 0 -> 7700 bytes .../2f5c32f094829c0278bce28fe2bbe074.ttf | Bin 0 -> 23204 bytes .../2f7c3c315334a99574ee4ceb21af654d.woff2 | Bin 0 -> 7544 bytes .../302b0425bf5ea66f37a822a61d723adc.ttf | Bin 0 -> 25112 bytes .../3177dacffeac1eb4102852811ae4a2c7.woff2 | Bin 0 -> 6236 bytes .../3254c528e2ab56454a9f22191035c5fe.ttf | Bin 0 -> 21356 bytes .../32c8a74ac0816253d69a7cc68a60986d.woff2 | Bin 0 -> 12764 bytes .../33c5d27ca0eaeb12ebe728ae2fc7106d.woff2 | Bin 0 -> 15360 bytes .../36e39c6463ae1c71c71e69c05e593e1b.woff2 | Bin 0 -> 4588 bytes .../3728fbdd191d75bad5b83a838dfe2fc1.woff2 | Bin 0 -> 9840 bytes .../38f3ee1f96b758f95672c632d8759594.ttf | Bin 0 -> 23172 bytes .../392ff374142585f7b886ee1fe66e686e.woff2 | Bin 0 -> 6560 bytes .../3a38c967413f7bce36d3baefc321aade.woff2 | Bin 0 -> 5468 bytes .../3c23eb02de6b34e30f18cfb7167abd81.woff2 | Bin 0 -> 11872 bytes .../3c505383d37d2078648e37868bbd1fad.woff2 | Bin 0 -> 14684 bytes .../3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2 | Bin 0 -> 17552 bytes .../3f1918538864f9681d47a4538d48289c.woff2 | Bin 0 -> 5876 bytes .../4039566f251699c4b421ed1a38a59b24.woff2 | Bin 0 -> 4688 bytes .../4207cbc8cb7bc2cbd0bcce565298cbbc.woff2 | Bin 0 -> 9768 bytes .../43358c04243de546caddd0898dbf0757.woff2 | Bin 0 -> 14004 bytes .../435e4b7f9f250d9d9243d4754799fc96.woff2 | Bin 0 -> 15000 bytes .../437939342255944b82a49f916404c5fc.woff2 | Bin 0 -> 6516 bytes .../455c2c1af0a2bf20047a1864d7d7c174.woff2 | Bin 0 -> 7120 bytes .../47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2 | Bin 0 -> 9576 bytes .../495d38d4b9741e8aa4204002414069e2.woff2 | Bin 0 -> 9628 bytes .../4c815fdc869f885520f7c8eae6730edf.woff2 | Bin 0 -> 16608 bytes .../4ec57f2a80b91090971b83970230ca09.woff2 | Bin 0 -> 5548 bytes .../4f17f22fc6bff4f3333ccf7ed7126e6d.woff2 | Bin 0 -> 1464 bytes .../4f93c2808e3b69e525c118074e5de31f.woff2 | Bin 0 -> 14184 bytes .../50aacf068f685be0dd903a91d5bab7d8.woff2 | Bin 0 -> 1508 bytes .../51f3f41805329fb8341beb56ded833ea.woff2 | Bin 0 -> 5468 bytes .../52f28cb4d065b4adfa78df4f9559c639.woff2 | Bin 0 -> 7392 bytes .../555ceea3a65ffbbecf8b7e6d04966c7f.woff2 | Bin 0 -> 14128 bytes .../5989ef3a21d7f252337ab3326f78bde7.woff2 | Bin 0 -> 4780 bytes .../5b6377da4c959db6d4b22738a27f1bee.woff2 | Bin 0 -> 1432 bytes .../5ce47d5195e59af38114d0b70217baf2.woff2 | Bin 0 -> 14024 bytes .../5d7ff31ac7bf945e8d61878f8a941239.woff2 | Bin 0 -> 1460 bytes .../5dc0e4b14e903ba7f45c581df7402b3f.woff2 | Bin 0 -> 14072 bytes .../60eb682678bbea5e8ad71f66f2f65536.woff2 | Bin 0 -> 10284 bytes .../63111d307c01b52ffccf7b0319cb7917.woff2 | Bin 0 -> 1540 bytes .../638764dc2513deb09c55fc025f6dd36c.woff2 | Bin 0 -> 9180 bytes .../63f4b74ebf127dbeb033126ea988f54e.woff2 | Bin 0 -> 7520 bytes .../64a6b4e954cf84685cbf8de77eb47344.woff2 | Bin 0 -> 12572 bytes .../661d4b208656c006e7aab58acf778485.woff2 | Bin 0 -> 17336 bytes .../6725a7e91680edd1cdc9ed5c26ac05fd.woff2 | Bin 0 -> 14224 bytes .../6a84eeee6a25e7c9a8a03191007a6720.woff2 | Bin 0 -> 9644 bytes .../6ac1ee292434fac2313c42b0dfb7897c.ttf | Bin 0 -> 23488 bytes .../6ad3f6bbe6220cc476a0d3c731d3fb04.ttf | Bin 0 -> 23672 bytes .../6be97ca17228a69c406231d89c003194.woff2 | Bin 0 -> 17032 bytes .../6de03a64aa8100032abc6e836b3ed803.ttf | Bin 0 -> 23520 bytes .../6deb20301c65a96db17c433ad0cf8158.woff2 | Bin 0 -> 10640 bytes .../6f8d857c5a8545e67de6b60aa0fe5c33.woff2 | Bin 0 -> 12740 bytes .../713780d8b30bda5583052ea847cdcb4f.woff2 | Bin 0 -> 7016 bytes .../71e06579279fba7436d58a1c49288909.ttf | Bin 0 -> 25364 bytes .../765bd4a97597a4d7781193793477a6cd.ttf | Bin 0 -> 25224 bytes .../76945c7494c20515bb45d1dedab8f706.woff2 | Bin 0 -> 10428 bytes .../76da333ab59c6d625cabfb0768f82b4a.woff2 | Bin 0 -> 1464 bytes .../770518db51bed1e082feecc532cfcbf8.woff2 | Bin 0 -> 7404 bytes .../77b24796a3d4ab521f66765651875338.woff2 | Bin 0 -> 5560 bytes .../77ff81100e5a1db3d925f713660700ad.woff2 | Bin 0 -> 4748 bytes .../78a9265759e7b861a1639a36f4c01d04.woff2 | Bin 0 -> 13860 bytes .../7af61b2367eba2b1852e837c46a75696.woff2 | Bin 0 -> 12848 bytes .../7b63598dcc2a26583b82594bd0e36d5b.woff2 | Bin 0 -> 5760 bytes .../7b8c2179b6b778308d2ff39bdb82e926.woff2 | Bin 0 -> 6012 bytes .../7e262106f82cc52663e403f5b73795bb.woff2 | Bin 0 -> 15752 bytes .../7f1c829b0c90fd664a03bb714a74f7d3.woff2 | Bin 0 -> 11800 bytes .../7fa86b886bee5d6ab420a8e89b9f3052.ttf | Bin 0 -> 23724 bytes .../8007dfe835cfb201b8caaa9651098588.woff2 | Bin 0 -> 1428 bytes .../83614c36460a4a9734968789cb535de7.woff2 | Bin 0 -> 5020 bytes .../84e959dd07f302392f0ffd86f87db888.ttf | Bin 0 -> 21452 bytes .../85a41b80c5fdc14e3dc48636a30d87dd.woff2 | Bin 0 -> 5884 bytes .../870e5928dd14fcfe0ce9386107666774.woff2 | Bin 0 -> 6040 bytes .../8898c4b754d5d96c1a5e1b1d54100554.woff2 | Bin 0 -> 6404 bytes .../89b4f174a5a728d2d8c85b87990c9ab4.ttf | Bin 0 -> 23420 bytes .../8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2 | Bin 0 -> 7476 bytes .../8aa562790559d61dd5178a88a296d70f.ttf | Bin 0 -> 23252 bytes .../8c3798e37724f71bc0c63c44a5307413.woff2 | Bin 0 -> 7012 bytes .../8c49ed8b472d38d3985ec9bbbccea601.ttf | Bin 0 -> 21560 bytes .../8e48cf20cf9f9e5feb7197c79028132b.woff2 | Bin 0 -> 14688 bytes .../9095d663e4d450059bcc2260bb75cd62.woff2 | Bin 0 -> 4696 bytes .../90ebb29b5cffa197b184773983ba7e91.woff2 | Bin 0 -> 13188 bytes .../93b6c99d936df38895a0d95e3ffea2fd.woff2 | Bin 0 -> 9556 bytes .../9582ced8a675bf267cc7ac392a86413e.woff2 | Bin 0 -> 12704 bytes .../99be4d68845d66c27c7f7d3a48687b66.woff2 | Bin 0 -> 7616 bytes .../99cf36e763be9cce7b4c59b91841af58.woff2 | Bin 0 -> 8280 bytes .../9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2 | Bin 0 -> 4896 bytes .../9bcbc88b33b2efc2aee821b831499f1c.woff2 | Bin 0 -> 8320 bytes .../9c9be791a58af8a04c611ca1d13f51c6.woff2 | Bin 0 -> 5088 bytes .../9fdb12ceee3a402d3a54afe354552459.woff2 | Bin 0 -> 9700 bytes .../a6933e678530b263486fa7b185a449ca.woff2 | Bin 0 -> 10292 bytes .../a6caf7b9888eb0c382948c1ca5e8bebb.woff2 | Bin 0 -> 16676 bytes .../a70ff2592da5e3453943f727633aff54.woff2 | Bin 0 -> 6344 bytes .../aa28d99c7db60ad23f96a5c317615c42.woff2 | Bin 0 -> 13696 bytes .../aab05142e0e2dadf7df633e061e612ad.woff2 | Bin 0 -> 14136 bytes .../ab03beb9091fa15ce4e783199e076bc6.woff2 | Bin 0 -> 8300 bytes .../ac848474638236e67a64bc654fb18de0.ttf | Bin 0 -> 21464 bytes .../acaac043ca238f0e56e61864456777fa.woff2 | Bin 0 -> 12620 bytes .../aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2 | Bin 0 -> 14968 bytes .../b019538234514166ec7665359d097403.woff2 | Bin 0 -> 15920 bytes .../b076e86301cbee8c5c9aef51863a9c0a.woff2 | Bin 0 -> 11796 bytes .../b19ac4e57f2a56639eebd1c35319e5a7.woff2 | Bin 0 -> 17060 bytes .../b4d3c40a77fd9e35a881a79077957055.woff2 | Bin 0 -> 14172 bytes .../b4e42731e8d667ae87c3450c345754ae.woff2 | Bin 0 -> 5996 bytes .../b57a5ada789f195d5d42f4073a6cf313.woff2 | Bin 0 -> 9960 bytes .../b5b4146d87e5d22d0a4e0d04f3ee5626.woff2 | Bin 0 -> 1512 bytes .../b7ef2cd1159a8cbfd271ff2abe07f237.woff2 | Bin 0 -> 15344 bytes .../b93199bb6f964f190f4da04ecdbaf5a4.woff2 | Bin 0 -> 15076 bytes .../bb8007225d94a099cddbade7ea904667.woff2 | Bin 0 -> 17508 bytes .../bc67bba106323289ea3eda0826de1912.ttf | Bin 0 -> 25404 bytes .../bcd47c2f3649cfcaa86a08fb741255d6.woff2 | Bin 0 -> 13944 bytes .../bd0efe13f0d9d591b337ddc7f289f494.woff2 | Bin 0 -> 15204 bytes .../bd51fb0ca67e64c809ffcf7e1370f969.woff2 | Bin 0 -> 8420 bytes .../bdbb6b52604c2451fdcba9cdfd44f4e1.woff2 | Bin 0 -> 5972 bytes .../bf2ad3287f13eb7076cccb516ec2986f.ttf | Bin 0 -> 23456 bytes .../bfd1a0c9c783e84595589f33e1828a57.woff2 | Bin 0 -> 12832 bytes .../c13b34dd5b6a35b309944b61c91b2ace.woff2 | Bin 0 -> 8408 bytes .../c22066c14662d6c80415ae04c5dd9d51.woff2 | Bin 0 -> 14780 bytes .../c28a41f656599f6694528b5463c6a445.woff2 | Bin 0 -> 12980 bytes .../c6dc61b627bbc5af9130518297bd4f17.ttf | Bin 0 -> 23720 bytes .../c8a9fd4eab4e83382cc66fde70911b41.woff2 | Bin 0 -> 10076 bytes .../ca7eea0cf248d6e8442c01074765bd33.woff2 | Bin 0 -> 5388 bytes .../cadfb311297a9362b07fab73934b432a.ttf | Bin 0 -> 25380 bytes .../cbfd26d5bcf084ee407a0b2b7599e84b.woff2 | Bin 0 -> 9524 bytes .../ccdebed88064e470c15f37c432922e57.woff2 | Bin 0 -> 16024 bytes .../cce2217cc8323fe49789adefb3596291.woff2 | Bin 0 -> 12980 bytes .../cd3d1f17e048e2116f438bd7157baccf.woff2 | Bin 0 -> 9504 bytes .../d07f561ba87d93460742b060727d9e0d.woff2 | Bin 0 -> 10276 bytes .../d368cf5bed7856dbafa2af36b51acb9c.woff2 | Bin 0 -> 1484 bytes .../d422317033deb87342a5e56c7be67458.ttf | Bin 0 -> 25444 bytes .../d6f9cdf1a40893111566fcdee3bbe5a9.woff2 | Bin 0 -> 14060 bytes .../d98f35e926c11f3d5c0c8e3205d43907.ttf | Bin 0 -> 25360 bytes .../d9e6a498dac7e9e91f6e0b4f8930eba0.woff2 | Bin 0 -> 10532 bytes .../da6cd48e6dad1888fccc91735e7522f7.woff2 | Bin 0 -> 10652 bytes .../daf12b5f1889502004bba85ad71f9fa4.woff2 | Bin 0 -> 7540 bytes .../daf51ab540602b2d0b87646621637bac.woff2 | Bin 0 -> 7112 bytes .../db0424fb67fb52e7e538490240cc7fb9.woff2 | Bin 0 -> 17368 bytes .../dc25cbf4baaf778bd8ae78fbc0e79479.woff2 | Bin 0 -> 14052 bytes .../dd719f1662079ce6a61260f9af972379.woff2 | Bin 0 -> 9876 bytes .../de018865c95896bb57265fc97c48ebd7.woff2 | Bin 0 -> 8108 bytes .../e33716333704ab19fdf9989e072ad49a.woff2 | Bin 0 -> 5928 bytes .../e56cc9fb5272752b78f144b4be43175d.woff2 | Bin 0 -> 7608 bytes .../e704ef18719c08839bc99a32437ef0f8.woff2 | Bin 0 -> 16700 bytes .../e99627cd27de169d23ece4573006af2a.woff2 | Bin 0 -> 15304 bytes .../ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2 | Bin 0 -> 1500 bytes .../f154d62b4879af7a22895af7a4ef03f0.woff2 | Bin 0 -> 10276 bytes .../f17ee050ada0453f3bd07bc466c2dde2.woff2 | Bin 0 -> 10564 bytes .../f265cee675c0e5b2d6ab263d0edcc754.woff2 | Bin 0 -> 14856 bytes .../f2f69e8cd15fdd15a4244c95ec8a8514.woff2 | Bin 0 -> 10344 bytes .../f534242dea2255c25b9d05c2371986e3.woff2 | Bin 0 -> 6380 bytes .../f53f3b5a15d717b6d21d7885285e90ed.woff2 | Bin 0 -> 12864 bytes .../f55dac651a40fce74a5cf5728d9f8ffc.woff2 | Bin 0 -> 9784 bytes .../f5aebdfea35d1e7656ef4acc5db1f243.woff2 | Bin 0 -> 15860 bytes .../f5f971e9640a9eb86ef553a7e7e999c7.woff2 | Bin 0 -> 6048 bytes .../f6734f8177112c0839b961f96d813fcb.woff2 | Bin 0 -> 15744 bytes .../f75911313e1c7802c23345ab57e754d8.woff2 | Bin 0 -> 15740 bytes .../fb17f56622e45dd4ecee00bb5c63cd2b.woff2 | Bin 0 -> 4580 bytes .../fb1aaa90783b8cb9375265abeb91b153.woff2 | Bin 0 -> 15336 bytes .../fc66f942651a9fe1a598770d3d896529.woff2 | Bin 0 -> 11824 bytes _static/language_data.js | 199 ++ ..._immaterial_theme.1b5b7a2d5891aec19.min.js | 27 + ...immaterial_theme.af531f03affe68837.min.css | 4 + _static/white.svg | 54 + api.html | 3005 +++++++++++++++++ genindex.html | 495 +++ http-routingtable.html | 502 +++ index.html | 608 ++++ introduction.html | 546 +++ objects.inv | Bin 0 -> 907 bytes rdfm-docs.pdf | Bin 0 -> 424805 bytes rdfm_android_device_client.html | 830 +++++ rdfm_artifact.html | 767 +++++ rdfm_frontend.html | 652 ++++ rdfm_linux_device_client.html | 923 +++++ rdfm_manager.html | 804 +++++ rdfm_mcumgr_device_client.html | 1201 +++++++ rdfm_mgmt_server.html | 1097 ++++++ rdfm_ota_manual.html | 733 ++++ searchindex.js | 1 + server_operation.html | 699 ++++ system_overview.html | 600 ++++ 232 files changed, 15858 insertions(+) create mode 100644 .buildinfo create mode 100644 .nojekyll create mode 100644 _images/summary.png create mode 100644 _sources/api.rst.txt create mode 100644 _sources/index.md.txt create mode 100644 _sources/introduction.md.txt create mode 100644 _sources/rdfm_android_device_client.md.txt create mode 100644 _sources/rdfm_artifact.md.txt create mode 100644 _sources/rdfm_frontend.md.txt create mode 100644 _sources/rdfm_linux_device_client.md.txt create mode 100644 _sources/rdfm_manager.md.txt create mode 100644 _sources/rdfm_mcumgr_device_client.md.txt create mode 100644 _sources/rdfm_mgmt_server.md.txt create mode 100644 _sources/rdfm_ota_manual.md.txt create mode 100644 _sources/server_operation.md.txt create mode 100644 _sources/system_overview.md.txt create mode 100644 _static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 create mode 100644 _static/fonts/029e176ad602329b4434892101db9cf3.woff2 create mode 100644 _static/fonts/07ff82964967feebb9c96288e0e0df05.woff2 create mode 100644 _static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 create mode 100644 _static/fonts/0a0ad0eae50e549ecd713b9ad417f1a1.woff2 create mode 100644 _static/fonts/0b68e8634c96265eb32a0c769416b5b0.woff2 create mode 100644 _static/fonts/0d1b73eee266eabb2cff35dfa4ce25a3.woff2 create mode 100644 _static/fonts/0e1f73c6737cdf273efb4b79504e4c0a.woff2 create mode 100644 _static/fonts/0e326670106c8eb6a11a8c30734ecfc8.ttf create mode 100644 _static/fonts/0ec3cc19652785204ea2e322330f0f1b.woff2 create mode 100644 _static/fonts/0f303f31706d39866cced9dcc17b61fb.woff2 create mode 100644 _static/fonts/101522bafe9c61c68698ecc784607772.woff2 create mode 100644 _static/fonts/10b31f4cad9ea78d43449886bfbb88ac.woff2 create mode 100644 _static/fonts/1181a8e619707033241139715eca64c6.woff2 create mode 100644 _static/fonts/122802d03aed4bf8cd6a03997a97aca4.woff2 create mode 100644 _static/fonts/1383417807f7965daaf94e7c497dcddb.woff2 create mode 100644 _static/fonts/144860ed1e48e186f08997e6388a9c3f.woff2 create mode 100644 _static/fonts/1488146d8b2e9859d6c90e6c2b48f7ef.woff2 create mode 100644 _static/fonts/1512b579343c6b61c7523cdd838d8328.ttf create mode 100644 _static/fonts/1c9cc76fd52238330f0aabac35acd2ca.woff2 create mode 100644 _static/fonts/1f1481679a64a39f3427547aa1b13f0f.woff2 create mode 100644 _static/fonts/2096d27efc16cbdd79183bf295c8ebde.ttf create mode 100644 _static/fonts/20dc200cc43ab904876fb0c1697ebe39.woff2 create mode 100644 _static/fonts/214adfc289a2f2af8b0008c59ed0c7f2.woff2 create mode 100644 _static/fonts/21953b998bab09c1f60c599caee56378.woff2 create mode 100644 _static/fonts/22aadc77cafa07b2db9ed560d0320616.woff2 create mode 100644 _static/fonts/2325b97b584755067ea4f7f56ee05430.woff2 create mode 100644 _static/fonts/2550c2e2d8495c3ed2d4d52f824374f1.woff2 create mode 100644 _static/fonts/255cf41e0317d95e3992683a76ef28a8.woff2 create mode 100644 _static/fonts/25c52b9af13f0d1b10719f5289e8c803.woff2 create mode 100644 _static/fonts/2781e9e7c3f369b8fc7965e679b17b60.woff2 create mode 100644 _static/fonts/28e6b81b1bc1964707edd4179e4268f5.ttf create mode 100644 _static/fonts/2a8c422bef4a7099e99dbf0e61ed5e49.woff2 create mode 100644 _static/fonts/2aadfad5aee7ceeaf4eb0924efabe5b4.ttf create mode 100644 _static/fonts/2c0f74be498d2da814c0a84dd6833f70.woff2 create mode 100644 _static/fonts/2e10480d4154762bc7c8fbb40877e104.woff2 create mode 100644 _static/fonts/2ea7a97b7c976b121112a088eb398561.woff2 create mode 100644 _static/fonts/2f5c32f094829c0278bce28fe2bbe074.ttf create mode 100644 _static/fonts/2f7c3c315334a99574ee4ceb21af654d.woff2 create mode 100644 _static/fonts/302b0425bf5ea66f37a822a61d723adc.ttf create mode 100644 _static/fonts/3177dacffeac1eb4102852811ae4a2c7.woff2 create mode 100644 _static/fonts/3254c528e2ab56454a9f22191035c5fe.ttf create mode 100644 _static/fonts/32c8a74ac0816253d69a7cc68a60986d.woff2 create mode 100644 _static/fonts/33c5d27ca0eaeb12ebe728ae2fc7106d.woff2 create mode 100644 _static/fonts/36e39c6463ae1c71c71e69c05e593e1b.woff2 create mode 100644 _static/fonts/3728fbdd191d75bad5b83a838dfe2fc1.woff2 create mode 100644 _static/fonts/38f3ee1f96b758f95672c632d8759594.ttf create mode 100644 _static/fonts/392ff374142585f7b886ee1fe66e686e.woff2 create mode 100644 _static/fonts/3a38c967413f7bce36d3baefc321aade.woff2 create mode 100644 _static/fonts/3c23eb02de6b34e30f18cfb7167abd81.woff2 create mode 100644 _static/fonts/3c505383d37d2078648e37868bbd1fad.woff2 create mode 100644 _static/fonts/3cf78ad3bcd1324e10a4acdc34bfc4a1.woff2 create mode 100644 _static/fonts/3f1918538864f9681d47a4538d48289c.woff2 create mode 100644 _static/fonts/4039566f251699c4b421ed1a38a59b24.woff2 create mode 100644 _static/fonts/4207cbc8cb7bc2cbd0bcce565298cbbc.woff2 create mode 100644 _static/fonts/43358c04243de546caddd0898dbf0757.woff2 create mode 100644 _static/fonts/435e4b7f9f250d9d9243d4754799fc96.woff2 create mode 100644 _static/fonts/437939342255944b82a49f916404c5fc.woff2 create mode 100644 _static/fonts/455c2c1af0a2bf20047a1864d7d7c174.woff2 create mode 100644 _static/fonts/47aa3bfad6cb9e2d63abdd58f4e6ce4f.woff2 create mode 100644 _static/fonts/495d38d4b9741e8aa4204002414069e2.woff2 create mode 100644 _static/fonts/4c815fdc869f885520f7c8eae6730edf.woff2 create mode 100644 _static/fonts/4ec57f2a80b91090971b83970230ca09.woff2 create mode 100644 _static/fonts/4f17f22fc6bff4f3333ccf7ed7126e6d.woff2 create mode 100644 _static/fonts/4f93c2808e3b69e525c118074e5de31f.woff2 create mode 100644 _static/fonts/50aacf068f685be0dd903a91d5bab7d8.woff2 create mode 100644 _static/fonts/51f3f41805329fb8341beb56ded833ea.woff2 create mode 100644 _static/fonts/52f28cb4d065b4adfa78df4f9559c639.woff2 create mode 100644 _static/fonts/555ceea3a65ffbbecf8b7e6d04966c7f.woff2 create mode 100644 _static/fonts/5989ef3a21d7f252337ab3326f78bde7.woff2 create mode 100644 _static/fonts/5b6377da4c959db6d4b22738a27f1bee.woff2 create mode 100644 _static/fonts/5ce47d5195e59af38114d0b70217baf2.woff2 create mode 100644 _static/fonts/5d7ff31ac7bf945e8d61878f8a941239.woff2 create mode 100644 _static/fonts/5dc0e4b14e903ba7f45c581df7402b3f.woff2 create mode 100644 _static/fonts/60eb682678bbea5e8ad71f66f2f65536.woff2 create mode 100644 _static/fonts/63111d307c01b52ffccf7b0319cb7917.woff2 create mode 100644 _static/fonts/638764dc2513deb09c55fc025f6dd36c.woff2 create mode 100644 _static/fonts/63f4b74ebf127dbeb033126ea988f54e.woff2 create mode 100644 _static/fonts/64a6b4e954cf84685cbf8de77eb47344.woff2 create mode 100644 _static/fonts/661d4b208656c006e7aab58acf778485.woff2 create mode 100644 _static/fonts/6725a7e91680edd1cdc9ed5c26ac05fd.woff2 create mode 100644 _static/fonts/6a84eeee6a25e7c9a8a03191007a6720.woff2 create mode 100644 _static/fonts/6ac1ee292434fac2313c42b0dfb7897c.ttf create mode 100644 _static/fonts/6ad3f6bbe6220cc476a0d3c731d3fb04.ttf create mode 100644 _static/fonts/6be97ca17228a69c406231d89c003194.woff2 create mode 100644 _static/fonts/6de03a64aa8100032abc6e836b3ed803.ttf create mode 100644 _static/fonts/6deb20301c65a96db17c433ad0cf8158.woff2 create mode 100644 _static/fonts/6f8d857c5a8545e67de6b60aa0fe5c33.woff2 create mode 100644 _static/fonts/713780d8b30bda5583052ea847cdcb4f.woff2 create mode 100644 _static/fonts/71e06579279fba7436d58a1c49288909.ttf create mode 100644 _static/fonts/765bd4a97597a4d7781193793477a6cd.ttf create mode 100644 _static/fonts/76945c7494c20515bb45d1dedab8f706.woff2 create mode 100644 _static/fonts/76da333ab59c6d625cabfb0768f82b4a.woff2 create mode 100644 _static/fonts/770518db51bed1e082feecc532cfcbf8.woff2 create mode 100644 _static/fonts/77b24796a3d4ab521f66765651875338.woff2 create mode 100644 _static/fonts/77ff81100e5a1db3d925f713660700ad.woff2 create mode 100644 _static/fonts/78a9265759e7b861a1639a36f4c01d04.woff2 create mode 100644 _static/fonts/7af61b2367eba2b1852e837c46a75696.woff2 create mode 100644 _static/fonts/7b63598dcc2a26583b82594bd0e36d5b.woff2 create mode 100644 _static/fonts/7b8c2179b6b778308d2ff39bdb82e926.woff2 create mode 100644 _static/fonts/7e262106f82cc52663e403f5b73795bb.woff2 create mode 100644 _static/fonts/7f1c829b0c90fd664a03bb714a74f7d3.woff2 create mode 100644 _static/fonts/7fa86b886bee5d6ab420a8e89b9f3052.ttf create mode 100644 _static/fonts/8007dfe835cfb201b8caaa9651098588.woff2 create mode 100644 _static/fonts/83614c36460a4a9734968789cb535de7.woff2 create mode 100644 _static/fonts/84e959dd07f302392f0ffd86f87db888.ttf create mode 100644 _static/fonts/85a41b80c5fdc14e3dc48636a30d87dd.woff2 create mode 100644 _static/fonts/870e5928dd14fcfe0ce9386107666774.woff2 create mode 100644 _static/fonts/8898c4b754d5d96c1a5e1b1d54100554.woff2 create mode 100644 _static/fonts/89b4f174a5a728d2d8c85b87990c9ab4.ttf create mode 100644 _static/fonts/8a8dca39f24b52e89e6fd6dcd8b6dd32.woff2 create mode 100644 _static/fonts/8aa562790559d61dd5178a88a296d70f.ttf create mode 100644 _static/fonts/8c3798e37724f71bc0c63c44a5307413.woff2 create mode 100644 _static/fonts/8c49ed8b472d38d3985ec9bbbccea601.ttf create mode 100644 _static/fonts/8e48cf20cf9f9e5feb7197c79028132b.woff2 create mode 100644 _static/fonts/9095d663e4d450059bcc2260bb75cd62.woff2 create mode 100644 _static/fonts/90ebb29b5cffa197b184773983ba7e91.woff2 create mode 100644 _static/fonts/93b6c99d936df38895a0d95e3ffea2fd.woff2 create mode 100644 _static/fonts/9582ced8a675bf267cc7ac392a86413e.woff2 create mode 100644 _static/fonts/99be4d68845d66c27c7f7d3a48687b66.woff2 create mode 100644 _static/fonts/99cf36e763be9cce7b4c59b91841af58.woff2 create mode 100644 _static/fonts/9a9bf2d91ebbb1b96eab8eb0b0514bcc.woff2 create mode 100644 _static/fonts/9bcbc88b33b2efc2aee821b831499f1c.woff2 create mode 100644 _static/fonts/9c9be791a58af8a04c611ca1d13f51c6.woff2 create mode 100644 _static/fonts/9fdb12ceee3a402d3a54afe354552459.woff2 create mode 100644 _static/fonts/a6933e678530b263486fa7b185a449ca.woff2 create mode 100644 _static/fonts/a6caf7b9888eb0c382948c1ca5e8bebb.woff2 create mode 100644 _static/fonts/a70ff2592da5e3453943f727633aff54.woff2 create mode 100644 _static/fonts/aa28d99c7db60ad23f96a5c317615c42.woff2 create mode 100644 _static/fonts/aab05142e0e2dadf7df633e061e612ad.woff2 create mode 100644 _static/fonts/ab03beb9091fa15ce4e783199e076bc6.woff2 create mode 100644 _static/fonts/ac848474638236e67a64bc654fb18de0.ttf create mode 100644 _static/fonts/acaac043ca238f0e56e61864456777fa.woff2 create mode 100644 _static/fonts/aeed0e51b0bac7c89e5c7e6cf086d7e0.woff2 create mode 100644 _static/fonts/b019538234514166ec7665359d097403.woff2 create mode 100644 _static/fonts/b076e86301cbee8c5c9aef51863a9c0a.woff2 create mode 100644 _static/fonts/b19ac4e57f2a56639eebd1c35319e5a7.woff2 create mode 100644 _static/fonts/b4d3c40a77fd9e35a881a79077957055.woff2 create mode 100644 _static/fonts/b4e42731e8d667ae87c3450c345754ae.woff2 create mode 100644 _static/fonts/b57a5ada789f195d5d42f4073a6cf313.woff2 create mode 100644 _static/fonts/b5b4146d87e5d22d0a4e0d04f3ee5626.woff2 create mode 100644 _static/fonts/b7ef2cd1159a8cbfd271ff2abe07f237.woff2 create mode 100644 _static/fonts/b93199bb6f964f190f4da04ecdbaf5a4.woff2 create mode 100644 _static/fonts/bb8007225d94a099cddbade7ea904667.woff2 create mode 100644 _static/fonts/bc67bba106323289ea3eda0826de1912.ttf create mode 100644 _static/fonts/bcd47c2f3649cfcaa86a08fb741255d6.woff2 create mode 100644 _static/fonts/bd0efe13f0d9d591b337ddc7f289f494.woff2 create mode 100644 _static/fonts/bd51fb0ca67e64c809ffcf7e1370f969.woff2 create mode 100644 _static/fonts/bdbb6b52604c2451fdcba9cdfd44f4e1.woff2 create mode 100644 _static/fonts/bf2ad3287f13eb7076cccb516ec2986f.ttf create mode 100644 _static/fonts/bfd1a0c9c783e84595589f33e1828a57.woff2 create mode 100644 _static/fonts/c13b34dd5b6a35b309944b61c91b2ace.woff2 create mode 100644 _static/fonts/c22066c14662d6c80415ae04c5dd9d51.woff2 create mode 100644 _static/fonts/c28a41f656599f6694528b5463c6a445.woff2 create mode 100644 _static/fonts/c6dc61b627bbc5af9130518297bd4f17.ttf create mode 100644 _static/fonts/c8a9fd4eab4e83382cc66fde70911b41.woff2 create mode 100644 _static/fonts/ca7eea0cf248d6e8442c01074765bd33.woff2 create mode 100644 _static/fonts/cadfb311297a9362b07fab73934b432a.ttf create mode 100644 _static/fonts/cbfd26d5bcf084ee407a0b2b7599e84b.woff2 create mode 100644 _static/fonts/ccdebed88064e470c15f37c432922e57.woff2 create mode 100644 _static/fonts/cce2217cc8323fe49789adefb3596291.woff2 create mode 100644 _static/fonts/cd3d1f17e048e2116f438bd7157baccf.woff2 create mode 100644 _static/fonts/d07f561ba87d93460742b060727d9e0d.woff2 create mode 100644 _static/fonts/d368cf5bed7856dbafa2af36b51acb9c.woff2 create mode 100644 _static/fonts/d422317033deb87342a5e56c7be67458.ttf create mode 100644 _static/fonts/d6f9cdf1a40893111566fcdee3bbe5a9.woff2 create mode 100644 _static/fonts/d98f35e926c11f3d5c0c8e3205d43907.ttf create mode 100644 _static/fonts/d9e6a498dac7e9e91f6e0b4f8930eba0.woff2 create mode 100644 _static/fonts/da6cd48e6dad1888fccc91735e7522f7.woff2 create mode 100644 _static/fonts/daf12b5f1889502004bba85ad71f9fa4.woff2 create mode 100644 _static/fonts/daf51ab540602b2d0b87646621637bac.woff2 create mode 100644 _static/fonts/db0424fb67fb52e7e538490240cc7fb9.woff2 create mode 100644 _static/fonts/dc25cbf4baaf778bd8ae78fbc0e79479.woff2 create mode 100644 _static/fonts/dd719f1662079ce6a61260f9af972379.woff2 create mode 100644 _static/fonts/de018865c95896bb57265fc97c48ebd7.woff2 create mode 100644 _static/fonts/e33716333704ab19fdf9989e072ad49a.woff2 create mode 100644 _static/fonts/e56cc9fb5272752b78f144b4be43175d.woff2 create mode 100644 _static/fonts/e704ef18719c08839bc99a32437ef0f8.woff2 create mode 100644 _static/fonts/e99627cd27de169d23ece4573006af2a.woff2 create mode 100644 _static/fonts/ef8f0236a7e8b46bc9d642ecf4ab0cb7.woff2 create mode 100644 _static/fonts/f154d62b4879af7a22895af7a4ef03f0.woff2 create mode 100644 _static/fonts/f17ee050ada0453f3bd07bc466c2dde2.woff2 create mode 100644 _static/fonts/f265cee675c0e5b2d6ab263d0edcc754.woff2 create mode 100644 _static/fonts/f2f69e8cd15fdd15a4244c95ec8a8514.woff2 create mode 100644 _static/fonts/f534242dea2255c25b9d05c2371986e3.woff2 create mode 100644 _static/fonts/f53f3b5a15d717b6d21d7885285e90ed.woff2 create mode 100644 _static/fonts/f55dac651a40fce74a5cf5728d9f8ffc.woff2 create mode 100644 _static/fonts/f5aebdfea35d1e7656ef4acc5db1f243.woff2 create mode 100644 _static/fonts/f5f971e9640a9eb86ef553a7e7e999c7.woff2 create mode 100644 _static/fonts/f6734f8177112c0839b961f96d813fcb.woff2 create mode 100644 _static/fonts/f75911313e1c7802c23345ab57e754d8.woff2 create mode 100644 _static/fonts/fb17f56622e45dd4ecee00bb5c63cd2b.woff2 create mode 100644 _static/fonts/fb1aaa90783b8cb9375265abeb91b153.woff2 create mode 100644 _static/fonts/fc66f942651a9fe1a598770d3d896529.woff2 create mode 100644 _static/language_data.js create mode 100644 _static/sphinx_immaterial_theme.1b5b7a2d5891aec19.min.js create mode 100644 _static/sphinx_immaterial_theme.af531f03affe68837.min.css create mode 100644 _static/white.svg create mode 100644 api.html create mode 100644 genindex.html create mode 100644 http-routingtable.html create mode 100644 index.html create mode 100644 introduction.html create mode 100644 objects.inv create mode 100644 rdfm-docs.pdf create mode 100644 rdfm_android_device_client.html create mode 100644 rdfm_artifact.html create mode 100644 rdfm_frontend.html create mode 100644 rdfm_linux_device_client.html create mode 100644 rdfm_manager.html create mode 100644 rdfm_mcumgr_device_client.html create mode 100644 rdfm_mgmt_server.html create mode 100644 rdfm_ota_manual.html create mode 100644 searchindex.js create mode 100644 server_operation.html create mode 100644 system_overview.html diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..71ddcee --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: c18b54d0390621a1f11e87e80b29ed32 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/_images/summary.png b/_images/summary.png new file mode 100644 index 0000000000000000000000000000000000000000..83e549f791ff81bfb52d8822335542369b892d35 GIT binary patch literal 56562 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 + +Any request that was not successfully authenticated (because of a missing or otherwise invalid token) will return the 401 Unauthorized status code. +Additionally, in the case of management tokens, if the given token does not provide sufficient access to the requested resource, the request will be rejected with a 403 Forbidden status code. +This can happen if the token does not claim all scopes required by the target endpoint (for example: trying to upload a package using a read-only token). + +Error Handling +~~~~~~~~~~~~~~ + +Should an error occur during the handling of an API request, either because of incorrect request data or other endpoint-specific scenarios, the server will return an error structure containing a user-friendly description of the error. +An example error response is shown below: + +.. sourcecode:: json + + { + "error": "delete failed, the package is assigned to at least one group" + } + + +Packages API +~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.packages + :undoc-static: + :order: path + +Group API +~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v2.groups + :undoc-static: + :order: path + +Group API (legacy) +~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.groups + :undoc-static: + :order: path + +Update API +~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.update + :undoc-static: + :order: path + +Device Management API +~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v2.devices + :undoc-static: + :order: path + +Device Management API (legacy) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.devices + :undoc-static: + :order: path + +Device Authorization API +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoflask:: rdfm_mgmt_server:create_docs_app() + :modules: api.v1.auth + :undoc-static: + :order: path diff --git a/_sources/index.md.txt b/_sources/index.md.txt new file mode 100644 index 0000000..d04a157 --- /dev/null +++ b/_sources/index.md.txt @@ -0,0 +1,18 @@ +# {{project}} + +```{toctree} +:maxdepth: 2 + +introduction +system_overview +rdfm_linux_device_client +rdfm_android_device_client +rdfm_mcumgr_device_client +rdfm_artifact +rdfm_manager +rdfm_mgmt_server +rdfm_ota_manual +server_operation +rdfm_frontend +api +``` diff --git a/_sources/introduction.md.txt b/_sources/introduction.md.txt new file mode 100644 index 0000000..5bbba02 --- /dev/null +++ b/_sources/introduction.md.txt @@ -0,0 +1,16 @@ +# Introduction + +RDFM - Remote Device Fleet Manager - is an open-source ecosystem of tools that enable Over-The-Air (OTA) update delivery and fleet management for systems of embedded devices. + +This manual describes the main components of RDFM. It is divided into the following chapters: + +- System Architecture - a short overview of the system architecture, and how each component of the system interacts with the other +- RDFM Linux Device Client - build instructions and manual for the Linux RDFM Client, used for installing updates on a device +- RDFM Android Device Client - integration guide and user manual for the RDFM Android Client/app used for providing OTA updates via RDFM on embedded Android devices +- RDFM MCUmgr Device Client - build instructions and manual for the RDFM MCUmgr Client app, used for providing updates via RDFM on embedded devices running ZephyrRTOS +- RDFM Artifact utility - instruction manual for the `rdfm-artifact` utility used for generating update packages for use with the RDFM Linux device client +- RDFM Manager utility - instruction manual for the `rdfm-mgmt` utility, which allows management of devices connected to the RDFM server +- RDFM Management Server - build instructions and deployment manual for the RDFM Management Server +- RDFM Server API Reference - comprehensive reference of the HTTP APIs exposed by the RDFM Management Server +- RDFM OTA Manual - introduces key concepts of the RDFM OTA system and explains it's basic operation principles +- RDFM Frontend - build instructions for the RDFM Frontend application diff --git a/_sources/rdfm_android_device_client.md.txt b/_sources/rdfm_android_device_client.md.txt new file mode 100644 index 0000000..5392ffd --- /dev/null +++ b/_sources/rdfm_android_device_client.md.txt @@ -0,0 +1,172 @@ +# RDFM Android Device Client + +## Introduction + +The RDFM Android Device Client allows for integrating an Android-based device with the RDFM server. +Currently, only OTA update functionality is implemented. + +## Integrating the app + +This app is **not meant to be built separately** (i.e in Android Studio), but as part of the source tree for an existing device. +The app integrates with the Android UpdateEngine to perform the actual update installation, which requires it to be a system app. +Some configuration is required to the existing system sources. + +### Copying the sources + +First, copy the sources of the app to the root directory of the AOSP source tree. +After cloning this repository, run the following: +``` +mkdir -v -p /vendor/antmicro/rdfm +cd devices/android-client/ +cp -r app/src/main/* /vendor/antmicro/rdfm +``` + +### Configuring the device Makefile + +The [product Makefile](https://source.android.com/docs/setup/create/new-device#build-a-product) must be configured to build the RDFM app into the system image. +To do this, add `rdfm` to the `PRODUCT_PACKAGES` variable in the target device Makefile: +``` +PRODUCT_PACKAGES += rdfm +``` + +### Building the app + +Afterwards, [the usual Android build procedure](https://source.android.com/docs/setup/build/building) can be used to build just the app. +From an already configured build environment, run: +``` +mma rdfm +``` +The resulting signed APK is in `out/target/product//system/app/rdfm/rdfm.apk`. + +### Using HTTPS for server requests + +The default Android system CA certificates are used when validating the certificate presented by the server. +If the RDFM server that is configured in the app uses a certificate that is signed by a custom Certificate Authority, the CA certificate must be added to the system roots. + +## System versioning + +The app performs update check requests to the configured RDFM server. +The build version and device type are retrieved from the system properties: +- `ro.build.version.incremental` - the current software version (matches `rdfm.software.version`) +- `ro.build.product` - device type (matches `rdfm.hardware.devtype`) + +When uploading an OTA package to the RDFM server, currently these values must be **manually** extracted from the update package, and passed as arguments to `rdfm-mgmt`: +``` +rdfm-mgmt packages upload --path ota.zip --version --device +``` + +You can extract the values from the [package metadata file](https://source.android.com/docs/core/ota/tools#ota-package-metadata) by unzipping the OTA package. + +## Configuring the app + +The application will automatically start on system boot. +Available configuration options are shown below. + +### Build-time app configuration + +The default build-time configuration can be modified by providing a custom `conf.xml` file in the `app/src/main/res/values/` folder, similar to the one shown below: + +```xml + + + + +``` + +This build-time configuration is applied **only once, at first startup of the app**, as the main use case for this is first-time configuration for newly provisioned devices. +Modifying it afterwards (for example, via an update containing a new version of the RDFM app) will not result in the change of existing configuration. + +### Runtime app configuration + +It is possible to change the app's configuration at runtime by simply starting the RDFM app from the drawer and selecting `Settings` from the context menu. + +### Configuration options + +The following configuration options are available: +- RDFM server URL (`http`/`https` scheme) +- Update check interval (in seconds) +- Maximum amount of concurrent shell sessions (set to `0` to disable reverse shell functionality) + +## Available intents + +### Update check intent + +This intent allows an external app to force perform an update check outside of the usual automatic update check interval. +To do this, the app that wants to perform the update check must have the `com.antmicro.update.rdfm.permission.UPDATE_CHECK` permission defined in its `AndroidManifest.xml` file: + +```xml + +``` + +Afterwards, an update check can then be forced like so: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.startUpdate"); +mContext.sendBroadcast(configIntent); +``` + +### External configuration via intents + +The app settings can also be configured via intents, for example in order to change between different deployment environments. +To do this, the app that performs the configuring step must have the `com.antmicro.update.rdfm.permission.CONFIGURATION` permission defined in its `AndroidManifest.xml` file: +```xml + +``` + +To configure the app, use the `com.antmicro.update.rdfm.configurationSet` intent and set extra values on the intent to the settings you wish to change. +For example, to set the server address: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationSet"); +configIntent.putExtra("ota_server_address", "http://CUSTOM-OTA-ADDRESS/"); +mContext.sendBroadcast(configIntent); +``` + +The supported configuration key names can be found in the `res/values/strings.xml` file with the `preference_` prefix. + +Aside from setting the configuration, you can also fetch the current configuration of the app: +```java +Intent configIntent = new Intent("com.antmicro.update.rdfm.configurationGet"); +mContext.sendBroadcast(configIntent); + +// Now listen for `com.antmicro.update.rdfm.configurationResponse` broadcast intent +// The intent's extras bundle will contain the configuration keys and values +``` + +## Development + +The provided Gradle files can be used for development purposes, simply open the `devices/android-client` directory in Android Studio. +Missing references to the `UpdateEngine` class are expected, but they do not prevent regular use of the IDE. + +Do note however that **the app is not buildable from Android Studio**, as it requires integration with the aforementioned system API. +To test the app, an existing system source tree must be used. +Copy the modified sources to the AOSP tree, and re-run the [application build](#building-the-app). +The modified APK can then be uploaded to the device via ADB by running: +``` +adb install +``` + +### Restarting the app + +With the target device connected via ADB, run: +``` +adb shell am force-stop com.antmicro.update.rdfm +adb shell am start -n com.antmicro.update.rdfm/.MainActivity +``` + +### Fetching app logs + +To view the application logs, run: +``` +adb logcat --pid=`adb shell pidof -s com.antmicro.update.rdfm` +``` diff --git a/_sources/rdfm_artifact.md.txt b/_sources/rdfm_artifact.md.txt new file mode 100644 index 0000000..964c7e0 --- /dev/null +++ b/_sources/rdfm_artifact.md.txt @@ -0,0 +1,150 @@ +# RDFM Artifact utility + +## Introduction + +The RDFM Artifact tool (`rdfm-artifact`) allows for easy creation and modification of RDFM Linux client-compatible artifacts containing rootfs partition images. +A basic RDFM artifact consists of a rootfs image, as well as its checksum, metadata and compatibility with certain device types. + +Additionally, `rdfm-artifact` allows for the generation of delta updates, which contain only the differences between two versions of an artifact rather than the entire artifact itself. +This can be useful for reducing the size of updates and improving the efficiency of the deployment process. + +`rdfm-artifact` can also be used for generation of Zephyr MCUboot artifacts, which allows for updating embedded devices running Zephyr. +Additionally, multiple Zephyr images can be combined into one grouped artifact to allow multiple boards to act as one logical device. + +Single file updates are also supported. +This option allows for creating, or updating specific files on the device, without the need to update the whole partition. + +## Getting started + +In order to support robust updates and rollback, the RDFM Client requires proper partition layout and a bootloader that supports A/B update scheme. To make it easy to integrate the RDFM Client into your Yocto image-building project, it's recommended to use the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer when building the BSPs. + +## Building from source + +### Requirements + +* Go compiler +* C Compiler +* liblzma-dev and libglib2.0-dev packages + +### Steps + +To build `rdfm-artifact` on a device from source, clone the repository and build the binary using `make`: + +``` +git clone https://github.com/antmicro/rdfm.git && cd tools/rdfm-artifact/ +make +``` + +## Basic usage + +The basic functionality of writing an artifact is available with the `write` subcommand: + +``` +NAME: + rdfm-artifact write - Allows creation of RDFM-compatible artifacts + +USAGE: + rdfm-artifact write command [command options] [arguments...] + +COMMANDS: + rootfs-image Create a full rootfs image artifact + delta-rootfs-image Create a delta rootfs artifact + zephyr-image Create a full Zephyr MCUboot image artifact + zephyr-group-image Create a Zephyr MCUboot group image artifact + single-file Create a single file artifact + +OPTIONS: + --help, -h show help +``` + +### Creating a full-rootfs artifact + +For example, to create a simple rootfs artifact for a given system image: + +``` +rdfm-artifact write rootfs-image \ + --file "my-rootfs-image.img" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" +``` + +### Creating a delta rootfs artifact + +For creating a delta artifact, you should have already created two separate full-rootfs artifacts: + +- base artifact - the rootfs image that the deltas will be applied on top of, or in other words: the currently running rootfs on the device +- target artifact - the updated rootfs image that will be installed on the device + +Given these two artifacts, a delta artifact can be generated like this: + +``` +rdfm-artifact write delta-rootfs-image \ + --base-artifact "base.rdfm" \ + --target-artifact "target.rdfm" \ + --output-path "base-to-target.rdfm" +``` + +### Creating a Zephyr MCUboot artifact + +To create a Zephyr MCUboot artifact, you'll have to have already created a Zephyr image with MCUboot support enabled. +You should use the signed bin image (by default `zephyr.signed.bin`). +Artifact version will be extracted from provided image. + +With this image, you can generate an artifact like so: + +``` +rdfm-artifact write zephyr-image \ + --file "my-zephyr-image.signed.bin" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" +``` + +### Creating a Zephyr MCUboot group artifact + +To create a grouped Zephyr MCUboot artifact, you should have already created at least two Zephyr images with MCUboot support enabled. +The version of individual images in a grouped artifact must be identical. + +Given images `one.bin` and `two.bin` for group targets `one` and `two` respectively, an artifact can be generated with: + +``` +rdfm-artifact write zephyr-group-image \ + --group-type "my-group" \ + --target "one:one.bin" \ + --target "two:two.bin" \ + --ouptput-path "path-to-output.rdfm" +``` + +:::{note} +It's possible to create a grouped artifact with just one image, +however in cases like that you should create simple [zephyr-image](#creating-a-zephyr-mcuboot-artifact) instead. +::: + +### Creating a single file artifact + +Apart from updating a whole partition, it's also possible to update a single file on the device. +The usage is the same as for rootfs artifacts, but with the `single-file` subcommand and two new options: + +- `--dest-dir` - the destination directory on the device where the file should be placed +- `--rollback-support` - (optional) determines, whether a backup of the file should be created for rollback purposes. + The backup file is stored in the same directory as the original file, with the `.tmp` extension added to the name. + By default, the rollback support is disabled. + +``` +rdfm-artifact write single-file \ + --file "my-file.txt" \ + --artifact-name "my-artifact-name" \ + --device-type "my-device-type" \ + --output-path "path-to-output.rdfm" \ + --dest-dir "/destination/device/directory" \ + --rollback-support +``` + +## Running tests + +To run `rdfm-artifact` tests, use the `test` Makefile target: + +``` +make test +``` diff --git a/_sources/rdfm_frontend.md.txt b/_sources/rdfm_frontend.md.txt new file mode 100644 index 0000000..1f15422 --- /dev/null +++ b/_sources/rdfm_frontend.md.txt @@ -0,0 +1,81 @@ +# RDFM Frontend + +## Introduction + +Repository contains code for a frontend application that is able to render data from and communicate with `rdfm-server` through HTTP requests. + +The application uses HTTP Polling to dynamically detect any changes in the data and update the UI accordingly, so multiple users can use the application simultaneously (as well as the `rdfm-mgmt` tool). + + +To use the frontend application, make sure that `rdfm-server` is up and running. +Details on how to run it can be found in [RDFM Management Server](./rdfm_mgmt_server.md). +To be able to send requests to `rdfm-server` its URL has to be defined in the `.env` file using `VITE_SERVER_URL` key. + +```{warning} +If no authentication is used in the frontend application make sure that the `RDFM_DISABLE_ENCRYPTION` and `RDFM_DISABLE_API_AUTH` values are set to `1`. +``` + +Before running any of the commands, make sure that you have `npm` installed. + +## Building the application + +To install dependencies and build the application for production run the following commands in the root directory of the project: + +```bash +npm install +npm run build +``` + +The built static files are located in the `dist` directory. +The frontend can be started alongside the RDFM API in the same [Docker image](rdfm_mgmt_server.md#setting-up-a-dockerized-development-environment). +The following changes must be applied: + +- `VITE_RDFM_BACKEND` in the `.env` file to `'true'`. +- `VITE_SERVER_URL` in the `.env` file to the URL of the backend server. +- `RDFM_INCLUDE_FRONTEND_ENDPOINT` in the docker-compose configuration. As a consequence, the frontend application will be served on `/api/static/frontend` endpoint once the HTTP server is started. + +The frontend may also be deployed independently of the RDFM API. +The following configuration settings must then be set: + +- `VITE_RDFM_BACKEND` in the `.env` file to `'false'`. +- `VITE_SERVER_URL` in the `.env` file to the URL of the backend server. +- `RDFM_ENABLE_CORS` in the docker-compose configuration to `1` to enable CORS requests. +- `RDFM_FRONTEND_APP_URL` in the docker-compose configuration to the URL of the frontend application server, as it is used for redirects. + +```{warning} +`RDFM_ENABLE_CORS` variable should not be set in production environment, as it allows for cross-origin requests. +``` + +## Running development server + +When developing the application it is recommended to use the `vite` development server, as features like Hot Module Replacement is enabled. +To install dependencies and start the development server run the following commands in the root directory of the project: + +```bash +npm install +npm run dev +``` + +To communicate with `rdfm-server` when using the development server, make sure to set all variables as described in the [Building](#building-the-application) section in the same as it is done for a separate server deployment. + + +## Configuration + +The frontend application can be configured using an `.env` file. +That file contains variables that can be set to change the behavior of the application. +Below there is a description of all available variables. + +* `VITE_SERVER_URL` - RDFM server URL +* `VITE_RDFM_BACKEND` - Indicates if the backend hosts the frontend application +* `VITE_LOGIN_URL` - OIDC login URL +* `VITE_LOGOUT_URL` - OIDC logout URL +* `VITE_OAUTH2_CLIENT` - OAUTH2 Client ID + +## Formatting + +To format the code using `prettier` run the following command: + +```bash +npm install +npm run format +``` diff --git a/_sources/rdfm_linux_device_client.md.txt b/_sources/rdfm_linux_device_client.md.txt new file mode 100644 index 0000000..61186a9 --- /dev/null +++ b/_sources/rdfm_linux_device_client.md.txt @@ -0,0 +1,190 @@ +# RDFM Linux Device Client + +## Introduction + +The RDFM Linux Device Client (`rdfm-client`) integrates an embedded Linux device with the RDFM Server. +This allows for performing robust Over-The-Air (OTA) updates of the running system and remote management of the device. + +`rdfm-client` runs on the target Linux device and handles the process of checking for updates in the background along with maintaining a connection to the RDFM Management Server. + +## Getting started + +In order to support robust updates and rollback, the RDFM Client requires proper partition layout and integration with the U-Boot bootloader. To make it easy to integrate the RDFM Client into your Yocto image-building project, it's recommended to use the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer when building the BSPs. + +## Installing from source + +### Requirements + +* C compiler +* Go compiler +* liblzma-dev, libssl-dev and libglib2.0-dev packages + +### Steps + +To install an RDFM client on a device from source, first clone the repository and build the binary: +``` +git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/ +make +``` + +Then run the install command: +``` +make install +``` + +### Installation notes + +Installing `rdfm` this way does not offer a complete system updater. +System updates require additional integration with the platform's bootloader and a dual-root partition setup for robust updates. +For this, it's recommended to build complete BSPs containing `rdfm` using the [meta-rdfm](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm) Yocto layer. + +## Building using Docker + +All build dependencies for compiling the RDFM Client are included in a dedicated Dockerfile. To build a development container image, you can use: + +``` +git clone https://github.com/antmicro/rdfm.git && cd devices/linux-client/ +sudo docker build -t rdfmbuilder . +``` + +This will create a Docker image that can be later used to compile the RDFM binary: + +``` +sudo docker run --rm -v :/data -it rdfmbuilder +cd data/devices/linux-client +make +``` + +## Configuring the client + +### RDFM default config + +The main config file contents are located in `/etc/rdfm/rdfm.conf`. It's JSON formatted and with the following keys of interest: + +#### RootfsPartA `string` + +Partition A for the A/B updating scheme. + +#### RootfsPartB `string` + +Partition B for the A/B updating scheme. + +### RDFM overlay config + +The file `/var/lib/rdfm/rdfm.conf` defines the high-level RDFM client configurations. They are overlaid over the configuration located in `/etc/rdfm/rdfm.conf` during client startup. + +#### DeviceTypeFile `string` + +Path to the device type file. + +#### UpdatePollIntervalSeconds `int` + +Poll interval for checking for new updates. + +#### RetryPollIntervalSeconds `int` + +Maximum number of seconds between each retry when authorizing. + +#### ServerCertificate `string` + +Path to a server SSL certificate. + +#### ServerURL `string` + +Management server URL. + +#### HttpCacheEnabled `bool` + +Describing if artifact caching is enabled. True by default. + +#### ReconnectRetryCount `int` + +HTTP reconnect retry count. + +#### ReconnectRetryTime `int` + +HTTP reconnect retry time. + +#### TelemetryEnable `bool` + +Describing if telemetry is enabled. False by default. + +#### TelemetryBatchSize `int` + +Number of log entries to be sent to a management server at a time. Fifty by default. + +### RDFM telemetry config + +The JSON structured `loggers.conf` file, laying under `/etc/rdfm/`, serves as a configuration file that defines a set of loggers to be executed once the client establishes a connection to the RDFM management server. Each logger can be any executable binary, which will be invoked by the client at predefined intervals. The client captures and processes the output generated by these loggers, providing a flexible mechanism for collecting and reporting system or application data during runtime. + +The `loggers.json` file contains an array of dictionaries, each of which describes a logger. + +Consider the following example: + +```json +[ + { + "name": "current date", + "path": "date", + "args": ["--rfc-email"], + "tick": 1000 + } +] +``` + +:::{note} Since the file gives the capacity to run arbitrary binaries, its permissions should be set to `-rw-r--r--`. +::: + +#### name `string` + +Denotes the name of the logger, each one should have a unique name. Loggers lower in the file will overwrite their counterparts that are above them. + +#### path `string` + +A path to an executable to be ran. + +#### args `[]string` + +A list of arguments for the given executable. + +#### tick `int` + +Number of milliseconds between each time a logger is ran. In the case of a logger taking more than `tick` to execute, it is killed and the client reports a timeout error. + +## Testing server-device integration with a demo Linux device client + +For development purposes, it's often necessary to test server integration with an existing device client. +To do this, it is possible to use the [RDFM Linux device client](rdfm_linux_device_client.md), without having to build a compatible system image utilizing the Yocto [meta-rdfm layer](https://github.com/antmicro/meta-antmicro/tree/master/meta-rdfm). +First, build the demo container image: + +``` +cd devices/linux-client/ +make docker-demo-client +``` + +You can then start a demo Linux client by running the following: +``` +docker-compose -f docker-compose.demo.yml up +``` + +If required, the following environment variables can be changed in the above `docker-compose.demo.yml` file: + +- `RDFM_CLIENT_SERVER_URL` - URL to the RDFM Management Server, defaults to `http://127.0.0.1:5000/`. +- `RDFM_CLIENT_SERVER_CERT` **(optional)** - path (within the container) to the CA certificate to use for verification of the connection to the RDFM server. When this variable is set, the server URL must also be updated to use HTTPS instead of HTTP. +- `RDFM_CLIENT_DEVTYPE` - device type that will be advertised to the RDFM server; used for determining package compatibility, defaults to `x86_64`. +- `RDFM_CLIENT_PART_A`, `RDFM_CLIENT_PART_B` **(optional)** - specifies path (within the container) to the rootfs A/B partitions that updates will be installed to. They do not need to be specified for basic integration testing; any updates that are installed will go to `/dev/zero` by default. + +The demo client will automatically connect to the specified RDFM server and fetch any available packages. +To manage the device and update deployment, you can use the [RDFM Manager utility](rdfm_manager.md). + +## Developer Guide + +### Running tests + +Use the `test` make target to run the unit tests: + +``` +make test +``` + +Additionally, run the scripts available in the `scripts/test-docker` directory. These scripts test basic functionality of the RDFM client. diff --git a/_sources/rdfm_manager.md.txt b/_sources/rdfm_manager.md.txt new file mode 100644 index 0000000..f2663b7 --- /dev/null +++ b/_sources/rdfm_manager.md.txt @@ -0,0 +1,201 @@ +# RDFM Manager utility + +## Introduction + +The RDFM Manager (`rdfm-mgmt`) utility allows authorized users to manage resources exposed by the RDFM Management Server. + +## Installation + +Before proceeding, make sure that you have installed Python (at least version 3.11) and the `pipx` utility: +- **Debian (Bookworm)** - run `sudo apt update && sudo apt install pipx` +- **Arch** - `sudo pacman -S python-pipx` + +The prefered mode of installation for `rdfm-mgmt` is via `pipx`. +To install `rdfm-mgmt`, you must first clone the RDFM repository: + +``` +git clone https://github.com/antmicro/rdfm.git +cd rdfm/ +``` + +Afterwards, run the following commands: + +``` +cd manager/ +pipx install . +``` + +This will install the `rdfm-mgmt` utility and its dependencies for the current user within a virtual environment located at `/home//.local/pipx/venv`. +The `rdfm-mgmt` executable will be placed in `/home//.local/bin/` and should be immediately accessible from the shell. +Depending on the current system configuration, adding the above directory to the `PATH` may be required. + +## Configuration + +Additional RDFM Manager configuration is stored in the current user's `$HOME` directory, in the `$HOME/.config/rdfm-mgmt/config.json` file. +By default, RDFM Manager will add authentication data to all requests made to the RDFM server, which requires configuration of an authorization server and client credentials for use with the OAuth2 `Client Credentials` flow. +If authentication was disabled on the server-side, you can disable it in the manager as well by passing the `--no-api-auth` CLI flag like so: + +``` +rdfm-mgmt --no-api-auth groups list +``` + +An example configuration file is shown below. +In this case, the [Keycloak authorization server](https://www.keycloak.org/) was used: + +```json +{ + "auth_url": "http://keycloak:8080/realms/master/protocol/openid-connect/token", + "client_id": "rdfm-client", + "client_secret": "RDSwDyUMOT7UXxMqMmq2Y4vQ1ezxqobi" +} +``` + +Explanation of each required configuration field is shown below: +- `auth_url` - URL to the authorization server's [token endpoint](https://swagger.io/docs/specification/authentication/openid-connect-discovery/) +- `client_id` - Client ID to use for authentication using OAuth2 Client Credentials flow +- `client_secret` - Client secret to use for authentication using OAuth2 Client Credentials flow + +:::{note} +If you're also setting up the server, please note that the above client credentials are **NOT** the same as the server's Token Introspection credentials. +Each user of ``rdfm-mgmt`` should receive different credentials and be assigned scopes based on their allowed access level. +::: + + +## Building the wheel + +For installation instructions, see the [Installation section](#installation). +Building the wheel is not required in this case. + +To build the `rdfm-mgmt` wheel, you must have Python 3 installed, along with the `Poetry` dependency manager. + +Building the wheel can be done as follows: + +``` +cd manager/ +poetry build +``` + +## Usage + +For more detailed information, see the help messages associated with each subcommand: + +``` +$ rdfm-mgmt -h +usage: rdfm-mgmt + +RDFM Manager utility + +options: + -h, --help show this help message and exit + --url URL URL to the RDFM Management Server (default: http://127.0.0.1:5000/) + --cert CERT path to the server CA certificate used for establishing an HTTPS connection (default: ./certs/CA.crt) + --no-api-auth disable OAuth2 authentication for API requests (default: False) + +available commands: + {devices,packages,groups} + devices device management + packages package management + groups group management +``` + +### Listing available resources + +Listing devices: + +``` +rdfm-mgmt devices list +``` + +Listing registration requests: + +``` +rdfm-mgmt devices pending +``` + + +Listing packages: + +``` +rdfm-mgmt packages list +``` + +Listing groups: + +``` +rdfm-mgmt groups list +``` + +### Uploading packages + +``` +rdfm-mgmt packages upload \ + --path file.img \ + --version "v0" \ + --device "x86_64" +``` + +### Deleting packages + +``` +rdfm-mgmt packages delete --package-id +``` + +### Creating groups + +``` +rdfm-mgmt groups create --name "Group #1" --description "A very long description of the group" +``` + +### Deleting groups + +``` +rdfm-mgmt groups delete --group-id +``` + +### Assign package to a group + +Assigning one package: + +``` +rdfm-mgmt groups assign-package --group-id --package-id +``` + +Assigning many packages: + +``` +rdfm-mgmt groups assign-package --group-id --package-id --package-id +``` + +Clearing package assignments: + +``` +rdfm-mgmt groups assign-package --group-id +``` + +### Assign devices to a group + +Adding devices: + +``` +rdfm-mgmt groups modify-devices --group-id --add +``` + +Removing devices: + +``` +rdfm-mgmt groups modify-devices --group-id --remove +``` + +### Setting a group's target version + +``` +rdfm-mgmt groups target-version --group-id --version +``` + +### Authorizing a device + +``` +rdfm-mgmt devices auth +``` + +You can then select the registration for this device to authorize. diff --git a/_sources/rdfm_mcumgr_device_client.md.txt b/_sources/rdfm_mcumgr_device_client.md.txt new file mode 100644 index 0000000..8faee03 --- /dev/null +++ b/_sources/rdfm_mcumgr_device_client.md.txt @@ -0,0 +1,475 @@ +# RDFM MCUmgr Device Client + +## Introduction + +The RDFM MCUmgr Device Client (`rdfm-mcumgr-client`) allows for integrating an embedded device running ZephyrRTOS with the RDFM server via its MCUmgr SMP server implementation. +Currently, only the update functionality is implemented with support for serial, UDP and BLE transports. + +`rdfm-mcumgr-client` runs on a proxy device that's connected to the targets via one of the supported transports that handles the process of checking for updates, fetching update artifacts and pushing update images down to correct targets. + +## Getting started + +In order to properly function, both the Zephyr application and the `rdfm-mcumgr-client` have to be correctly configured in order for the update functionality to work. +Specifically: +* Zephyr applications must be built with MCUmgr support, with any transport method of your choice and with image management and reboot command groups enabled. +* The device running Zephyr must be connected to a proxy device running `rdfm-mcumgr-client` as the updates are coming from it. +* For reliable updates, the SMP server must be running alongside your application and be accessible at all times. + + +## Building client from source + +### Requirements + +* C compiler +* Go compiler (1.22+) +* liblzma-dev and libssl-dev packages + +### Steps + +To install the proxy client from source, first clone the repository and build the binary: +```sh +git clone https://github.com/antmicro/rdfm.git +cd rdfm/devices/mcumgr-client/ +make +``` + +Then run the install command: +```sh +make install +``` + +## Setting up target device + +### Setting up the bootloader + +To allow rollbacks and update verification, the MCUboot bootloader is used. +Images uploaded by `rdfm-mcumgr-client` are written to a secondary flash partition, while leaving the primary (currently running) image intact. +During update, the images are swapped by the bootloader. +If the update was successful, the new image is permanently set as the primary one, otherwise the images are swapped back to restore the previous version. +For more details on MCUboot, you can read the [official guide](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/readme-zephyr.html#building-and-using-mcuboot-with-zephyr) from MCUboot's website. + +#### Generating image signing key + +In order to enable updates, MCUboot requires all images to be signed. +During update, the bootloader will first validate the image using this key. + +MCUboot provides `imgtool.py` image tool script which can be used to generate appropriate signing key. +Below are the steps needed to generate a new key using this tool: + +Install additional packages required by the tool (replace `~/zephyrproject` with path to your Zephyr workspace): +```sh +cd ~/zephyrproject/bootloader/mcuboot +pip3 install --user -r ./scripts/requirements.txt +``` + +Generate new key: +```sh +cd ~/zephyrproject/bootloader/mcuboot/scripts +./imgtool.py keygen -k -t +``` +MCUboot currently supports `rsa-2048`, `rsa-3072`, `ecdsa-p256` or `ed25519` key types. +For more details on the image tool, please refer to its [official documentation](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/imgtool.html). + +#### Building the bootloader + +Besides the signing key, MCUboot also requires that the target board has specific flash partitions defined in its devicetree. +These partitions are: +* `boot_partition`: for MCUboot itself +* `slot0_partition`: the priamry slot of image 0 +* `slot1_partition`: the secondary slot of image 0 + +If you choose the *swap-using-scratch* update algorithm, one more partition has to be defined: +* `scratch_partition`: the scratch slot + +You can check whether your board has those partitions predefined by looking at its devicetree file (`boards///.dts`). +Look for `fixed-partitions` compatible entry. If your default board configuration doesn't specify those partitions (or you would like to modify them), +you can either modify the devicetree file directly or use [devicetree overlays](https://docs.zephyrproject.org/latest/build/dts/howtos.html#set-devicetree-overlays). + +Sample overlay file for the `stm32f746g_disco` board: + +```dts +#include + +/delete-node/ &quadspi; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 DT_SIZE_K(64)>; + }; + + slot0_partition: partition@40000 { + label = "image-0"; + reg = <0x00040000 DT_SIZE_K(256)>; + }; + + slot1_partition: partition@80000 { + label = "image-1"; + reg = <0x00080000 DT_SIZE_K(256)>; + }; + + scratch_partition: partition@c0000 { + label = "scratch"; + reg = <0x000c0000 DT_SIZE_K(256)>; + }; + }; +}; + +/ { + aliases { + /delete-property/ spi-flash0; + }; + + chosen { + zephyr,flash = &flash0; + zephyr,flash-controller = &flash; + zephyr,boot-partition = &boot_partition; + zephyr,code-partition = &slot0_partition; + }; +}; +``` + +:::{note} +If you do use devicetree overlay, make sure to add `app.overlay` as the last overlay file +since it's needed to correctly store the MCUboot image in `boot_partition`. +::: + +Besides the devicetree, you also have to specify: +* `BOOT_SIGNATURE_KEY_FILE`: path to the previously generate signing key +* `BOOT_SIGNATURE_TYPE`: signing key type: + * `BOOT_SIGNATURE_TYPE_RSA` and `BOOT_SIGNATURE_TYPE_RSA_LEN` + * `BOOT_SIGNATURE_TYPE_ECDSA_P256` + * `BOOT_SIGNATURE_TYPE_ED25519` +* `BOOT_IMAGE_UPGRADE_MODE`: the [update algorithm](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/design.html#image-slots) used for swapping images in primary and secondary slots: + * `BOOT_SWAP_USING_MOVE` + * `BOOT_SWAP_USING_SCRATCH` + +For example, if you wanted to build the bootloader for the `stm32f746g_disco` board with partitions defined in `stm32_disco.overlay`, +using *swap-using-scratch* update algorithm and using `rsa-2048` `key.pem` signing key, +you would run (replace `~/zephyrproject` with path to your Zephyr workspace): + +```sh + west build \ + -d mcuboot \ + -b stm32f746g_disco \ + ~/zephyrproject/bootloader/mcuboot/boot/zephyr \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay;app.overlay" \ + -DCONFIG_BOOT_SIGNATURE_KEYFILE='"key.pem"' \ + -DCONFIG_BOOT_SIGNATURE_TYPE_RSA=y \ + -DCONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN=2048 \ + -DCONFIG_BOOT_SWAP_USING_SCRATCH=y +``` + +The produced image can be flashed to your device. +For more details on building and using MCUboot with Zephyr, please refer to [official MCUboot guide](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/readme-zephyr.html#building-and-using-mcuboot-with-zephyr). + +### Setting up the Zephyr application + +#### Building the image + +To allow your application to be used with MCUmgr client, you will have to enable Zephyr's [device management subsystem](https://docs.zephyrproject.org/latest/services/device_mgmt/index.html#device-mgmt). +For the client to function properly, both [*image management*](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html) +and [*OS management*](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html) groups need to be enabled. +You will also have to enable and configure [SMP transport](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_transport.html) +(either serial, BLE or udp) that you wish to use. +To learn how to do that, you can reference Zephyr's [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) +which provides configuration for all of them. + +You will also have set `MCUBOOT_BOOTLOADER_MODE` setting to match the *swapping algorithm* you've configured for the [bootloader](#building-the-bootloader): + +:::{table} +:name: Swap algorithm configuration options + +MCUboot | Zephyr +--- | --- +`BOOT_SWAP_USING_MOVE` | `MCUBOOT_BOOTLOADER_MODE_SWAP_WITHOUT_SCRATCH` +`BOOT_SWAP_USING_SCRATCH` | `MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH` +::: + +:::{important} +#### Bluetooth specific + +Bluetooth transport additionally requires you to manually start SMB Bluetooth advertising. +Refer to the [`main.c`](https://github.com/zephyrproject-rtos/zephyr/blob/dbfc1aaec697b78573c18d83fd40ba66ff63c0b3/samples/subsys/mgmt/mcumgr/smp_svr/src/main.c#L70-L72) +and [`bluetooth.c`](https://github.com/zephyrproject-rtos/zephyr/blob/dbfc1aaec697b78573c18d83fd40ba66ff63c0b3/samples/subsys/mgmt/mcumgr/smp_svr/src/bluetooth.c) +from the [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) for details on that. +::: + +To build the [`smp_svr` sample](https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr/smp_svr) +for the `stm32f746g_disco` board with `stm32_disco.overlay` devicetree overlay, +configured to use serial transport with *swap-using-scratch* update algorithm, +you would run (replace `~/zephyrproject` with path to your Zephyr workspace): + +```sh + west build \ + -d build \ + -b stm32f746g_disco \ + "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay" \ + -DEXTRA_CONF_FILE="overlay-serial.conf" \ + -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y +``` + +For more information on the `smp_svr` sample, please refer to [Zephyr's documentation](https://docs.zephyrproject.org/latest/samples/subsys/mgmt/mcumgr/smp_svr/README.html#smp-svr). + +#### Signing the image + +By default MCUboot will only accept images that are properly signed with the same key as the bootloader itself. +Only `BIN` and `HEX` output types can be signed. +The recommended way for managing signing keys is using [MCUboot's image tool](#generating-image-signing-key), +which is shipped together with Zephyr's MCUboot implementation. +When signing an image, you also have to provide an image version, that's embedded in the signed image header. +This is also the value that will be reported by the MCUmgr client as the current running software version back to the [RDFM server](./rdfm_mgmt_server.md). +Image version is specified in `major.minor.revision+build` format. + +##### Automatically + +Zephyr build system can automatically sign the final image for you. +To enable this functionality, you will have to set: + +* `MCUBOOT_SIGNATURE_KEY_FILE`: path to the signing key +* `MCUBOOT_IMGTOOL_SIGN_VERSION`: version of the produced image +before building your application. +Here's a modification of the build command from [building the image](#building-the-image) with those settings applied: + +```sh + west build \ + -d build \ + -b stm32f746g_disco \ + "~/zephyrproject/zephyr/samples/subsys/mgmt/mcumgr/smp_svr" \ + -- \ + -DDTC_OVERLAY_FILE="stm32_disco.overlay" \ + -DEXTRA_CONF_FILE="overlay-serial.conf" \ + -DCONFIG_MCUBOOT_BOOTLOADER_MODE_SWAP_SCRATCH=y \ + -DCONFIG_MCUBOOT_SIGNATURE_KEY_FILE='"key.pem"' \ + -DCONFIG_IMGTOOL_SIGN_VERSION='"1.2.3+4"' +``` + +##### Manually + +You can also sign the produced images yourself using the [image tool](#generating-image-signing-key). +Below is a sample showing how to sign previously built image: + +```sh +west sign -d build -t imgtool -- --key --version +``` + +Either way, the signed images will be stored next to their unsigned counterparts. They will have `signed` inserted into the filename (e.g. unsigned `zephyr.bin` will produce `zephyr.signed.bin` signed image). + +### Self-confirmed updates + +By default, MCUmgr client will try to manually confirm a new image during an update. +While this works in simple cases, you might wish to run some additional test logic that should be used to determine if an update should be finalized. +For example, you might want to reject an update in case one of the drivers failed to start or if the network stack is misconfigured. +The client supports these kinds of use cases using self-confirming images. +Rather than confirming an update by itself, +the client will instead watch the primary image slot of the device to determine if an update was marked as permanent or if it was rejected. +In that case, the final decision falls on the updated device. + +For this feature to work correctly, you will have to modify your application to include the self-testing logic. + +```c +/* + * An example of self-test function. + * It will first check if this is a fresh update and run the testing logic. + * Based on results, it will either mark the update as permanent or reboot, + * causing MCUboot to revert to the previous version. + * + * This function should be called before the main application logic starts, + * preferably at the beginning of the `main` function. + */ + +#include +#include + +void run_self_tests() { + if (!boot_is_img_confirmed()) { + bool passed; + + /* Testing logic goes here */ + + if (!passed) { + sys_reboot(SYS_REBOOT_COLD); // (1) + return; + } + + boot_write_img_confirmed(); // (2) + } +} +``` +::::{code-annotations} +1. Tests failed - device reboots itself, returning to previous version +2. Tests passed - device confirms the update, marking it as permanent +:::: + +## Configuring MCUmgr client + +### Search locations + +The client is configured using `config.json` configuration file. +By default, the client will look for this file in: + +- current working directory +- `$HOME/.config/rdfm-mcumgr` +- `/etc/rdfm-mcumgr` + +stopping at first configuration file found. +You can override this by specifying path to a different configuration file with `-c/--config` flag: + +```sh +rdfm-mcumgr-client --config +``` + +All of the non-device specific options can also be overwritten by specifying their flag counterpart. +For a full list you can run: + +```sh +rdfm-mcumgr-client --help +``` + +### Configuration values + +- `server` - URL of the RDFM server the client should connect to +- `key_dir` - path (relative or absolute) to the directory where all device keys are stored +- `update_interval` - interval between each update poll to RDFM server (accepts time suffixes 's', 'm', 'h') +- `retries` - (optional) how many times should an update be attempted for a device in case of an error + (no value or value `0` means no limit) +- `devices` - an array containing configuration for each device the client should handle + - `name` - display name for device, used only for logging + - `id` - unique device identifier used when communicating with RDFM server + - `device_type` - device type reported to RDFM server used to specify compatible artifacts + - `key` - name of the file containing device private key in PEM format. Key should be stored in `key_dir` directory. + - `self_confirm` - (optional) bool indicating whether the device will confirm updates by itself. False by default + - `update_interval` - (optional) override global `update_interval` for this device + - `transport` - specifies the transport type for the device and it's specific options + +- `groups` - an array containing configuration for device groups + - `name` - display name for group, used for logging + - `id` - unique group identifier used when communicating with RDFM server + - `type` - type reported to RDFM server to specify compatible artifacts + - `key` - name of the file containing group private key in PEM format. Key should be stored in `key_dir` directory. + - `update_interval` - (optional) override global `update_interval` for this group + - `members` - an array containing configuration for each device that's a member of this group + - `name` - display name for device, used for logging + - `device` - name of target image to match from an artifact + - `self_confirm` - (optional) bool indicating whether the device will confirm updates by itself. False by default + - `transport` - specifies the transport type for the device and its specific options + +Transport specific: +- `type` - specific transport type for this device. Currently supported: `ble`, `serial`, `udp` + +* BLE transport: + - `device_index` - controller index to be used for connection (e.g. `hci0` -> `0`) + - `peer_name` - the name the target BLE device advertises. Should match with `CONFIG_BT_DEVICE_NAME` + +* Serial transport: + - `device` - device name used for communicating with device. OS specific (e.g. `"/dev/ttyUSB0"`, `"/dev/tty.usbserial"`) + - `baud` - communication speed; must match the baudrate of connected device + - `mtu` - Maximum Transmission Unit, maximum protocol packet size + +* UDP transport: + - `address`: IPv4 / IPv6 address and port in `IP`:`port` form + +### Device groups + +The client supports grouping multiple Zephyr MCUboot boards to act as one complete device from management server's perspective. +While each device in a group can be running different Zephyr application, +all devices are synchronized by the MCUmgr client to be running the exact same software version. +Group updates are performed using [zephyr group artifacts](./rdfm_artifact.md#creating-a-zephyr-mcuboot-group-artifact) +which contain update images for each member of the group and metadata on how to match image to device. + +During an update, the MCUmgr client matches each image to its target member and tries to apply it. +Group update is considered successful only if **all** members of the group went through the update process without errors. +Otherwise all members are rolled back by the client to the previous version. + +### Example configuration + +```json +{ + "server": "http://localhost:5000", + "key_dir": "keys", + "update_interval": "10s", + "retries": 3, + "devices": [ + { + "name": "zephyr-ble", + "id": "11:11:11:11:11:11", + "dev_type": "zeph-ble", + "update_interval": "15s", + "key": "ble.key", + "transport": { + "type": "ble", + "device_index": 0, + "peer_name": "test0" + } + }, + { + "name": "zephyr-serial", + "id": "22:22:22:22:22:22", + "dev_type": "zeph-ser", + "key": "serial.key", + "self_confirm": true, + "transport": { + "type": "serial", + "device": "/dev/ttyACM0", + "baud": 115200, + "mtu": 128 + } + } + ], + "groups": [ + { + "name": "group-one", + "id": "gr1", + "type": "group1", + "key": "group1.key", + "members": [ + { + "name": "udpl", + "device": "udp-left", + "transport": { + "type": "udp", + "address": "192.168.1.2:1337" + } + }, + { + "name": "udpr", + "device": "udp-right", + "transport": { + "type": "udp", + "address": "192.168.1.3:1337" + } + }, + { + "name": "bleh", + "device": "ble", + "self_confirm": true, + "transport": { + "type": "ble", + "device_index": 0, + "peer_name": "ble_head", + } + } + ] + } + ] +} +``` + +### Device keys + +Each device uses its own private key for authentication with `rdfm-server` as described in [device authentication](./server_operation.md#device-authentication). +Each key should be stored under `key_dir` specified in configuration. +If the client doesn't find corresponding device key for configured device, it will attempt to generate one itself. +The resulting key will be saved to the configured location with `0600` permissions. + +:::{note} +Device keys are different from the signing key used for signing the bootloader and application images! +::: diff --git a/_sources/rdfm_mgmt_server.md.txt b/_sources/rdfm_mgmt_server.md.txt new file mode 100644 index 0000000..8b13e29 --- /dev/null +++ b/_sources/rdfm_mgmt_server.md.txt @@ -0,0 +1,438 @@ +# RDFM Management Server + +## Introduction + +The RDFM Management Server is a core part of the RDFM ecosystem. The server manages incoming device connections and grants authorization only to those which are allowed to check-in with the server. +It also handles package upload and management, deploy group management and other crucial functionality required for robust and secure device Over-The-Air (OTA) updates along with allowing remote system management without exposing devices to the outside world. + +## REST API + +The server exposes a management and device API that is used by management software and end devices. A comprehensive list of all API endpoints is available in the [RDFM Server API Reference chapter](api.rst). + +## Setting up a Dockerized development environment + +The preferred method for running the RDFM server is by using a Docker container. +To set up a local development environment, first clone the RDFM repository: + +```bash +git clone https://github.com/antmicro/rdfm.git +cd rdfm/ +``` + +A `Dockerfile` is provided in the `server/deploy/` directory that builds a container suitable for running the server. +Currently, it is required to build the container image manually. +To do this, run the following from the **cloned RDFM repository root** folder: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +A simple `docker-compose` file that can be used to run the server is provided below, and in the `server/deploy/docker-compose.development.yml` file. + +```yaml +services: + rdfm-server: + image: antmicro/rdfm-server:latest + restart: unless-stopped + environment: + - RDFM_JWT_SECRET= + - RDFM_DB_CONNSTRING=sqlite:////database/development.db + - RDFM_HOSTNAME=rdfm-server + - RDFM_API_PORT=5000 + - RDFM_DISABLE_ENCRYPTION=1 + - RDFM_DISABLE_API_AUTH=1 + - RDFM_LOCAL_PACKAGE_DIR=/packages/ + - RDFM_WSGI_SERVER=werkzeug + ports: + - "5000:5000" + volumes: + - db:/database/ + - pkgs:/packages/ + +volumes: + db: + pkgs: +``` + +The server can then be started using the following command: + +```bash +docker-compose -f server/deploy/docker-compose.development.yml up +``` + +## Configuration via environment variables + +Configuration of the RDFM server can be changed by using the following environment variables: + +- `RDFM_JWT_SECRET` - secret key used by the server when issuing JWT tokens, this value must be kept secret and not easily guessable (for example, a random hexadecimal string). +- `RDFM_DB_CONNSTRING` - database connection string, for examples please refer to: [SQLAlchemy - Backend-specific URLs](https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls). Currently, only the SQLite and PostgreSQL engines were verified to work with RDFM (however: the PostgreSQL engine requires adding additional dependencies which are currently not part of the default server image, this may change in the future). + +Development configuration: + +- `RDFM_DISABLE_ENCRYPTION` - if set, disables the use of HTTPS, falling back to exposing the API over HTTP. This can only be used in production if an additional HTTPS reverse proxy is used in front of the RDFM server. +- `RDFM_DISABLE_API_AUTH` - if set, disables request authentication on the exposed API routes. **WARNING: This is a development flag only! Do not use in production!** This causes all API methods to be freely accessible, without any access control in place! +- `RDFM_ENABLE_CORS` - if set, disables CORS checks, which in consequence allows any origin to access the server. **WARNING: This is a development flag only! Do not use in production!** + +HTTP/WSGI configuration: + +- `RDFM_HOSTNAME` - hostname/IP address to listen on. This is additionally used for constructing package URLs when storing packages in a local directory. +- `RDFM_API_PORT` - API port. +- `RDFM_SERVER_CERT` - required when HTTPS is enabled; path to the server's certificate. The certificate can be stored on a Docker volume mounted to the container. For reference on generating the certificate/key pairs, see the `server/tests/certgen.sh` script. +- `RDFM_SERVER_KEY` - required when HTTPS is enabled; path to the server's private key. Additionally, the above also applies here. +- `RDFM_WSGI_SERVER` - WSGI server to use, this value should be left default. Accepted values: `gunicorn` (**default**, production-ready), `werkzeug` (recommended for development). +- `RDFM_WSGI_MAX_CONNECTIONS` - (when using Gunicorn) maximum amount of connections available to the server worker. This value must be set to at minimum the amount of devices that are expected to be maintaining a persistent (via WebSocket) connection with the server. Default: `4000`. +- `RDFM_INCLUDE_FRONTEND_ENDPOINT` - specifies whether the RDFM server should serve the frontend application. If set, the server will serve the frontend application from endpoint `/api/static/frontend`. Before setting this variable, the frontend application must be built and placed in the `frontend/dist` directory. +- `RDFM_FRONTEND_APP_URL` - specifies URL to the frontend application. This variable is required when `RDFM_INCLUDE_FRONTEND_ENDPOINT` is not set, as backend HTTP server has to know where to redirect the **user**. + +API OAuth2 configuration (must be present when `RDFM_DISABLE_API_AUTH` is omitted): + +- `RDFM_OAUTH_URL` - specifies the URL to an authorization server endpoint compatible with the RFC 7662 OAuth2 Token Introspection extension. This endpoint is used to authorize access to the RDFM server based on tokens provided in requests made by API users. +- `RDFM_LOGIN_URL` - specifies the URL to a login page of the authorization server. It is used to authorize users and generate an access token and start a session. +- `RDFM_LOGOUT_URL` - specified the URL to a logout page of the authorization server. It is used to end the session and revoke the access token. +- `RDFM_OAUTH_CLIENT_ID` - if the authorization server endpoint provided in `RDFM_OAUTH_URL` requires the RDFM server to authenticate, this variable defines the OAuth2 `client_id` used for authentication. +- `RDFM_OAUTH_CLIENT_SEC` - if the authorization server endpoint provided in `RDFM_OAUTH_URL` requires the RDFM server to authenticate, this variable defines the OAuth2 `client_secret` used for authentication. + +Package storage configuration: + +- `RDFM_STORAGE_DRIVER` - storage driver to use for storing artifacts. Accepted values: `local` (default), `s3`. +- `RDFM_LOCAL_PACKAGE_DIR` - specifies a path (local for the server) to a directory where the packages are stored. +- `RDFM_S3_BUCKET` - when using S3 storage, name of the bucket to upload the packages to. +- `RDFM_S3_ACCESS_KEY_ID` - when using S3 storage, Access Key ID to access the specified bucket. +- `RDFM_S3_ACCESS_SECRET_KEY` - when using S3 storage, Secret Access Key to access the specified bucket. + +## Configuring package storage location + +### Storing packages locally + +By default (when not using one of the above deployment setups), the server stores all uploaded packages to a temporary folder under `/tmp/.rdfm-local-storage/`. +To persist package data, configuration of an upload folder is required. +This can be done by using the `RDFM_LOCAL_PACKAGE_DIR` environment variable (in the Dockerized deployment), which should contain a path to the desired upload folder. + +:::{warning} +This storage method should NOT be used for production deployments! +The performance of the built-in file server is severely limited and provides NO caching, which will negatively affect the update speed for all devices even when a few of them try downloading an update package at the same time. +It is recommended to use a dedicated storage solution such as S3 to store packages. +::: + +### Storing packages on S3-compatible storage + +The RDFM server can also store package data on S3 and other S3 API-compatible object storage servers. +The following environment variables allow changing the configuration of the S3 integration: +- `RDFM_S3_BUCKET` - name of the bucket to upload the packages to +- `RDFM_S3_ACCESS_KEY_ID` - Access Key ID to access the specified bucket +- `RDFM_S3_ACCESS_SECRET_KEY` - Secret Access Key to access the specified bucket +Additionally, when using S3 storage, the environment variable `RDFM_STORAGE_DRIVER` must be set to `s3`. + +An example reference setup utilizing the MinIO Object Storage server is provided in the `server/deploy/docker-compose.minio.yml` file. +To run it, first build the RDFM server container like in the above setup guides: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +Then, run the following: + +``` +docker-compose -f server/deploy/docker-compose.minio.development.yml up +``` + +## Configuring API authentication + +### Basic configuration + +The above development setup does not provide any authentication for the RDFM API. +This is helpful for development or debugging purposes, however **under no circumstance should this be used in production deployments, as it exposes the entire API with no restrictions in place**. + +By default, the RDFM server requires configuration of an external authorization server to handle token creation and scope management. +To be compatible with RDFM Management Server, the authentication server **MUST** support the OAuth2 Token Introspection extension ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)). + +The authorization server is configured using the following environment variables: +- `RDFM_OAUTH_URL` - specifies the URL to the Token Introspection endpoint of the authorization server. +- `RDFM_OAUTH_CLIENT_ID` - specifies the client identifier to use for authenticating the RDFM server to the authorization server. +- `RDFM_OAUTH_CLIENT_SEC` - specifies the client secret to use for authenticating the RDFM server to the authorization server. + +For accessing the management API, the RDFM server does not issue any tokens itself. +This task is delegated to the authorization server that is used in conjunction with RDFM. +The following scopes are used for controlling access to different methods of the RDFM API: +- `rdfm_admin_ro` - read-only access to the API (fetching devices, groups, packages) +- `rdfm_admin_rw` - complete administrative access to the API with modification rights + +Additional rules are defined for package uploading route from [Packages API](api.rst#Packages_API). +- `rdfm_upload_single_file` - allows uploading an artifact of type `single-file`. +- `rdfm_upload_rootfs_image` - allows uploading artifacts `rootfs-image` and `delta-rootfs-image`. +Each package type requires its corresponding scope, or the complete admin access - `rdfm_admin_rw`. + +Refer to the [RDFM Server API Reference chapter](api.rst) for a breakdown of the scopes required for accessing each API method. + +### API authentication using Keycloak + +#### Running the services + +An example `docker-compose` file that can be used to run the RDFM server using [Keycloak Identity and Access Management server](https://www.keycloak.org/) as an authorization server is provided below, and in the `server/deploy/docker-compose.keycloak.development.yml` file. + +```yaml +services: + rdfm-server: + image: antmicro/rdfm-server:latest + restart: unless-stopped + environment: + - RDFM_JWT_SECRET= + - RDFM_DB_CONNSTRING=sqlite:////database/development.db + - RDFM_HOSTNAME=rdfm-server + - RDFM_API_PORT=5000 + - RDFM_DISABLE_ENCRYPTION=1 + - RDFM_LOCAL_PACKAGE_DIR=/packages/ + - RDFM_OAUTH_URL=http://keycloak:8080/realms/master/protocol/openid-connect/token/introspect + - RDFM_OAUTH_CLIENT_ID=rdfm-server-introspection + - RDFM_OAUTH_CLIENT_SEC= + networks: + - rdfm + ports: + - "5000:5000" + volumes: + - db:/database/ + - pkgs:/packages/ + + keycloak: + image: quay.io/keycloak/keycloak:22.0.1 + restart: unless-stopped + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + networks: + - rdfm + ports: + - "8080:8080" + command: + - start-dev + volumes: + - keycloak:/opt/keycloak/data/ + - ../keycloak-themes:/opt/keycloak/themes + +volumes: + db: + pkgs: + keycloak: + +networks: + rdfm: +``` + +Before running the above services, you must first build the RDFM server container by running the following from the RDFM repository root folder: + +``` +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +You can then run the services by running: + +``` +docker-compose -f server/deploy/docker-compose.keycloak.development.yml up +``` + +#### Keycloak configuration + +Further configuration on the Keycloak server is required before any requests are successfully authenticated. +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to **Clients** and press **Create client**. +This client is required for the RDFM server to perform token validation. +The following settings must be set when configuring the client: +- **Client ID** - must match `RDFM_OAUTH_CLIENT_ID` provided in the RDFM server configuration, can be anything (for example: `rdfm-server-introspection`) +- **Client Authentication** - set to `On` +- **Authentication flow** - select only `Service accounts roles` + +After saving the client, go to the `Credentials` tab found under the client details. +Make sure the authenticator used is `Client Id and Secret`, and copy the `Client secret`. +This secret must be configured in the RDFM server under the `RDFM_OAUTH_CLIENT_SEC` environment variable. + +:::{note} +After changing the `docker-compose` variables, remember to restart the services (by pressing `Ctrl+C` and re-running the `docker-compose up` command). +::: + +Additionally, you must create proper client scopes and user roles to define which users have access to the read-only and read-write parts of the RDFM API. +To create new scopes, navigate to the `Client scopes` tab and select `Create client scope`. +Create four separate scopes with the following names; the rest of the settings can be left as default (if required, you may also add a description to the scope): +- `rdfm_admin_ro` +- `rdfm_admin_rw` +- `rdfm_upload_single_file` +- `rdfm_upload_rootfs_image` + +To create new roles, navigate to the `Realm roles` tab and select `Create role`. +Create separate roles with the same names. The rest of the settings can be left as default (if required, you may also add a description to the role). + +After restarting the services, the RDFM server will now validate requests against the Keycloak server. +To further setup the `rdfm-mgmt` manager to use the Keycloak server, refer to the [RDFM manager manual](rdfm_manager.md). To add users with roles to the Keycloak server, which can then be used to access the RDFM API using the frontend application, refer to the [Adding a User](#adding-a-user) section below. + +#### Adding an API client + +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to **Clients** and press **Create client**. +This client will represent a user of the RDFM API. +The following settings must be set when configuring the client: +- **Client Authentication** - set to `On` +- **Authentication flow** - select only `Service accounts roles` + +After saving the client, go to the `Credentials` tab found under the client details. +Make sure the authenticator used is `Client Id and Secret`, and copy the `Client secret`. + +Finally, assign the required scope to the client: under the `Client scopes` tab, click `Add client scope` and select one of the two RDFM scopes: read-only `rdfm_admin_ro` or read-write `rdfm_admin_rw`. + +:::{note} +The newly-created client will now have access to the RDFM API. +To configure `rdfm-mgmt` to use this client, follow the [Configuration section](rdfm_manager.md#configuration) of the RDFM manager manual. +::: + +#### Adding a User + +First, navigate to the Keycloak Administration Console found at `http://localhost:8080/` and login with the initial credentials provided in Keycloak's configuration above (by default: `admin`/`admin`). + +Next, go to `Users` tab and press **Add user**. This will open up a form to create a new user. +Fill in the **Username** field and press **Create**. + +Next, go to `Credentials` tab found under the user details and press **Set password**. +This form allows you to set a password for the user and determine whether creating a new one is required on the next login. + +After configuring the user, go to `Role mapping` tab under the user details. +There, appropriate roles can be assigned to the user using the **Assign role** button. + +:::{note} +The newly created users can now log in using the RDFM frontend application. +To configure and run the frontend application, refer to the [RDFM Frontend chapter](rdfm_frontend.md). +::: + +##### Configuring frontend application + +When using the frontend application, logging in functionality is provided by the Keycloak server. +To integrate the Keycloak server with the frontend application first go to the client details created in the [Keycloak configuration](#keycloak-configuration) section. + +Go to `Capability config` and make sure that **Implicit flow** and **Standard flow** are enabled. + +Open `Settings` panel and set **Valid redirect URIs** and **Valid post logout redirect URIs** values to the URL of the frontend application. +The value depends on the deployment method, if the `rdfm-server` is used to host the frontend application the value can be inferred from the `RDFM_HOSTNAME` and `RDFM_API_PORT` environment variables and will most likely be `http[s]://{RDFM_HOSTNAME}:{RDFM_API_PORT}`. +Otherwise, the value should be equal to `RDFM_FRONTEND_APP_URL` variable. + +Additionally, you can change the theme of the login page to match the frontend application. +To do this, go to `Login settings` section and `rdfm` in the **Login theme** dropdown. + +## Configuring HTTPS + +For simple deployments, the server can expose an HTTPS API directly without requiring an additional reverse proxy. +Configuration of the server's HTTPS can be done using the following environment variables: + +- `RDFM_SERVER_CERT` - path to the server's signed certificate +- `RDFM_SERVER_KEY` - path to the server's private key + +Both of these files must be accessible within the server Docker container. + +### HTTPS demo deployment + +:::{warning} +This demo deployment explicitly disables API authentication, and is only meant to be used as a reference on how to configure your particular deployment. +::: + +An example HTTPS deployment can be found in the `server/deploy/docker-compose.https.development.yml` file. +Before running it, you must execute the `tests/certgen.sh` in the `server/deploy/` directory: + +```bash +cd server/deploy/ +../tests/certgen.sh +``` + +This script generates a root CA and an associated signed certificate to be used for running the server. +The following files are generated: + +- `certs/CA.{crt,key}` - CA certificate/private key that is used as the root of trust +- `certs/SERVER.{crt,key}` - signed certificate/private key used by the server + +To run the deployment, you must first build the RDFM server container by running the following from the RDFM repository root folder: + +```bash +docker build -f server/deploy/Dockerfile -t antmicro/rdfm-server:latest . +``` + +You can then start the deployment by running: +```bash +docker-compose -f server/deploy/docker-compose.https.development.yml up +``` + +To verify the connection to the server, you must provide the CA certificate. +For example, when using `curl` to access API methods: + +```bash +curl --cacert server/deploy/certs/CA.crt https://127.0.0.1:5000/api/v1/devices +``` + +When using `rdfm-mgmt`: + +```bash +rdfm-mgmt --url https://127.0.0.1:5000/ \ + --cert server/deploy/certs/CA.crt \ + --no-api-auth \ + devices list +``` + + +## Production deployments + +### Production considerations + +The following is a list of considerations when deploying the RDFM server: + +1. HTTPS **must** be enabled; `RDFM_DISABLE_ENCRYPTION` **must not** be set (or the server is behind a dedicated reverse proxy that adds HTTPS on the edge). +2. API authentication **must** be enabled; `RDFM_DISABLE_API_AUTH` **must not** be set. +3. RDFM **must** use a production WSGI server; `RDFM_WSGI_SERVER` **must not** be set to `werkzeug`. + When not provided, the server defaults to using a production-ready WSGI server (`gunicorn`). + The development server (`werkzeug`) does not provide sufficient performance to handle production workloads, and a high percentage of requests will be dropped under heavy load. +4. RDFM **must** use a dedicated (S3) package storage location; the local directory driver does not provide adequate performance when compared to dedicated object storage. + +Refer to the above configuration chapters for how to configure each aspect of the RDFM server: + +1. [Configuring HTTPS](#configuring-https) +2. [Configuring API authentication](#configuring-api-authentication) +3. [Configuring the WSGI server](#configuration-via-environment-variables) +4. [Configuring S3 package storage](#configuring-package-storage-location) + +A practical example of a deployment that includes all the above considerations can be found below, in the [Production example deployment](#production-example-deployment) section. + +### Production example deployment + +:::{warning} +For simplicity, this example deployment has static credentials pre-configured pretty much everywhere, and as such should never be used directly as a production setup. +At least the following secrets are pre-configured and would require changes: +- S3 Access Key ID/Access Secret Key +- rdfm-server JWT secret +- Keycloak Administrator username/password +- Keycloak Client: rdfm-server introspection Client ID/Secret +- Keycloak Client: rdfm-mgmt admin user Client ID/Secret + +Additionally, the Keycloak server requires further configuration for production deployments. +For more information, refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) page in Keycloak documentation. +::: + +A reference setup is provided in `server/deploy/docker-compose.production.yml` that can be used for customizing production server deployments. +Prior to starting the deployment, you must generate a signed server certificate that will be used for establishing the HTTPS connection to the server. +This can be done by either providing your own certificate, or by running the provided example certificate generation script: + +```bash +cd server/deploy/ +../tests/certgen.sh +``` + +When using the `certgen.sh` script, the CA certificate found at `server/deploy/certs/CA.crt` can be used for validating the connection made to the server. + +Similarly to previous example deployments, it can be started by running the following command from the **RDFM monorepository root folder**: + +```bash +docker-compose -f server/deploy/docker-compose.production.yml up +``` + +`rdfm-mgmt` configuration for this deployment can be found in `server/deploy/test-rdfm-mgmt-config.json`. +After copying the configuration to `$HOME/.config/rdfm-mgmt/config.json`, you can access the server by running: + +```bash +rdfm-mgmt --url https://127.0.0.1:5000/ --cert server/deploy/CA.crt \ + devices list +``` + diff --git a/_sources/rdfm_ota_manual.md.txt b/_sources/rdfm_ota_manual.md.txt new file mode 100644 index 0000000..e8aab16 --- /dev/null +++ b/_sources/rdfm_ota_manual.md.txt @@ -0,0 +1,113 @@ +# RDFM OTA Manual + +This chapter contains key information about the RDFM OTA update system. + +## Key concepts + +Below is a brief explanation of the key entities of the RDFM update system. + +### Devices + +From the server's point of view, a device is any system that is running an RDFM-compatible update client. +For example, see [RDFM Linux Device Client](./rdfm_linux_device_client.md). +Each device actively reports its metadata to the server: +- Currently running software version (`rdfm.software.version`) +- Device type (`rdfm.hardware.devtype`) +- Other client-specific metadata + +### Packages + +A package is any file that can be used by a compatible update client to update the running system. +From the server's point of view, update packages are simple binary blobs and no specific structure is enforced. +Each package has metadata assigned to it that indicates its contents. +The following metadata fields are mandatory for all packages: +- Software version (`rdfm.software.version`) - indicates the version of the contained software +- Device type (`rdfm.hardware.devtype`) - indicates the device type a package is compatible with + +The device type is used as the first filter when searching for a compatible update package. +Any package that does not match the device type reported by the update client will be considered incompatible. + +A package may also contain metadata with `requires:` clauses. +The `requires` clause is used to indicate dependencies on certain metadata properties of the device. +In its most basic form, it can be used to indicate a dependency on a certain system image to be installed for proper delta update installation. +For more complex use cases involving many intermediate update steps, it can also be used to enforce an order in which certain packages must be installed. + +### Groups + +A group consists of many assigned devices. Each group can also be assigned one or many packages. +The group itself also contains metadata about the group name, description, update policy, and other arbitrary information which can be used by custom frontends interacting with the server. + +### Update policy + +An update policy defines the target version the devices within a given group will be updated to. +The policy is a string with the syntax `,[arguments]`. +Required arguments depend on the specific policy being used. +Currently, the following policies are supported: +- `no_update` (**default**) - requires no arguments, the server will treat all devices within the group as up-to-date, and will not return any packages to devices requesting an update check. **This is the default update policy for all newly created groups**. +- `exact_match` - specifies that the server will attempt to install the target software version on each of the devices in the group. +Example usage: `exact_match,version1` - this specifies that the server will attempt to bring all of the devices to the software version `version1`. +This process may involve installing many intermediate packages, but the end result is a device that's running the specified version. +The server will use group-assigned packages when resolving the dependency graph required for reaching the target version. + +## Update resolution + +When resolving a path to the correct target version, the server utilizes only the group-assigned packages. +When a device is requesting an update check, a package dependency graph is created. +The edges of the graph correspond to different packages available during the update process (which are compatible with the device, as indicated by the `rdfm.hardware.devtype` field), while the nodes indicate the software versions (as indicated by the `rdfm.software.version` fields of each package). +Next, the group's update policy is queried, which indicates the target version/node each device should be attempting to reach. +The shortest path between the currently running node and the target node is used as instructions for how the server should lead the device to the specified version. + +## Example scenario: simple update assignment + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v1` +- P1 - `devtype=bar`, `version=v2` +- P2 - `devtype=baz`, `version=v3` + +The group is specified to update to version `v3` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v3` +- D1 - `devtype=bar`, `version=v3` +- D2 - `devtype=baz`, `version=v3` + +In this scenario, devices `D0` and `D1` shall receive the update packages `P0` and `P1` respectively. +The device `D2` is considered up-to-date, as its version matches the target specified in the group policy. + +## Example scenario: downgrades + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v4` + +The group is specified to update to version `v4` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v5` + +In this scenario, the device `D0` will receive the package `P0` to be installed next. + +## Example scenario: sequential updates + +Consider a group with the following packages assigned: +- P0 - `devtype=foo`, `version=v2`, `requires:version=v1` +- P1 - `devtype=foo`, `version=v3`, `requires:version=v2` + +The group is specified to update to version `v3` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v1` + +In this scenario, the device `D0` will first be updated to the package `P0`, as it's the only package that is compatible (matching device type and different version than the one running on the device). +The package's only `requires` clause also matches against the device's metadata. + +After successful installation, during the next update check on the newly installed version (`v1`), the device will receive the next available package. +As the device is now reporting a version field of `v1` and the package's `requires:` clause passes, package `P1` becomes the next candidate package available for installation. +After successful instalation of `P1`, no more packages are available and the device is considered to be up-to-date. + +## Example scenario: delta updatess + +Consider a group with the following packages assigned: +- P0 (delta) - `devtype=foo`, `version=v5`, `rootfs=e6e2531..`, `requires:version=v0`, `requires:rootfs=2f646ac..` +- P1 (delta) - `devtype=foo`, `version=v5`, `rootfs=e6e2531..`, `requires:version=v2`, `requires:rootfs=6d9aee4..` + +The group is specified to update to version `v5` per the policy. Devices are reporting the following metadata: +- D0 - `devtype=foo`, `version=v0`, `rootfs=2f646ac..` +- D1 - `devtype=foo`, `version=v2`, `rootfs=6d9aee4..` + +In this scenario, devices `D0` and `D1` will receive packages `P0` and `P1` as updates respectively. +The packages themselves contain different binary contents, in this case a delta between a given base version's system partition (`v0` and `v2`) and the target (`v5`), but the end result is an identical system on both devices. +This way, many delta packages may be provided for updating a fleet consisting of a wide range of running versions. diff --git a/_sources/server_operation.md.txt b/_sources/server_operation.md.txt new file mode 100644 index 0000000..be124f5 --- /dev/null +++ b/_sources/server_operation.md.txt @@ -0,0 +1,111 @@ +# Server Integration flows + +This chapter describes the various integration flows between device clients and the RDFM Management server. + +## Device authentication + +At the start of their execution, all RDFM-compatible device clients shall authenticate with the server. +This shall be done by utilizing the `/api/v1/auth/device` endpoint. +For details on the request schema, refer to the [Server API Reference](api.rst) chapter. +An example request made to this endpoint is shown below: + +```json +{ + "metadata": { + "rdfm.hardware.devtype": "device-type", + "rdfm.software.version": "foo", + "rdfm.hardware.macaddr": "00:11:22:33:44:55", + } + "public_key": "", + "timestamp": 1694681536, +} +``` + +The JSON payload bytes must be signed by the device client with its securely stored RSA private key using PKCS #1 v1.5 signature with SHA-256 digest (function `RSASSA-PKCS1-V1_5-SIGN` defined in [RFC 8017](https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.1)) +The calculated signature must then be attached, encoded as base64, to the authorization request in the header `X-RDFM-Device-Signature`. +If the server successfully validates the attached signature, the device will be registered in the server's database, if it wasn't previously registered already. +The device-specified MAC address is used as a unique identifier for this specific device. + +Before the device is authorized to access the RDFM API, it must be accepted first by an administrative entity interacting via a separate API with the RDFM server. +If the device was not accepted, or its acceptation status was revoked, the above request shall fail with the `401 Unauthorized` HTTP status code. **The device client must handle this status code gracefully**, for example by retrying the attempted request after a certain time has passed. + +Once the device is accepted into the RDFM server, the above request shall return a device-specific app token, that can be used to interact with device-side API endpoints. +The app token is not permanent, and will expire after a certain time period. +The device client must not make any assumptions about the length of the usability period, and instead should take a defensive approach to any requests made to the device-side API and reauthenticate when a response with the `401` status code is received. + +## Device update check + +Once authorized, a device client will have access to the device-side API of the RDFM server. +The device client is expected to regularly poll for updates by utilizing the `/api/v1/update/check` endpoint. + +In the update check request, the device client must provide all of its local metadata. +The metadata, which consists of simple key/value pairs, uniquely describes the set of software and/or hardware present on the device, but may also represent other transient +properties not persisted in storage, such as temperature sensor values. + +When making the update check, the device client is advised to provide all of its metadata to the server in the update request. +At the time of writing, below three metadata properties are mandatory and must be present in all update checks: + +- `rdfm.software.version` - version identifier of the currently running software package +- `rdfm.hardware.devtype` - device type, used for limiting package compatibility only to a subset of devices +- `rdfm.hardware.macaddr` - MAC address of the device's main network interface + +For future compatibility, device clients are advised to provide all of their metadata, not only the mandatory keys, in the update check request. +For more details on the structure of an update check request, consult the [Update API Reference](api.rst#post--api-v1-update-check) + +When a new package is available, the response shall be as described in the API Reference, and a one-time download URL to the package is generated. +The device client shall use this URL to download and install, or in the case of clients capable of stream installation, directly install the package. +The device client **MUST** verify the hash of the package as described in the update check response. + +Additionally, the device client **MUST** verify whether the package contents look sane before attempting to install it. +The server shall never return a package that is not of the same device type as the one advertised by the client. +However, the server itself currently imposes **no limitations** on the binary contents of the packages themselves. + +## Management WebSocket + +If supported, the device may also connect to a device management WebSocket. +This provides additional management functionality of registered devices, such as reverse shell and file transfer. +To connect to the WebSocket, a device token is required to be provided in the `Authorization` header of the WebSocket handshake. +The format of the header is exactly the same as in other device routes and is described [in the API Reference chapter](api.rst#api-authentication). + +The general management flow is as follows: +1. Device connects to the management WebSocket: `/api/v1/devices/ws` +1. Device sends a `CapabilityReport` message indicating the capabilities it supports +1. Device reads incoming management messages from the server and handles them accordingly +1. Device may also send messages to the server to notify of certain situations + +### RDFM Management Protocol + +The management protocol is message-oriented and all messages are expected to be sent in WebSocket text mode. +Each message is a JSON object in the form: +```json +{ + "method": "", + "arg0": "...", + "arg1": {"...": "..."}, + "...": "..." +} +``` + +The type of message sent is identified by the `method` field. +The rest of the object fields are unspecified and depend on the specific message type. +Schema for messages used by the server can be found in `common/communication/src/request_models.py`. +On error during handling of a request, the server may return a custom WebSocket status code. +A list of status codes used by the server can be found in `common/communication/src/rdfm/ws.py`. + +### Capabilities + +A capability indicates what management functionality is supported by a device. +The device should report its capabilities using the `CapabilityReport` message immediately after connecting to the server. +By default it is assumed that the device does not provide any capabilities. + +#### Capability - `shell` + +This capability indicates that a device supports spawning a reverse shell. +The following methods must be supported by the device: +- `shell_attach` + +A device with the `shell` capability should react to `shell_attach` messages by connecting to a shell WebSocket at `/api/v1/devices//shell/attach/`. +This establishes a connection between the requesting manager and the device. +This WebSocket can then be used to stream the contents of the shell session and receive user input. +The format of messages sent over this endpoint is implementation defined. +However, generally the shell output/input are simply sent as binary WebSocket messages containing the standard output/input as raw bytes. diff --git a/_sources/system_overview.md.txt b/_sources/system_overview.md.txt new file mode 100644 index 0000000..13bd7f3 --- /dev/null +++ b/_sources/system_overview.md.txt @@ -0,0 +1,44 @@ +# System Architecture + +The reference architecture of an RDFM system consists of: + +- `RDFM Management Server` - handles device connections, packages, deployment, remote device management +- `Devices` - devices connect to a central management server and utilize the exposed `REST API` and device-server RDFM protocol for providing remote management functionality +- `Users` - individual users that are authenticated and allowed read-only/read-write access to resources exposed by the server + +The system architecture can be visualized as follows: + +:::{figure-md} summary +![Architecture summary](images/summary.png) + +Summary of the system architecture +::: + +## HTTP REST API + +For functionality not requiring a persistent connection, the server exposes an HTTP API. A complete list of available endpoints can be found +in the [RDFM Server API Reference](api.rst) chapter. The clients use this API to perform update checks. + +## Device-server RDFM Protocol + +The devices also maintain a persistent connection to the RDFM Management Server by utilizing JSON-based messages sent over a WebSocket route. +This is used to securely expose additional management functionality without directly exposing device ports to the Internet. + +Each message sent using the RDFM protocol is structured as follows: + +```text +0 h ++----------------------------+ +| utf-8 encoded JSON message | ++----------------------------+ +``` + +The message is a UTF-8 encoded JSON object, where each message is distinguished by the mandatory ``'method'`` field. + +An example request sent to the server may look like: + +``{'method': 'capability_report', 'capabilities': {'shell': True}}`` + +A response from the server may look like: + +``{'method': 'alert', 'alert': {'devices': ['d1', 'd2']}}`` diff --git a/_static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 b/_static/fonts/0053ba6958e79f26751eabb555bd73d0.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ab30100982f087a925abe34641b5909b145516b2 GIT binary patch literal 4728 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 G0%`y>M(+3k literal 0 HcmV?d00001 diff --git a/_static/fonts/029e176ad602329b4434892101db9cf3.woff2 b/_static/fonts/029e176ad602329b4434892101db9cf3.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..09e03c952296f0ba1e46dadb0ef38c0ad1950130 GIT binary patch literal 6044 zcmV;N7h~vmPew8T0RR9102iD94gdfE04SUQ02e?20RR9100000000000000000000 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 aZ;p=U&6{>stk@{AqPB76aF%E9h7|z0(@g*X literal 0 HcmV?d00001 diff --git a/_static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 b/_static/fonts/0948409a22b5979aa7e1ec20da9e61f1.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6b0b4afef95479fff34683895510f679a5ebd45a GIT binary patch literal 5604 zcmV3CmxKT719J?g1=`;t*omE%DVl-mx!FcDRLO)EiWF%n+<2k; z{z!elnfh#A=$8#;!oA;6mu@pLR*NAVtg+#ALiW^f_ySP*^v)3)`y^K*nwsVo&Xm+~ zQXtm}w{@t54sii7Pz1cVK=4512TJ*thqv4N+ieFDwlzzlR>%WV4EEd^apPH1)UnWjm&>+$*LSaQ!hypx2nQCG zLhtfhOk#69N}~|A+V6@oo&h)=sa=$6+}x!>ZvQJIv~sZo+&sCcwbt$t^Z{`^3im?U zGzV5L7lcE?JmoT2GI(_I)OJU03Sp2nPVKu#I*j4}be8S^GaAA1$oHNPDqaW|ooxlN zOP8jRzIHA0*giz>5qU$T-9tM7gdIV7ZC;utU75B_nj){mF7m#G&0oIiJ(RT-N~x^K zRL1-MvkcA~-9uXWIyi_X*x~g?(J*1ckSeMpaTbHLGXMu=2)=BaC|-QY%QkzXn`Wr*+Hdm{ zHA3A`0ksP*@{k~PC2Od$rkZQ1wYE~E$&iIq%X=EDIQ0SXdb?br?u4G(P14&4$%