pax_global_header00006660000000000000000000000064150352025660014516gustar00rootroot0000000000000052 comment=2711c0e02781eb9d97285c87197f1bafef75d0d1 rauc-hawkbit-updater-1.4/000077500000000000000000000000001503520256600154055ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/.github/000077500000000000000000000000001503520256600167455ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/.github/workflows/000077500000000000000000000000001503520256600210025ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/.github/workflows/codeql.yml000066400000000000000000000016351503520256600230010ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "master", "codeql" ] pull_request: branches: [ "master", "codeql" ] schedule: - cron: "23 8 * * 5" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: cpp queries: +security-and-quality - name: Install Dependencies run: | sudo apt-get update sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev - name: Build C Code run: | meson setup build meson compile -C build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:cpp" rauc-hawkbit-updater-1.4/.github/workflows/tests.yml000066400000000000000000000052211503520256600226670ustar00rootroot00000000000000name: tests on: [push, pull_request, workflow_dispatch] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: options: - -Dsystemd=enabled - -Dsystemd=disabled flags: - null - CFLAGS="-fsanitize=address -fsanitize=leak -g" LDFLAGS="-fsanitize=address -fsanitize=leak" steps: - name: Inspect environment run: | whoami gcc --version - uses: actions/checkout@v3 - name: Install required packages run: | sudo apt-get update sudo apt-get install meson libcurl4-openssl-dev libsystemd-dev libjson-glib-dev - name: Build (with ${{ matrix.options }} ${{ matrix.flags }}) run: | ${{ matrix.flags }} meson setup build ${{ matrix.options }} -Dwerror=true ninja -C build - name: Build release run: | ninja -C build dist - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install test dependencies run: | sudo apt-get install libcairo2-dev libgirepository1.0-dev nginx-full libnginx-mod-http-ndk libnginx-mod-http-lua python -m pip install --upgrade pip pip install -r test-requirements.txt - name: Login to DockerHub uses: docker/login-action@v2 if: github.ref == 'refs/heads/master' with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Update/launch hawkBit docker container run: | docker pull hawkbit/hawkbit-update-server docker run -d --name hawkbit -p ::1:8080:8080 -p 127.0.0.1:8080:8080 \ hawkbit/hawkbit-update-server \ --hawkbit.server.security.dos.filter.enabled=false \ --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 \ --server.forward-headers-strategy=NATIVE - name: Run test suite run: | ./test/wait-for-hawkbit-online ASAN_OPTIONS=fast_unwind_on_malloc=0 dbus-run-session -- pytest -v docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install required packages run: | sudo apt-get update sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev python3-sphinx python3-sphinx-rtd-theme doxygen - name: Meson Build documentation (Sphinx & Doxygen) run: | meson setup build ninja -C build docs/html ninja -C build doxygen uncrustify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run uncrustify check run: | ./uncrustify.sh git diff --exit-code rauc-hawkbit-updater-1.4/.gitignore000066400000000000000000000002361503520256600173760ustar00rootroot00000000000000# Auto gen files src/*-gen.c include/*-gen.h include/*.gch # build build/ # Backup files done by uncrustify .uncrustify/ *~ # tests venv/ test/__pycache__/ rauc-hawkbit-updater-1.4/.readthedocs.yaml000066400000000000000000000002471503520256600206370ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt rauc-hawkbit-updater-1.4/.uncrustify.cfg000066400000000000000000000012531503520256600203600ustar00rootroot00000000000000# Uncrustify-0.66.1-2-f9c285db string_replace_tab_chars = true sp_assign = add sp_func_proto_paren = remove sp_func_def_paren = remove sp_func_call_paren = remove sp_cond_ternary_short = remove indent_func_proto_param = true indent_func_param_double = true nl_fdef_brace = add align_keep_tabs = true sp_before_sparen = add # different from rauc: indent_with_tabs = 0 indent_func_call_param = false indent_func_def_param = false indent_switch_case = 0 # option(s) with 'not default' value: 13 # rauc-hawkbit-updater-1.4/CHANGES.rst000066400000000000000000000125121503520256600172100ustar00rootroot00000000000000Release 1.4 (released Jul 14, 2025) ----------------------------------- .. rubric:: Enhancements * Add support for hawkBit "download-only" deployments. [#160] (by Vyacheslav Yurkov) * Share connection between curl requests. [#170] (by Robin van der Gracht) * Add support for SSL client certificate authentication, see the new config options ``ssl_cert``, ``ssl_key`` and ``ssl_engine``. [#169] (by Florian Bezannier, Robin van der Gracht) * Add ``send_download_authentication`` option to allow disabling sending authentication data for bundle downloads (needed if external storage providers are used). [#174] (by Kevin Fardel) .. rubric:: Bug Fixes * Allow ``stream_bundle=true`` without setting ``bundle_download_location``. [#150] .. rubric:: Testing * Add ``workflow_dispatch`` trigger allowing manually triggered CI runs. [#154] * Print subprocess's stdout/stderr on timeout errors for debugging purposes. [#163] (by Vyacheslav Yurkov) * Add CodeQL workflow. [#167] * Add libcairo2-dev to test dependencies. [#182] * Bind hawkbit docker container and nginx to localhost only. [#185] * Drop hawkBit option ``anonymous.download.enabled`` removed in >= 0.8.0. [#190] * Use hawkBit's ``server.forward-headers-strategy=NATIVE`` option allowing a reverse proxy between rauc-hawkbit-updater and hawkBit. [#169] (by Robin van der Gracht) * Add SSL client certificate authentication tests using nginx with a test PKI. [#169] (by Robin van der Gracht) * Move nginx configs to dedicated files. [#188] * Make partial download tests more reliable with nginx lua scripting. [#188] * Fix non-root nginx execution in some rare cases. [#179] (by Thibaud Dufour) * Add test for ``send_download_authentication=false``. [#174] (by Kevin Fardel) .. rubric:: Documentation * Use correct ``stream_bundle`` configuration option in ``README.md``. [#145] (by Lukas Reinecke) * Improve documentation of ``stream_bundle`` configuration option. [#146] * Update links to hawkBit documentation. [#164] (by Vyacheslav Yurkov) * Mention minimal build requirements. [#167] * Fix readthedocs builds. [#167], [#173] * Provide full-blown config in ``README.md`` and minimal one in the reference documentation. [#195] .. rubric:: Build System * Lower ``warning_level`` to 2, because ``-Wpedantic`` is not supported for compiling GLib-based code. [#182] Release 1.3 (released Oct 14, 2022) ----------------------------------- .. rubric:: Enhancements * Add option ``stream_bundle=true`` to allow using RAUC's HTTP(S) bundle streaming capabilities instead of downloading and storing bundles separately. [#130] * Make error messages more consistent. [#138] .. rubric:: Build System * Switch to meson [#113] Release 1.2 (released Jul 1, 2022) ---------------------------------- .. rubric:: Enhancements * Let rauc-hawkbit-updater use the recent InstallBundle() DBus method instead of legacy Install() method. [#129] .. rubric:: Bug Fixes * Fixed NULL pointer dereference if build_api_url() is called for base deployment URL without having GLIB_USING_SYSTEM_PRINTF defined [#115] * Fixed compilation against musl by not including glibc-specific bits/types/struct_tm.h [#123] (by Zygmunt Krynicki) .. rubric:: Code * Drop some unused variables [#126] .. rubric:: Testing * Enable and fix testing for IPv6 addresses [#116] * Enhance test output by not aborting too early on process termination [#128] * Set proper names for python logger [#127] .. rubric:: Documentation * Corrected retry_wait default value in reference [#118] * Suggest using systemd-tmpfiles for creating and managing tmp directories as storage location for plain bundles [#124] (by Jonas Licht) * Update and clarify python3 venv usage and dependencies for testing [#125] Release 1.1 (released Nov 15, 2021) ----------------------------------- .. rubric:: Enhancements * RAUC hawkBit Updater does now handle hawkBit cancellation requests. This allows to cancel deployments that were not yet received/downloaded/installed. Once the installation has begun, cancellations are rejected. [#89] * RAUC hawkBit Updater now explicitly rejects deployments with multiple chunks/artifacts as these are conceptually unsupported by RAUC. [#103] * RAUC hawkBit Updater now implements waiting and retrying when receiving HTTP errors 409 (Conflict) or 429 (Too Many Requests) on DDI API calls. [#102] * Enable TCP keep-alive probing to recognize and deal with connection outages earlier. [#101] * New configuration options ``low_speed_time`` and ``low_speed_time`` allow to adjust the detection of slow connections to match the expected environmental conditions. [#101] * A new option ``resume_downloads`` allows to configure RAUC hawkBit Updater to resume aborted downloads if possible. [#101] * RAUC hawkBit Updater now evaluates the deployment API's 'skip' options for download and update (as e.g. used for maintenance window handling). Depending on what attributes are set, this will skip installation after download or even the entire update. [#111] .. rubric:: Testing * replaced manual injection of temporary env modification by monkeypatch fixture * test cases for all new features were added .. rubric:: Documentation * Added note on requirements for storage location when using plain bundle format Release 1.0 (released Sep 15, 2021) ----------------------------------- This is the initial release of RAUC hawkBit Updater. rauc-hawkbit-updater-1.4/Doxyfile.in000066400000000000000000000003751503520256600175250ustar00rootroot00000000000000PROJECT_NAME = "RAUC HawkBit updater" PROJECT_BRIEF = "The RAUC hawkBit updater is a simple commandline tool and daemon." OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT@ INPUT = @DOXYGEN_INPUT@ OPTIMIZE_OUTPUT_FOR_C = YES rauc-hawkbit-updater-1.4/LICENSE000066400000000000000000000624261503520256600164240ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author`s reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user`s freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users` freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library`s complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer`s own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user`s computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients` exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the library`s name and an idea of what it does. Copyright (C) year name of author This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob` (a library for tweaking knobs) written by James Random Hacker. signature of Ty Coon, 1 April 1990 Ty Coon, President of Vice That`s all there is to it! rauc-hawkbit-updater-1.4/README.md000066400000000000000000000111141503520256600166620ustar00rootroot00000000000000RAUC hawkBit Updater ==================== [![Build Status](https://github.com/rauc/rauc-hawkbit-updater/workflows/tests/badge.svg)](https://github.com/rauc/rauc-hawkbit-updater/actions) [![License](https://img.shields.io/badge/license-LGPLv2.1-blue.svg)](https://raw.githubusercontent.com/rauc/rauc-hawkbit-updater/master/LICENSE) [![CodeQL](https://github.com/rauc/rauc-hawkbit-updater/workflows/CodeQL/badge.svg)](https://github.com/rauc/rauc-hawkbit-updater/actions/workflows/codeql.yml) [![Documentation](https://readthedocs.org/projects/rauc-hawkbit-updater/badge/?version=latest)](https://rauc-hawkbit-updater.readthedocs.io/en/latest/?badge=latest) [![Matrix](https://img.shields.io/matrix/rauc:matrix.org?label=matrix%20chat)](https://app.element.io/#/room/#rauc:matrix.org) The RAUC hawkBit updater is a simple command-line tool/daemon written in C (glib). It is a port of the RAUC hawkBit Client written in Python. The daemon runs on your target and operates as an interface between the [RAUC D-Bus API](https://github.com/rauc/rauc) and the [hawkBit DDI API](https://github.com/eclipse/hawkbit). Quickstart ---------- The RAUC hawkBit updater is primarily meant to be used as a daemon, but it also allows you to do a one-shot instantly checking and install new software. To quickly get started with hawkBit server, follow [this](https://github.com/eclipse/hawkbit#getting-started) instruction. Setup target (device) configuration file: ```ini [client] hawkbit_server = hawkbit.example.com target_name = target-1234 auth_token = bhVahL1Il1shie2aj2poojeChee6ahShu #gateway_token = chietha8eiD8Ujaxerifoxoh6Aed1koof #ssl_key = pkcs11:token=mytoken;object=mykey #ssl_cert = /path/to/certificate.pem bundle_download_location = /tmp/bundle.raucb #tenant_id = DEFAULT #ssl = true #ssl_verify = true #ssl_engine = pkcs11 #connect_timeout = 20 #timeout = 60 #retry_wait = 300 #low_speed_time = 60 #low_speed_rate = 100 #resume_downloads = false #stream_bundle = false #post_update_reboot = false #log_level = message #send_download_authentication = true [device] product = Terminator model = T-1000 serialnumber = 8922673153 hw_revision = 2 key1 = value key2 = value ``` All key/values under [device] group are sent to hawkBit as data (attributes). The attributes in hawkBit can be used in target filters. Finally start the updater as daemon: ```shell $ ./rauc-hawkbit-updater -c config.conf ``` Debugging --------- When setting the log level to 'debug' the RAUC hawkBit client will print JSON payload sent and received. This can be done by using option -d. ```shell $ ./rauc-hawkbit-updater -d -c config.conf ``` Compile ------- Install build pre-requisites: * meson * libcurl * libjson-glib ```shell $ sudo apt-get update $ sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev ``` ```shell $ meson setup build $ ninja -C build ``` Test Suite ---------- Prepare test suite: ```shell $ sudo apt install libcairo2-dev libgirepository1.0-dev nginx-full libnginx-mod-http-ndk libnginx-mod-http-lua $ python3 -m venv venv $ source venv/bin/activate (venv) $ pip install --upgrade pip (venv) $ pip install -r test-requirements.txt ``` Run hawkBit docker container: ```shell $ docker pull hawkbit/hawkbit-update-server $ docker run -d --name hawkbit -p ::1:8080:8080 -p 127.0.0.1:8080:8080 \ hawkbit/hawkbit-update-server \ --hawkbit.server.security.dos.filter.enabled=false \ --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 \ --server.forward-headers-strategy=NATIVE ``` Run test suite: ```shell (venv) $ ./test/wait-for-hawkbit-online && dbus-run-session -- pytest -v ``` Pass `-o log_cli=true` to pytest in order to enable live logging for all test cases. Usage / Options --------------- ```shell $ /usr/bin/rauc-hawkbit-updater --help Usage: rauc-hawkbit-updater [OPTION?] Help Options: -h, --help Show help options Application Options: -c, --config-file Configuration file -v, --version Version information -d, --debug Enable debug output -r, --run-once Check and install new software and exit -s, --output-systemd Enable output to systemd ``` rauc-hawkbit-updater-1.4/build-uncrustify.sh000077500000000000000000000003201503520256600212470ustar00rootroot00000000000000#!/bin/sh set -ex cd `dirname $0` git clone https://github.com/uncrustify/uncrustify.git --branch uncrustify-0.68.1 .uncrustify cd .uncrustify mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release .. make rauc-hawkbit-updater-1.4/config.conf.example000066400000000000000000000033111503520256600211510ustar00rootroot00000000000000[client] # host or IP and optional port hawkbit_server = 10.10.0.254:8080 # true = HTTPS, false = HTTP ssl = false # validate ssl certificate (only use if ssl is true) ssl_verify = false # Tenant id tenant_id = DEFAULT # Target name (controller id) target_name = test-target # Security token auth_token = cb115a721af28f781b493fa467819ef5 # Or gateway_token can be used instead of auth_token #gateway_token = cb115a721af28f781b493fa467819ef5 # Or ssl key/cert locations if mTLS is used #ssl_engine = pkcs11 #ssl_key = pkcs11:token=mytoken;object=mykey #ssl_cert = /path/to/certificate.pem # Temporay file RAUC bundle should be downloaded to bundle_download_location = /tmp/bundle.raucb # Do not download bundle, let RAUC use its HTTP streaming feature instead #stream_bundle = true # time in seconds to wait before retrying retry_wait = 60 # connection timeout in seconds connect_timeout = 20 # request timeout in seconds timeout = 60 # time to be below "low_speed_rate" to trigger the low speed abort low_speed_time = 0 # average transfer speed to be below during "low_speed_time" seconds low_speed_rate = 0 # reboot after a successful update post_update_reboot = false # debug, info, message, critical, error, fatal log_level = message # Every key / value under [device] is sent to HawkBit (target attributes), # and can be used in target filter. [device] mac_address = ff:ff:ff:ff:ff:ff hw_revision = 2 model = T1 rauc-hawkbit-updater-1.4/docs/000077500000000000000000000000001503520256600163355ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/docs/Makefile000066400000000000000000000011711503520256600177750ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) rauc-hawkbit-updater-1.4/docs/changes.rst000066400000000000000000000001711503520256600204760ustar00rootroot00000000000000:tocdepth: 2 .. _changes: Changes in RAUC hawkBit Updater =============================== .. include:: ../CHANGES.rst rauc-hawkbit-updater-1.4/docs/conf.py000066400000000000000000000040411503520256600176330ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- project = 'RAUC hawkBit Updater' copyright = '2018-2025, Lasse Klok Mikkelsen, Enrico Jörns, Bastian Krause' author = 'Lasse Klok Mikkelsen, Enrico Jörns, Bastian Krause' # The full version, including alpha/beta/rc tags release = '1.4' # -- General configuration --------------------------------------------------- # The master toctree document. master_doc = 'index' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ ] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] rauc-hawkbit-updater-1.4/docs/contributing.rst000066400000000000000000000057511503520256600216060ustar00rootroot00000000000000Contributing ============ Thank you for thinking about contributing to RAUC hawkBit Updater! Various backgrounds and use-cases are essential for making RAUC hawkBit Updater work well for all users. The following should help you with submitting your changes, but don't let these guidelines keep you from opening a pull request. If in doubt, we'd prefer to see the code earlier as a work-in-progress PR and help you with the submission process. Workflow -------- - Changes should be submitted via a `GitHub pull request `_. - Try to limit each commit to a single conceptual change. - Add a signed-of-by line to your commits according to the `Developer's Certificate of Origin` (see below). - Check that the tests still work before submitting the pull request. Also check the CI's feedback on the pull request after submission. - When adding new features, please also add the corresponding documentation and test code. - If your change affects backward compatibility, describe the necessary changes in the commit message and update the examples where needed. Code ---- - Basically follow the Linux kernel coding style Documentation ------------- - Use `semantic linefeeds `_ in .rst files. Developer's Certificate of Origin --------------------------------- RAUC hawkBit Updater uses the `Developer's Certificate of Origin 1.1 `_ with the same `process `_ as used for the Linux kernel: Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. Then you just add a line (using ``git commit -s``) saying: Signed-off-by: Random J Developer using your real name (sorry, no pseudonyms or anonymous contributions). rauc-hawkbit-updater-1.4/docs/index.rst000066400000000000000000000014221503520256600201750ustar00rootroot00000000000000.. RAUC hawkBit Updater documentation master file, created by sphinx-quickstart on Thu Feb 4 07:40:09 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to RAUC hawkBit Updater's documentation! ================================================ .. toctree:: :glob: :numbered: :maxdepth: 1 using reference contributing changes The RAUC hawkBit updater is a simple commandline tool / daemon written in C (glib). The daemon runs on your target and operates as an interface between the `RAUC D-Bus API `_ and the `hawkBit DDI API `_. .. image:: media/rauc-hawkbit-updater-scheme.png :height: 300 :align: center rauc-hawkbit-updater-1.4/docs/media/000077500000000000000000000000001503520256600174145ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/docs/media/rauc-hawkbit-scheme.svg000066400000000000000000000401451503520256600237640ustar00rootroot00000000000000 image/svg+xml DDI-API(REST) RAUC API(D-Bus) RAUC rauc-hawkbit-updater hawkbit device server rauc-hawkbit-updater-1.4/docs/media/rauc-hawkbit-updater-scheme.png000066400000000000000000001075271503520256600254230ustar00rootroot00000000000000PNG  IHDRNO pHYsGtEXtSoftwarewww.inkscape.org< IDATxy|Tϙ}MEAT@qW%cZV[Jiħ*ZHX7$@T@֬sǝ$0If!_isﹿL%sЊ,Y}IױAe:xaAID'U%N@OV$#X`BDDDVT)-m^u"""^^}>m~ki4%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""J:i+Ij)`N^M9o}_?v16'tK1l g9l!Y2p27T@v 0pXH]sIAFJHP* E1v>n$c O}XOH,YNs%$,E=\bښ"a:p84Ŵ}!p| d71 ^'.""ӗt&;?Xn!0'%_7V(qFN۰_Ӏ4Cj3Eo3-(q W>:Ş#MNg'HdG}8PZT,(>a>9xӧIDD!N P0\Hi \aroIDD>wcJN,qt@oIDD>~^^!ayC%N"""u^20yZ3;;Xu0?7s^D/Á}ư`.ο@VN: aL0,@!Dx,"""gs, .:̱sEǔ @v>|dC""""8`1 kw$iI@nKozf%NzQ]벝S2m^ sȁ'Em\Z6[t""""x9T˜; ߖO' WwW?CΣ?g.>ъx89^;I '`6a: OWg X^#ʳ)5RV2Wko*w,/#C_'φrPrLrcRg_4[""""xoD> Mc?k" =ׁ1"yқED^G H)O. ; 7;H5kWaQH63iwniHB ^^Df{l686 2<7s&EDՄLu(V9x0!im,t*8UuZp$}K:ֶԴzyIWep0Ln t?|rw>+>ir.z/Iǘ5xizb[0$H+7[xj3nqz4kx*#Y!¢YY 0NWH+4{Fڔ ,瓛U%N% ;<8u?+/fN`S^ĨTDD$(g~o"pZ'J9YY{ܒ}^d:0[Ez0ZpXy3ܬpN,qCELb7nq|Ы_Θ ff!mxR R 8ܬ9K]9e= IDD$j9L_t26p%)@G#;}a.esyprwezFsь&5?Ibi0`cNU,kM/`%]pkv=I{'|JUǦ30P />X@tġ؆[l°|prWq)pK1;c _{&NLb pbư챬=_ XCvC=,=*P9ķ: opK+yYG0vPg~~@N;4;ݥ5U8W۫j߼}@X~r`GzG$.zL㐖h ]ˁh֣D4?cO:+ |@St!Xv/Kz2>cHM1uts>[ѯcmK*X_x Q}]Iq_ƆdUX˷Jٲ7z;b>wlCZRtڝ?6ﲣ9k`gLU,lSMͷJ(,.cƗG \u1J$&Zbm!?Eqٲ=|ruhõgcb طSο>=p9)UIowSpHMq1=y!d&q|L~zr~#~uFd=:$#Sw&3vUAYqCx">/,F`_>Z9lSD-v&ovrO6ф:GЯC2Ӓs#oWUP~~zڽz"")%w"͢oZ*m2u< E%|__,^W'գV\Qpw~WQ+wU5ľHM:ה fi%=.әkam ^[4_7D?Yol0il3Z,PpX|g<E%nR3{g6.]Z ),.׮©:^X\gPD$t@v>_*'-o˖cze=cmCX BúW%?y)~Ns*tHyGv#W=1U?ݼovEWkz E$WD-/,+,oڿ^#"Ғ0 ȵ#DW_;h~630wzj۴%ghNhU{0L-\$kzGQ[ޫ.iI_;ɽ}⺇ҿC$RD:8XB@aW$E%Oҵ 3tm_YGۥ9k:$G=yѪT8%? KQ5j|iePqhU`5QpJ;ckV||@c}}?%ghDi\i)8\Tp4%Mk>c=:d6eoYWd]8G{zOesңa E%Z9CrnӖe|7èNЩj±?G4)lSJ4(qV#a(s)p3TTK$\ulwikwטZ> Vle]0SK7rX^*AfҕS9_XWH$FqQb[չ YWX̎J$ O]rQdXٶ*qjoli6{DVy 8ݽQ>XjcPd rQi{ HsP~zYL:W ߭ыW 8D-!&BZvSxp-H>05`;[N4.?-/p,nl*MڸA1iw%`vTt NU=ZHW/6Ы}Z~%cDZΆOU(^7s+ۛ~Zݬ_cb}Jiپj>[Ɗ,ƈ^tkbhp}-xm#;3}:p7qzuN67M{OYp=k_TS߷;W~ri~`_hH[ͣ. c%N+XK5<&s܉%.AJ U 7밨]|+{wvh]J/,Fqy6~fOqg{i iIϙ9{;FE}ơ؉7IBa] پ<'l~׾yBN;#~aU#Xm'3Aݩv/);=p^^F1Pֺu7Sulu[ V_y>O;aqB?0{׿>d&,@zmyyc]!+WVͅeoo&a8FɬJ]_ovUܙKdx5{|ydSZU]jA摇d.Hcֽ'e"1'埄C1KɊfKx4k<XT'XMy>#czv נSSL8 ҪsS9n5zҕGˆZˬ͙]9owT/2k:ctJ|-9ߛFRoUx˺+/\{Tn;w}j'3u疓GN?ҏ]S`hw8* =󗥬+,WF*g#V%M£ooඅ+k\uXuz̏uz(n=׾?{HW;͕#$67y0)؋9xٻ'^^ec Xx ;wS?g cj >{**.gV/JC(-Y\_Xcy+GblCqEeGaoZϑU]r۾ Uܶxm tr&qoSV簫o=ɱT|i6WŸ>r7f'>zmu~hKM]Щ l(*sgrƔ0ekn:Vn ɝ7Og i,Y j̓}.Ν5{Um{RcHP+I7|Hl 7A&`̟pGI$|njiW߻I (K>ōir YIr6(""٬P1C@oȝpI:`p|S*X3":&#""dt,im3kə/3 XL4DZeo] u1{|d hgFpR֒3fo'7]B$N9 h[ur0'X&c$<^3Yah"""O" ~L݌VLmpF#!F2"] R}ٓY9 R n,hӴ|.yR*[] \p T*""t//`̜e[t{'r,hKIP9=;@ l>ۀUXc_2kf[+I^C 9@ܗ4q;Í;%p]Xzp?4sSaQK3O#""ڹXdO,L||t׋=0M_,}n@W n:Dق[f|Cy:$əBI {i'&M>1K g2y 8“,s wrT s9X~f͙BItHCƶŚ7~|q)3&Jb|k)&/dY4=S- eA=\s e}`kL_\*x~8m1""w d7{9e D,r^Id<?M/ZLx89p0smnIScqK\>$ `(N !j1ͩ*.bC.kgB|Y?K?JeaY =/ ("""O+f?} g dC,}ְYƳ*$"a& w?w,""""iSEDDDk,,~͵y,'BoסHטs'55@ס_dNgظM IDD$5[Uoqń'Zc ;bC#ñΡL_,}EZ[)`0JZb++qI$_5弒ƾ>`m+vŒ%c:itɤFFv{}am[l1-&<|S$""rS JCΘ f: *JA%P\DOkՉDHH8DHH8$9s~GRCDD\JDܔӼCDD\JD81CDD\JD#aH""'fh.Le DD>uL""Re I0D`"ͩOEI;~K$uk|6QH]ZP^ GJ(9FшH¯М$}ۛ8kb%""2<Ъ8w|:ė.V-kSV,,۟4۔/,/N>rߟdxƶk5XVq?_'D?q1| \ᣳpj4!>Ϳ&>p{ ɭ2q{*euFgS]t @nȱgTs9g^ yKE$F8@wȬ^࣐כ 0`i> IDcۼ"Gz MRBvo@r]k 5u[2jFϤ<08ꞻs0UڂًH3P$" wr" `f\;g@jxP;07yl.oIcJD19k1hr{KMX5Tz?͟'df4@42!5d0UeŠͦ&qN^TD$JDF^wz`_mDKns76y ꞯPEpm]m<Ϧ^pEQ'"P$"5C7C{ib4#8'5"q9N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N""""R$"""!%N" m t:9'il=`ׁ >N:I4l`&<%,N8 u"rК ܾWpDe/P, |SϹg^{JlP lM`&4*x <=D 8Hc <~E@J9sOoü?eͩdO@ZSq\ 7_ܤRVW7샛*bti$%N"-<mשoKp \ub^T 5u7)ZH^N5`ec#Qe;.N6n:Y(qh] op@TG{;<Y<> 7w(.7eIcYY| a/ȯuT'N1M ܤnn=_q*&H ~-M*Np~.n a>֐UQE,`pnoxHD3 ?)F_'v=gyǷ_}Dw~ܡB?[pJE#hN~:mo6WI '|BIm} C͉uH]P{p'Okunx@=N"_[1n;b%tyuUA24O8z%d|/"AJD$R}ρ=1l#.f\ʃcpt}͐3 BD8Hٮ]Z) p{s}zby![!~>Nb&ҪhDjxvU[z?0w³E =6Qm9@2z{k "`|^)M&֓H&R$"9k s'յ&W pdCSk݁kπ9P_%N"P$" ۨ\޸ CeE?_)ҧ-9 ,œ?#7w)p[蜲_DP$"َ6qOݕ'쎛 =ΧZ[|6D৸% YY"Ҭ4PD"?*kUf8] AzR6#8Ǹ0rO65(γDiND"w ,yH(`|ȱ=5~+4!xxwp|í]nu}?nفqk?m=k';ou9.c'Zs.B_,ivJD$QD'No)zɕ>a{X[Y)t[v1+݂_A~|;#9T~*"%N"<朥?UWnr.6>DU{Bz:gZu} /tyO{#ND'NI܂'GsՉMh05炯P =θINm:DUG}(FmH#irDWɄ}Z||G㒦XJMR_JD$zq'H\ ](-p^U?8A4lܧ 0 ItNnn׎08HcqxQ2xg\oJJDQ-0pZ'p{ )=IJD)V\xKK\BuvI*G "M 8 Z ""^PH8DHH8DHH8DHH8D9|4/i6~R޳YL-!c6x63|= 8Ou0")|qf?i'mqHDQzďR$"" \zHkgJL׻CqQrHu_> DZ@a$O6`Y&>=!meVٳ5-ML*=~Rۗ.@ +;x#Z$sߞ-KuzϨ7>GSߵ%{nIJ l u aXHMp-u=>=k9V?=e{y놎;^STTOeP?5XfE+=MD<CvPXXwq ]8Nk`L!gI)""5_iOD͖-[>ٳV[|>_'cƘ+?ڌHzD<BYkOoФ)_׹""[JD1zCD$N8x?{&Uuq}gf.t"b5$FIL/ !& h%$&vX,QEP;K[?Δ;3wafgm0 IDATy̽s g9{Y?p˖-ǫ=""R?'$w۶ADA`ODDHy<;>qOI$4Pkr$H(5m۟30MD,ZY7rODDHj8$"DR%ة2""MOI$@Y!I'ј9KmkND$ DR0TS]]2""I$"leY{]O] oԡ$"shh?˲>j戈H N")>e5$H |;7oa5GDDjQpI!. H)8 Hr)8XHeDDLI$ROU@D$DRL`S% Ӊ$H 6ɲdEDDDREdY!<I"',',K2""AI$EM!I N")j۶m_` HPpIQId7@Db mZv$*++޽{wq!""H ۊ SpJw?: G1dCP2kSd!"fmDqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\RpqIIDDD%'DDDD\%"]gEtJEvU KS?te8=Ɲ''<":DcߦjOY罙>z_"3ȑACu"""".Or.7t_tT/duX>}闏Y5ŕ_-=xysB+wYdvlեVO?J ս*al{~1ʚ85ˠOO^ʢbK_:E&L3zݽ%4* K7o=5fAky|@]o=AZ>e[ק9~PChyQu_RVg'_>5U웿u͉h#G缪wCIoz_F뇘Qq_mw)ni{^ջX{>':s9N7 3ﳤ} 6yN7=cSS^žO7 ߓwzD/rzNuQEof?ֹH!7C] oFs @@!(YYȎW%~;_fd'=]v2l{i k|SpJ!>mBW헧m_YZ0Ю4? t<m Ȍ¨lݞ:w{L5K丷o _~Xzy&.n^IvVYsjz,o`{ڌMςmGfTo}{̪zNNן2^*xvuS!%ޑg)C^>tu_?unc™t`YZԕN7 9f&Uu9}y>6>QGEw8޹C)w`ȟ{5ˤǝgFt0NGߩ<,9+o'۞!on-<⤮tDVk'zi|s[߱Np> z\,o?op_9η g%OS]d.k>KQj5+%~D$4TB( Ny{>he?Rl'9Sq쿳}K/OB_l'<@ᬺ=HA N}= {9)bńY7eMr{aSWT,a5/0︩|y3]6WsLz2ʝ%,m {}`Sf7ެ4וd8z6>6;Mb윱Meh37nC|Mprs[$vO^gs3y- s[[sdGYoSS\y&C{=5eU| V'Y˷B-m$ }Ulӧ<Ϊ߽xyu?_lǢ#hǵ/Yxlzc G>>t<'zbNeǮ}zf&gukA/` l:M]תּ~D$Bwzս%4>;:CnNlc /[}7pʊ_٩?]zdE!eۆ?Wݳ5Kyz-˪إxG`.&4$Wv_L_},^Z-O}1DSҕfcB_Զ6C.%oPr3j[_L]q z|^r/,mOY:U6z_{ǬM/qIϥOOf@bvm/Tl)bKp!ȢYOŮ寬Tl_9Oɷ;ؿ$yZ_~ 6RNx ms"'B,|zt*wv ?'t=~5+N~]}on:޹G|fC;F_;̢Yr mGaߣ|^5_$+ˆ渮%"V8kE/}}eNg|/cS_瘊E}}n\Ѣ-T- ;oe:7;0;kFVOV5~ gsjAf^ew6Ww^K޲vQzEuǶ\}˥E`sC)_^1Z[Ua Eof'k3:.f֪*(]Y+"Mޑ&46n ̋4+ԌiP@Si6CՊ uBj Y @Bt[(Oyuhb|@ýMϳSi }duo12 ;m>[?_TlOz\{;_zO?< 6F]7U:} [՛Q\>>O/Tiܻ` 3DDrypF]lQzp89u fȡIdW)`,kw1;C3z0|Pg[wY+B*=:+~Sb+<{>uTK>}U~}BsV|\JӍB=>:pǿsBB5~rœskvTU㺗7u{§)$_>s}ُ}*Z#tߛ΀'g8m~7C={6rgIhOO&ݱKff uU3WE)s̿>fm>`F_hufrcy=+)^m/@q{(,_69̢|^BͰқ߲W3"$>|)T.lnsڂ.NdukArYϢK vIzX)ڹ`i-h6CRxgˍ.02r͟U[Y3{o>ެvL{i~ҕ}%׽wn4@r*w٥LVuTKr|> g,gWciS)z5/u+C3l+_٧-|Ӱ6(dM|׋ȿܫ {)^e|;?vRv7͆ul* K ;^E?eЋא7}hz_mc_{,S%9}/fkKeuZs{q\h,;ҼiCb^GŮo&&>3:|<0"}mûz3(=? f[(ߴHoþ6xӓZvWꢊjʪشvyePv*w꬞T+g|B62:6㴵b~vy{P%NVʷ&{SG]&SSRIm';}(ȕ籫x ܭ$Vi ?𪳎m4HB=N)ͨ l dh6#N^guߧO^m妳/7Qvoddh:CTm1jZelXg9,Py&mqGp?F|3h37͇!>Zg꬞d4?+͇wm.<9}چ r^2gݫ 5eU|qӔ|kڕٹyp몋*'Q|G2-od/1a=k ڜׇWEQdҜwƛN NW? v ˠh6$\fS<>:|om/Q1vW=G}x2}Tp- Ny<4j<8+Qև띕8mF&eyS>xA;x?|x<>/+_Jh߬-ɿ@_Q{|1.0htfPL@[r T*%}LӻGg-\Ăs[N=͇wa_2ifC:nfVCaY98qJq: RQ0t[b+ #&KvV+)]'E&90[Ydjqy]͠bKQ 晔uڜ=Nʝ%&'~;&wjJ*gSfӧ-CɲY|Pe{cJӫ5T)tAj,8$8 !_QZn@ԔVE]xխuuySWq_^ fpjW){wmh1K;q@z۫]oNHWDDD%8HReՒ.cOM_/p5'"TD$w#˿?gD$(8HRpMH{uH)8HRU-? DD&$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""⒂K N"""".)8$"""/ 8dG9n;ezTYedEmlKvD N p/$rV3dEDD4T' DD$I$E=9eee1$2ӧOzDR?ϲQ$6GD7.KgZ%6-eĉ#H,kyCj$քd[+ ,`,kCʼnW<~t>$G2 ՉiӦz::GMV{D$qpm1z:4lhel'lXJ(8 [J3I c5'X ,v9pׄmʶ:dYmG<JND$ƽKs?6X&Ǫ:;X 8Ѷ%o”Q,nVH !8eᤆ 3oQ| ;$!ғ IDATln<ۛ͢$jej>mڴ>z&nR&b 4"YWW2(x^Ӂc@3`7b|!GT<Zܷ(id[]fRJ㮘) WQ6,Il+6qI1!}j ?n%/Wcl89g_3 `5x=4#vÝ[1P| O NxѾÚe?ָ=n♔6 dI)8ZejKFplľi@{'@F ~+v'Թ<6'6 ul4LI$*KSa5pC5?rܮN=8xnbQ0C` K߅ u'8.#y r0INg"f`#;@o$` 3 Bí+m uyl x"m&\̰)D^ v#e{j n8fϏQ X87X QpI!mm` wE4&nL-e%LK}k`>y>ន`K0Cf;}%voT5-CXB^[m +0=gwl{chCPcz0=a-%e9`2ptڋ 9oMhal<`Ye(2 ?XP780XLER9vl>ԟ' fI8lLM[L />M<(onЎ-0?'`, U$`O.|%\RX.(OczO=\@m\ \1~f.J\EI$E4xeY7E{`cO# ؘO1A$`A!<<6C vciL/ K 8|OIy[eV>$A<[o=U9lD:(C9$K+sl{p5+> =x1P`a1e I#R*KrnZ68PF&<>>œEǭScg1cp[r;y &'r3K8"D,P8a+{j=m3$pf@%<9LE^L!o{y1J3O2T:|Ѹu3 b!pwM76hmYg'˻H(8$Y ,}}nj,mrY_8, .y0a.SpMnv2.w~N1=EWl9ނ:uؘ%TcdGy~1h/ Ra\7,+MOI$S 0US' zcV.3NKۃ-1!4!i\p] )a,.kxk:ZpgbyLkI-.d)CN*oJ õ2FORy!U#r-N\ ')fLm'ʱa}X<Bj4,WM^QpIƖ!-Emqlg?Gc\d`,“]  mw[(Lݧp NwuN(Tb0zi^LM= 8@0߱}6#?sq e=0+)XC7Řn>\.a )` c^ǔQ8v#)<;! 5ujj N"Ie@Ku8ת{(Mk7FpPGuj{pS^S79=򘵘 L; qd[جNM> z RÖH<9m;'DX熎,kR:ϩ>LU:`zehlLɂXN0W[;ߩ5AН!Xf^0Η[aq,,Tc` fEn_3LnP\Mv94`pIzOAy48pD~tƄ=7s͎;mK? oK:N"I(CPcY cƌqƎ;euƽG*{5 <gz^s6迿f|>ÄS.!"`,bRֻ"O A3(|Fs1[]RcͿr3*ŁS-D^ FϢ@D^Մd[۶yGs4fY > >ZUa>Pq%nO<nAPBe& (f^E3v6b'ۢz٦<ÖėDc1c<ܙv7KDīEߛ0b3 M̱+!-hDx1c<`TR>EI#] S-n.a'uiN$ ƌPxMISA'6=MedcSORpIA ˮvG=6[&nMCu"""xl֘r҄DDD! nǑJCu""")nKq g{GI0'utrBDDDRȕ/ќ-[c3j6bQJ'p{Ws0j=] әZQοm+zI,'d6Ws<=Т/` /-MDIDD$ p4x}k=UbKONhN##oԦ$""tɆ/mUw:,F] l&%$"`4pS`''^F ގ[W<c[CLd7d6HG8q"wYX܂ͥԭ$MHCu"I`v$5HρY?}UPx|+&p[W8>uL *r!x+s<FԎDy=*1q"ɣxoH{8 ;nבJI$ x[O' ^&0E*̜/d1C}bqY]{s6o٘k!jT~* lw$ЋazHMG 0)A@8~CҀkCsjZDyQ|%+N2ϵF0Y l8N℉}l ZVV$eYo0m gx<1C*[abZP8&wb) ń0=6MVWӽ v<;LɅg0cpY=eaz_ X dC0uU3 \y%>rOZwh'w~]GrG<(a N"Ir->}*_vCv~9ZnA3&޽5MYxiαLфO\4/pZqc}2?G=y 3LTlx6}a=(8śD.q&,˪A51:8pcErJ0a.L?1f|ڜCooP74 lV9K'$m{eY?=3՞&\nKa;bW}aw{4!A/>eZTPlrlwKa6=#Z, Oh`#'$*-- '' xC DNn Fy<苹*d?bzunL/Yb#!,ڄuLnxm8is@II$nӧ;t 2^"+z'*893@99y.e1"c&ϧ0vbtfV rQnfJ i(8$L08e gL|KpgM"9W1K'p N`/,p|Blܥz^Y0fJPFS%)8$Y`pZLh*"<qY[&8/'aL"|?p\\,Ҁ0 NLbƹHL8{8kKCc?ŢHLŘMߒ϶0҆밸Gm񽣛qw2|PH %WYu!pz; &;pu3L¿ʁ9X́DK[y=3(S6}1wp#=Um0?#) 5f|̂*he$&NSaLh׫Wvzʗpt?->~u ^^DRGct`|P̒( ] מ .a;u{rN !p=D] }|uKb֢4p[5 Ovф{ܢS {4}a#Jɜd&E16ӁWazhN$E4bRm5!!8|F֧ dB\՘3!@{6s̛\{̺saK}WR݌{ N[0E9O v!+ц%jB33#u5yOXSж81M$"e u!^)̗yk̗eP dgh-:*pLwh 0QŠ30k6Y0`@~vόzct63Yі&$Zbs60f0swX8 6&$5j3o -Z$, `mi8iN$h:j.Nf'ؖ#e:| Nw{ĉx,7҄DRHYYٛ_`͏㯛=M3g(0.9"݅{UXަ&` U]=3 6KT$:'rY'aR ;wcYq5'IlQl-M5'y,n?76pELjJHá A,S{1Cu/^EYyj݆/wsm;6cq&㮉gj=$b)KP~!Nu1 JbLF 3L)se3z&z۰Y` KE};scfgVDD#},RDy1mr{Bd۱YFDvGP,Z3syam.*5X~2:w}{q 7ēW;/6,^n0oa|ļRZO똫u醿>%;zVZ[^wIWO6ISnWGksW=WNԪXl:p0Ij޸BȎI4ǭ+ZpjwwTnEG}蟸[ng/PI~!YvUcF+Tk[TCU #?M_AǤ\O5QGd=$vr>x{+ƾs5(C>az#=%; M-)L_mviѤA?ϯ -dok@_C8u-rTO$=vgQ4";Ẻ|>0C x`9T®3Wev):pڏI,Iߤ|%+4c؅^|6divR7wlMңV Ř]Y׬cu[nRIzp0W`tMM.^UGjviqx7𶠣i/ԙ=/b8wPt TӸK"ǛzY-{(tV3Siߣ_|Qzex/cUp{hV+F$ŃN vO*ZޏDv$qG{}pK;OJ%yz^K:d SsUd<;sVY 3|ݬ%ffI$N6O.|֣8掉=vń~pBUw3K}=?-Y=^ІY#X`do3D}LGț{r[;3SJ ㍇3m'JI+Uo.MԜwO& ]?0-J9Nw=WWL6|}ZڴBO@g5eI9νC4~[e(9)c>Nt~hGIzFbtNk}Z5RI1*W3F*8m h*T ͇{ݕ<8k{Uآo3T.4{tf.$#qT $WyES$ݖ>:ջ*42c+"^ :4<ߎ> K*Ub.2˥씎hriˑjݷr~DcBٿAM]C[TwZ55Am֚ep+G/l_ZUkEO6֐,7#xmqIn$I 1r|٣d?<[ó{UԪ5M%U^;/KY~,')TuCUիN-VD8߹تĢEU]"hY?WE\Ákx{_qp0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'# p0"'#OpKZ6MK[:]`` The IP or hostname of the hawkbit server to connect to (Punycode representation must be used for host names containing Unicode characters). The ``port`` can be provided optionally, separated by a colon. ``target_name=`` Unique ``name`` string to identify controller. ``auth_token=`` Controller-specific authentication token. This is set for each device individually. For details, refer to https://eclipse.dev/hawkbit/concepts/authentication/. .. note:: Either ``auth_token``, ``gateway_token`` or ``ssl_key``/``ssl_cert`` must be provided. ``gateway_token=`` Gateway authentication token. This is a tenant-wide token and must explicitly be enabled in hakwBit first. It is actually meant to authenticate a gateway that itself manages/authenticates multiple targets, thus use with care. For details, refer to https://eclipse.dev/hawkbit/concepts/authentication/. .. note:: Either ``auth_token``, ``gateway_token`` or ``ssl_key``/``ssl_cert`` must be provided. ``ssl_key=`` Set SSL private key for TLS/SSL client certificate authentication. Only used if ``ssl_cert`` is also set. See https://curl.se/libcurl/c/CURLOPT_SSLKEY.html. See also ``ssl_engine``, ``send_download_authentication``. .. note:: Either ``auth_token``, ``gateway_token`` or ``ssl_key``/``ssl_cert`` must be provided. ``ssl_cert=`` Set SSL client certificate for TLS/SSL client certificate authentication. Only used if ``ssl_key`` is also set. See https://curl.se/libcurl/c/CURLOPT_SSLCERT.html. See also ``ssl_engine``, ``send_download_authentication``. .. note:: Either ``auth_token``, ``gateway_token`` or ``ssl_key``/``ssl_cert`` must be provided. ``bundle_download_location=`` Full path to where the bundle should be downloaded to. E.g. set to ``/tmp/_bundle.raucb`` to let rauc-hawkbit-updater use this location within ``/tmp``. .. note:: Option can be ommited if ``stream_bundle`` is enabled. Optional options: ``tenant_id=`` ID of the tenant to connect to. Defaults to ``DEFAULT``. ``ssl=`` Whether to use SSL connections (``https``) or not (``http``). Defaults to ``true``. ``ssl_verify=`` Whether to enforce SSL verification or not. Defaults to ``true``. ``ssl_engine=`` Set OpenSSL engine/provider for TLS/SSL client certificate authentication. Only used if both ``ssl_key`` and ``ssl_cert`` are set. See https://curl.se/libcurl/c/CURLOPT_SSLENGINE.html. See also ``send_download_authentication``. ``connect_timeout=`` HTTP connection setup timeout [seconds]. Defaults to ``20`` seconds. Has no effect on bundle downloading when used with ``stream_bundle=true``. ``timeout=`` HTTP request timeout [seconds]. Defaults to ``60`` seconds. ``retry_wait=`` Time to wait before retrying in case an error occurred [seconds]. Defaults to ``300`` seconds. ``low_speed_time=`` Time to be below ``low_speed_rate`` to trigger the low speed abort. Defaults to ``60``. See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_TIME.html. Has no effect when used with ``stream_bundle=true``. ``low_speed_rate=`` Average transfer speed to be below during ``low_speed_time`` seconds to consider transfer as "too slow" and abort it. Defaults to ``100``. See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html. Has no effect when used with ``stream_bundle=true``. ``resume_downloads=`` Whether to resume aborted downloads or not. Defaults to ``false``. Has no effect when used with ``stream_bundle=true``. ``stream_bundle=`` Whether to install bundles via `RAUC's HTTP streaming installation support `_. Defaults to ``false``. rauc-hawkbit-updater does not download the bundle in this case, but rather hands the hawkBit bundle URL and the :ref:`authentication header ` to RAUC. .. important:: hawkBit's default configuration limits the number of HTTP range requests to ~1000 per action and 200 per second. Depending on the bundle size and bandwidth available, streaming a bundle might exceed these limitations. Starting hawkBit with ``--hawkbit.server.security.dos.filter.enabled=false`` ``--hawkbit.server.security.dos.maxStatusEntriesPerAction=-1`` disables these limitations. .. note:: hawkBit generates an "ActionStatus" for each range request, see `this hawkBit issue `_. ``post_update_reboot=`` Whether to reboot the system after a successful update. Defaults to ``false``. .. important:: Note that this results in an immediate reboot without contacting the system manager and without terminating any processes or unmounting any file systems. This may result in data loss. ``log_level=`` Log level to print, where ``level`` is a string of * ``debug`` * ``info`` * ``message`` * ``critical`` * ``error`` * ``fatal`` Defaults to ``message``. ``send_download_authentication=`` Whether to send authentication data (token or client certificate) for download requests. hawkBit can be configured to use external storage providers for artifact downloads. rauc-hawkbit-updater's default behavior is to send authentication data, same as for all other DDI API requests. Sending unexpected authentication data can lead to errors in such configuration (e.g. on Azure Blob Storage or AWS S3). Defaults to ``true``. .. _keyring-section: **[device] section** This section allows to set a custom list of key-value pairs that will be used as config data target attribute for device registration. They can be used for target filtering. .. important:: The [device] section is mandatory and at least one key-value pair must be configured. rauc-hawkbit-updater-1.4/docs/release-checklist.txt000066400000000000000000000021411503520256600224630ustar00rootroot00000000000000Release Process rauc-hawkbit-updater ==================================== Preparation ----------- - check for GitHub milestone to be completed - review & merge open PRs if necessary - update CHANGES.rst - update version in docs/conf.py and meson.build - create preparation PR, merge PR Release ------- - update release date in CHANGES.rst and commit - create signed git tag:: git tag -m 'release v1.0' -s -u 925F79DAA74AF221 v1.0 - create release tar archive:: meson setup build ninja -C build dist The resulting archive will be placed at build/meson-dist/rauc-hawkbit-updater-.tar.xz - sign (and verify) source archive:: gpg --detach-sign -u 925F79DAA74AF221 --armor build/meson-dist/rauc-hawkbit-updater-.tar.xz gpg --verify build/meson-dist/rauc-hawkbit-updater-.tar.xz.asc - push master commit (if necessary) - push signed git tag - Creating GitHub release - Start creating release from git tag - upload source archive and signature - add release text using CHANGES:: pandoc -f rst -t markdown_github CHANGES.rst - Submit release button rauc-hawkbit-updater-1.4/docs/requirements.in000066400000000000000000000001251503520256600214060ustar00rootroot00000000000000# use pip-compile requirements.in to update requirements.txt sphinx sphinx-rtd-theme rauc-hawkbit-updater-1.4/docs/requirements.txt000066400000000000000000000021701503520256600216210ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile requirements.in # alabaster==0.7.16 # via sphinx babel==2.14.0 # via sphinx certifi==2024.7.4 # via requests charset-normalizer==3.3.2 # via requests docutils==0.20.1 # via # sphinx # sphinx-rtd-theme idna==3.7 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.6 # via sphinx markupsafe==2.1.5 # via jinja2 packaging==24.0 # via sphinx pygments==2.17.2 # via sphinx requests==2.32.4 # via sphinx snowballstemmer==2.2.0 # via sphinx sphinx==7.2.6 # via # -r requirements.in # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==2.0.0 # via -r requirements.in sphinxcontrib-applehelp==1.0.8 # via sphinx sphinxcontrib-devhelp==1.0.6 # via sphinx sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.7 # via sphinx sphinxcontrib-serializinghtml==1.1.10 # via sphinx urllib3==2.5.0 # via requests rauc-hawkbit-updater-1.4/docs/using.rst000066400000000000000000000100631503520256600202140ustar00rootroot00000000000000Using the RAUC hawkbit Updater ============================== .. _authentication-section: Authentication -------------- Target token ^^^^^^^^^^^^ As described on the `hawkBit Authentication page `_ in the "DDI API Authentication Modes" section, a device can be authenticated with a security token. A security token can be either a "Target" token or a "Gateway" token. The "Target" security token is specific to a single target defined in hawkBit. In the RAUC hawkBit updater's configuration file it's referred to as ``auth_token``. Gateway token ^^^^^^^^^^^^^ Targets can also be connected through a gateway which manages the targets directly and as a result these targets are indirectly connected to the hawkBit update server. The "Gateway" token is used to authenticate this gateway and allow it to manage all the targets under its tenant. With RAUC hawkBit updater such token can be used to authenticate all targets on the server. I.e. same gateway token can be used in a configuration file replicated on many targets. In the RAUC hawkBit updater's configuration file it's called ``gateway_token``. Although gateway token is very handy during development or testing, it's recommended to use this token with care because it can be used to authenticate any device. Mutual TLS with client key/certificate ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ hawkBit also offers a certificate-based authentication mechanism, also known as mutual TLS (mTLS), which eliminates the need to share a security token with the server. This is the preferred authentication mode for targets connecting to bosch-iot-suite.com. The target needs to send a complete (self-contained) certificate chain along with the request which is then validated by a trusted reverse proxy. The certificate chain can contain multiple certificates, e.g. a target-specific client certificate, an intermediate certificate, and a root certificate. A full certificate chain is required because the reverse proxy only keeps fingerprints of issuer(s) certificates. In RAUC hawkBit updater's configuration file the options are called ``ssl_key`` and ``ssl_cert``. They need to be set to the target's private key and a full certificate chain. If a file is supplied it needs to be in PEM format. Optionally, the ``ssl_engine`` option can be set if an OpenSSL engine needs to be loaded to access the private key. In that case the format of the value supplied to ``ssl_key`` depends on the engine configured. Streaming Support ----------------- By default, rauc-hawkbit-updater downloads the bundle to a temporary storage location and then invokes RAUC to install the bundle. In order to save bundle storage and also potentially download bandwidth (when combined with adaptive updates), rauc-hawkbit-updater can also leverage `RAUC's built-in HTTP streaming support `_. To enable it, set ``stream_bundle=true`` in the :ref:`sec_ref_config_file`. .. note:: rauc-hawkbit-updater will add required authentication headers and options to its RAUC D-Bus `InstallBundle API call `_. Plain Bundle Support -------------------- RAUC takes ownership of `plain format bundles `_ during installation. Thus rauc-hawkbit-updater can remove these bundles after installation only if it they are located in a directory belonging to the user executing rauc-hawkbit-updater. systemd Example ^^^^^^^^^^^^^^^ To store the bundle in such a directory, a configuration file for systemd-tmpfiles can be created and placed in ``/usr/lib/tmpfiles.d/rauc-hawkbit-updater.conf``. This tells systemd-tmpfiles to create a directory in ``/tmp`` with proper ownership: .. code-block:: cfg d /tmp/rauc-hawkbit-updater - rauc-hawkbit rauc-hawkbit - - The bundle location needs to be set in rauc-hawkbit-updater's config: .. code-block:: cfg bundle_download_location = /tmp/rauc-hawkbit-updater/bundle.raucb rauc-hawkbit-updater-1.4/include/000077500000000000000000000000001503520256600170305ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/include/config-file.h000066400000000000000000000050001503520256600213560ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __CONFIG_FILE_H__ #define __CONFIG_FILE_H__ #include /** * @brief struct that contains the Rauc HawkBit configuration. */ typedef struct Config_ { gchar* hawkbit_server; /**< hawkBit host or IP and port */ gboolean ssl; /**< use https or http */ gboolean ssl_verify; /**< verify https certificate */ gchar* ssl_key; /**< SSL/TLS authentication private key */ gchar* ssl_cert; /**< SSL/TLS client certificate */ gchar* ssl_engine; /**< SSL engine to use with ssl_key */ gboolean post_update_reboot; /**< reboot system after successful update */ gboolean resume_downloads; /**< resume downloads or not */ gboolean stream_bundle; /**< streaming installation or not */ gchar* auth_token; /**< hawkBit target security token */ gchar* gateway_token; /**< hawkBit gateway security token */ gchar* tenant_id; /**< hawkBit tenant id */ gchar* controller_id; /**< hawkBit controller id*/ gchar* bundle_download_location; /**< file to download rauc bundle to */ int connect_timeout; /**< connection timeout */ int timeout; /**< reply timeout */ int retry_wait; /**< wait between retries */ int low_speed_time; /**< time to be below the speed to trigger low speed abort */ int low_speed_rate; /**< low speed limit to abort transfer */ GLogLevelFlags log_level; /**< log level */ GHashTable* device; /**< Additional attributes sent to hawkBit */ gboolean send_download_authentication; /**< Send security header in download requests */ } Config; /** * @brief Get Config for config_file. * * @param[in] config_file String value containing path to config file * @param[out] error Error * @return Config on success, NULL otherwise (error is set) */ Config* load_config_file(const gchar *config_file, GError **error); /** * @brief Frees the memory allocated by a Config * * @param[in] config Config to free */ void config_file_free(Config *config); G_DEFINE_AUTOPTR_CLEANUP_FUNC(Config, config_file_free) #endif // __CONFIG_FILE_H__ rauc-hawkbit-updater-1.4/include/hawkbit-client.h000066400000000000000000000133641503520256600221150ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __HAWKBIT_CLIENT_H__ #define __HAWKBIT_CLIENT_H__ #include #include #include #include "config-file.h" #define RHU_HAWKBIT_CLIENT_ERROR rhu_hawkbit_client_error_quark() GQuark rhu_hawkbit_client_error_quark(void); typedef enum { RHU_HAWKBIT_CLIENT_ERROR_ALREADY_IN_PROGRESS, RHU_HAWKBIT_CLIENT_ERROR_JSON_RESPONSE_PARSE, RHU_HAWKBIT_CLIENT_ERROR_MULTI_CHUNKS, RHU_HAWKBIT_CLIENT_ERROR_MULTI_ARTIFACTS, RHU_HAWKBIT_CLIENT_ERROR_DOWNLOAD, RHU_HAWKBIT_CLIENT_ERROR_STREAM_INSTALL, RHU_HAWKBIT_CLIENT_ERROR_CANCELATION, } RHUHawkbitClientError; // uses CURLcode as error codes #define RHU_HAWKBIT_CLIENT_CURL_ERROR rhu_hawkbit_client_curl_error_quark() GQuark rhu_hawkbit_client_curl_error_quark(void); // uses HTTP codes as error codes #define RHU_HAWKBIT_CLIENT_HTTP_ERROR rhu_hawkbit_client_http_error_quark() GQuark rhu_hawkbit_client_http_error_quark(void); #define HAWKBIT_USERAGENT "rauc-hawkbit-c-agent/1.0" #define DEFAULT_CURL_REQUEST_BUFFER_SIZE 512 #define DEFAULT_CURL_DOWNLOAD_BUFFER_SIZE 64 * 1024 // 64KB extern gboolean run_once; /**< only run software check once and exit */ /** * @brief HTTP methods. */ enum HTTPMethod { GET, HEAD, PUT, POST, PATCH, DELETE }; enum ActionState { ACTION_STATE_NONE, ACTION_STATE_CANCELED, ACTION_STATE_ERROR, ACTION_STATE_SUCCESS, ACTION_STATE_PROCESSING, ACTION_STATE_DOWNLOADING, ACTION_STATE_INSTALLING, ACTION_STATE_CANCEL_REQUESTED, }; /** * @brief struct that contains the context of an HawkBit action. */ struct HawkbitAction { gchar *id; /**< HawkBit action id */ GMutex mutex; /**< mutex used for accessing all other members */ enum ActionState state; /**< state of this action */ GCond cond; /**< condition on state */ }; /** * @brief struct containing the payload and size of REST body. */ typedef struct RestPayload_ { gchar *payload; /**< string representation of payload */ size_t size; /**< size of payload */ } RestPayload; /** * @brief struct containing data about an artifact that is currently being deployed. */ typedef struct Artifact_ { gchar *name; /**< name of software */ gchar *version; /**< software version */ gint64 size; /**< size of software bundle file */ gchar *download_url; /**< download URL of software bundle file */ gchar *feedback_url; /**< URL status feedback should be sent to */ gchar *sha1; /**< sha1 checksum of software bundle file */ gchar *maintenance_window; /**< maintenance flag, possible values: available, unavailable, null */ gboolean do_install; /**< whether the installation should be started or not */ } Artifact; /** * @brief struct containing the new downloaded file. */ struct on_new_software_userdata { GSourceFunc install_progress_callback; /**< callback function to be called when new progress */ GSourceFunc install_complete_callback; /**< callback function to be called when installation is complete */ gchar *file; /**< downloaded new software file */ gchar *auth_header; /**< authentication header for bundle streaming */ gchar *ssl_key; /**< authentication key for bundle streaming */ gchar *ssl_cert; /**< authentication certificate for bundle streaming */ gboolean ssl_verify; /**< whether to ignore server cert verification errors */ gboolean install_success; /**< whether the installation succeeded or not (only meaningful for run_once mode!) */ }; /** * @brief struct containing the result of the installation. */ struct on_install_complete_userdata { gboolean install_success; /**< status of installation */ }; /** * @brief Pass config, callback for installation ready and initialize libcurl. * Intended to be called from program's main(). * * @param[in] config Config* to make global * @param[in] on_install_ready GSourceFunc to call after artifact download, to * trigger RAUC installation */ void hawkbit_init(Config *config, GSourceFunc on_install_ready); /** * @brief Sets up timeout and event sources, initializes and runs main loop. * * @return numeric return code, to be returned by main() */ int hawkbit_start_service_sync(); /** * @brief Callback for install thread, sends msg as progress feedback to * hawkBit. * * @param[in] msg Progress message * @return G_SOURCE_REMOVE is always returned */ gboolean hawkbit_progress(const gchar *msg); /** * @brief Callback for install thread, sends installation feedback to hawkBit. * * @param[in] ptr on_install_complete_userdata* containing set install_success * @return G_SOURCE_REMOVE is always returned */ gboolean install_complete_cb(gpointer ptr); /** * @brief Frees the memory allocated by a RestPayload * * @param[in] payload RestPayload to free */ void rest_payload_free(RestPayload *payload); /** * @brief Frees the memory allocated by an Artifact * * @param[in] artifact Artifact to free */ void artifact_free(Artifact *artifact); G_DEFINE_AUTOPTR_CLEANUP_FUNC(RestPayload, rest_payload_free) G_DEFINE_AUTOPTR_CLEANUP_FUNC(Artifact, artifact_free) #endif // __HAWKBIT_CLIENT_H__ rauc-hawkbit-updater-1.4/include/json-helper.h000066400000000000000000000033501503520256600214300ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __JSON_HELPER_H__ #define __JSON_HELPER_H__ #include #include #include /** * @brief Get the string inside the first JsonNode element matching path in json_node. * * @param[in] json_node JsonNode to evaluate expression on * @param[in] path JSONPath expression * @param[out] error Error * @return gchar*, string value (must be freed), NULL on error (error set) */ gchar* json_get_string(JsonNode *json_node, const gchar *path, GError **error); /** * @brief Get the integer inside the first JsonNode element matching path in json_node. * * @param[in] json_node JsonNode to evaluate expression on * @param[in] path JSONPath expression * @param[out] error Error * @return gint64, integer value, 0 on error (error set) */ gint64 json_get_int(JsonNode *json_node, const gchar *path, GError **error); /** * @brief Get the JsonArray inside the first JsonNode element matching path in json_node. * * @param[in] json_node JsonNode to evaluate expression on * @param[in] path JSONPath expression * @param[out] error Error * @return JsonArray*, array (must be freed), NULL on error (error set) */ JsonArray* json_get_array(JsonNode *json_node, const gchar *path, GError **error); /** * @brief Check if the given path matches an element in json_node. * * @param[in] json_node JsonNode to evaluate expression on * @param[in] path JSONPath expression * @return gboolean, TRUE if path matches an element, FALSE otherwise */ gboolean json_contains(JsonNode *root, gchar *key); #endif // __JSON_HELPER_H__ rauc-hawkbit-updater-1.4/include/log.h000066400000000000000000000010471503520256600177640ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __LOG_H__ #define __LOG_H__ #include #ifdef WITH_SYSTEMD #include #endif /** * @brief Setup Glib log handler * * @param[in] domain Log domain * @param[in] level Log level * @param[in] p_output_to_systemd output to systemd journal */ void setup_logging(const gchar *domain, GLogLevelFlags level, gboolean output_to_systemd); #endif // __LOG_H__ rauc-hawkbit-updater-1.4/include/rauc-installer.h000066400000000000000000000051021503520256600221240ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __RAUC_INSTALLER_H__ #define __RAUC_INSTALLER_H__ #include /** * @brief struct that contains the context of an Rauc installation. */ struct install_context { gchar *bundle; /**< Rauc bundle file to install */ gchar *auth_header; /**< Authentication header for bundle streaming */ gchar *ssl_key; /**< SSL client authentication key */ gchar *ssl_cert; /**< SSL client authentication certificate */ gboolean ssl_verify; /**< Whether to ignore server cert verification errors */ GSourceFunc notify_event; /**< Callback function */ GSourceFunc notify_complete; /**< Callback function */ GMutex status_mutex; /**< Mutex used for accessing status_messages */ GQueue status_messages; /**< Queue of status messages from Rauc DBUS */ gint status_result; /**< The result of the installation */ GMainLoop *mainloop; /**< The installation GMainLoop */ GMainContext *loop_context; /**< GMainContext for the GMainLoop */ gboolean keep_install_context; /**< Whether the installation thread should free this struct or keep it */ }; /** * @brief RAUC install bundle * * @param[in] bundle RAUC bundle file (.raucb) to install. * @param[in] auth_header Authentication header on HTTP streaming installation or NULL on normal * installation. * @param[in] ssl_key Client authentication key or NULL on normal installation. * @param[in] ssl_cert Client authentication certificate or NULL on normal installation. * @param[in] ssl_verify Whether to ignore server cert verification errors. * @param[in] on_install_notify Callback function to be called with status info during * installation. * @param[in] on_install_complete Callback function to be called with the result of the * installation. * @param[in] wait Whether to wait until install thread finished or not. * @return for wait=TRUE, TRUE if installation succeeded, FALSE otherwise; for * wait=FALSE TRUE is always returned immediately */ gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gchar *ssl_key, gchar *ssl_cert, gboolean ssl_verify, GSourceFunc on_install_notify, GSourceFunc on_install_complete, gboolean wait); #endif // __RAUC_INSTALLER_H__ rauc-hawkbit-updater-1.4/include/sd-helper.h000066400000000000000000000024321503520256600210650ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) */ #ifndef __SD_HELPER_H__ #define __SD_HELPER_H__ #include #include #include /** * @brief Binding GSource and sd_event together. */ struct SDSource { GSource source; sd_event *event; GPollFD pollfd; }; /** * @brief Attach GSource to GMainLoop * * @param[in] source Glib GSource * @param[in] loop GMainLoop the GSource should be attached to. * @return 0 on success, value != 0 on error * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GMainLoop * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ int sd_source_attach(GSource *source, GMainLoop *loop); /** * @brief Create GSource from a sd_event * * @param[in] event Systemd event that should be converted to a Glib GSource * @return the newly-created GSource * @see https://www.freedesktop.org/software/systemd/man/sd-event.html * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ GSource * sd_source_new(sd_event *event); G_DEFINE_AUTOPTR_CLEANUP_FUNC(sd_event, sd_event_unref) #endif // __SD_HELPER_H__ rauc-hawkbit-updater-1.4/meson.build000066400000000000000000000052241503520256600175520ustar00rootroot00000000000000project( 'rauc-hawkbit-updater', 'c', version : '1.4', meson_version : '>=0.50', default_options: [ 'warning_level=2', ], license : 'LGPL-2.1-only', ) conf = configuration_data() conf.set_quoted('PROJECT_VERSION', meson.project_version()) libcurldep = dependency('libcurl', version : '>=7.47.0') giodep = dependency('gio-2.0', version : '>=2.26.0') giounixdep = dependency('gio-unix-2.0', version : '>=2.26.0') jsonglibdep = dependency('json-glib-1.0') incdir = include_directories('include') sources_updater = [ 'src/rauc-hawkbit-updater.c', 'src/rauc-installer.c', 'src/config-file.c', 'src/hawkbit-client.c', 'src/json-helper.c', 'src/log.c', ] c_args = ''' -Wbad-function-cast -Wcast-align -Wdeclaration-after-statement -Wformat=2 -Wshadow -Wno-unused-parameter -Wno-missing-field-initializers '''.split() add_project_arguments(c_args, language : 'c') systemddep = dependency('systemd', required : get_option('systemd')) libsystemddep = dependency('libsystemd', required : get_option('systemd')) if systemddep.found() conf.set('WITH_SYSTEMD', '1') sources_updater += 'src/sd-helper.c' systemdsystemunitdir = get_option('systemdsystemunitdir') if systemdsystemunitdir == '' systemdsystemunitdir = systemddep.get_pkgconfig_variable('systemdsystemunitdir') endif install_data('script/rauc-hawkbit-updater.service', install_dir : systemdsystemunitdir) endif gnome = import('gnome') dbus = 'rauc-installer-gen' dbus_ifaces = files('src/rauc-installer.xml') dbus_sources = gnome.gdbus_codegen( dbus, sources : dbus_ifaces, interface_prefix : 'de.pengutronix.rauc.', namespace: 'R', ) config_h = configure_file( output : 'config.h', configuration : conf ) add_project_arguments('-include' + meson.current_build_dir() / 'config.h', language: 'c') doxygen = find_program('doxygen', required : get_option('apidoc')) if doxygen.found() doc_config = configuration_data() doc_config.set('DOXYGEN_OUTPUT', meson.current_build_dir() / 'doxygen') doc_config.set('DOXYGEN_INPUT', meson.current_source_dir() / 'src' + ' ' + meson.current_source_dir() / 'include') doxyfile = configure_file(input : 'Doxyfile.in', output : 'Doxyfile', configuration : doc_config, install : false) custom_target('doxygen', output : 'doxygen', input : doxyfile, command : [doxygen, '@INPUT@'], depend_files : sources_updater, build_by_default : get_option('apidoc').enabled(), ) endif subdir('docs') executable('rauc-hawkbit-updater', sources_updater, dbus_sources, config_h, dependencies : [libcurldep, giodep, giounixdep, jsonglibdep, libsystemddep], include_directories : incdir, install: true) rauc-hawkbit-updater-1.4/meson_options.txt000066400000000000000000000007461503520256600210510ustar00rootroot00000000000000# feature options option( 'systemd', type : 'feature', value : 'disabled', description : 'Build for systemd (sd-notify support)') option( 'doc', type : 'feature', value : 'auto', description : 'Build user documentation') option( 'apidoc', type : 'feature', value : 'auto', description : 'Build API documentation (doxygen)') # path options option( 'systemdsystemunitdir', type : 'string', value : '', description : 'Directory for systemd service files') rauc-hawkbit-updater-1.4/pytest.ini000066400000000000000000000001601503520256600174330ustar00rootroot00000000000000[pytest] log_file_level = INFO log_format = %(levelname)s %(name)s %(message)s addopts = --show-capture=log -rs rauc-hawkbit-updater-1.4/script/000077500000000000000000000000001503520256600167115ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/script/LICENSE.0BSD000066400000000000000000000011371503520256600204070ustar00rootroot00000000000000Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. rauc-hawkbit-updater-1.4/script/hawkbit_mgmt.py000077500000000000000000000457771503520256600217670ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: 0BSD # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix # SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix import time import attr import requests as r class HawkbitError(Exception): pass class HawkbitIdStore(dict): """dict raising a HawkbitMgmtTestClient related error on KeyError.""" def __getitem__(self, key): try: return super().__getitem__(key) except KeyError: raise HawkbitError(f'{key} not yet created via HawkbitMgmtTestClient') @attr.s(eq=False) class HawkbitMgmtTestClient: """ Test oriented client for hawkBit's Management API. Does not cover the whole Management API, only the parts required for the rauc-hawkbit-updater test suite. https://eclipse.dev/hawkbit/apis/management_api/ """ host = attr.ib(validator=attr.validators.instance_of(str)) port = attr.ib(validator=attr.validators.instance_of(int)) username = attr.ib(default='admin', validator=attr.validators.instance_of(str)) password = attr.ib(default='admin', validator=attr.validators.instance_of(str)) version = attr.ib(default=1.0, validator=attr.validators.instance_of(float)) def __attrs_post_init__(self): self.url = f'http://{self.host}:{self.port}/rest/v1/{{endpoint}}' self.id = HawkbitIdStore() def get(self, endpoint: str): """ Performs an authenticated HTTP GET request on `endpoint`. Endpoint can either be a full URL or a path relative to /rest/v1/. Expects and returns the JSON response. """ url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) req = r.get( url, headers={'Content-Type': 'application/json;charset=UTF-8'}, auth=(self.username, self.password) ) if req.status_code != 200: try: raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') except: raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') return req.json() def post(self, endpoint: str, json_data: dict = None, file_name: str = None): """ Performs an authenticated HTTP POST request on `endpoint`. If `json_data` is given, it is sent along with the request and JSON data is expected in the response, which is in that case returned. If `file_name` is given, the file's content is sent along with the request and JSON data is expected in the response, which is in that case returned. json_data and file_name must not be specified in the same call. Endpoint can either be a full URL or a path relative to /rest/v1/. """ assert not (json_data and file_name) url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) files = {'file': open(file_name, 'rb')} if file_name else None headers = {'Content-Type': 'application/json;charset=UTF-8'} if json_data else None req = r.post( url, headers=headers, auth=(self.username, self.password), json=json_data, files=files ) if not 200 <= req.status_code < 300: try: raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') except: raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') if json_data or file_name: return req.json() return None def put(self, endpoint: str, json_data: dict): """ Performs an authenticated HTTP PUT request on `endpoint`. `json_data` is sent along with the request. `endpoint` can either be a full URL or a path relative to /rest/v1/. """ url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) req = r.put( url, auth=(self.username, self.password), json=json_data ) if not 200 <= req.status_code < 300: try: raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') except: raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') def delete(self, endpoint: str): """ Performs an authenticated HTTP DELETE request on endpoint. Endpoint can either be a full URL or a path relative to /rest/v1/. """ url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) req = r.delete( url, auth=(self.username, self.password) ) if not 200 <= req.status_code < 300: try: raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') except: raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') def set_config(self, key: str, value: str): """ Changes a configuration `value` of a specific configuration `key`. https://eclipse.dev/hawkbit/rest-api/tenant-api-guide.html#_put_restv1systemconfigskeyname """ self.put(f'system/configs/{key}', {'value' : value}) def get_config(self, key: str): """ Returns the configuration value of a specific configuration `key`. https://eclipse.dev/hawkbit/rest-api/tenant-api-guide.html#_get_restv1systemconfigskeyname """ return self.get(f'system/configs/{key}')['value'] def add_target(self, target_id: str = None, token: str = None): """ Adds a new target with id and name `target_id`. If `target_id` is not given, a generic id is made up. If `token` is given, set it as target's token, otherwise hawkBit sets a random token itself. Stores the id of the created target for future use by other methods. Returns the target's id. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_post_restv1targets """ target_id = target_id or f'test-{time.monotonic()}' testdata = { 'controllerId': target_id, 'name': target_id, } if token: testdata['securityToken'] = token self.post('targets', [testdata]) self.id['target'] = target_id return self.id['target'] def get_target(self, target_id: str = None): """ Returns the target matching `target_id`. If `target_id` is not given, returns the target created by the most recent `add_target()` call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetid """ target_id = target_id or self.id['target'] return self.get(f'targets/{target_id}') def delete_target(self, target_id: str = None): """ Deletes the target matching `target_id`. If target_id is not given, deletes the target created by the most recent add_target() call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_delete_restv1targetstargetid """ target_id = target_id or self.id['target'] self.delete(f'targets/{target_id}') if 'target' in self.id and target_id == self.id['target']: del self.id['target'] def get_attributes(self, target_id: str = None): """ Returns the attributes of the target matching `target_id`. If `target_id` is not given, uses the target created by the most recent `add_target()` call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidattributes """ target_id = target_id or self.id['target'] return self.get(f'targets/{target_id}/attributes') def add_softwaremodule(self, name: str = None, module_type: str = 'os'): """ Adds a new software module with `name`. If `name` is not given, a generic name is made up. Stores the id of the created software module for future use by other methods. Returns the id of the created software module. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_post_restv1softwaremodules """ name = name or f'software module {time.monotonic()}' data = [{ 'name': name, 'version': str(self.version), 'type': module_type, }] self.id['softwaremodule'] = self.post('softwaremodules', data)[0]['id'] return self.id['softwaremodule'] def get_softwaremodule(self, module_id: str = None): """ Returns the sotware module matching `module_id`. If `module_id` is not given, returns the software module created by the most recent `add_softwaremodule()` call. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_get_restv1softwaremodulessoftwaremoduleid """ module_id = module_id or self.id['softwaremodule'] return self.get(f'softwaremodules/{module_id}') def delete_softwaremodule(self, module_id: str = None): """ Deletes the software module matching `module_id`. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_delete_restv1softwaremodulessoftwaremoduleid """ module_id = module_id or self.id['softwaremodule'] self.delete(f'softwaremodules/{module_id}') if 'softwaremodule' in self.id and module_id == self.id['softwaremodule']: del self.id['softwaremodule'] def add_distributionset(self, name: str = None, module_ids: list = [], dist_type: str = 'os'): """ Adds a new distribution set with `name` containing the software modules matching `module_ids`. If `name` is not given, a generic name is made up. If `module_ids` is not given, uses the software module created by the most recent `add_softwaremodule()` call. Stores the id of the created distribution set for future use by other methods. Returns the id of the created distribution set. https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_post_restv1distributionsets """ assert isinstance(module_ids, list) name = name or f'distribution {self.version} ({time.monotonic()})' module_ids = module_ids or [self.id['softwaremodule']] data = [{ 'name': name, 'description': 'Test distribution', 'version': str(self.version), 'modules': [], 'type': dist_type, }] for module_id in module_ids: data[0]['modules'].append({'id': module_id}) self.id['distributionset'] = self.post('distributionsets', data)[0]['id'] return self.id['distributionset'] def get_distributionset(self, dist_id: str = None): """ Returns the distribution set matching `dist_id`. If `dist_id` is not given, returns the distribution set created by the most recent `add_distributionset()` call. https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_get_restv1distributionsetsdistributionsetid """ dist_id = dist_id or self.id['distributionset'] return self.get(f'distributionsets/{dist_id}') def delete_distributionset(self, dist_id: str = None): """ Deletes the distrubition set matching `dist_id`. If `dist_id` is not given, deletes the distribution set created by the most recent `add_distributionset()` call. https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_delete_restv1distributionsetsdistributionsetid """ dist_id = dist_id or self.id['distributionset'] self.delete(f'distributionsets/{dist_id}') if 'distributionset' in self.id and dist_id == self.id['distributionset']: del self.id['distributionset'] def add_artifact(self, file_name: str, module_id: str = None): """ Adds a new artifact specified by `file_name` to the software module matching `module_id`. If `module_id` is not given, adds the artifact to the software module created by the most recent `add_softwaremodule()` call. Stores the id of the created artifact for future use by other methods. Returns the id of the created artifact. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_post_restv1softwaremodulessoftwaremoduleidartifacts """ module_id = module_id or self.id['softwaremodule'] self.id['artifact'] = self.post(f'softwaremodules/{module_id}/artifacts', file_name=file_name)['id'] return self.id['artifact'] def get_artifact(self, artifact_id: str = None, module_id: str = None): """ Returns the artifact matching `artifact_id` from the software module matching `module_id`. If `artifact_id` is not given, returns the artifact created by the most recent `add_artifact()` call. If `module_id` is not given, uses the software module created by the most recent `add_softwaremodule()` call. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_get_restv1softwaremodulessoftwaremoduleidartifactsartifactid """ module_id = module_id or self.id['softwaremodule'] artifact_id = artifact_id or self.id['artifact'] return self.get(f'softwaremodules/{module_id}/artifacts/{artifact_id}')['id'] def delete_artifact(self, artifact_id: str = None, module_id: str = None): """ Deletes the artifact matching `artifact_id` from the software module matching `module_id`. If `artifact_id` is not given, deletes the artifact created by the most recent `add_artifact()` call. If `module_id` is not given, uses the software module created by the most recent `add_softwaremodule()` call. https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_delete_restv1softwaremodulessoftwaremoduleidartifactsartifactid """ module_id = module_id or self.id['softwaremodule'] artifact_id = artifact_id or self.id['artifact'] self.delete(f'softwaremodules/{module_id}/artifacts/{artifact_id}') if 'artifact' in self.id and artifact_id == self.id['artifact']: del self.id['artifact'] def assign_target(self, dist_id: str = None, target_id: str = None, params: dict = None): """ Assigns the distribution set matching `dist_id` to a target matching `target_id`. If `dist_id` is not given, uses the distribution set created by the most recent `add_distributionset()` call. If `target_id` is not given, uses the target created by the most recent `add_target()` call. Stores the id of the assignment action for future use by other methods. https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_post_restv1distributionsetsdistributionsetidassignedtargets """ dist_id = dist_id or self.id['distributionset'] target_id = target_id or self.id['target'] testdata = [{'id': target_id}] if params: testdata[0].update(params) response = self.post(f'distributionsets/{dist_id}/assignedTargets', testdata) # Increment version to be able to flash over an already deployed distribution self.version += 0.1 self.id['action'] = response.get('assignedActions')[-1].get('id') return self.id['action'] def get_action(self, action_id: str = None, target_id: str = None): """ Returns the action matching `action_id` on the target matching `target_id`. If `action_id` is not given, returns the action created by the most recent `assign_target()` call. If `target_id` is not given, uses the target created by the most recent `add_target()` call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidactionsactionid """ action_id = action_id or self.id['action'] target_id = target_id or self.id['target'] return self.get(f'targets/{target_id}/actions/{action_id}') def get_action_status(self, action_id: str = None, target_id: str = None): """ Returns the first (max.) 50 action states of the action matching `action_id` of the target matching `target_id` sorted by id. If `action_id` is not given, uses the action created by the most recent `assign_target()` call. If `target_id` is not given, uses the target created by the most recent `add_target()` call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidactionsactionidstatus """ action_id = action_id or self.id['action'] target_id = target_id or self.id['target'] req = self.get(f'targets/{target_id}/actions/{action_id}/status?offset=0&limit=50&sort=id:DESC') return req['content'] def cancel_action(self, action_id: str = None, target_id: str = None, *, force: bool = False): """ Cancels the action matching `action_id` of the target matching `target_id`. If `force=True` is given, cancels the action without telling the target. If `action_id` is not given, uses the action created by the most recent `assign_target()` call. If `target_id` is not given, uses the target created by the most recent `add_target()` call. https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_delete_restv1targetstargetidactionsactionid """ action_id = action_id or self.id['action'] target_id = target_id or self.id['target'] self.delete(f'targets/{target_id}/actions/{action_id}') if force: self.delete(f'targets/{target_id}/actions/{action_id}?force=true') if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('bundle', help='RAUC bundle to add as artifact') args = parser.parse_args() client = HawkbitMgmtTestClient('localhost', 8080) client.set_config('pollingTime', '00:00:30') client.set_config('pollingOverdueTime', '00:03:00') client.set_config('authentication.targettoken.enabled', True) client.add_target('test', 'ieHai3du7gee7aPhojeth4ong') client.add_softwaremodule() client.add_artifact(args.bundle) client.add_distributionset() client.assign_target() try: target = client.get_target() print(f'Created target (target_name={target["controllerId"]}, auth_token={target["securityToken"]}) assigned distribution containing {args.bundle} to it') print('Clean quit with a single ctrl-c') while True: time.sleep(1) except KeyboardInterrupt: print('Cleaning up..') finally: try: client.cancel_action(force=True) except: pass client.delete_distributionset() client.delete_artifact() client.delete_softwaremodule() client.delete_target() print('Done') rauc-hawkbit-updater-1.4/script/rauc-hawkbit-updater.service000066400000000000000000000006471503520256600243250ustar00rootroot00000000000000[Unit] Description=HawkBit client for Rauc After=network-online.target rauc.service Wants=network-online.target [Service] User=rauc-hawkbit Group=rauc-hawkbit AmbientCapabilities=CAP_SYS_BOOT ExecStart=/usr/bin/rauc-hawkbit-updater -s -c /etc/rauc-hawkbit-updater/config.conf TimeoutSec=60s WatchdogSec=5m Restart=on-failure RestartSec=1m NotifyAccess=main ProtectSystem=full Nice=10 [Install] WantedBy=multi-user.target rauc-hawkbit-updater-1.4/src/000077500000000000000000000000001503520256600161745ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/src/config-file.c000066400000000000000000000373171503520256600205350ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief Configuration file parser */ #include "config-file.h" #include #include static const gint DEFAULT_CONNECTTIMEOUT = 20; // 20 sec. static const gint DEFAULT_TIMEOUT = 60; // 1 min. static const gint DEFAULT_RETRY_WAIT = 5 * 60; // 5 min. static const gboolean DEFAULT_SSL = TRUE; static const gboolean DEFAULT_SSL_VERIFY = TRUE; static const gboolean DEFAULT_REBOOT = FALSE; static const gchar* DEFAULT_LOG_LEVEL = "message"; static const gboolean DEFAULT_SEND_DOWNLOAD_AUTHENTICATION = TRUE; /** * @brief Get string value from key_file for key in group, optional default_value can be specified * that will be used in case key is not found in group. * * @param[in] key_file GKeyFile to look value up * @param[in] group A group name * @param[in] key A key * @param[out] value Output string value * @param[in] default_value String value to return in case no value found, or NULL (not found * leads to error) * @param[out] error Error * @return TRUE if found, TRUE if not found and default_value given, FALSE otherwise (error is set) */ static gboolean get_key_string(GKeyFile *key_file, const gchar *group, const gchar *key, gchar **value, const gchar *default_value, GError **error) { g_autofree gchar *val = NULL; g_return_val_if_fail(key_file, FALSE); g_return_val_if_fail(group, FALSE); g_return_val_if_fail(key, FALSE); g_return_val_if_fail(value && *value == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); val = g_key_file_get_string(key_file, group, key, error); if (!val) { if (default_value) { *value = g_strdup(default_value); g_clear_error(error); return TRUE; } return FALSE; } val = g_strchomp(val); *value = g_steal_pointer(&val); return TRUE; } /** * @brief Get gboolean value from key_file for key in group, default_value must be specified, * returned in case key not found in group. * * @param[in] key_file GKeyFile to look value up * @param[in] group A group name * @param[in] key A key * @param[out] value Output gboolean value * @param[in] default_value Return this value in case no value found * @param[out] error Error * @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in * group is not found, value is set to default_value in this case. */ static gboolean get_key_bool(GKeyFile *key_file, const gchar *group, const gchar *key, gboolean *value, const gboolean default_value, GError **error) { g_autofree gchar *val = NULL; g_return_val_if_fail(key_file, FALSE); g_return_val_if_fail(group, FALSE); g_return_val_if_fail(key, FALSE); g_return_val_if_fail(value, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); val = g_key_file_get_string(key_file, group, key, NULL); if (!val) { *value = default_value; return TRUE; } val = g_strchomp(val); if (g_strcmp0(val, "0") == 0 || g_ascii_strcasecmp(val, "no") == 0 || g_ascii_strcasecmp(val, "false") == 0) { *value = FALSE; return TRUE; } if (g_strcmp0(val, "1") == 0 || g_ascii_strcasecmp(val, "yes") == 0 || g_ascii_strcasecmp(val, "true") == 0) { *value = TRUE; return TRUE; } g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Value '%s' cannot be interpreted as a boolean.", val); return FALSE; } /** * @brief Get integer value from key_file for key in group, default_value must be specified, * returned in case key not found in group. * * @param[in] key_file GKeyFile to look value up * @param[in] group A group name * @param[in] key A key * @param[out] value Output integer value * @param[in] default_value Return this value in case no value found * @param[out] error Error * @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in * group is not found, value is set to default_value in this case. */ static gboolean get_key_int(GKeyFile *key_file, const gchar *group, const gchar *key, gint *value, const gint default_value, GError **error) { GError *ierror = NULL; gint val; g_return_val_if_fail(key_file, FALSE); g_return_val_if_fail(group, FALSE); g_return_val_if_fail(key, FALSE); g_return_val_if_fail(value, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); val = g_key_file_get_integer(key_file, group, key, &ierror); if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { g_clear_error(&ierror); *value = default_value; return TRUE; } else if (ierror) { g_propagate_error(error, ierror); return FALSE; } *value = val; return TRUE; } /** * @brief Get GHashTable containing keys/values from group in key_file. * * @param[in] key_file GKeyFile to look value up * @param[in] group A group name * @param[out] hash Output GHashTable * @param[out] error Error * @return TRUE on keys/values stored successfully, FALSE on empty group/value or on other errors * (error set) */ static gboolean get_group(GKeyFile *key_file, const gchar *group, GHashTable **hash, GError **error) { g_autoptr(GHashTable) tmp_hash = NULL; guint key; gsize num_keys; g_auto(GStrv) keys = NULL; g_return_val_if_fail(key_file, FALSE); g_return_val_if_fail(group, FALSE); g_return_val_if_fail(hash && *hash == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); tmp_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); keys = g_key_file_get_keys(key_file, group, &num_keys, error); if (!keys) return FALSE; if (!num_keys) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, "Group '%s' has no keys set", group); return FALSE; } for (key = 0; key < num_keys; key++) { g_autofree gchar *value = g_key_file_get_value(key_file, group, keys[key], error); if (!value) return FALSE; value = g_strchomp(value); g_hash_table_insert(tmp_hash, g_strdup(keys[key]), g_steal_pointer(&value)); } *hash = g_steal_pointer(&tmp_hash); return TRUE; } /** * @brief Get GLogLevelFlags for error string. * * @param[in] log_level Log level string * @return GLogLevelFlags matching error string, else default log level (message) */ static GLogLevelFlags log_level_from_string(const gchar *log_level) { g_return_val_if_fail(log_level, 0); if (g_strcmp0(log_level, "error") == 0) { return G_LOG_LEVEL_ERROR; } else if (g_strcmp0(log_level, "critical") == 0) { return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL; } else if (g_strcmp0(log_level, "warning") == 0) { return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING; } else if (g_strcmp0(log_level, "message") == 0) { return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE; } else if (g_strcmp0(log_level, "info") == 0) { return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO; } else if (g_strcmp0(log_level, "debug") == 0) { return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG; } else { g_warning("Invalid log level given, defaulting to level \"message\""); return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE; } } Config* load_config_file(const gchar *config_file, GError **error) { g_autoptr(Config) config = NULL; g_autofree gchar *val = NULL; g_autoptr(GKeyFile) ini_file = NULL; gboolean key_auth_token_exists = FALSE; gboolean key_gateway_token_exists = FALSE; gboolean bundle_location_given = FALSE; gboolean ssl_key_exists = FALSE; gboolean ssl_cert_exists = FALSE; gboolean ssl_auth = FALSE; gboolean token_auth = FALSE; g_return_val_if_fail(config_file, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); config = g_new0(Config, 1); ini_file = g_key_file_new(); if (!g_key_file_load_from_file(ini_file, config_file, G_KEY_FILE_NONE, error)) return NULL; if (!get_key_string(ini_file, "client", "hawkbit_server", &config->hawkbit_server, NULL, error)) return NULL; if (!get_key_bool(ini_file, "client", "ssl", &config->ssl, DEFAULT_SSL, error)) return NULL; if (!get_key_bool(ini_file, "client", "ssl_verify", &config->ssl_verify, DEFAULT_SSL_VERIFY, error)) return NULL; if (config->ssl) { ssl_key_exists = get_key_string(ini_file, "client", "ssl_key", &config->ssl_key, NULL, NULL); ssl_cert_exists = get_key_string(ini_file, "client", "ssl_cert", &config->ssl_cert, NULL, NULL); ssl_auth = ssl_cert_exists && ssl_key_exists; if ((ssl_cert_exists || ssl_key_exists) && !ssl_auth) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Only one of 'ssl_key' and 'ssl_cert' is set"); return NULL; } get_key_string(ini_file, "client", "ssl_engine", &config->ssl_engine, NULL, NULL); if (config->ssl_engine && !ssl_auth) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "SSL engine set without 'ssl_key' or 'ssl_cert'"); return NULL; } } key_auth_token_exists = get_key_string(ini_file, "client", "auth_token", &config->auth_token, NULL, NULL); key_gateway_token_exists = get_key_string(ini_file, "client", "gateway_token", &config->gateway_token, NULL, NULL); token_auth = key_auth_token_exists || key_gateway_token_exists; if (!token_auth && !ssl_auth) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Neither token nor ssl authentication set"); return NULL; } if (token_auth && ssl_auth) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Both token and ssl authentication set"); return NULL; } if (key_auth_token_exists && key_gateway_token_exists) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Both 'auth_token' and 'gateway_token' set"); return NULL; } if (!get_key_string(ini_file, "client", "target_name", &config->controller_id, NULL, error)) return NULL; if (!get_key_string(ini_file, "client", "tenant_id", &config->tenant_id, "DEFAULT", error)) return NULL; bundle_location_given = get_key_string(ini_file, "client", "bundle_download_location", &config->bundle_download_location, NULL, NULL); if (!get_group(ini_file, "device", &config->device, error)) return NULL; if (!get_key_int(ini_file, "client", "connect_timeout", &config->connect_timeout, DEFAULT_CONNECTTIMEOUT, error)) return NULL; if (!get_key_int(ini_file, "client", "timeout", &config->timeout, DEFAULT_TIMEOUT, error)) return NULL; if (!get_key_int(ini_file, "client", "retry_wait", &config->retry_wait, DEFAULT_RETRY_WAIT, error)) return NULL; if (!get_key_int(ini_file, "client", "low_speed_rate", &config->low_speed_rate, 100, error)) return NULL; if (!get_key_int(ini_file, "client", "low_speed_time", &config->low_speed_time, 60, error)) return NULL; if (!get_key_bool(ini_file, "client", "resume_downloads", &config->resume_downloads, FALSE, error)) return NULL; if (!get_key_bool(ini_file, "client", "stream_bundle", &config->stream_bundle, FALSE, error)) return NULL; if (!get_key_string(ini_file, "client", "log_level", &val, DEFAULT_LOG_LEVEL, error)) return NULL; config->log_level = log_level_from_string(val); if (!get_key_bool(ini_file, "client", "post_update_reboot", &config->post_update_reboot, DEFAULT_REBOOT, error)) return NULL; if (!get_key_bool(ini_file, "client", "send_download_authentication", &config->send_download_authentication, DEFAULT_SEND_DOWNLOAD_AUTHENTICATION, error)) return NULL; if (config->timeout > 0 && config->connect_timeout > 0 && config->timeout < config->connect_timeout) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "'timeout' (%d) must be greater than 'connect_timeout' (%d)", config->timeout, config->connect_timeout); return NULL; } if (!bundle_location_given && !config->stream_bundle) { g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND, "'bundle_download_location' is required if 'stream_bundle' is disabled"); return NULL; } return g_steal_pointer(&config); } void config_file_free(Config *config) { if (!config) return; g_free(config->hawkbit_server); g_free(config->controller_id); g_free(config->tenant_id); g_free(config->auth_token); g_free(config->gateway_token); g_free(config->ssl_engine); g_free(config->ssl_key); g_free(config->ssl_cert); g_free(config->bundle_download_location); if (config->device) g_hash_table_destroy(config->device); g_free(config); } rauc-hawkbit-updater-1.4/src/hawkbit-client.c000066400000000000000000001655021503520256600212560ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief Implementation of the hawkBit DDI API * * @see https://github.com/rauc/rauc-hawkbit * @see https://eclipse.dev/hawkbit/apis/ddi_api/ */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "json-helper.h" #ifdef WITH_SYSTEMD #include "sd-helper.h" #endif #include "hawkbit-client.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, fclose) G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURL, curl_easy_cleanup) gboolean run_once = FALSE; static const gint MAX_RETRIES_ON_API_ERROR = 10; /** * @brief String representation of HTTP methods. */ static const char *HTTPMethod_STRING[] = { "GET", "HEAD", "PUT", "POST", "PATCH", "DELETE" }; /** * @brief CURLcodes that should lead to download resuming, 0 terminated */ static const gint resumable_codes[] = { CURLE_OPERATION_TIMEDOUT, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT, CURLE_PARTIAL_FILE, CURLE_SEND_ERROR, CURLE_RECV_ERROR, CURLE_HTTP2, CURLE_HTTP2_STREAM, 0 }; static Config *hawkbit_config = NULL; static GSourceFunc software_ready_cb; static struct HawkbitAction *active_action = NULL; static GThread *thread_download = NULL; static gsize curl_share_initialized = 0; static CURLSH *curl_share = NULL; static GMutex curl_share_locks[CURL_LOCK_DATA_LAST + 1]; GQuark rhu_hawkbit_client_error_quark(void) { return g_quark_from_static_string("rhu_hawkbit_client_error_quark"); } GQuark rhu_hawkbit_client_curl_error_quark(void) { return g_quark_from_static_string("rhu_hawkbit_client_curl_error_quark"); } GQuark rhu_hawkbit_client_http_error_quark(void) { return g_quark_from_static_string("rhu_hawkbit_client_http_error_quark"); } void curl_share_lock(CURL *handle, curl_lock_data data, curl_lock_access access, void *clientp) { g_return_if_fail(data <= CURL_LOCK_DATA_LAST); g_mutex_lock(&curl_share_locks[data]); } void curl_share_unlock(CURL *handle, curl_lock_data data, curl_lock_access access, void *clientp) { g_return_if_fail(data <= CURL_LOCK_DATA_LAST); g_mutex_unlock(&curl_share_locks[data]); } /** * @brief Create and initialize an HawkbitAction. * * @return Pointer to initialized HawkbitAction.. */ static struct HawkbitAction *action_new(void) { struct HawkbitAction *action = g_new0(struct HawkbitAction, 1); g_mutex_init(&action->mutex); g_cond_init(&action->cond); action->id = NULL; action->state = ACTION_STATE_NONE; return action; } /** * @brief Get available free space of a mounted file system. * * @param[in] path Absolute Path of a disk device node containing a mounted file system * @param[out] free_space Pointer to goffset containing the free space in bytes * @return TRUE if free space calculation succeeded, FALSE otherwise */ static gboolean get_available_space(const char *path, goffset *free_space, GError **error) { struct statvfs stat; g_autofree gchar *npath = g_strdup(path); const char *rpath = dirname(npath); g_return_val_if_fail(path, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (statvfs(rpath, &stat)) { int err = errno; g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to calculate free space for %s: %s", path, g_strerror(err)); return FALSE; } // the available free space is f_bsize * f_bavail *free_space = (goffset) stat.f_bsize * (goffset) stat.f_bavail; return TRUE; } /** * @brief Calculate checksum for file. * * @param[in] fp File to read data from * @param[in] type Desired type of checksum * @param[out] checksum Calculated checksum digest hex string * @param[out] error Error * @return TRUE if checksum calculation succeeded, FALSE otherwise (error set) */ static gboolean get_file_checksum(FILE *fp, const GChecksumType type, gchar **checksum, GError **error) { g_autoptr(GChecksum) ctx = g_checksum_new(type); guchar buf[4096]; size_t r; g_return_val_if_fail(fp, FALSE); g_return_val_if_fail(checksum && *checksum == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_assert_nonnull(ctx); fseek(fp, 0, SEEK_SET); while (1) { r = fread(buf, 1, sizeof(buf), fp); if (ferror(fp)) { g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Read failed"); return FALSE; } g_checksum_update(ctx, buf, r); if (feof(fp)) break; } *checksum = g_strdup(g_checksum_get_string(ctx)); return TRUE; } /** * @brief Add string to Curl headers, avoiding overwriting an existing * non-empty list on failure. * * @param[out] headers curl_slist** of already set headers * @param[in] string Header to add * @param[out] error Error * @return TRUE if string was added to headers successfully, FALSE otherwise (error set) */ static gboolean add_curl_header(struct curl_slist **headers, const char *string, GError **error) { struct curl_slist *temp = NULL; g_return_val_if_fail(headers, FALSE); g_return_val_if_fail(string, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); temp = curl_slist_append(*headers, string); if (!temp) { curl_slist_free_all(*headers); g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, CURLE_FAILED_INIT, "Could not add header %s", string); return FALSE; } *headers = temp; return TRUE; } /** * @brief Returns the header required to authenticate against hawkBit, either target or gateway * token. * * @return header required to authenticate against hawkBit */ static char* get_auth_header() { if (hawkbit_config->auth_token) return g_strdup_printf("Authorization: TargetToken %s", hawkbit_config->auth_token); if (hawkbit_config->gateway_token) return g_strdup_printf("Authorization: GatewayToken %s", hawkbit_config->gateway_token); return NULL; } /** * @brief Add hawkBit authorization header to Curl headers. * * @param[out] headers curl_slist** of already set headers * @param[out] error Error * @return TRUE if authorization method set in config and header was added successfully, TRUE if no * authorization method set, FALSE otherwise (error set) */ static gboolean set_auth_curl_header(struct curl_slist **headers, GError **error) { gboolean res = TRUE; g_autofree gchar *token = NULL; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); token = get_auth_header(); if (token) res = add_curl_header(headers, token, error); return res; } /** * @brief Set Curl options for TLS/SSL client authentication * * @param[in] curl Curl handle * @param[out] error Error * @return TRUE if ssl authorization method set in config was set successfully, * FALSE otherwise (error set) */ static gboolean set_auth_curl_ssl(CURL *curl, GError **error) { curl_easy_setopt(curl, CURLOPT_SSLKEY, hawkbit_config->ssl_key); curl_easy_setopt(curl, CURLOPT_SSLCERT, hawkbit_config->ssl_cert); if (!hawkbit_config->ssl_engine) return TRUE; if (curl_easy_setopt(curl, CURLOPT_SSLENGINE, hawkbit_config->ssl_engine) != CURLE_OK) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, CURLE_FAILED_INIT, "Failed to set ssl engine"); return FALSE; } curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "ENG"); if (curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L) != CURLE_OK) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, CURLE_FAILED_INIT, "Failed to set engine as default"); return FALSE; } g_debug("Using SSL engine %s", hawkbit_config->ssl_engine); return TRUE; } /** * @brief Set Curl options for client authentication * * @param[in] curl Curl handle * @param[out] headers curl_slist** of already set headers * @param[out] error Error * @return TRUE if authorization method set in config and applied successfully, * FALSE otherwise (error set) */ static gboolean set_auth_curl(CURL *curl, struct curl_slist **headers, GError **error) { // SSL client certificate authentication if (hawkbit_config->ssl_key && hawkbit_config->ssl_cert) return set_auth_curl_ssl(curl, error); // Token authentication if (hawkbit_config->auth_token || hawkbit_config->gateway_token) return set_auth_curl_header(headers, error); g_assert_not_reached(); } /** * @brief Set common Curl options, namely user agent, connect timeout, SSL * verify peer and SSL verify host options. * * @param[in] curl Curl handle */ static void set_default_curl_opts(CURL *curl) { g_return_if_fail(curl); curl_easy_setopt(curl, CURLOPT_USERAGENT, HAWKBIT_USERAGENT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, hawkbit_config->connect_timeout); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, hawkbit_config->ssl_verify ? 1L : 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, hawkbit_config->ssl_verify ? 1L : 0L); if (g_once_init_enter(&curl_share_initialized)) { /* initialize mutexes for different types of data that curl uses locks for */ for (gint i = 0; i <= CURL_LOCK_DATA_LAST; i++) g_mutex_init(&curl_share_locks[i]); curl_share = curl_share_init(); g_assert_nonnull(curl_share); curl_share_setopt(curl_share, CURLSHOPT_LOCKFUNC, curl_share_lock); curl_share_setopt(curl_share, CURLSHOPT_UNLOCKFUNC, curl_share_unlock); curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL); curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); g_once_init_leave(&curl_share_initialized, 1); } curl_easy_setopt(curl, CURLOPT_SHARE, curl_share); } /** * @brief Download download_url to file. * * @param[in] download_url URL to download from * @param[in] file Download destination * @param[in] resume_from Offset to resume download from * @param[out] sha1sum Calculated checksum or NULL * @param[out] speed Average download speed * @param[out] error Error * @return TRUE if download succeeded, FALSE otherwise (error set) */ static gboolean get_binary(const gchar *download_url, const gchar *file, curl_off_t resume_from, gchar **sha1sum, curl_off_t *speed, GError **error) { g_autoptr(CURL) curl = NULL; g_autoptr(FILE) fp = NULL; CURLcode curl_code; glong http_code = 0; struct curl_slist *headers = NULL; g_return_val_if_fail(download_url, FALSE); g_return_val_if_fail(file, FALSE); g_return_val_if_fail(sha1sum == NULL || *sha1sum == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (resume_from) g_debug("Resuming download from offset %" CURL_FORMAT_CURL_OFF_T, resume_from); fp = g_fopen(file, (resume_from) ? "ab+" : "wb+"); if (!fp) { int err = errno; g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Failed to open %s for download: %s", file, g_strerror(err)); return FALSE; } curl = curl_easy_init(); if (!curl) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, CURLE_FAILED_INIT, "Unable to start libcurl easy session"); return FALSE; } set_default_curl_opts(curl); curl_easy_setopt(curl, CURLOPT_URL, download_url); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 8L); curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); // abort if slower than configured download rate during configured time span curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, hawkbit_config->low_speed_time); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, hawkbit_config->low_speed_rate); curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, resume_from); if (hawkbit_config->send_download_authentication && !set_auth_curl(curl, &headers, error)) return FALSE; // set up request headers if (!add_curl_header(&headers, "Accept: application/octet-stream", error)) return FALSE; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // perform transfer curl_code = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_getinfo(curl, CURLINFO_SPEED_DOWNLOAD_T, speed); curl_slist_free_all(headers); if (curl_code != CURLE_OK) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, curl_code, "%s", curl_easy_strerror(curl_code)); return FALSE; } // consider ok/partial download/range not satisfiable (EOF reached) as success if (http_code != 200 && http_code != 206 && http_code != 416) { g_set_error(error, RHU_HAWKBIT_CLIENT_HTTP_ERROR, http_code, "HTTP request failed: %ld", http_code); return FALSE; } // if checksum enabled then return the value if (sha1sum && !get_file_checksum(fp, G_CHECKSUM_SHA1, sha1sum, error)) return FALSE; return TRUE; } /** * @brief Curl callback writing REST response to RestPayload*->payload buffer. * * @see https://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.html */ static size_t curl_write_cb(const void *content, size_t size, size_t nmemb, void *data) { RestPayload *p = NULL; size_t real_size = size * nmemb; g_return_val_if_fail(content, 0); g_return_val_if_fail(data, 0); p = (RestPayload *) data; p->payload = (gchar *) g_realloc(p->payload, p->size + real_size + 1); // copy content to buffer memcpy(&(p->payload[p->size]), content, real_size); p->size += real_size; p->payload[p->size] = '\0'; return real_size; } /** * @brief Perform REST request with JSON data, expecting response JSON data. * * @param[in] method HTTP Method, e.g. GET * @param[in] url URL used in HTTP REST request * @param[in] jsonRequestBody REST request body. If NULL, no body is sent * @param[out] jsonResponseParser Return location for a REST response or NULL to skip response * parsing * @param[out] error Error * @return TRUE if request and response parser (if given) suceeded, FALSE otherwise (error set). */ static gboolean rest_request(enum HTTPMethod method, const gchar *url, JsonBuilder *jsonRequestBody, JsonParser **jsonResponseParser, GError **error) { g_autofree gchar *postdata = NULL; g_autoptr(RestPayload) fetch_buffer = NULL; struct curl_slist *headers = NULL; g_autoptr(CURL) curl = NULL; glong http_code = 0; CURLcode res; g_return_val_if_fail(url, FALSE); g_return_val_if_fail(jsonResponseParser == NULL || *jsonResponseParser == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); curl = curl_easy_init(); if (!curl) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, CURLE_FAILED_INIT, "Unable to start libcurl easy session"); return FALSE; } // init response buffer fetch_buffer = g_new0(RestPayload, 1); fetch_buffer->size = 0; fetch_buffer->payload = g_malloc0(DEFAULT_CURL_REQUEST_BUFFER_SIZE); // set up CURL options set_default_curl_opts(curl); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, HTTPMethod_STRING[method]); curl_easy_setopt(curl, CURLOPT_TIMEOUT, hawkbit_config->timeout); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fetch_buffer); if (jsonRequestBody) { g_autoptr(JsonGenerator) generator = json_generator_new(); g_autoptr(JsonNode) req_root = json_builder_get_root(jsonRequestBody); g_autofree gchar *json_req_str = NULL; json_generator_set_root(generator, req_root); postdata = json_generator_to_data(generator, NULL); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata); json_req_str = json_to_string(req_root, TRUE); g_debug("Request body: %s", json_req_str); } // set up request headers if (!add_curl_header(&headers, "Accept: application/json;charset=UTF-8", error)) return FALSE; if (!set_auth_curl(curl, &headers, error)) return FALSE; if (jsonRequestBody && !add_curl_header(&headers, "Content-Type: application/json;charset=UTF-8", error)) return FALSE; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // perform request res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_slist_free_all(headers); if (res != CURLE_OK) { g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, res, "%s", curl_easy_strerror(res)); return FALSE; } if (http_code != 200) { g_set_error(error, RHU_HAWKBIT_CLIENT_HTTP_ERROR, http_code, "HTTP request failed: %ld; server response: %s", http_code, fetch_buffer->payload); return FALSE; } if (jsonResponseParser && fetch_buffer->size > 0) { // process JSON repsonse g_autoptr(JsonParser) parser = json_parser_new_immutable(); JsonNode *resp_root = NULL; g_autofree gchar *json_resp_str = NULL; if (!json_parser_load_from_data(parser, fetch_buffer->payload, fetch_buffer->size, error)) return FALSE; resp_root = json_parser_get_root(parser); json_resp_str = json_to_string(resp_root, TRUE); g_debug("Response body: %s", json_resp_str); *jsonResponseParser = g_steal_pointer(&parser); } return TRUE; } /** * @brief Perform REST request with JSON data, expecting response JSON data. On HTTP error * 409 (Conflict) and 429 (Too Many Requests), try again (up to MAX_RETRIES_ON_API_ERROR). * * @param[in] method HTTP Method, e.g. GET * @param[in] url URL used in HTTP REST request * @param[in] jsonRequestBody REST request body. If NULL, no body is sent * @param[out] jsonResponseParser Return location for a REST response or NULL to skip response * parsing * @param[out] error Error * @return TRUE if request and response parser (if given) suceeded, FALSE otherwise (error set). */ static gboolean rest_request_retriable(enum HTTPMethod method, const gchar *url, JsonBuilder *jsonRequestBody, JsonParser **jsonResponseParser, GError **error) { gboolean res, retry; gint retry_count = 0; GError *ierror = NULL; g_return_val_if_fail(url, FALSE); g_return_val_if_fail(jsonResponseParser == NULL || *jsonResponseParser == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); while (1) { res = rest_request(method, url, jsonRequestBody, jsonResponseParser, &ierror); retry = (g_error_matches(ierror, RHU_HAWKBIT_CLIENT_HTTP_ERROR, 409) || g_error_matches(ierror, RHU_HAWKBIT_CLIENT_HTTP_ERROR, 429)) && retry_count < MAX_RETRIES_ON_API_ERROR; if (!retry) break; g_debug("%s Trying again (%d/%d)..", ierror->message, retry_count+1, MAX_RETRIES_ON_API_ERROR); g_clear_error(&ierror); g_usleep(1000000); retry_count++; } if (!res) g_propagate_error(error, ierror); return res; } /** * @brief Build hawkBit JSON request. * * @see https://eclipse.dev/hawkbit/rest-api/rootcontroller-api-guide.html#_post_tenantcontrollerv1controlleriddeploymentbaseactionidfeedback * * @param[in] id hawkBit action ID or NULL (configData usecase) * @param[in] detail Detail message or NULL (configData usecase) * @param[in] finished hawkBit status of the result * @param[in] execution hawkBit status of the action execution * @param[in] attributes hawkBit controller attributes or NULL (feedback usecase) * @return JsonBuilder* with built hawkBit request */ static JsonBuilder* json_build_status(const gchar *id, const gchar *detail, const gchar *finished, const gchar *execution, GHashTable *attributes) { GHashTableIter iter; gpointer key, value; g_autoptr(JsonBuilder) builder = NULL; time_t current_time; struct tm time_info; char timeString[16]; g_return_val_if_fail(finished, NULL); g_return_val_if_fail(execution, NULL); // get current time in UTC time(¤t_time); gmtime_r(¤t_time, &time_info); strftime(timeString, sizeof(timeString), "%Y%m%dT%H%M%S", &time_info); builder = json_builder_new(); // build json status json_builder_begin_object(builder); if (id) { json_builder_set_member_name(builder, "id"); json_builder_add_string_value(builder, id); } json_builder_set_member_name(builder, "time"); json_builder_add_string_value(builder, timeString); json_builder_set_member_name(builder, "status"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "result"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "finished"); json_builder_add_string_value(builder, finished); json_builder_end_object(builder); json_builder_set_member_name(builder, "execution"); json_builder_add_string_value(builder, execution); if (detail) { json_builder_set_member_name(builder, "details"); json_builder_begin_array(builder); json_builder_add_string_value(builder, detail); json_builder_end_array(builder); } json_builder_end_object(builder); if (attributes) { json_builder_set_member_name(builder, "data"); json_builder_begin_object(builder); g_hash_table_iter_init(&iter, attributes); while (g_hash_table_iter_next(&iter, &key, &value)) { json_builder_set_member_name(builder, key); json_builder_add_string_value(builder, value); } json_builder_end_object(builder); } json_builder_end_object(builder); return g_steal_pointer(&builder); } /** * @brief Send feedback to hawkBit. * * @param[in] url hawkBit URL used for request * @param[in] id hawkBit action ID * @param[in] detail Detail message * @param[in] finished hawkBit status of the result * @param[in] execution hawkBit status of the action execution * @param[out] error Error * @return TRUE if feedback was sent successfully, FALSE otherwise (error set) */ static gboolean feedback(const gchar *url, const gchar *id, const gchar *detail, const gchar *finished, const gchar *execution, GError **error) { g_autoptr(JsonBuilder) builder = NULL; gboolean res = FALSE; g_return_val_if_fail(url, FALSE); g_return_val_if_fail(id, FALSE); g_return_val_if_fail(detail, FALSE); g_return_val_if_fail(finished, FALSE); g_return_val_if_fail(execution, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!g_strcmp0(finished, "failure")) g_warning("%s", detail); else g_message("%s", detail); builder = json_build_status(id, detail, finished, execution, NULL); res = rest_request_retriable(POST, url, builder, NULL, error); if (!res) g_prefix_error(error, "Failed to report \"%s\" feedback: ", detail); return res; } /** * @brief Send progress feedback to hawkBit (finished=none, execution=proceeding). * * @param[in] url hawkBit URL used for request * @param[in] id hawkBit action ID * @param[in] detail Detail message * @param[out] error Error * @return TRUE if feedback was sent successfully, FALSE otherwise (error set) */ static gboolean feedback_progress(const gchar *url, const gchar *id, const gchar *detail, GError **error) { gboolean res = FALSE; g_return_val_if_fail(url, FALSE); g_return_val_if_fail(id, FALSE); g_return_val_if_fail(detail, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); res = feedback(url, id, detail, "none", "proceeding", error); g_prefix_error(error, "Progress feedback: "); return res; } /** * @brief Get polling sleep time from hawkBit JSON response. * * @param[in] root JsonNode* with hawkBit response * @return time to sleep in seconds, either from JSON or (if not found) from config's retry_wait (or 5s during active action) */ static long json_get_sleeptime(JsonNode *root) { g_autofree gchar *sleeptime_str = NULL; g_autoptr(GError) error = NULL; struct tm time; g_return_val_if_fail(root, 0L); /* When processing an action, return fixed sleeptime of 5s to allow * receiving cancelation requests etc.*/ g_mutex_lock(&active_action->mutex); if (active_action->state == ACTION_STATE_PROCESSING || active_action->state == ACTION_STATE_DOWNLOADING || active_action->state == ACTION_STATE_CANCEL_REQUESTED) { g_mutex_unlock(&active_action->mutex); return 5L; } g_mutex_unlock(&active_action->mutex); sleeptime_str = json_get_string(root, "$.config.polling.sleep", &error); if (!sleeptime_str) { g_warning("Polling sleep time not found: %s. Using fallback: %ds", error->message, hawkbit_config->retry_wait); return hawkbit_config->retry_wait; } strptime(sleeptime_str, "%T", &time); return (time.tm_sec + (time.tm_min * 60) + (time.tm_hour * 60 * 60)); } /** * @brief Build API URL * * @param path[in] a printf()-like format string describing the API path or NULL for base path * @param ... The arguments to be inserted in path * * @return a newly allocated full API URL */ __attribute__((__format__(__printf__, 1, 2))) static gchar* build_api_url(const gchar *path, ...) { g_autofree gchar *buffer = NULL; va_list args; if (path) { va_start(args, path); buffer = g_strdup_vprintf(path, args); va_end(args); } return g_strdup_printf( "%s://%s/%s/controller/v1/%s%s%s", hawkbit_config->ssl ? "https" : "http", hawkbit_config->hawkbit_server, hawkbit_config->tenant_id, hawkbit_config->controller_id, buffer ? "/" : "", buffer ? buffer : ""); } gboolean hawkbit_progress(const gchar *msg) { g_autofree gchar *feedback_url = NULL; g_autoptr(GError) error = NULL; g_return_val_if_fail(msg, FALSE); g_mutex_lock(&active_action->mutex); feedback_url = build_api_url("deploymentBase/%s/feedback", active_action->id); if (!feedback_progress(feedback_url, active_action->id, msg, &error)) g_warning("%s", error->message); g_mutex_unlock(&active_action->mutex); return G_SOURCE_REMOVE; } /** * @brief Provide meta information that will allow the hawkBit to identify the device on a hardware * level. * * @see https://eclipse.dev/hawkbit/rest-api/rootcontroller-api-guide.html#_put_tenantcontrollerv1controlleridconfigdata * * @param[out] error Error * @return TRUE if identification succeeded, FALSE otherwise (error set) */ static gboolean identify(GError **error) { g_autofree gchar *put_config_data_url = NULL; g_autoptr(JsonBuilder) builder = NULL; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_debug("Providing meta information to hawkbit server"); put_config_data_url = build_api_url("configData"); builder = json_build_status(NULL, NULL, "success", "closed", hawkbit_config->device); return rest_request_retriable(PUT, put_config_data_url, builder, NULL, error); } /** * @brief Deletes RAUC bundle at config's bundle_download_location (if given). */ static void process_deployment_cleanup() { if (!hawkbit_config->bundle_download_location) return; if (!g_file_test(hawkbit_config->bundle_download_location, G_FILE_TEST_IS_REGULAR)) return; if (g_remove(hawkbit_config->bundle_download_location)) g_warning("Failed to delete file: %s", hawkbit_config->bundle_download_location); } gboolean install_complete_cb(gpointer ptr) { gboolean res = FALSE; g_autoptr(GError) error = NULL; struct on_install_complete_userdata *result = ptr; g_autofree gchar *feedback_url = NULL; g_return_val_if_fail(ptr, FALSE); g_mutex_lock(&active_action->mutex); active_action->state = result->install_success ? ACTION_STATE_SUCCESS : ACTION_STATE_ERROR; feedback_url = build_api_url("deploymentBase/%s/feedback", active_action->id); res = feedback( feedback_url, active_action->id, result->install_success ? "Software bundle installed successfully." : "Failed to install software bundle.", result->install_success ? "success" : "failure", "closed", &error); if (!res) g_warning("%s", error->message); process_deployment_cleanup(); g_mutex_unlock(&active_action->mutex); if (result->install_success && hawkbit_config->post_update_reboot) { sync(); if (reboot(RB_AUTOBOOT) < 0) g_critical("Failed to reboot: %s", g_strerror(errno)); } return G_SOURCE_REMOVE; } /** * @brief Thread to download given Artifact, verfiy its checksum, send hawkBit * feedback and call software_ready_cb() callback on success. * * @param[in] data Artifact* to process * @return gpointer being 1 (TRUE) if download succeeded, 0 (FALSE) otherwise. The return value is * meant to be used with the GPOINTER_TO_INT() macro only. * Note that if the download thread waited for installation to finish ('run_once' mode), * TRUE means both installation and download succeeded. */ static gpointer download_thread(gpointer data) { struct on_new_software_userdata userdata = { .install_progress_callback = (GSourceFunc) hawkbit_progress, .install_complete_callback = install_complete_cb, .file = hawkbit_config->bundle_download_location, .auth_header = NULL, .ssl_key = NULL, .ssl_cert = NULL, .ssl_verify = hawkbit_config->ssl_verify, .install_success = FALSE, }; g_autoptr(GError) error = NULL, feedback_error = NULL; g_autofree gchar *msg = NULL, *sha1sum = NULL; g_autoptr(Artifact) artifact = data; curl_off_t speed; g_return_val_if_fail(data, NULL); g_assert_nonnull(hawkbit_config->bundle_download_location); g_mutex_lock(&active_action->mutex); if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) goto cancel; active_action->state = ACTION_STATE_DOWNLOADING; g_mutex_unlock(&active_action->mutex); g_message("Start downloading: %s", artifact->download_url); while (1) { gboolean resumable = FALSE; GStatBuf bundle_stat; curl_off_t resume_from = 0; g_clear_pointer(&sha1sum, g_free); // Download software bundle (artifact) if (g_stat(hawkbit_config->bundle_download_location, &bundle_stat) == 0) resume_from = (curl_off_t) bundle_stat.st_size; if (get_binary(artifact->download_url, hawkbit_config->bundle_download_location, resume_from, &sha1sum, &speed, &error)) break; for (const gint *code = &resumable_codes[0]; *code; code++) resumable |= g_error_matches(error, RHU_HAWKBIT_CLIENT_CURL_ERROR, *code); if (!hawkbit_config->resume_downloads || !resumable) { g_prefix_error(&error, "Download failed: "); goto report_err; } g_debug("%s, resuming download..", curl_easy_strerror(error->code)); g_mutex_lock(&active_action->mutex); if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) goto cancel; g_mutex_unlock(&active_action->mutex); g_clear_error(&error); // sleep 0.5 s before attempting to resume download g_usleep(500000); } // notify hawkbit that download is complete msg = g_strdup_printf("Download complete. %.2f MB/s", (double)speed/(1024*1024)); g_mutex_lock(&active_action->mutex); if (!feedback_progress(artifact->feedback_url, active_action->id, msg, &error)) { g_warning("%s", error->message); g_clear_error(&error); } g_mutex_unlock(&active_action->mutex); // validate checksum if (g_strcmp0(artifact->sha1, sha1sum)) { g_set_error(&error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_DOWNLOAD, "Software: %s V%s. Invalid checksum: %s expected %s", artifact->name, artifact->version, sha1sum, artifact->sha1); goto report_err; } g_mutex_lock(&active_action->mutex); // skip installation if hawkBit asked us to do so if (!artifact->do_install && (!artifact->maintenance_window || g_strcmp0(artifact->maintenance_window, "available") == 0)) { active_action->state = ACTION_STATE_SUCCESS; if (!feedback(artifact->feedback_url, active_action->id, "File checksum OK.", "success", "downloaded", &feedback_error)) g_warning("%s", feedback_error->message); g_mutex_unlock(&active_action->mutex); return GINT_TO_POINTER(TRUE); } if (!feedback_progress(artifact->feedback_url, active_action->id, "File checksum OK.", &error)) { g_warning("%s", error->message); g_clear_error(&error); } g_mutex_unlock(&active_action->mutex); // last chance to cancel installation g_mutex_lock(&active_action->mutex); if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) goto cancel; // skip installation if hawkBit asked us to do so if (!artifact->do_install) { active_action->state = ACTION_STATE_NONE; g_mutex_unlock(&active_action->mutex); return GINT_TO_POINTER(TRUE); } // start installation, cancelations are impossible now active_action->state = ACTION_STATE_INSTALLING; g_cond_signal(&active_action->cond); g_mutex_unlock(&active_action->mutex); software_ready_cb(&userdata); return GINT_TO_POINTER(userdata.install_success); report_err: g_mutex_lock(&active_action->mutex); if (!feedback(artifact->feedback_url, active_action->id, error->message, "failure", "closed", &feedback_error)) g_warning("%s", feedback_error->message); active_action->state = ACTION_STATE_ERROR; cancel: if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) active_action->state = ACTION_STATE_CANCELED; process_deployment_cleanup(); g_cond_signal(&active_action->cond); g_mutex_unlock(&active_action->mutex); return GINT_TO_POINTER(FALSE); } /** * @brief Start a RAUC HTTP streaming installation without prior bundle download. * This skips the download thread and starts the install thread (via rauc_install() * directly). * Must be called under locked active_action->mutex. * * @param[in] artifcat Artifact* to install * @param[out] error Error * @return TRUE if installation prcessing succeeded, FALSE otherwise (error set). * Note that this functions returns TRUE even for canceled/skipped installations. */ static gboolean start_streaming_installation(Artifact *artifact, GError **error) { g_autofree gchar *auth_header = get_auth_header(); struct on_new_software_userdata userdata = { .install_progress_callback = (GSourceFunc) hawkbit_progress, .install_complete_callback = install_complete_cb, .file = artifact->download_url, .auth_header = auth_header, .ssl_key = hawkbit_config->ssl_key, .ssl_cert = hawkbit_config->ssl_cert, .ssl_verify = hawkbit_config->ssl_verify, .install_success = FALSE, }; // installation might already be canceled if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) { active_action->state = ACTION_STATE_CANCELED; g_cond_signal(&active_action->cond); return TRUE; } // skip installation if hawkBit asked us to do so if (!artifact->do_install) { active_action->state = ACTION_STATE_NONE; return TRUE; } active_action->state = ACTION_STATE_INSTALLING; g_cond_signal(&active_action->cond); g_mutex_unlock(&active_action->mutex); software_ready_cb(&userdata); g_mutex_lock(&active_action->mutex); // in case of run_once, userdata.install_success is set and must be passed on if (!userdata.install_success) { g_set_error(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_STREAM_INSTALL, "Streaming installation failed"); return FALSE; } return TRUE; } /** * @brief Process hawkBit deployment described by req_root. * Must be called under locked active_action->mutex. * * @param[in] req_root JsonNode* describing the deployment to process * @param[out] error Error * @return TRUE if processing deployment succeeded, FALSE otherwise (error set) */ static gboolean process_deployment(JsonNode *req_root, GError **error) { g_autoptr(Artifact) artifact = g_new0(Artifact, 1); g_autofree gchar *deployment = NULL, *temp_id = NULL, *deployment_download = NULL, *deployment_update = NULL, *maintenance_window = NULL, *maintenance_msg = NULL; g_autoptr(JsonParser) json_response_parser = NULL; g_autoptr(JsonArray) json_chunks = NULL, json_artifacts = NULL; JsonNode *resp_root = NULL, *json_chunk = NULL, *json_artifact = NULL; goffset freespace; g_return_val_if_fail(req_root, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (active_action->state >= ACTION_STATE_PROCESSING) { g_set_error(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_ALREADY_IN_PROGRESS, "Deployment %s is already in progress.", active_action->id); // no need to tell hawkBit about this return FALSE; } active_action->state = ACTION_STATE_PROCESSING; // get deployment URL deployment = json_get_string(req_root, "$._links.deploymentBase.href", error); if (!deployment) goto error; // get deployment resource if (!rest_request(GET, deployment, NULL, &json_response_parser, error)) goto error; resp_root = json_parser_get_root(json_response_parser); // handle deployment.maintenanceWindow (only available if maintenance window is defined) maintenance_window = json_get_string(resp_root, "$.deployment.maintenanceWindow", NULL); artifact->maintenance_window = g_strdup(maintenance_window); maintenance_msg = maintenance_window ? g_strdup_printf(" (maintenance window is '%s')", maintenance_window) : g_strdup(""); // handle deployment.download=skip deployment_download = json_get_string(resp_root, "$.deployment.download", error); if (!deployment_download) goto error; if (!g_strcmp0(deployment_download, "skip")) { g_message("hawkBit requested to skip download, not downloading yet%s.", maintenance_msg); active_action->state = ACTION_STATE_NONE; return TRUE; } // handle deployment.update=skip deployment_update = json_get_string(resp_root, "$.deployment.update", error); if (!deployment_update) goto error; artifact->do_install = g_strcmp0(deployment_update, "skip") != 0; if (!artifact->do_install) g_message("hawkBit requested to skip installation, not invoking RAUC yet%s.", maintenance_msg); // remember deployment's action id temp_id = json_get_string(resp_root, "$.id", error); if (!artifact->do_install && !g_strcmp0(temp_id, active_action->id)) { g_debug("Deployment %s is still waiting%s.", active_action->id, maintenance_msg); active_action->state = ACTION_STATE_NONE; return TRUE; } // clean up on changed deployment id if (g_strcmp0(temp_id, active_action->id)) process_deployment_cleanup(); else g_debug("Continuing scheduled deployment %s%s.", active_action->id, maintenance_msg); g_free(active_action->id); active_action->id = g_steal_pointer(&temp_id); if (!active_action->id) goto error; artifact->feedback_url = build_api_url("deploymentBase/%s/feedback", active_action->id); // downloading multiple chunks not supported, only first chunk is downloaded (RAUC bundle) json_chunks = json_get_array(resp_root, "$.deployment.chunks", error); if (!json_chunks) goto proc_error; if (json_array_get_length(json_chunks) > 1) { g_set_error(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_MULTI_CHUNKS, "Deployment %s unsupported: cannot handle multiple chunks.", active_action->id); goto proc_error; } json_chunk = json_array_get_element(json_chunks, 0); // downloading multiple artifacts not supported, only first artifact is downloaded (RAUC bundle) json_artifacts = json_get_array(json_chunk, "$.artifacts", error); if (!json_artifacts) goto proc_error; if (json_array_get_length(json_artifacts) > 1) { g_set_error(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_MULTI_ARTIFACTS, "Deployment %s unsupported: cannot handle multiple artifacts.", active_action->id); goto proc_error; } json_artifact = json_array_get_element(json_artifacts, 0); // get artifact information artifact->version = json_get_string(json_chunk, "$.version", error); if (!artifact->version) goto proc_error; artifact->name = json_get_string(json_chunk, "$.name", error); if (!artifact->name) goto proc_error; artifact->size = json_get_int(json_artifact, "$.size", error); if (!artifact->size && error) goto proc_error; artifact->sha1 = json_get_string(json_artifact, "$.hashes.sha1", error); if (!artifact->sha1) goto proc_error; // favour https download artifact->download_url = json_get_string(json_artifact, "$._links.download.href", NULL); if (!artifact->download_url) artifact->download_url = json_get_string( json_artifact, "$._links.download-http.href", error); if (!artifact->download_url) { g_prefix_error(error, "\"$._links.download{-http,}.href\": "); goto proc_error; } g_message("New software ready for download (Name: %s, Version: %s, Size: %" G_GINT64_FORMAT " bytes, URL: %s)", artifact->name, artifact->version, artifact->size, artifact->download_url); // stream_bundle path exits early if (hawkbit_config->stream_bundle) return start_streaming_installation(artifact, error); // check if there is enough free diskspace if (!get_available_space(hawkbit_config->bundle_download_location, &freespace, error)) goto proc_error; if (freespace < artifact->size) { // notify hawkbit that there is not enough free space g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOSPC, "File size %" G_GINT64_FORMAT " exceeds available space %" G_GOFFSET_FORMAT, artifact->size, freespace); goto proc_error; } // unref/free previous download thread by joining it if (thread_download) g_thread_join(thread_download); // start download thread thread_download = g_thread_new("downloader", download_thread, (gpointer) g_steal_pointer(&artifact)); return TRUE; proc_error: feedback(artifact->feedback_url, active_action->id, (*error)->message, "failure", "closed", NULL); error: // clean up failed deployment process_deployment_cleanup(); active_action->state = ACTION_STATE_NONE; return FALSE; } /** * @brief Process hawkBit cancel action described by req_root. * * @param[in] req_root JsonNode* describing the cancel action * @param[out] error Error * @return TRUE if cancel action succeeded, FALSE otherwise (error set) */ static gboolean process_cancel(JsonNode *req_root, GError **error) { gboolean res = TRUE; g_autofree gchar *cancel_url = NULL, *feedback_url = NULL, *stop_id = NULL, *msg = NULL; g_autoptr(JsonParser) json_response_parser = NULL; JsonNode *resp_root = NULL; g_return_val_if_fail(req_root, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); // get cancel url cancel_url = json_get_string(req_root, "$._links.cancelAction.href", error); if (!cancel_url) return FALSE; // retrieve cancel details if (!rest_request(GET, cancel_url, NULL, &json_response_parser, error)) return FALSE; resp_root = json_parser_get_root(json_response_parser); // retrieve stop id stop_id = json_get_string(resp_root, "$.cancelAction.stopId", error); if (!stop_id) return FALSE; g_message("Received cancelation for action %s", stop_id); // send cancel feedback feedback_url = build_api_url("cancelAction/%s/feedback", stop_id); // cancel action if install not started yet g_mutex_lock(&active_action->mutex); if (!g_strcmp0(stop_id, active_action->id) && (active_action->state == ACTION_STATE_PROCESSING || active_action->state == ACTION_STATE_DOWNLOADING)) { g_debug("Action %s is in state %d, waiting for cancel request to be processed", stop_id, active_action->state); active_action->state = ACTION_STATE_CANCEL_REQUESTED; while (active_action->state == ACTION_STATE_CANCEL_REQUESTED) g_cond_wait(&active_action->cond, &active_action->mutex); } if (g_strcmp0(stop_id, active_action->id)) active_action->state = ACTION_STATE_NONE; // send feedback switch (active_action->state) { case ACTION_STATE_NONE: // action unknown, acknowledge cancelation nonetheless g_debug("Received cancelation for unprocessed action %s, acknowledging.", stop_id); // fall through case ACTION_STATE_CANCELED: res = feedback(feedback_url, stop_id, "Action canceled.", "success", "closed", error); break; case ACTION_STATE_SUCCESS: g_debug("Cancelation impossible, installation succeeded already"); break; case ACTION_STATE_ERROR: g_debug("Cancelation impossible, installation failed already"); break; case ACTION_STATE_INSTALLING: msg = g_strdup("Cancelation impossible, installation started already."); res = feedback(feedback_url, stop_id, msg, "success", "rejected", error); if (res) { res = FALSE; g_set_error(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_CANCELATION, "%s", msg); } break; default: // other states are not expected here g_critical("Unexpected action state after cancel request: %d", active_action->state); g_assert_not_reached(); break; } g_mutex_unlock(&active_action->mutex); return res; } void hawkbit_init(Config *config, GSourceFunc on_install_ready) { g_return_if_fail(config); hawkbit_config = config; software_ready_cb = on_install_ready; curl_global_init(CURL_GLOBAL_ALL); } typedef struct ClientData_ { GMainLoop *loop; gboolean res; long hawkbit_interval_check_sec; long last_run_sec; } ClientData; /** * @brief Callback for main loop, should run regularly, polls controller base poll resource and * triggers appropriate actions. * * @param[in] user_data ClientData* * @return TRUE if polling controller base resource and running appropriate actions succeeded, * FALSE otherwise */ static gboolean hawkbit_pull_cb(gpointer user_data) { ClientData *data = user_data; gboolean res = FALSE; g_autoptr(GError) error = NULL; g_autofree gchar *get_tasks_url = NULL; g_autoptr(JsonParser) json_response_parser = NULL; JsonNode *json_root = NULL; g_return_val_if_fail(user_data, FALSE); if (++data->last_run_sec < data->hawkbit_interval_check_sec) return G_SOURCE_CONTINUE; data->last_run_sec = 0; // build hawkBit get tasks URL get_tasks_url = build_api_url(NULL); g_message("Checking for new software..."); res = rest_request(GET, get_tasks_url, NULL, &json_response_parser, &error); if (!res) { if (g_error_matches(error, RHU_HAWKBIT_CLIENT_HTTP_ERROR, 401)) { if (hawkbit_config->auth_token) g_warning("Failed to authenticate. Check if auth_token is correct?"); if (hawkbit_config->gateway_token) g_warning("Failed to authenticate. Check if gateway_token is correct?"); if (hawkbit_config->ssl_key && hawkbit_config->ssl_cert) g_warning("Failed to authenticate. Check if ssl_key/ssl_cert are correct?"); } else if (error->code == CURLE_SSL_CERTPROBLEM) { g_warning("Failed to authenticate. Check if ssl_key/cert are correct?"); } else { g_warning("Scheduled check for new software failed: %s (%d)", error->message, error->code); } data->hawkbit_interval_check_sec = hawkbit_config->retry_wait; goto out; } // owned by the JsonParser and should never be modified or freed json_root = json_parser_get_root(json_response_parser); if (json_contains(json_root, "$._links.configData")) { // hawkBit has asked us to identify ourselves res = identify(&error); if (!res) { g_warning("%s", error->message); g_clear_error(&error); } } if (json_contains(json_root, "$._links.deploymentBase")) { // hawkBit has a new deployment for us g_mutex_lock(&active_action->mutex); res = process_deployment(json_root, &error); g_mutex_unlock(&active_action->mutex); if (!res) { if (g_error_matches(error, RHU_HAWKBIT_CLIENT_ERROR, RHU_HAWKBIT_CLIENT_ERROR_ALREADY_IN_PROGRESS)) g_debug("%s", error->message); else g_warning("%s", error->message); } } else { g_message("No new software."); } if (json_contains(json_root, "$._links.cancelAction")) { res = process_cancel(json_root, &error); if (!res) { g_warning("%s", error->message); g_clear_error(&error); } } // get hawkbit sleep time (how often should we check for new software) data->hawkbit_interval_check_sec = json_get_sleeptime(json_root); out: if (run_once) { if (thread_download) { gpointer thread_ret = g_thread_join(thread_download); res = GPOINTER_TO_INT(thread_ret); } data->res = res; g_main_loop_quit(data->loop); return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } int hawkbit_start_service_sync() { g_autoptr(GMainContext) ctx = NULL; ClientData cdata; g_autoptr(GSource) timeout_source = NULL; int res = 0; #ifdef WITH_SYSTEMD g_autoptr(GSource) event_source = NULL; g_autoptr(sd_event) event = NULL; #endif active_action = action_new(); ctx = g_main_context_new(); cdata.loop = g_main_loop_new(ctx, FALSE); cdata.hawkbit_interval_check_sec = hawkbit_config->retry_wait; cdata.last_run_sec = hawkbit_config->retry_wait; // pull every second timeout_source = g_timeout_source_new(1000); g_source_set_name(timeout_source, "Add timeout"); g_source_set_callback(timeout_source, (GSourceFunc) hawkbit_pull_cb, &cdata, NULL); g_source_attach(timeout_source, ctx); #ifdef WITH_SYSTEMD res = sd_event_default(&event); if (res < 0) goto finish; // enable automatic service watchdog support res = sd_event_set_watchdog(event, TRUE); if (res < 0) goto finish; event_source = sd_source_new(event); if (!event_source) { res = -ENOMEM; goto finish; } // attach systemd source to glib mainloop res = sd_source_attach(event_source, cdata.loop); if (res < 0) goto finish; sd_notify(0, "READY=1\nSTATUS=Init completed, start polling HawkBit for new software."); #endif g_main_loop_run(cdata.loop); res = cdata.res ? 0 : 1; #ifdef WITH_SYSTEMD sd_notify(0, "STOPPING=1\nSTATUS=Stopped polling HawkBit for new software."); #endif #ifdef WITH_SYSTEMD finish: g_source_destroy(event_source); sd_event_set_watchdog(event, FALSE); #endif curl_share_cleanup(curl_share); for (gint i = 0; i <= CURL_LOCK_DATA_LAST; i++) g_mutex_clear(&curl_share_locks[i]); g_main_loop_unref(cdata.loop); if (res < 0) g_warning("%s", strerror(-res)); return res; } void artifact_free(Artifact *artifact) { if (!artifact) return; g_free(artifact->name); g_free(artifact->version); g_free(artifact->download_url); g_free(artifact->feedback_url); g_free(artifact->sha1); g_free(artifact->maintenance_window); g_free(artifact); } void rest_payload_free(RestPayload *payload) { if (!payload) return; g_free(payload->payload); g_free(payload); } rauc-hawkbit-updater-1.4/src/json-helper.c000066400000000000000000000112171503520256600205700ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief JSON helper functions */ #include "json-helper.h" #include /** * @brief Get the first JsonNode element matching path in json_node. * * @param[in] json_node JsonNode to query * @param[in] path Query path * @param[out] error Error * @return JsonNode*, matching JsonNode element (must be freed), NULL on error */ static JsonNode* json_get_first_matching_element(JsonNode *json_node, const gchar *path, GError **error) { g_autoptr(JsonNode) match = NULL, node = NULL; JsonArray *arr = NULL; g_return_val_if_fail(json_node, NULL); g_return_val_if_fail(path, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); match = json_path_query(path, json_node, error); if (!match) return NULL; arr = json_node_get_array(match); if (!arr) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Failed to retrieve array from node for path %s", path); return NULL; } if (json_array_get_length(arr) > 0) node = json_array_dup_element(arr, 0); if (!node) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Failed to retrieve element from array for path %s", path); return NULL; } return g_steal_pointer(&node); } gchar* json_get_string(JsonNode *json_node, const gchar *path, GError **error) { g_autofree gchar *res_str = NULL; g_autoptr(JsonNode) result = NULL; g_return_val_if_fail(json_node, NULL); g_return_val_if_fail(path, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); result = json_get_first_matching_element(json_node, path, error); if (!result) return NULL; res_str = json_node_dup_string(result); if (!res_str) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Failed to retrieve string element from array for path %s", path); return NULL; } return g_steal_pointer(&res_str); } gint64 json_get_int(JsonNode *json_node, const gchar *path, GError **error) { g_autoptr(JsonNode) result = NULL; g_return_val_if_fail(json_node, 0); g_return_val_if_fail(path, 0); g_return_val_if_fail(error == NULL || *error == NULL, 0); result = json_get_first_matching_element(json_node, path, error); if (!result) return 0; if (!JSON_NODE_HOLDS_VALUE(result)) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Failed to retrieve value from node for path %s", path); return 0; } return json_node_get_int(result); } JsonArray* json_get_array(JsonNode *json_node, const gchar *path, GError **error) { g_autoptr(JsonArray) res_arr = NULL; g_autoptr(JsonNode) result = NULL; g_return_val_if_fail(error == NULL || *error == NULL, NULL); g_return_val_if_fail(json_node, NULL); g_return_val_if_fail(path, NULL); result = json_get_first_matching_element(json_node, path, error); if (!result) return NULL; if (!JSON_NODE_HOLDS_ARRAY(result)) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Failed to retrieve value from node for path %s", path); return NULL; } res_arr = json_node_dup_array(result); if (!res_arr || !json_array_get_length(res_arr)) { g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, "Empty JSON array for path %s", path); return NULL; } return g_steal_pointer(&res_arr); } gboolean json_contains(JsonNode *json_node, gchar *path) { g_autoptr(GError) error = NULL; g_autoptr(JsonNode) node = NULL; g_return_val_if_fail(json_node, FALSE); g_return_val_if_fail(path, FALSE); node = json_path_query(path, json_node, &error); if (!node) { // failed to compile expression to JSONPath g_warning("%s", error->message); return FALSE; } if (json_array_get_length(json_node_get_array(node)) > 0) return TRUE; return FALSE; } rauc-hawkbit-updater-1.4/src/log.c000066400000000000000000000056101503520256600171230ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief Log handling */ #include "log.h" #include static gboolean output_to_systemd = FALSE; /** * @brief convert GLogLevelFlags to string * * @param[in] level Log level that should be returned as string. * @return log level string */ static const gchar *log_level_to_string(GLogLevelFlags level) { switch (level) { case G_LOG_LEVEL_ERROR: return "ERROR"; case G_LOG_LEVEL_CRITICAL: return "CRITICAL"; case G_LOG_LEVEL_WARNING: return "WARNING"; case G_LOG_LEVEL_MESSAGE: return "MESSAGE"; case G_LOG_LEVEL_INFO: return "INFO"; case G_LOG_LEVEL_DEBUG: return "DEBUG"; default: return "UNKNOWN"; } } /** * @brief map glib log level to syslog * * @param[in] level Log level that should be returned as string. * @return syslog level */ #ifdef WITH_SYSTEMD static int log_level_to_int(GLogLevelFlags level) { switch (level) { case G_LOG_LEVEL_ERROR: return LOG_ERR; case G_LOG_LEVEL_CRITICAL: return LOG_CRIT; case G_LOG_LEVEL_WARNING: return LOG_WARNING; case G_LOG_LEVEL_MESSAGE: return LOG_NOTICE; case G_LOG_LEVEL_INFO: return LOG_INFO; case G_LOG_LEVEL_DEBUG: return LOG_DEBUG; default: return LOG_INFO; } } #endif /** * @brief Glib log handler callback * * @param[in] log_domain Log domain * @param[in] log_level Log level * @param[in] message Log message * @param[in] user_data Not used */ static void log_handler_cb(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { const gchar *log_level_str; #ifdef WITH_SYSTEMD if (output_to_systemd) { int log_level_int = log_level_to_int(log_level & G_LOG_LEVEL_MASK); sd_journal_print(log_level_int, "%s", message); } else { #endif log_level_str = log_level_to_string(log_level & G_LOG_LEVEL_MASK); if (log_level <= G_LOG_LEVEL_WARNING) { g_printerr("%s: %s\n", log_level_str, message); } else { g_print("%s: %s\n", log_level_str, message); } #ifdef WITH_SYSTEMD } #endif } void setup_logging(const gchar *domain, GLogLevelFlags level, gboolean p_output_to_systemd) { output_to_systemd = p_output_to_systemd; g_log_set_handler(NULL, level | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, log_handler_cb, NULL); } rauc-hawkbit-updater-1.4/src/rauc-hawkbit-updater.c000066400000000000000000000137301503520256600223670ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief RAUC HawkBit updater daemon */ #include #include #include #include "rauc-installer.h" #include "hawkbit-client.h" #include "config-file.h" #include "log.h" #define PROGRAM "rauc-hawkbit-updater" #define VERSION PROJECT_VERSION // program arguments static gchar *config_file = NULL; static gboolean opt_version = FALSE; static gboolean opt_debug = FALSE; static gboolean opt_run_once = FALSE; static gboolean opt_output_systemd = FALSE; // Commandline options static GOptionEntry entries[] = { { "config-file", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &config_file, "Configuration file", NULL }, { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version, "Version information", NULL }, { "debug", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_debug, "Enable debug output", NULL }, { "run-once", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_run_once, "Check and install new software and exit", NULL }, #ifdef WITH_SYSTEMD { "output-systemd", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_output_systemd, "Enable output to systemd", NULL }, #endif { NULL } }; // hawkbit callbacks static GSourceFunc notify_hawkbit_install_progress; static GSourceFunc notify_hawkbit_install_complete; /** * @brief GSourceFunc callback for install thread, consumes RAUC progress messages, logs them and * passes them on to notify_hawkbit_install_progress(). * * @param[in] data install_context pointer allowing access to received status messages * @return G_SOURCE_REMOVE is always returned */ static gboolean on_rauc_install_progress_cb(gpointer data) { struct install_context *context = data; g_return_val_if_fail(data, G_SOURCE_REMOVE); g_mutex_lock(&context->status_mutex); while (!g_queue_is_empty(&context->status_messages)) { g_autofree gchar *msg = g_queue_pop_head(&context->status_messages); g_message("Installing: %s : %s", context->bundle, msg); // notify hawkbit server about progress notify_hawkbit_install_progress(msg); } g_mutex_unlock(&context->status_mutex); return G_SOURCE_REMOVE; } /** * @brief GSourceFunc callback for install thread, consumes RAUC installation status result * (on complete) and passes it on to notify_hawkbit_install_complete(). * * @param[in] data install_context pointer allowing access to received status result * @return G_SOURCE_REMOVE is always returned */ static gboolean on_rauc_install_complete_cb(gpointer data) { struct install_context *context = data; struct on_install_complete_userdata userdata; g_return_val_if_fail(data, G_SOURCE_REMOVE); userdata.install_success = (context->status_result == 0); // notify hawkbit about install result notify_hawkbit_install_complete(&userdata); return G_SOURCE_REMOVE; } /** * @brief GSourceFunc callback for download thread, or main thread in case of HTTP streaming * installation. Triggers RAUC installation. * * @param[in] data on_new_software_userdata pointer * @return G_SOURCE_REMOVE is always returned */ static gboolean on_new_software_ready_cb(gpointer data) { struct on_new_software_userdata *userdata = data; g_return_val_if_fail(data, G_SOURCE_REMOVE); notify_hawkbit_install_progress = userdata->install_progress_callback; notify_hawkbit_install_complete = userdata->install_complete_callback; userdata->install_success = rauc_install(userdata->file, userdata->auth_header, userdata->ssl_key, userdata->ssl_cert, userdata->ssl_verify, on_rauc_install_progress_cb, on_rauc_install_complete_cb, run_once); return G_SOURCE_REMOVE; } int main(int argc, char **argv) { g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = NULL; g_auto(GStrv) args = NULL; GLogLevelFlags log_level; g_autoptr(Config) config = NULL; GLogLevelFlags fatal_mask; fatal_mask = g_log_set_always_fatal(G_LOG_FATAL_MASK); fatal_mask |= G_LOG_LEVEL_CRITICAL; g_log_set_always_fatal(fatal_mask); args = g_strdupv(argv); context = g_option_context_new(""); g_option_context_add_main_entries(context, entries, NULL); if (!g_option_context_parse_strv(context, &args, &error)) { g_printerr("option parsing failed: %s\n", error->message); return 1; } if (opt_version) { g_printf("Version %s\n", PROJECT_VERSION); return 0; } if (!config_file) { g_printerr("No configuration file given\n"); return 2; } if (!g_file_test(config_file, G_FILE_TEST_EXISTS)) { g_printerr("No such configuration file: %s\n", config_file); return 3; } run_once = opt_run_once; config = load_config_file(config_file, &error); if (!config) { g_printerr("Loading config file failed: %s\n", error->message); return 4; } log_level = (opt_debug) ? G_LOG_LEVEL_MASK : config->log_level; setup_logging(PROGRAM, log_level, opt_output_systemd); hawkbit_init(config, on_new_software_ready_cb); return hawkbit_start_service_sync(); } rauc-hawkbit-updater-1.4/src/rauc-installer.c000066400000000000000000000220361503520256600212700ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief RAUC client */ #include #include #include #include #include "gobject/gclosure.h" #include "rauc-installer.h" #include "rauc-installer-gen.h" static GThread *thread_install = NULL; /** * @brief RAUC DBUS property changed callback * * @see https://github.com/rauc/rauc/blob/master/src/de.pengutronix.rauc.Installer.xml */ static void on_installer_status(GDBusProxy *proxy, GVariant *changed, const gchar* const *invalidated, gpointer data) { struct install_context *context = data; gint32 percentage; g_autofree gchar *message = NULL; g_return_if_fail(changed); g_return_if_fail(context); if (invalidated && invalidated[0]) { g_warning("RAUC DBUS service disappeared"); g_mutex_lock(&context->status_mutex); context->status_result = 2; g_mutex_unlock(&context->status_mutex); g_main_loop_quit(context->mainloop); return; } if (context->notify_event) { gboolean status_received = FALSE; g_mutex_lock(&context->status_mutex); if (g_variant_lookup(changed, "Operation", "s", &message)) g_queue_push_tail(&context->status_messages, g_steal_pointer(&message)); else if (g_variant_lookup(changed, "Progress", "(isi)", &percentage, &message, NULL)) g_queue_push_tail(&context->status_messages, g_strdup_printf("%3" G_GINT32_FORMAT "%% %s", percentage, message)); else if (g_variant_lookup(changed, "LastError", "s", &message) && message[0] != 0) g_queue_push_tail(&context->status_messages, g_strdup_printf("LastError: %s", message)); status_received = !g_queue_is_empty(&context->status_messages); g_mutex_unlock(&context->status_mutex); if (status_received) g_main_context_invoke(context->loop_context, context->notify_event, context); } } /** * @brief RAUC DBUS complete signal callback * * @see https://github.com/rauc/rauc/blob/master/src/de.pengutronix.rauc.Installer.xml */ static void on_installer_completed(GDBusProxy *proxy, gint result, gpointer data) { struct install_context *context = data; g_return_if_fail(context); g_mutex_lock(&context->status_mutex); context->status_result = result; g_mutex_unlock(&context->status_mutex); if (result >= 0) g_main_loop_quit(context->mainloop); } /** * @brief Create and init a install_context * * @return Pointer to initialized install_context struct. Should be freed by calling * install_context_free(). */ static struct install_context *install_context_new(void) { struct install_context *context = g_new0(struct install_context, 1); g_mutex_init(&context->status_mutex); g_queue_init(&context->status_messages); context->status_result = -2; return context; } /** * @brief Free a install_context and its members * * @param[in] context the install_context struct that should be freed. * If NULL */ static void install_context_free(struct install_context *context) { if (!context) return; g_free(context->bundle); g_free(context->auth_header); g_mutex_clear(&context->status_mutex); // make sure all pending events are processed while (g_main_context_iteration(context->loop_context, FALSE)); g_main_context_unref(context->loop_context); g_assert_cmpint(context->status_result, >=, 0); g_assert_true(g_queue_is_empty(&context->status_messages)); g_main_loop_unref(context->mainloop); g_free(context); } /** * @brief RAUC client mainloop * * Install mainloop running until installation completes. * @param[in] data pointer to a install_context struct. * @return NULL is always returned. */ static gpointer install_loop_thread(gpointer data) { GBusType bus_type = (!g_strcmp0(g_getenv("DBUS_STARTER_BUS_TYPE"), "session")) ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM; RInstaller *r_installer_proxy = NULL; g_autoptr(GError) error = NULL; g_auto(GVariantDict) args = G_VARIANT_DICT_INIT(NULL); struct install_context *context = NULL; g_return_val_if_fail(data, NULL); context = data; g_main_context_push_thread_default(context->loop_context); if (context->auth_header) { gchar *headers[2] = {NULL, NULL}; headers[0] = context->auth_header; g_variant_dict_insert(&args, "http-headers", "^as", headers); g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify); } if (context->ssl_key && context->ssl_cert) { g_variant_dict_insert(&args, "tls-key", "s", context->ssl_key); g_variant_dict_insert(&args, "tls-cert", "s", context->ssl_cert); g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify); } g_debug("Creating RAUC DBUS proxy"); r_installer_proxy = r_installer_proxy_new_for_bus_sync( bus_type, G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, "de.pengutronix.rauc", "/", NULL, &error); if (!r_installer_proxy) { g_warning("Failed to create RAUC DBUS proxy: %s", error->message); goto notify_complete; } if (g_signal_connect(r_installer_proxy, "g-properties-changed", G_CALLBACK(on_installer_status), context) <= 0) { g_warning("Failed to connect properties-changed signal"); goto out_loop; } if (g_signal_connect(r_installer_proxy, "completed", G_CALLBACK(on_installer_completed), context) <= 0) { g_warning("Failed to connect completed signal"); goto out_loop; } g_debug("Trying to contact RAUC DBUS service"); if (!r_installer_call_install_bundle_sync(r_installer_proxy, context->bundle, g_variant_dict_end(&args), NULL, &error)) { g_warning("%s", error->message); goto out_loop; } g_main_loop_run(context->mainloop); out_loop: g_signal_handlers_disconnect_by_data(r_installer_proxy, context); notify_complete: // Notify the result of the RAUC installation if (context->notify_complete) context->notify_complete(context); g_clear_pointer(&r_installer_proxy, g_object_unref); g_main_context_pop_thread_default(context->loop_context); // on wait, calling function will take care of freeing after reading context->status_result if (!context->keep_install_context) install_context_free(context); return NULL; } gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gchar *ssl_key, gchar *ssl_cert, gboolean ssl_verify, GSourceFunc on_install_notify, GSourceFunc on_install_complete, gboolean wait) { GMainContext *loop_context = NULL; struct install_context *context = NULL; g_return_val_if_fail(bundle, FALSE); loop_context = g_main_context_new(); context = install_context_new(); context->bundle = g_strdup(bundle); context->auth_header = g_strdup(auth_header); context->ssl_key = ssl_key, context->ssl_cert = ssl_cert, context->ssl_verify = ssl_verify; context->notify_event = on_install_notify; context->notify_complete = on_install_complete; context->mainloop = g_main_loop_new(loop_context, FALSE); context->loop_context = loop_context; context->status_result = 2; context->keep_install_context = wait; // unref/free previous install thread by joining it if (thread_install) g_thread_join(thread_install); // start install thread thread_install = g_thread_new("installer", install_loop_thread, (gpointer) context); if (wait) { gboolean result; g_thread_join(thread_install); result = context->status_result == 0; install_context_free(context); return result; } // return immediately if we did not wait for the install thread return TRUE; } rauc-hawkbit-updater-1.4/src/rauc-installer.xml000066400000000000000000000076211503520256600216510ustar00rootroot00000000000000 rauc-hawkbit-updater-1.4/src/sd-helper.c000066400000000000000000000074161503520256600202330ustar00rootroot00000000000000/** * SPDX-License-Identifier: LGPL-2.1-only * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) * * @file * @brief Systemd helper */ #include "sd-helper.h" /** * @brief Callback function: prepare GSource * * @param[in] source sd_event_source that should be prepared. * @param[in] timeout not used * @return gboolean, TRUE on success, FALSE otherwise * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ static gboolean sd_source_prepare(GSource *source, gint *timeout) { g_return_val_if_fail(source, FALSE); return sd_event_prepare(((struct SDSource *) source)->event) > 0 ? TRUE : FALSE; } /** * @brief Callback function: check GSource * * @param[in] source sd_event_source that should be checked * @return gboolean, TRUE on success, FALSE otherwise * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ static gboolean sd_source_check(GSource *source) { g_return_val_if_fail(source, FALSE); return sd_event_wait(((struct SDSource *) source)->event, 0) > 0 ? TRUE : FALSE; } /** * @brief Callback function: dispatch * * @param[in] source sd_event_source that should be dispatched * @param[in] callback not used * @param[in] userdata not used * @return gboolean, TRUE on success, FALSE otherwise * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ static gboolean sd_source_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) { g_return_val_if_fail(source, FALSE); return sd_event_dispatch(((struct SDSource *) source)->event) >= 0 ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE; } /** * @brief Callback function: finalize GSource * * @param[in] source sd_event_source that should be finalized * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource */ static void sd_source_finalize(GSource *source) { g_return_if_fail(source); sd_event_unref(((struct SDSource *) source)->event); } /** * @brief Callback function: when source exits * * @param[in] source sd_event_source that exits * @param[in] userdata the GMainLoop the source is attached to. * @return always return 0 * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GMainLoop */ static int sd_source_on_exit(sd_event_source *source, void *userdata) { g_return_val_if_fail(source, -1); g_return_val_if_fail(userdata, -1); g_main_loop_quit(userdata); sd_event_source_set_enabled(source, FALSE); sd_event_source_unref(source); return 0; } int sd_source_attach(GSource *source, GMainLoop *loop) { g_return_val_if_fail(source, -1); g_return_val_if_fail(loop, -1); g_source_set_name(source, "sd-event"); g_source_add_poll(source, &((struct SDSource *) source)->pollfd); g_source_attach(source, g_main_loop_get_context(loop)); return sd_event_add_exit(((struct SDSource *) source)->event, NULL, sd_source_on_exit, loop); } GSource * sd_source_new(sd_event *event) { static GSourceFuncs funcs = { sd_source_prepare, sd_source_check, sd_source_dispatch, sd_source_finalize, }; GSource *s = NULL; g_return_val_if_fail(event, NULL); s = g_source_new(&funcs, sizeof(struct SDSource)); if (s) { ((struct SDSource *) s)->event = sd_event_ref(event); ((struct SDSource *) s)->pollfd.fd = sd_event_get_fd(event); ((struct SDSource *) s)->pollfd.events = G_IO_IN | G_IO_HUP; } return s; } rauc-hawkbit-updater-1.4/test-requirements.txt000066400000000000000000000000571503520256600216500ustar00rootroot00000000000000pytest attrs requests pydbus pygobject pexpect rauc-hawkbit-updater-1.4/test/000077500000000000000000000000001503520256600163645ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/test/conftest.py000066400000000000000000000324301503520256600205650ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix # SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix import os import sys from configparser import ConfigParser from string import Template import pytest from hawkbit_mgmt import HawkbitMgmtTestClient, HawkbitError from helper import run_pexpect, available_port def pytest_addoption(parser): """Register custom argparse-style options.""" parser.addoption( '--hawkbit-instance', help='HOST:PORT of hawkBit instance to use (default: %(default)s)', default='localhost:8080') @pytest.fixture(autouse=True) def env_setup(monkeypatch): monkeypatch.setenv('PATH', f'{os.path.dirname(os.path.abspath(__file__))}/../build', prepend=os.pathsep) monkeypatch.setenv('DBUS_STARTER_BUS_TYPE', 'session') @pytest.fixture(scope='session') def hawkbit(pytestconfig): """Instance of HawkbitMgmtTestClient connecting to a hawkBit instance.""" from uuid import uuid4 host, port = pytestconfig.option.hawkbit_instance.rsplit(':', 1) client = HawkbitMgmtTestClient(host, int(port)) client.set_config('pollingTime', '00:00:30') client.set_config('pollingOverdueTime', '00:03:00') client.set_config('authentication.targettoken.enabled', True) client.set_config('authentication.gatewaytoken.enabled', True) client.set_config('authentication.gatewaytoken.key', uuid4().hex) return client @pytest.fixture def hawkbit_target_added(hawkbit): """Creates a hawkBit target.""" # target ID must match /CN= in PKI's client key for mTLS tests (see test/gen_pki.sh) target = hawkbit.add_target(target_id='test-target') yield target hawkbit.delete_target(target) @pytest.fixture def config(tmp_path, hawkbit, hawkbit_target_added): """ Creates a temporary rauc-hawkbit-updater configuration matching the hawkBit (target) configuration of the hawkbit and hawkbit_target_added fixtures. """ target = hawkbit.get_target() target_token = target.get('securityToken') target_name = target.get('name') bundle_location = tmp_path / 'bundle.raucb' hawkbit_config = ConfigParser() hawkbit_config['client'] = { 'hawkbit_server': f'{hawkbit.host}:{hawkbit.port}', 'ssl': 'false', 'ssl_verify': 'false', 'tenant_id': 'DEFAULT', 'target_name': target_name, 'auth_token': target_token, 'bundle_download_location': str(bundle_location), 'retry_wait': '60', 'connect_timeout': '20', 'timeout': '60', 'log_level': 'debug', } hawkbit_config['device'] = { 'product': 'Terminator', 'model': 'T-1000', 'serialnumber': '8922673153', 'hw_revision': '2', 'mac_address': 'ff:ff:ff:ff:ff:ff', } tmp_config = tmp_path / 'rauc-hawkbit-updater.conf' with tmp_config.open('w') as f: hawkbit_config.write(f) return tmp_config @pytest.fixture def adjust_config(config): """ Adjusts the rauc-hawkbit-updater configuration created by the config fixture by adding/overwriting or removing options. """ def _adjust_config(options={'client': {}}, remove={}, add_trailing_space=False): adjusted_config = ConfigParser() adjusted_config.read(config) # update for section, option in options.items(): for key, value in option.items(): adjusted_config.set(section, key, value) # remove for section, option in remove.items(): adjusted_config.remove_option(section, option) # add trailing space if add_trailing_space: for orig_section, orig_options in adjusted_config.items(): for orig_option in orig_options.items(): adjusted_config.set(orig_section, orig_option[0], orig_option[1] + ' ') with config.open('w') as f: adjusted_config.write(f) return config return _adjust_config @pytest.fixture(scope='session') def rauc_bundle(tmp_path_factory): """Creates a temporary 512 KB file to be used as a dummy RAUC bundle.""" bundle = tmp_path_factory.mktemp('data') / 'bundle.raucb' bundle.write_bytes(os.urandom(512)*1024) return str(bundle) @pytest.fixture def assign_bundle(hawkbit, hawkbit_target_added, rauc_bundle, tmp_path): """ Creates a softwaremodule containing the file from the rauc_bundle fixture as an artifact. Creates a distributionset from this softwaremodule. Assigns this distributionset to the target created by the hawkbit_target_added fixture. Returns the corresponding action ID of this assignment. """ swmodules = [] artifacts = [] distributionsets = [] actions = [] def _assign_bundle(swmodules_num=1, artifacts_num=1, params=None): for i in range(swmodules_num): swmodule_type = 'application' if swmodules_num > 1 else 'os' swmodules.append(hawkbit.add_softwaremodule(module_type=swmodule_type)) for k in range(artifacts_num): # hawkBit will reject files with the same name, so symlink to unique names symlink_dest = tmp_path / f'{os.path.basename(rauc_bundle)}_{k}' try: os.symlink(rauc_bundle, symlink_dest) except FileExistsError: pass artifacts.append(hawkbit.add_artifact(symlink_dest, swmodules[-1])) dist_type = 'app' if swmodules_num > 1 else 'os' distributionsets.append(hawkbit.add_distributionset(module_ids=swmodules, dist_type=dist_type)) actions.append(hawkbit.assign_target(distributionsets[-1], params=params)) return actions[-1] yield _assign_bundle for action in actions: try: hawkbit.cancel_action(action, hawkbit_target_added, force=True) except HawkbitError: pass for distributionset in distributionsets: hawkbit.delete_distributionset(distributionset) for swmodule in swmodules: for artifact in artifacts: try: hawkbit.delete_artifact(artifact, swmodule) except HawkbitError: # artifact does not necessarily belong to this swmodule pass hawkbit.delete_softwaremodule(swmodule) @pytest.fixture def bundle_assigned(assign_bundle): """ Creates a softwaremodule containing the file from the rauc_bundle fixture as an artifact. Creates a distributionset from this softwaremodule. Assigns this distributionset to the target created by the hawkbit_target_added fixture. Returns the corresponding action ID of this assignment. """ assign_bundle() @pytest.fixture def rauc_dbus_install_success(rauc_bundle): """ Creates a RAUC D-Bus dummy interface on the SessionBus mimicing a successful installation on InstallBundle(). """ import pexpect proc = run_pexpect(f'{sys.executable} -m rauc_dbus_dummy {rauc_bundle}', cwd=os.path.dirname(__file__)) proc.expect('Interface published') yield assert proc.isalive() assert proc.terminate(force=True) proc.expect(pexpect.EOF) @pytest.fixture def rauc_dbus_install_failure(rauc_bundle): """ Creates a RAUC D-Bus dummy interface on the SessionBus mimicing a failing installation on InstallBundle(). """ proc = run_pexpect(f'{sys.executable} -m rauc_dbus_dummy {rauc_bundle} --completed-code=1', cwd=os.path.dirname(__file__), timeout=None) proc.expect('Interface published') yield assert proc.isalive() assert proc.terminate(force=True) @pytest.fixture(scope='session') def pki_dir(): return f'{os.path.dirname(__file__)}/pki' @pytest.fixture(scope='session') def nginx_config(tmp_path_factory, pki_dir): """ Creates a temporary nginx proxy configuration incorporating additional given options to the location section. See https://eclipse.dev/hawkbit/concepts/authentication/ for examples. """ def _to_nginx_option(option): key_values = (f'{key} {value};' for key, value in option.items()) return ' '.join(key_values) def _nginx_config(port, location_options, *, mtls=False): server_options = {} if mtls: server_options['ssl_verify_client'] = 'on' server_options['ssl_verify_depth'] = '3' nginx_temp = tmp_path_factory.mktemp('nginx') proxy_config = nginx_temp / 'nginx.conf' os.symlink(pki_dir, nginx_temp / 'pki') with open(f'{os.path.dirname(os.path.abspath(__file__))}/nginx/base.conf.in') as f: base_config_template = Template(f.read()) proxy_config_str = base_config_template.substitute( ssl='ssl' if mtls else '', port=port, location_options=_to_nginx_option(location_options), server_options=_to_nginx_option(server_options), module_path=os.environ.get('NGINX_MODULES', '/usr/lib/nginx/modules'), nginx_temp=nginx_temp, ) proxy_config.write_text(proxy_config_str) return proxy_config return _nginx_config @pytest.fixture(scope='session') def nginx_proxy(nginx_config): """ Runs an nginx proxy. HTTP requests are forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the proxy is running on. This port can be set in the rauc-hawkbit-updater config to proxy HTTP requests with custom options. """ import pexpect procs = [] def _nginx_proxy(options, *, mtls=False): port = available_port() proxy_config = nginx_config(port, options, mtls=mtls) try: proc = run_pexpect(f'nginx -c {proxy_config} -p .', timeout=None) except (pexpect.exceptions.EOF, pexpect.exceptions.ExceptionPexpect): pytest.skip('nginx unavailable') try: proc.expect('start worker process ') except pexpect.exceptions.EOF: pytest.skip('nginx failed, use -s to see logs') procs.append(proc) return port yield _nginx_proxy for proc in procs: assert proc.isalive() proc.terminate(force=True) proc.expect(pexpect.EOF) @pytest.fixture(scope='session') def ssl_issuer_hash(pki_dir): """Return's the PKI's issuer hash.""" with open(f"{pki_dir}/issuer_hash.txt") as f: return f.readline().strip() @pytest.fixture(scope='session') def rate_limited_port(nginx_proxy): """ Runs an nginx rate liming proxy, limiting download speeds to 70 KB/s. HTTP requests are forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the proxy is running on. This port can be set in the rauc-hawkbit-updater config to rate limit its HTTP requests. """ def _rate_limited_port(rate): location_options = {'proxy_limit_rate': rate} return nginx_proxy(location_options) return _rate_limited_port @pytest.fixture(scope='session') def partial_download_port(tmp_path_factory, rauc_bundle, nginx_proxy): """ Runs an nginx proxy, forcing partial downloads. HTTP requests are forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the proxy is running on. This port can be set in the rauc-hawkbit-updater config to test partial downloads. """ with open(f'{os.path.dirname(os.path.abspath(__file__))}/nginx/partial.inc.in') as f: partial_include_template = Template(f.read()) partial_include_str = partial_include_template.substitute( rauc_bundle=rauc_bundle, ) proxy_partial_config = tmp_path_factory.mktemp('nginx') / 'partial.inc' proxy_partial_config.write_text(partial_include_str) location_options = {'include': proxy_partial_config} return nginx_proxy(location_options) @pytest.fixture def mtls_download_port(nginx_proxy, ssl_issuer_hash): """ Runs an nginx proxy. HTTPS requests are forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the proxy is running on. This port can be set in the rauc-hawkbit-updater config to test mTLS authentication. """ location_options = {"proxy_set_header X-Ssl-Issuer-Hash-1": ssl_issuer_hash} return nginx_proxy(location_options, mtls=True) @pytest.fixture def download_without_auth_headers_port(tmp_path_factory, rauc_bundle, nginx_proxy): """ Runs an nginx proxy which requires artifact requests without authentication headers. HTTP requests are forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the proxy is running on. This port can be set in the rauc-hawkbit-updater config to test downloads without auth headers. """ nginx_conf_dir = f'{os.path.dirname(os.path.abspath(__file__))}/nginx' with open(f'{nginx_conf_dir}/download_without_auth_headers.inc.in') as f: dl_without_auth_include_template = Template(f.read()) dl_without_auth_include_str = dl_without_auth_include_template.substitute(rauc_bundle=rauc_bundle) dl_without_auth_config = tmp_path_factory.mktemp('nginx') / 'download_without_auth_headers.inc' dl_without_auth_config.write_text(dl_without_auth_include_str) location_options = {'include': dl_without_auth_config} return nginx_proxy(location_options) rauc-hawkbit-updater-1.4/test/gen_pki.sh000077500000000000000000000021151503520256600203360ustar00rootroot00000000000000#!/bin/sh set -e # TEST PKI - DO NOT USE FOR PRODUCTION! CERT_DIR="${1}" CONTROLLER_ID="test-target" CA_CERT="root-ca.crt" CA_KEY="root-ca.key" CA_CSR="root-csr.pem" CERT_CONFIG=' [client] basicConstraints = CA:FALSE nsCertType = client, email nsComment = "Local Test Client Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth ' mkdir -p ${CERT_DIR} cd ${CERT_DIR} echo "Development CA" openssl req -newkey rsa -keyout ${CA_KEY} -out ${CA_CSR} -subj "/O=Test/CN=localhost" --nodes openssl req -x509 -sha256 -new -nodes -key ${CA_KEY} -days 36500 -out ${CA_CERT} -subj '/CN=localhost' openssl genrsa -out "client.key" 4096 openssl req -new -key "client.key" -out "client.csr" -sha256 -subj "/CN=${CONTROLLER_ID}" openssl x509 -req -days 36500 -in "client.csr" -sha256 -CA ${CA_CERT} -CAkey ${CA_KEY} -CAcreateserial -out "client.crt" -extensions client -extfile <(printf ${CERT_CONFIG}) openssl x509 -in client.crt -issuer_hash -noout > issuer_hash.txt rauc-hawkbit-updater-1.4/test/hawkbit_mgmt.py000077700000000000000000000000001503520256600261542../script/hawkbit_mgmt.pyustar00rootroot00000000000000rauc-hawkbit-updater-1.4/test/helper.py000066400000000000000000000070441503520256600202220ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix # SPDX-FileCopyrightText: 2021-2023 Bastian Krause , Pengutronix import os import subprocess import shlex import logging import socket from contextlib import closing class PExpectLogger: """ pexpect Logger, allows to use Python's logging stdlib. To be passed as pexpect 'logfile". Logs linewise to given logger at given level. """ def __init__(self, level=logging.INFO, logger=None): self.level = level self.data = b'' self.logger = logger or logging.getLogger() def write(self, data): self.data += data def flush(self): for line in self.data.splitlines(): self.logger.log(self.level, line.decode()) self.data = b'' def logger_from_command(command): """ Returns a logger named after the executable, or in case of a python executable, after the python module, """ cmd_parts = command.split() base_cmd = os.path.basename(cmd_parts[0]) try: if base_cmd.startswith('python') and cmd_parts[1] == '-m': base_cmd = command.split()[2] except IndexError: pass return logging.getLogger(base_cmd) def run_pexpect(command, *, timeout=30, cwd=None): """ Runs given command via pexpect with DBUS_STARTER_BUS_TYPE=session and PATH+=./build. Returns process handle immediately allowing further expect calls. Logs command and its stdout/stderr/exit code. """ import pexpect logger = logger_from_command(command) logger.info('running: %s', command) pexpect_log = PExpectLogger(logger=logger) return pexpect.spawn(command, timeout=timeout, cwd=cwd, logfile=pexpect_log) def run(command, *, timeout=30): """ Runs given command as subprocess with DBUS_STARTER_BUS_TYPE=session and PATH+=./build. Blocks until command terminates. Logs command and its stdout/stderr/exit code. Returns tuple (stdout, stderr, exit code). """ logger = logger_from_command(command) logger.info('running: %s', command) def stdout_print_helper(logger, prefix, stdout): if stdout is None: return for line in stdout.splitlines(): if line: logger.info(f'{prefix}: %s', line) try: proc = subprocess.run(shlex.split(command), capture_output=True, text=True, check=False, timeout=timeout) except subprocess.TimeoutExpired as e: stdout_print_helper(logger, "stdout", e.stdout) stdout_print_helper(logger, "stderr", e.stderr) raise stdout_print_helper(logger, "stdout", proc.stdout) stdout_print_helper(logger, "stderr", proc.stderr) logger.info('exitcode: %d', proc.returncode) return proc.stdout, proc.stderr, proc.returncode def available_port(): """Returns an available local port.""" with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: sock.bind(('localhost', 0)) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock.getsockname()[1] def timezone_offset_utc(date): utc_offset = int(date.astimezone().utcoffset().total_seconds()) utc_offset_hours, remainder = divmod(utc_offset, 60*60) utc_offset_minutes, remainder = divmod(remainder, 60) sign_offset = '+' if utc_offset >=0 else '-' if remainder != 0: raise Exception('UTC offset contains fraction of a minute') return f'{sign_offset}{utc_offset_hours:02}:{utc_offset_minutes:02}' rauc-hawkbit-updater-1.4/test/nginx/000077500000000000000000000000001503520256600175075ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/test/nginx/base.conf.in000066400000000000000000000027451503520256600217050ustar00rootroot00000000000000load_module ${module_path}/ndk_http_module.so; load_module ${module_path}/ngx_http_lua_module.so; daemon off; pid /tmp/hawkbit-nginx-${port}.pid; # non-fatal alert for /var/log/nginx/error.log will still be shown # https://trac.nginx.org/nginx/ticket/147 error_log stderr notice; events { } http { access_log /dev/null; client_body_temp_path ${nginx_temp}/client_temp; proxy_temp_path ${nginx_temp}/proxy_temp_path; map $$ssl_client_s_dn $$ssl_client_s_dn_cn { default ""; ~CN=(?[^,]+) $$CN; } server { listen 127.0.0.1:${port} ${ssl}; listen [::1]:${port} ${ssl}; ssl_certificate pki/root-ca.crt; ssl_certificate_key pki/root-ca.key; ssl_client_certificate pki/root-ca.crt; ${server_options} location ~*/.*/controller/ { proxy_pass http://localhost:8080; proxy_set_header Host $$http_host; proxy_set_header X-Real-IP $$remote_addr; proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $$scheme; proxy_set_header X-Forwarded-Protocol $$scheme; proxy_set_header X-Forwarded-Port ${port}; proxy_set_header X-Ssl-Client-Cn $$ssl_client_s_dn_cn; # These are required for clients to upload and download software. proxy_request_buffering off; client_max_body_size 1000m; ${location_options} } } } rauc-hawkbit-updater-1.4/test/nginx/download_without_auth_headers.inc.in000066400000000000000000000007031503520256600267150ustar00rootroot00000000000000# serves artifacts only if no auth headers are provided location ~*/.*/controller/v1/test-target/softwaremodules/.*/artifacts/ { content_by_lua_block { local auth_header = ngx.req.get_headers()["authorization"] if auth_header then ngx.exit(ngx.HTTP_UNAUTHORIZED) else local file = io.open("${rauc_bundle}", "rb") ngx.print(file:read("*all")) file:close() end } } rauc-hawkbit-updater-1.4/test/nginx/partial.inc.in000066400000000000000000000020151503520256600222410ustar00rootroot00000000000000# serves only the first half of the dummy RAUC bundle by default, rest via # (pre-defined) range requests location ~*/.*/controller/v1/test-target/softwaremodules/.*/artifacts/ { content_by_lua_block { local file = io.open("${rauc_bundle}", "rb") local range = ngx.req.get_headers()["Range"] local size = file:seek("end") if not range then -- send only first half file:seek("set", 0) ngx.print(file:read(size / 2)) file:close() ngx.flush() ngx.exit(499) else local offset = range:match("bytes=(%d+)-") if not offset then return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end offset = tonumber(offset) file:seek("set", offset) ngx.header["Content-Length"] = size - offset ngx.header["Content-Range"] = string.format("bytes %d-%d/%d", offset, size - 1, size) ngx.print(file:read(size - offset)) file:close() end } } rauc-hawkbit-updater-1.4/test/pki/000077500000000000000000000000001503520256600171475ustar00rootroot00000000000000rauc-hawkbit-updater-1.4/test/pki/client.crt000066400000000000000000000026441503520256600211450ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIID/DCCAuSgAwIBAgIULpd6e/RTb5+OV+Hxpmsm/qWyv1EwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MDEwNzE1NTU1NFoYDzIxMjQx MjE0MTU1NTU0WjAWMRQwEgYDVQQDDAt0ZXN0LXRhcmdldDCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBALT2sLyHxqw8U/QwVoifv9DQL89nsiF5n8D29M8w Y+7cQ7kLIQYKJ/P40Rpj2FwmdQKpYaHNCUTH7wNe+4cI2tWpCnD9ft2+yRoOQ7MR msWI0dUDf4SIkBCdctqvvEqWfuEghKs0rJTEl+kl0DBFg7bGcWqTG+U1h6npgUA1 iLEV/9PZA+uOA+Ge0QsAUVybt84sZIejSx6YoK4PW2HgG2VjvMOUVPi+xZNQrNkW XnXASwl3KmMVra3KSR6AyK7Nm4xE7oP3k9deUN2eLWOowB8gKvNYi0G8ccyw8+Id mcVbZks92rIzijHhORE4CFVbk0ytriTJ3Uuf+aBHydqPEcxP0W+k23kQlO3xEovs 7PMWtlUxXlneHrhzdyX2rtkNf5NVl7D10HTf5iERNZiSwGPUKSlgvzpackgJnyOJ N+bna7nXQLVzROUuZt4/5hCied5zafQZRgEjC+x+FV/n6F2NJq5+hBx/LDDpNCRK gJic57XEiSeAQdtIcg0emSd5kt3q29icTI5rRrQF2qtJACATxECjTn4/JPI3G/lY xliJ6Hb22NepDgjgfbctZ9Q5Uri8UJMARhCi+haFH2Ay5bP1TUqNbG6lFR4hj7pI B4qAsqjpBk31k7kgm8pwQ4IHqoyxoJR/1aBFfebcJxMNrUIFjl1AFb+D20phuxyo HPF1AgMBAAGjQjBAMB0GA1UdDgQWBBQs2NOPhfMwo0MVIzrddHFUM3L4LzAfBgNV HSMEGDAWgBRZAah4gHQWkGRSzW4XStoffbynPjANBgkqhkiG9w0BAQsFAAOCAQEA n0D4XJQ/qiaQzRGhr9NLhBmCW5XRgj/hsOHWzLiDTaCPdl1qJeInahmw+5YLm0Id 3eUA0SGirMu4wgOaNwr8ZVpK/8+Ofjlh6rtWzwkkC1wOC4z+gtS1cxEde9eS3mz6 4CcNH5sTboRzsvSqi14BDn8IoI1YqqlUjTu5gp0tVu4kvWp/khJgGPL30gBHFfqM qJTdLj/J/eLwhpTDSfYCUsVfV3DyG7zzLdzaqsq79vXiUAXQyT0m07oZki2VW25P j2yWrbpapP+H3gymc3E/lfVQpJyu6dMBBs4LjkS5OBVFREGC/XiUL1gzzJu8ubfp BKmWvQvOznn+sgVPXz7S5Q== -----END CERTIFICATE----- rauc-hawkbit-updater-1.4/test/pki/client.csr000066400000000000000000000030621503520256600211370ustar00rootroot00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIEWzCCAkMCAQAwFjEUMBIGA1UEAwwLdGVzdC10YXJnZXQwggIiMA0GCSqGSIb3 DQEBAQUAA4ICDwAwggIKAoICAQC09rC8h8asPFP0MFaIn7/Q0C/PZ7IheZ/A9vTP MGPu3EO5CyEGCifz+NEaY9hcJnUCqWGhzQlEx+8DXvuHCNrVqQpw/X7dvskaDkOz EZrFiNHVA3+EiJAQnXLar7xKln7hIISrNKyUxJfpJdAwRYO2xnFqkxvlNYep6YFA NYixFf/T2QPrjgPhntELAFFcm7fOLGSHo0semKCuD1th4BtlY7zDlFT4vsWTUKzZ Fl51wEsJdypjFa2tykkegMiuzZuMRO6D95PXXlDdni1jqMAfICrzWItBvHHMsPPi HZnFW2ZLPdqyM4ox4TkROAhVW5NMra4kyd1Ln/mgR8najxHMT9FvpNt5EJTt8RKL 7OzzFrZVMV5Z3h64c3cl9q7ZDX+TVZew9dB03+YhETWYksBj1CkpYL86WnJICZ8j iTfm52u510C1c0TlLmbeP+YQonnec2n0GUYBIwvsfhVf5+hdjSaufoQcfyww6TQk SoCYnOe1xIkngEHbSHINHpkneZLd6tvYnEyOa0a0BdqrSQAgE8RAo05+PyTyNxv5 WMZYieh29tjXqQ4I4H23LWfUOVK4vFCTAEYQovoWhR9gMuWz9U1KjWxupRUeIY+6 SAeKgLKo6QZN9ZO5IJvKcEOCB6qMsaCUf9WgRX3m3CcTDa1CBY5dQBW/g9tKYbsc qBzxdQIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBADpyyR8mTi8acs6zOCylVSeV SLxoj1pIPPwUwft2X3nSj0U6OWmYFnIMKJ6/x7znuaufdC/SpK+j9VBZE1YjOyRp snaPCw+DCqrdP09f5qn4gmPAT0BpPdrK3hW4t+zBIgdXZVTIT4jfGh6gikHIUEuo /5RN3jaITf1kVBbmbftkccPCQ4onsYMDVykLiPe7IIewpx+7vA6l2U/SpJoxPp1w y5IFxj/hXTgSKA4Fqn4WqSJfaLVNzePlssbkWFkLsPVgJzouwlgIMYlYX2ZOyphh eHkvWV0R/po9nKCtROKFR5Rm5VfozKi2T+U3wtpBg4mk372wrg1KZnlfszB7Vc/P 0h2nk/zFej26+s+EbvDJHGSDUq/j/F6maNOkx1sEJKL82ZuSBj7WO8kZUrclEmuw bxBeO/DjFe5gB2A1WTB7SzVU0Qo8zCnFMdTor3fBjS910BbbZWFbmKa44iV1hQeS 9ORH6pjhqapV7CT88Zce32rn00BQ4TtsGU4Y/ABgtx2VgZ0cYaZgojQf47sOCBfr 4DCzof+ky2ZOzEzL77fX/TXS9C+phzWrU8XU7rV6HlonEkgdb5VQjY091bksOxDT gETvKHRW5I1YpXzj+XeZk66VO9/nEcIoaG4ZWWKQ/Ty4oDbe7ejpA/25LRINlTnX 8yXfyLHIXYiHbaU3NSd7 -----END CERTIFICATE REQUEST----- rauc-hawkbit-updater-1.4/test/pki/client.key000066400000000000000000000063101503520256600211370ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC09rC8h8asPFP0 MFaIn7/Q0C/PZ7IheZ/A9vTPMGPu3EO5CyEGCifz+NEaY9hcJnUCqWGhzQlEx+8D XvuHCNrVqQpw/X7dvskaDkOzEZrFiNHVA3+EiJAQnXLar7xKln7hIISrNKyUxJfp JdAwRYO2xnFqkxvlNYep6YFANYixFf/T2QPrjgPhntELAFFcm7fOLGSHo0semKCu D1th4BtlY7zDlFT4vsWTUKzZFl51wEsJdypjFa2tykkegMiuzZuMRO6D95PXXlDd ni1jqMAfICrzWItBvHHMsPPiHZnFW2ZLPdqyM4ox4TkROAhVW5NMra4kyd1Ln/mg R8najxHMT9FvpNt5EJTt8RKL7OzzFrZVMV5Z3h64c3cl9q7ZDX+TVZew9dB03+Yh ETWYksBj1CkpYL86WnJICZ8jiTfm52u510C1c0TlLmbeP+YQonnec2n0GUYBIwvs fhVf5+hdjSaufoQcfyww6TQkSoCYnOe1xIkngEHbSHINHpkneZLd6tvYnEyOa0a0 BdqrSQAgE8RAo05+PyTyNxv5WMZYieh29tjXqQ4I4H23LWfUOVK4vFCTAEYQovoW hR9gMuWz9U1KjWxupRUeIY+6SAeKgLKo6QZN9ZO5IJvKcEOCB6qMsaCUf9WgRX3m 3CcTDa1CBY5dQBW/g9tKYbscqBzxdQIDAQABAoICAAT3f17RUDHifwBlAj8COXLL AADujOxPyQN0E8CLCLch2wb9Z/ThOvr+gYi6pFHVKWF/EiOte7tkTGpYhUlNxCnY l6WFw6Fk7uKU8SU9TrTsviudSrbxw5h9Jc2cRKv8aPOXX6TOT41OvweiZs4oXkba N/svmfSmzTgr5UUEoyGPI8QmAO5Kac9bu0uUwObsjDUvsTuqzvFCSai1WbH8Q2w7 Ok9Y5yMjo0sZjADyVPng4v5Zw7NQAUITmwGoEboAgSwuqSg8l5Vx7IDEqmTBmTJR gPRIYhwWBr6SPcNfQhzxVwOqKTI6aWjYkGcpVUs4dfjFDJJMaxM8Cw8r4T2rLxcT TspHARj1FBm3t9CV3g6MPGMtx+2kVdqJZDuB0kd2mWqlW2fkq8ghuA6H4Q/Rcucb CpRBBlTDtDrJ8kjuiyHHHoKnXZnllFuAix2Z0jyujXF3VPCs4kkVB34fM/P2nDLz vhEWihGvIrxJ8L6Rc2wF6PfwGnnnh7BajeE1BtbVthNTG1+zGSWk9w+DcUS8+9GU OMwlBBvUznlduaRXdlvhY2PSzXqA6QdGgH9bKN1NnKM7OVs0CUVEZOEftGxEG/UN 1gHL7dTLbg8S3qNufOYIudu8LF0H/J1UMXWVnmgQwMvPhen0omaNkY7DukDY5nrW 84pLNl4d1ZZHr6MbRH/RAoIBAQD2Kpx8PP9KNT+9DHgqIiyDN+5/IXm9x45Q670u EpRujXUcU0I7BxuM4Z/cBjE7UWYeKbkvo1wGd7Alx3MvVWvBArVSTMamiXpGw22T NpY/AUuf5vptySwxcQUoD2WNI5F6VrnEtlYj1WIv1sLbuRC07dHUbM6W4KKwecOW ntVhwc/CLvPMHz7k9bsaN2jJy+ckEQHtA1QKAa2OXp6UumnK3HdSF3RHIWZggOaM 9NJe11Tqz3fHEzzRzsYhGwn+kPYqe+VxTbm1PT3oEVYyU9a5lsxbFIKoJn2G+Gyr 3lifI8oiXmoHpmA6ciCjuvLtuQXPMF+t+IIKR1FbQ+zqYnJFAoIBAQC8MUqGLWg1 cnXVls9iokwjbNYuSnpmgWGIunX562TEZK/r6JBy47TWLNpPUtBEJzfJL1KlOxEY mKRRa1QmyEipVxT5nq5EWjNgztz0S8ggNNyUnnpRTBd4OuwTcShZ0g9K/apG23EB lN7IBOQpk8PrG7+0/RI2eAtoXadeoIAOOQQHM6DSwraVsYExQ4Xr0ouwtIJgcB5x S84c6HfkJaTYsoUKys8ZjK8XTqJZnPYvvaCJuflUq5RIadN01vneesn9eb3tYobJ pqHc0AE5jjKRbvuKVj5j7aH00y6l1T+64PuDhUX3VVR4eaJi1Nz2hxONzhGsWXde 71y3xhjynw1xAoIBADPBg8MvQ/GDPpJt07nwE3HHSbKbBDCdi1OCLPVJ3MFdpni9 HJiyht1Y9rZY6vLwy0qeNRxJ+Wg2s+dNhu81w4ECI8NY9w8+qmHEZv5jpLU8fXzy IEcC2/LNM2tXyV8iUkzpfQdZ2sSP8aPjQHbX8yZHNi22br0UH8CA6Vl6rZxvFlJa ctHA5AVZkOwTKEn9P+y6UrBVWc78yVO8mxTkGKgZMFEnM0BdSlwR3edW9gAQv/7a ffFyHwBxvABr4e8O5WLLR9NJpzju1lw7gOb175h3lyGzL0FRTmUZSCfeiL101ePh ++L93Q+MvUYPYVIP66PDJOq77ANjOTy6DE6/Qg0CggEBAIhGZUHOZbS2Qs+9GkXB YsMQT/RLaGEL9XXOGBo2s8xnYODCkr8vVsb0yc1BGaEQiRv9UapmoFWgSvTOdKx0 wfEmJwEvP+AtNSE4CtY4fh2cSdKxA8AVCrW8bTES8vY/32UdVQ1kYGuVwtEygYn9 /5QjjunfjC23NgOINeeW78Pc06bnYuDbsN2rIPNsgvCmkRMPU43EArdl/kX+rLqf 68QjWaXcAKXusud8wKGFwNwHQ9YXvo3qhUD+qOnltiC958DORJM+kn45VOKSGSD1 cHbR2AJvu1QfQvUHa7MYPcL0ogy2GFknCDkJU2af37YBUE6SV0fnrUIosIo/P9eJ B3ECggEAI1ZTZJMDz3tLqantGVk4WvBM+B94od2xt8mFTBTaOla2XWv7cMT/ppQ/ kf9zV0Lkt0Z2I01PGK3HOnaLaNZuhdjqR+hL1DI3pSqgOVbug0CBxrqdtwOeWyjj 5nBHVf51ch8fgh2xGO6dsWUrR9yH+JnRkE5bQCz9g6JqzUJKPg5R9imar/2poD1d buW/SC/Fek3vN1fdKzFeYTHGkBJv/PRzyXl0loAbPISiqYsdYif/nbiRtUcNhddq RQIbNrMK7UDkPrYqZt6mXex8taT60+tUCD5FE8BXB2Y6D0g5i/ngNJaV7F6l3Cun Edm3pi9a4gTMl00YZm1pNR3KtcjxPA== -----END PRIVATE KEY----- rauc-hawkbit-updater-1.4/test/pki/issuer_hash.txt000066400000000000000000000000111503520256600222150ustar00rootroot00000000000000ce275665 rauc-hawkbit-updater-1.4/test/pki/root-ca.crt000066400000000000000000000021331503520256600212240ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUaJhh5fj41Euu4xNXKntyyxOu/k4wDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDEwNzExNTkxM1oXDTM1MDEw NTExNTkxM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAoS7ygEXHLW855TuHhbu/1rTRtkHccJRu4X7PSrnMDnZZ NIDFdDJXbkL2XCUDEhgrnp8CMrt2KL1tpZgn6tJibQqWHFTI9cj6jrg0UgtHpZUQ L297i1OXMW3j3wfUNRId4AifGBFsTxzcg3p4iXTiRYpcq8wn+ryEZjWO3bw8B4O9 dVC+oSE1DQH3Ojuz0rwq9gang4UIbf+HEdspRg+x9aHG0g+jUXxyX10jHkARdqvu ZGF0tVdPvTeiGOo1WmVAO8NmnrmrodLiWKn3ppiL6WY0DTbwHQunE+qEUtgQHpVX hvP+LGdSjpRV1BbZo7xYG72HGiChDkEcClD1o7fJqQIDAQABo1MwUTAdBgNVHQ4E FgQUWQGoeIB0FpBkUs1uF0raH328pz4wHwYDVR0jBBgwFoAUWQGoeIB0FpBkUs1u F0raH328pz4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAa49+ /lhwttHOCfRROIyggi6mfbXa7VUejf47dpZsJys7UfYk40cQIBazG1Kk2EOUcax0 NDslp1dSbTb3TTpU84izNYuOhPhbNYUrDPDHKqCrkgu1hRuHrOtT77aPg/1Hqcx7 kRZs7AFyrEBAkoUX1pb8uaGFF7NWPMHkaudidn+HqW3JCzc1NBhnqeZ+gJKEE7gs JeKEMEsR2zzvqgcEn2HcC+cYJ5HejNxWjlt6ROJlHVjN+kb/x6h0Y349UOrC6Nsp TD72gxr07DSLQ/DZvhSjLEBMaf/QOXJWsuuf4GvOmZOm/yWkINwCh4XJRCrBvRej 08KK9v9BbynlRi0E0w== -----END CERTIFICATE----- rauc-hawkbit-updater-1.4/test/pki/root-ca.key000066400000000000000000000032501503520256600212250ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChLvKARcctbznl O4eFu7/WtNG2QdxwlG7hfs9KucwOdlk0gMV0MlduQvZcJQMSGCuenwIyu3YovW2l mCfq0mJtCpYcVMj1yPqOuDRSC0ellRAvb3uLU5cxbePfB9Q1Eh3gCJ8YEWxPHNyD eniJdOJFilyrzCf6vIRmNY7dvDwHg711UL6hITUNAfc6O7PSvCr2BqeDhQht/4cR 2ylGD7H1ocbSD6NRfHJfXSMeQBF2q+5kYXS1V0+9N6IY6jVaZUA7w2aeuauh0uJY qfemmIvpZjQNNvAdC6cT6oRS2BAelVeG8/4sZ1KOlFXUFtmjvFgbvYcaIKEOQRwK UPWjt8mpAgMBAAECggEAQrStuqVmJWkDMlndH90QKzjhE4cVRmg/rUXwXxIen5ue Fmr2jLyqz2CaGY9dwnbLUpWm8L0BTSH8R0x9tKKGBm+bqTTziDi5bShRyuSNLbtP m/oMzJ/3EgdTB4HzclVBlO6sfOb1BpbAsg8U5HpBjJsS+CyBHTU4rB4dNqoIw+i6 2xEBjUP10zRZaECGd17dMSUP7S7Be8W1RAdSguM0l6bM66vdF0VVDUnIAF8U/odc Ch6e54Yp6ilw5Di6tdgWTMV1TRTAa3uWsXvh/WP4ZWXPnuI6SjoJahbzaKZEZY+h 9+sAABeXk8EErwt3Y7Zzn5pnIj7QtN+RtzaQ/cBDUwKBgQDSQP3+6uWwBFIxXw3u aKsxu0ZUxj6mS2T1PKnTy3qBfgaKXDbmKkMNz0/WdLqVqOm8XvMe/XJGAl9Q3JBm FjnSsG6FjkF9EPrZTSKZ6uer6zg3PmydDwFwtlwazguk0lqQ8i5UjglCeXa2+1FS 9TyA5uQsNpSbdXD2MkOWV2Tu7wKBgQDEQMKNql9p/ZlpYUUMrpByXq/B94dTD7Xj tf4NpNoMmzvOUibTv4NQN28YavOnI+qApnBXH5Kue7h5uxPr9r3UXs6V0t4Yhjmu ahM5ROChwqXnz0S2wEf5D2fNqXYY8nVPLBVjEFQz2gr/pPJ83lOZNntPjAI0KSfn JR71kp7Q5wKBgQCyDN3LoYpvG8mrC68VhUfsboZpxhRJ6TEOyJecxwTwYhQR2XOY SJPOPxCCMQnNMWT244WJaeH6zYmARbh26z1+YXDG4ygsTFPR75NsutQD78cEaXW7 L3jtxVCMVo7fvk95zc1UR5Ap3gidfoho80qQcncpxfLlD3hg1UINL+dGuwKBgCQ+ 8a/Ib6bbt6HG3UHiW7tD0aI5XTzyAd30lt9eOwdpBDqu4YzqKg5+rn4MAsQz1fO8 ybLNmgGvx/pzmtJR1+2JNQ5my64r3CtiW/qPxg0aLhoKJd661JAjUECjs7QX06Kz uZ96PJL3CmD8zexSA46giVW/vgh0MXJ3YKhqU/qxAoGAbyEC7eMqNzXakug2sa2e 98a7/w67HsPAh6kOmOnWa+PjtanLgLZN/qC9FNfok3/k+Ii+8NCwaeEeMKwoglvr 4vAmplaNHEQF+c8edozgj66CVP7uhJw3/5aLcJqIoBJCscB/ppshxLjJTnf2WixW MX7vdZ5UM+iMT+17BZqyH4s= -----END PRIVATE KEY----- rauc-hawkbit-updater-1.4/test/pki/root-ca.srl000066400000000000000000000000511503520256600212310ustar00rootroot000000000000002E977A7BF4536F9F8E57E1F1A66B26FEA5B2BF51 rauc-hawkbit-updater-1.4/test/pki/root-csr.pem000066400000000000000000000016171503520256600214270ustar00rootroot00000000000000-----BEGIN CERTIFICATE REQUEST----- MIICaDCCAVACAQAwIzENMAsGA1UECgwEVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoS7ygEXHLW855TuHhbu/ 1rTRtkHccJRu4X7PSrnMDnZZNIDFdDJXbkL2XCUDEhgrnp8CMrt2KL1tpZgn6tJi bQqWHFTI9cj6jrg0UgtHpZUQL297i1OXMW3j3wfUNRId4AifGBFsTxzcg3p4iXTi RYpcq8wn+ryEZjWO3bw8B4O9dVC+oSE1DQH3Ojuz0rwq9gang4UIbf+HEdspRg+x 9aHG0g+jUXxyX10jHkARdqvuZGF0tVdPvTeiGOo1WmVAO8NmnrmrodLiWKn3ppiL 6WY0DTbwHQunE+qEUtgQHpVXhvP+LGdSjpRV1BbZo7xYG72HGiChDkEcClD1o7fJ qQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAJ9ht5CuAwQup/quic9ihZGdAv/N D6XaNTMr/t2DaxSTJUUaF/IwVlpf8CMp3hX90uem7yUtOhyJVJPxPNLG+wVQHbBr 2GgVbpM2K6Avl43naBbq9elRpu/EV8Ulo+ewSB1kEE9+2w/MEOBdmBWRD0bqteS3 dzqOwkSht+SrhzVYzSeTq0Jz6Ju0N4C+IaPS4aOxqg9eQ9CqUgaDjx32D2nI3H+y oF5mN82fBLuEB9K3OZgzc7aECJPlJneY+gkZSBVzoU2n7j7E9qosT79cVvJ93qIN cSu5jaLQaG8FXWEqRyJiwvELwyKhWDRgyY5/SnNbsBcH7oQKGqv1tGwb5Kg= -----END CERTIFICATE REQUEST----- rauc-hawkbit-updater-1.4/test/rauc_dbus_dummy.py000077500000000000000000000154601503520256600221310ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix import hashlib import time from pathlib import Path from gi.repository import GLib from pydbus.generic import signal import requests class Installer: """ D-Bus interface `de.pengutronix.rauc.Installer`, to be used with `pydbus.{SessionBus,SystemBus}.publish()` The interface is defined via the xml read in the dbus class property. The relevant D-Bus properties are implemented as Python's @property/@Progress.setter. The D-Bus methods are Python methods. See https://github.com/LEW21/pydbus/blob/master/doc/tutorial.rst#class-preparation """ dbus = Path('../src/rauc-installer.xml').read_text() interface = 'de.pengutronix.rauc.Installer' Completed = signal() PropertiesChanged = signal() def __init__(self, bundle, completed_code=0): self._bundle = bundle self._completed_code = completed_code self._operation = 'idle' self._last_error = '' self._progress = 0, '', 1 def InstallBundle(self, source, args): def mimic_install(): """Mimics a sucessful/failing installation, depending on `self._completed_code`.""" progresses = [ 'Installing', 'Determining slot states', 'Determining slot states done.', 'Checking bundle', 'Verifying signature', 'Verifying signature done.', 'Checking bundle done.', 'Loading manifest file', 'Loading manifest file done.', 'Determining target install group', 'Determining target install group done.', 'Updating slots', 'Checking slot rootfs.1', 'Checking slot rootfs.1 done.', 'Copying image to rootfs.1', 'Copying image to rootfs.1 done.', 'Updating slots done.', 'Install failed.' if self._completed_code else 'Installing done.', ] self.Operation = 'installing' for i, progress in enumerate(progresses): percentage = (i+1)*100 / len(progresses) self.Progress = percentage, progress, 1 time.sleep(0.1) self.Completed(self._completed_code) if not self._completed_code: self.LastError = 'Installation error' self.Operation = 'idle' # do not call again return False print(f'installing {source}') try: self._check_install_requirements(source, args) except Exception as e: self.Completed(1) self.LastError = f'Installation error: {e}' self.Operation = 'idle' raise GLib.timeout_add_seconds(interval=1, function=mimic_install) @staticmethod def _get_bundle_sha1(bundle): """Calculates the SHA1 checksum of `self._bundle`.""" sha1 = hashlib.sha1() with open(bundle, 'rb') as f: while True: chunk = f.read(sha1.block_size) if not chunk: break sha1.update(chunk) return sha1.hexdigest() @staticmethod def _get_http_bundle_sha1(url, *, auth_header=None, cert=None, verify=True): """Download file from URL using HTTP range requests and compute its sha1 checksum.""" sha1 = hashlib.sha1() headers = auth_header or {} range_size = 128 * 1024 # default squashfs block size offset = 0 while True: headers['Range'] = f'bytes={offset}-{offset + range_size - 1}' r = requests.get(url, headers=headers, cert=cert, verify=verify) try: r.raise_for_status() sha1.update(r.content) except requests.HTTPError: if r.status_code == 416: # range not satisfiable, assuming download completed break raise offset += range_size return sha1.hexdigest() def _check_install_requirements(self, source, args): """ Check that required headers are set, bundle is accessible (HTTP or locally) and its checksum matches. """ if 'tls-key' in args and 'tls-cert' in args: cert = (args['tls-cert'], args['tls-key']) if 'tls-no-verify' in args and args['tls-no-verify']: verify = False elif 'tls-ca' in args: verify = args['tls-ca'] else: verify = True source_sha1 = self._get_http_bundle_sha1(source, cert=cert, verify=verify) elif 'http-headers' in args: assert len(args['http-headers']) == 1 [auth_header] = args['http-headers'] key, value = auth_header.split(': ', maxsplit=1) source_sha1 = self._get_http_bundle_sha1(source, auth_header={key: value}) # assume ssl_verify=false is set in test setup assert args['tls-no-verify'] is True else: source_sha1 = self._get_bundle_sha1(source) # check bundle checksum matches expected checksum assert source_sha1 == self._get_bundle_sha1(self._bundle) @property def Operation(self): return self._operation @Operation.setter def Operation(self, value): self._operation = value self.PropertiesChanged(Installer.interface, {'Operation': self.Operation}, []) @property def Progress(self): return self._progress @Progress.setter def Progress(self, value): self._progress = value self.PropertiesChanged(Installer.interface, {'Progress': self.Progress}, []) @property def LastError(self): return self._last_error @LastError.setter def LastError(self, value): self._last_error = value self.PropertiesChanged(Installer.interface, {'LastError': self.LastError}, []) @property def Compatible(self): return "not implemented" @property def Variant(self): return "not implemented" @property def BootSlot(self): return "not implemented" if __name__ == '__main__': import argparse from pydbus import SessionBus parser = argparse.ArgumentParser() parser.add_argument('bundle', help='Expected RAUC bundle') parser.add_argument('--completed-code', type=int, default=0, help='Code to emit as D-Bus Completed signal') args = parser.parse_args() loop = GLib.MainLoop() bus = SessionBus() installer = Installer(args.bundle, args.completed_code) with bus.publish('de.pengutronix.rauc', ('/', installer)): print('Interface published') loop.run() rauc-hawkbit-updater-1.4/test/test_basics.py000066400000000000000000000146541503520256600212530ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix # SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix import re from configparser import ConfigParser import pytest from helper import run def test_version(): """Test version argument.""" out, err, exitcode = run('rauc-hawkbit-updater -v') assert exitcode == 0 assert out.startswith('Version ') assert err == '' def test_invalid_arg(): """Test invalid argument.""" out, err, exitcode = run('rauc-hawkbit-updater --invalidarg') assert exitcode == 1 assert out == '' assert err.strip() == 'option parsing failed: Unknown option --invalidarg' def test_config_unspecified(): """Test call without config argument.""" out, err, exitcode = run('rauc-hawkbit-updater') assert exitcode == 2 assert out == '' assert err.strip() == 'No configuration file given' def test_config_file_non_existent(): """Test call with inexistent config file.""" out, err, exitcode = run('rauc-hawkbit-updater -c does-not-exist.conf') assert exitcode == 3 assert out == '' assert err.strip() == 'No such configuration file: does-not-exist.conf' def test_config_no_auth(adjust_config): """Test config without authentication option in client section.""" config = adjust_config(remove={'client': 'auth_token'}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 4 assert out == '' assert err.strip() == \ "Loading config file failed: Neither token nor ssl authentication set" def test_config_multiple_token_auth_methods(adjust_config): """Test config with auth_token and gateway_token options in client section.""" config = adjust_config({'client': {'gateway_token': 'wrong-gateway-token'}}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 4 assert out == '' assert err.strip() == \ "Loading config file failed: Both 'auth_token' and 'gateway_token' set" def test_config_multiple_auth_methods(adjust_config): """Test config with both token and ssl auth options in client section.""" config = adjust_config( {'client': {'ssl': 'true', 'ssl_key': 'key', 'ssl_cert': 'cert'}} ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 4 assert out == '' assert err.strip() == \ "Loading config file failed: Both token and ssl authentication set" def test_register_and_check_invalid_gateway_token(adjust_config): """Test config with invalid gateway_token.""" config = adjust_config( {'client': {'gateway_token': 'wrong-gateway-token'}}, remove={'client': 'auth_token'} ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 1 assert 'MESSAGE: Checking for new software...' in out assert err.strip() == 'WARNING: Failed to authenticate. Check if gateway_token is correct?' @pytest.mark.parametrize("trailing_space", ('no_trailing_space', 'trailing_space')) def test_register_and_check_valid_gateway_token(hawkbit, adjust_config, trailing_space): """Test config with valid gateway_token.""" gateway_token = hawkbit.get_config('authentication.gatewaytoken.key') config = adjust_config( {'client': {'gateway_token': gateway_token}}, remove={'client': 'auth_token'}, add_trailing_space=(trailing_space == 'trailing_space'), ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 0 assert 'MESSAGE: Checking for new software...' in out assert err == '' def test_register_and_check_invalid_auth_token(adjust_config): """Test config with invalid auth_token.""" config = adjust_config({'client': {'auth_token': 'wrong-auth-token'}}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 1 assert 'MESSAGE: Checking for new software...' in out assert err.strip() == 'WARNING: Failed to authenticate. Check if auth_token is correct?' @pytest.mark.parametrize("trailing_space", ('no_trailing_space', 'trailing_space')) def test_register_and_check_valid_auth_token(adjust_config, trailing_space): """Test config with valid auth_token.""" config = adjust_config( add_trailing_space=(trailing_space == 'trailing_space'), ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 0 assert 'MESSAGE: Checking for new software...' in out assert err == '' def test_register_and_check_no_download_location_no_streaming(adjust_config): """Test config without bundle_download_location and without stream_bundle.""" config = adjust_config(remove={'client': 'bundle_download_location'}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 4 assert out == '' assert err.strip() == \ "Loading config file failed: 'bundle_download_location' is required if 'stream_bundle' is disabled" def test_identify(hawkbit, config): """ Test that supplying target meta information works and information are received correctly by hawkBit. """ out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 0 assert 'Providing meta information to hawkbit server' in out assert err == '' ref_config = ConfigParser() ref_config.read(config) assert dict(ref_config.items('device')) == hawkbit.get_attributes() @pytest.mark.parametrize("multi_object", ('chunks', 'artifacts')) def test_unsupported_multi_objects(hawkbit, config, assign_bundle, multi_object): """ Test that deployments with multiple software modules (called chunks in the DDI API) or multiple artifacts are rejected. """ expected_error = rf'Deployment \d*? unsupported: cannot handle multiple {multi_object}.' if multi_object == 'chunks': assign_param = {'swmodules_num': 2} elif multi_object == 'artifacts': assign_param = {'artifacts_num': 2} assign_bundle(**assign_param) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert exitcode == 1 assert re.fullmatch(f'(WARNING: {expected_error}\n){{2}}', err) status = hawkbit.get_action_status() assert status[0]['type'] == 'error' assert re.fullmatch(expected_error, status[0]['messages'][0]) rauc-hawkbit-updater-1.4/test/test_cancel.py000066400000000000000000000101621503520256600212220ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix from pexpect import TIMEOUT, EOF import pytest from helper import run, run_pexpect @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_cancel_before_poll(hawkbit, adjust_config, bundle_assigned, rauc_dbus_install_success, mode): """ Assign distribution containing bundle to target and cancel it right away. Then run rauc-hawkbit-updater and make sure it acknowledges the not yet processed action. """ hawkbit.cancel_action() config_params = {'client': {'stream_bundle': 'true'}} if mode == 'streaming' else {} config = adjust_config(config_params) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert f'Received cancelation for unprocessed action {hawkbit.id["action"]}, acknowledging.' \ in out assert 'Action canceled.' in out assert err == '' assert exitcode == 0 cancel = hawkbit.get_action() assert cancel['type'] == 'cancel' assert cancel['status'] == 'finished' cancel_status = hawkbit.get_action_status() assert cancel_status[0]['type'] == 'canceled' assert 'Action canceled.' in cancel_status[0]['messages'] def test_cancel_during_download(hawkbit, adjust_config, bundle_assigned, rate_limited_port): """ Assign distribution containing bundle to target. Run rauc-hawkbit-updater configured to comminucate via rate-limited proxy with hawkBit. Cancel the assignment once the download started and make sure the cancelation is acknowledged and no installation is started. """ port = rate_limited_port('70k') config_params = {'client': {'hawkbit_server': f'{hawkbit.host}:{port}'}} config = adjust_config(config_params) # note: we cannot use -r here since that prevents further polling of the base resource # announcing the cancelation proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') proc.expect('Start downloading: ') proc.expect(TIMEOUT, timeout=1) # assuming: # - rauc-hawkbit-updater polls base resource every 5 s for cancelations during download # - download of 512 KB bundle @ 70 KB/s takes ~7.3 s # -> cancelation should be received and processed before download finishes hawkbit.cancel_action() # do not wait longer than 5 s (poll interval) + 3 s (processing margin) proc.expect(f'Received cancelation for action {hawkbit.id["action"]}', timeout=8) proc.expect('Action canceled.') # wait for feedback to arrive at hawkbit server proc.expect(TIMEOUT, timeout=2) proc.terminate(force=True) proc.expect(EOF) cancel = hawkbit.get_action() assert cancel['type'] == 'cancel' assert cancel['status'] == 'finished' cancel_status = hawkbit.get_action_status() assert cancel_status[0]['type'] == 'canceled' assert 'Action canceled.' in cancel_status[0]['messages'] @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_cancel_during_install(hawkbit, adjust_config, bundle_assigned, rauc_dbus_install_success, mode): """ Assign distribution containing bundle to target. Run rauc-hawkbit-updater and cancel the assignment once the installation started. Make sure the cancelation does not disrupt the installation. """ config_params = {'client': {'stream_bundle': 'true'}} if mode == 'streaming' else {} config = adjust_config(config_params) proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') proc.expect('MESSAGE: Installing: ') hawkbit.cancel_action() # wait for installation to finish proc.expect('Software bundle installed successfully.') # wait for feedback to arrive at hawkbit server proc.expect(TIMEOUT, timeout=2) proc.terminate(force=True) proc.expect(EOF) cancel = hawkbit.get_action() assert cancel['type'] == 'update' assert cancel['status'] == 'finished' cancel_status = hawkbit.get_action_status() assert cancel_status[0]['type'] == 'finished' assert 'Software bundle installed successfully.' in cancel_status[0]['messages'] rauc-hawkbit-updater-1.4/test/test_download.py000066400000000000000000000164021503520256600216070ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021-2025 Bastian Krause , Pengutronix import re from helper import run def test_download_inexistent_location(hawkbit, bundle_assigned, adjust_config): """ Assign bundle to target and test download to an inexistent location specified in config. """ location = '/tmp/does_not_exist/foo' config = adjust_config( {'client': {'bundle_download_location': location}} ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'New software ready for download' in out # same warning from feedback() and from hawkbit_pull_cb() assert err == \ f'WARNING: Failed to calculate free space for {location}: No such file or directory\n'*2 assert exitcode == 1 status = hawkbit.get_action_status() assert status[0]['type'] == 'error' assert f'Failed to calculate free space for {location}: No such file or directory' in \ status[0]['messages'] def test_download_unallowed_location(hawkbit, bundle_assigned, adjust_config): """ Assign bundle to target and test download to an unallowed location specified in config. """ location = '/root/foo' config = adjust_config( {'client': {'bundle_download_location': location}} ) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Start downloading' in out assert err.strip() == \ f'WARNING: Download failed: Failed to open {location} for download: Permission denied' assert exitcode == 1 status = hawkbit.get_action_status() assert status[0]['type'] == 'error' assert f'Download failed: Failed to open {location} for download: Permission denied' in \ status[0]['messages'] def test_download_too_slow(hawkbit, bundle_assigned, adjust_config, rate_limited_port): """Assign bundle to target and test too slow download of bundle.""" # limit to 50 bytes/s port = rate_limited_port(50) config = adjust_config({ 'client': { 'hawkbit_server': f'{hawkbit.host}:{port}', 'low_speed_time': '3', 'low_speed_rate': '100', } }) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r', timeout=90) assert 'Start downloading: ' in out assert err.strip() == 'WARNING: Download failed: Timeout was reached' assert exitcode == 1 def test_download_partials_without_resume(hawkbit, bundle_assigned, adjust_config, partial_download_port): """ Assign bundle to target and test download of partial bundle parts without having download resuming configured. """ config = adjust_config( {'client': {'hawkbit_server': f'{hawkbit.host}:{partial_download_port}'}} ) # ignore failing installation out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Start downloading: ' in out assert err.strip() == 'WARNING: Download failed: Transferred a partial file' assert exitcode == 1 def test_download_partials_with_resume(hawkbit, bundle_assigned, adjust_config, partial_download_port): """ Assign bundle to target and test download of partial bundle parts with download resuming configured. """ config = adjust_config({ 'client': { 'hawkbit_server': f'{hawkbit.host}:{partial_download_port}', 'resume_downloads': 'true', } }) # ignore failing installation out, _, _ = run(f'rauc-hawkbit-updater -c "{config}" -r') assert re.findall('Resuming download from offset [1-9]', out) assert 'Download complete.' in out assert 'File checksum OK.' in out def test_download_slow_with_resume(hawkbit, bundle_assigned, adjust_config, rate_limited_port): """ Assign bundle to target and test slow download of bundle with download resuming enabled. That should lead to resuming downloads. """ port = rate_limited_port(50000) config = adjust_config({ 'client': { 'hawkbit_server': f'{hawkbit.host}:{port}', 'resume_downloads': 'true', 'low_speed_time': '1', 'low_speed_rate': '100000', } }) # ignore failing installation out, _, _ = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Timeout was reached, resuming download..' in out assert 'Resuming download from offset' in out assert 'Download complete.' in out assert 'File checksum OK.' in out def test_download_only(hawkbit, config, assign_bundle): """Test "downloadonly" deployment.""" assign_bundle(params={'type': 'downloadonly'}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Start downloading' in out assert 'hawkBit requested to skip installation, not invoking RAUC yet.' in out assert 'Download complete' in out assert 'File checksum OK' in out assert err == '' assert exitcode == 0 status = hawkbit.get_action_status() assert status[0]['type'] == 'downloaded' # check last status message assert 'File checksum OK.' in status[0]['messages'] def test_download_only_with_auth_header(hawkbit, adjust_config, assign_bundle, download_without_auth_headers_port): """ Test that rauc-hawkbit-updater fails when sending authentication header to external storage provider with send_download_authentication=true. """ config = adjust_config({'client': { 'send_download_authentication': 'true', 'hawkbit_server': f'{hawkbit.host}:{download_without_auth_headers_port}' }}) assign_bundle(params={'type': 'downloadonly'}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Start downloading' in out assert 'hawkBit requested to skip installation, not invoking RAUC yet.' in out assert 'Download failed: HTTP request failed: 401' in err assert exitcode == 1 status = hawkbit.get_action_status() assert status[0]['type'] == 'error' # check last status message assert 'Download failed: HTTP request failed: 401' in status[0]['messages'] def test_download_only_without_auth_header(hawkbit, adjust_config, assign_bundle, download_without_auth_headers_port): """ Test that rauc-hawkbit-updater does not send authentication header with send_download_authentication=false. Test that rauc-hawkbit-updater succeeds when not sending authentication header to external storage provider with send_download_authentication=false. """ config = adjust_config({'client': { 'send_download_authentication': 'false', 'hawkbit_server': f'{hawkbit.host}:{download_without_auth_headers_port}' }}) assign_bundle(params={'type': 'downloadonly'}) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'Start downloading' in out assert 'hawkBit requested to skip installation, not invoking RAUC yet.' in out assert 'Download complete' in out assert 'File checksum OK' in out assert err == '' assert exitcode == 0 status = hawkbit.get_action_status() assert status[0]['type'] == 'downloaded' # check last status message assert 'File checksum OK.' in status[0]['messages'] rauc-hawkbit-updater-1.4/test/test_install.py000066400000000000000000000114141503520256600214440ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix from datetime import datetime, timedelta from pathlib import Path from pexpect import TIMEOUT, EOF import pytest from helper import run, run_pexpect, timezone_offset_utc @pytest.fixture def install_config(config, adjust_config): def _install_config(mode): if mode == 'streaming': return adjust_config( {'client': {'stream_bundle': 'true'}}, remove={'client': 'bundle_download_location'}, ) return config return _install_config @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_install_bundle_no_dbus_iface(hawkbit, install_config, bundle_assigned, mode): """Assign bundle to target and test installation without RAUC D-Bus interface available.""" config = install_config(mode) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') err_lines = err.splitlines() assert 'New software ready for download' in out if mode == 'download': assert 'Download complete' in out assert err_lines.pop(0) == \ 'WARNING: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name de.pengutronix.rauc was not provided by any .service files' assert err_lines.pop(0) == 'WARNING: Failed to install software bundle.' if mode == 'streaming': assert err_lines.pop(0) == 'WARNING: Streaming installation failed' assert not err_lines assert exitcode == 1 status = hawkbit.get_action_status() assert status[0]['type'] == 'error' @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_install_success(hawkbit, install_config, bundle_assigned, rauc_dbus_install_success, mode): """ Assign bundle to target and test successful download and installation. Make sure installation result is received correctly by hawkBit. """ config = install_config(mode) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'New software ready for download' in out if mode == 'download': assert 'Download complete' in out assert 'Software bundle installed successfully.' in out assert err == '' assert exitcode == 0 status = hawkbit.get_action_status() assert status[0]['type'] == 'finished' @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_install_failure(hawkbit, install_config, bundle_assigned, rauc_dbus_install_failure, mode): """ Assign bundle to target and test successful download and failing installation. Make sure installation result is received correctly by hawkBit. """ config = install_config(mode) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'New software ready for download' in out assert 'WARNING: Failed to install software bundle.' in err assert exitcode == 1 status = hawkbit.get_action_status() assert status[0]['type'] == 'error' assert 'Failed to install software bundle.' in status[0]['messages'] @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_install_maintenance_window(hawkbit, install_config, rauc_bundle, assign_bundle, rauc_dbus_install_success, mode): bundle_size = Path(rauc_bundle).stat().st_size maintenance_start = datetime.now() + timedelta(seconds=15) maintenance_window = { 'maintenanceWindow': { 'schedule' : maintenance_start.strftime('%-S %-M %-H ? %-m * %-Y'), 'timezone' : timezone_offset_utc(maintenance_start), 'duration' : '00:01:00' } } assign_bundle(params=maintenance_window) config = install_config(mode) proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') proc.expect(r"hawkBit requested to skip installation, not invoking RAUC yet \(maintenance window is 'unavailable'\)") if mode == 'download': proc.expect('Start downloading') proc.expect('Download complete') proc.expect('File checksum OK') # wait for the maintenance window to become available and the next poll of the base resource proc.expect(TIMEOUT, timeout=30) proc.expect(r"Continuing scheduled deployment .* \(maintenance window is 'available'\)") # RAUC bundle should have been already downloaded completely if mode == 'download': proc.expect(f'Resuming download from offset {bundle_size}') proc.expect('Download complete') proc.expect('File checksum OK') proc.expect('Software bundle installed successfully') # let feedback propagate to hawkBit before termination proc.expect(TIMEOUT, timeout=2) proc.terminate(force=True) proc.expect(EOF) status = hawkbit.get_action_status() assert status[0]['type'] == 'finished' rauc-hawkbit-updater-1.4/test/test_mtls.py000066400000000000000000000030761503520256600207620ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2024-2025 Florian Bezannier # SPDX-FileCopyrightText: 2024-2025 Robin van der Gracht , Protonic Holland import pytest from helper import run @pytest.mark.parametrize('mode', ('download', 'streaming')) def test_install_success_mtls(hawkbit, adjust_config, bundle_assigned, mtls_download_port, pki_dir, ssl_issuer_hash, rauc_dbus_install_success, mode): """ Assign bundle to target and test successful download and installation via mTLS. Make sure installation result is received correctly by hawkBit. """ config = adjust_config( {'client': { 'hawkbit_server': f'localhost:{mtls_download_port}', 'ssl': 'true', 'ssl_key': f'{pki_dir}/client.key', 'ssl_cert': f'{pki_dir}/client.crt', 'ssl_verify': 'false', 'stream_bundle': 'true' if mode == 'streaming' else 'false'} }, remove={'client': 'auth_token'} ) hawkbit.set_config('authentication.header.authority', ssl_issuer_hash) hawkbit.set_config('authentication.header.enabled', True) out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') assert 'New software ready for download' in out if mode == 'download': assert 'Download complete' in out assert 'Software bundle installed successfully.' in out assert err == '' assert exitcode == 0 status = hawkbit.get_action_status() assert status[0]['type'] == 'finished' rauc-hawkbit-updater-1.4/test/wait-for-hawkbit-online000077500000000000000000000006761503520256600227640ustar00rootroot00000000000000#!/bin/bash # # Loops with 5 second intervals to Wait for hawkbit server to be ready to # accept connections. # This should be run once before continuing with tests and scripts that # interact with hawkbit. printf "Waiting for hawkbit to come up " cycles=0 until $(curl --output /dev/null --silent --head --fail http://localhost:8080); do printf '.' [ $((cycles++)) -gt 10 ] && printf " failed\n" && exit 1 sleep 5 done printf '\n' rauc-hawkbit-updater-1.4/uncrustify.sh000077500000000000000000000003021503520256600201520ustar00rootroot00000000000000#!/bin/sh set -ex cd `dirname $0` if [ ! -e .uncrustify/build/uncrustify ]; then ./build-uncrustify.sh fi .uncrustify/build/uncrustify -c .uncrustify.cfg -l C --replace src/*.c include/*.h