├── .github └── workflows │ ├── codeql.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── .uncrustify.cfg ├── CHANGES ├── Doxyfile.in ├── LICENSE ├── README.md ├── build-uncrustify.sh ├── config.conf.example ├── docs ├── Makefile ├── changes.rst ├── conf.py ├── contributing.rst ├── index.rst ├── media │ ├── rauc-hawkbit-scheme.svg │ └── rauc-hawkbit-updater-scheme.png ├── meson.build ├── reference.rst ├── release-checklist.txt ├── requirements.in ├── requirements.txt └── using.rst ├── include ├── config-file.h ├── hawkbit-client.h ├── json-helper.h ├── log.h ├── rauc-installer.h └── sd-helper.h ├── meson.build ├── meson_options.txt ├── pytest.ini ├── script ├── LICENSE.0BSD ├── hawkbit_mgmt.py └── rauc-hawkbit-updater.service ├── src ├── config-file.c ├── hawkbit-client.c ├── json-helper.c ├── log.c ├── rauc-hawkbit-updater.c ├── rauc-installer.c ├── rauc-installer.xml └── sd-helper.c ├── test-requirements.txt ├── test ├── conftest.py ├── hawkbit_mgmt.py ├── helper.py ├── rauc_dbus_dummy.py ├── test_basics.py ├── test_cancel.py ├── test_download.py ├── test_install.py └── wait-for-hawkbit-online └── uncrustify.sh /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master", "codeql" ] 6 | pull_request: 7 | branches: [ "master", "codeql" ] 8 | schedule: 9 | - cron: "23 8 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | with: 27 | languages: cpp 28 | queries: +security-and-quality 29 | 30 | - name: Install Dependencies 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev 34 | 35 | - name: Build C Code 36 | run: | 37 | meson setup build 38 | meson compile -C build 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | with: 43 | category: "/language:cpp" 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | options: 12 | - -Dsystemd=enabled 13 | - -Dsystemd=disabled 14 | flags: 15 | - null 16 | - CFLAGS="-fsanitize=address -fsanitize=leak -g" LDFLAGS="-fsanitize=address -fsanitize=leak" 17 | steps: 18 | - name: Inspect environment 19 | run: | 20 | whoami 21 | gcc --version 22 | 23 | - uses: actions/checkout@v3 24 | 25 | - name: Install required packages 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install meson libcurl4-openssl-dev libsystemd-dev libjson-glib-dev 29 | 30 | - name: Build (with ${{ matrix.options }} ${{ matrix.flags }}) 31 | run: | 32 | ${{ matrix.flags }} meson setup build ${{ matrix.options }} -Dwerror=true 33 | ninja -C build 34 | 35 | - name: Build release 36 | run: | 37 | ninja -C build dist 38 | 39 | - name: Set up Python 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: 3.8 43 | 44 | - name: Install test dependencies 45 | run: | 46 | sudo apt-get install libcairo2-dev libgirepository1.0-dev nginx-full 47 | python -m pip install --upgrade pip 48 | pip install -r test-requirements.txt 49 | 50 | - name: Login to DockerHub 51 | uses: docker/login-action@v2 52 | if: github.ref == 'refs/heads/master' 53 | with: 54 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 55 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 56 | 57 | - name: Update/launch hawkBit docker container 58 | run: | 59 | docker pull hawkbit/hawkbit-update-server 60 | docker run -d --name hawkbit -p ::1:8080:8080 -p 127.0.0.1:8080:8080 \ 61 | hawkbit/hawkbit-update-server \ 62 | --hawkbit.server.security.dos.filter.enabled=false \ 63 | --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 64 | 65 | - name: Run test suite 66 | run: | 67 | ./test/wait-for-hawkbit-online 68 | ASAN_OPTIONS=fast_unwind_on_malloc=0 dbus-run-session -- pytest -v 69 | 70 | docs: 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v3 74 | 75 | - name: Install required packages 76 | run: | 77 | sudo apt-get update 78 | sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev python3-sphinx python3-sphinx-rtd-theme doxygen 79 | 80 | - name: Meson Build documentation (Sphinx & Doxygen) 81 | run: | 82 | meson setup build 83 | ninja -C build docs/html 84 | ninja -C build doxygen 85 | 86 | uncrustify: 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v3 90 | 91 | - name: Run uncrustify check 92 | run: | 93 | ./uncrustify.sh 94 | git diff --exit-code 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Auto gen files 2 | src/*-gen.c 3 | include/*-gen.h 4 | include/*.gch 5 | # build 6 | build/ 7 | 8 | # Backup files done by uncrustify 9 | .uncrustify/ 10 | *~ 11 | 12 | # tests 13 | venv/ 14 | test/__pycache__/ 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /.uncrustify.cfg: -------------------------------------------------------------------------------- 1 | # Uncrustify-0.66.1-2-f9c285db 2 | string_replace_tab_chars = true 3 | sp_assign = add 4 | sp_func_proto_paren = remove 5 | sp_func_def_paren = remove 6 | sp_func_call_paren = remove 7 | sp_cond_ternary_short = remove 8 | indent_func_proto_param = true 9 | indent_func_param_double = true 10 | nl_fdef_brace = add 11 | align_keep_tabs = true 12 | sp_before_sparen = add 13 | # different from rauc: 14 | indent_with_tabs = 0 15 | indent_func_call_param = false 16 | indent_func_def_param = false 17 | indent_switch_case = 0 18 | # option(s) with 'not default' value: 13 19 | # 20 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Release 1.3 (released Oct 14, 2022) 2 | ----------------------------------- 3 | 4 | .. rubric:: Enhancements 5 | 6 | * Add option ``stream_bundle=true`` to allow using RAUC's HTTP(S) bundle 7 | streaming capabilities instead of downloading and storing bundles separately. 8 | [#130] 9 | * Make error messages more consistent. [#138] 10 | 11 | .. rubric:: Build System 12 | 13 | * Switch to meson [#113] 14 | 15 | Release 1.2 (released Jul 1, 2022) 16 | ---------------------------------- 17 | 18 | .. rubric:: Enhancements 19 | 20 | * Let rauc-hawkbit-updater use the recent InstallBundle() DBus method instead of 21 | legacy Install() method. [#129] 22 | 23 | .. rubric:: Bug Fixes 24 | 25 | * Fixed NULL pointer dereference if build_api_url() is called for base 26 | deployment URL without having GLIB_USING_SYSTEM_PRINTF defined [#115] 27 | * Fixed compilation against musl by not including glibc-specific 28 | bits/types/struct_tm.h [#123] (by Zygmunt Krynicki) 29 | 30 | .. rubric:: Code 31 | 32 | * Drop some unused variables [#126] 33 | 34 | .. rubric:: Testing 35 | 36 | * Enable and fix testing for IPv6 addresses [#116] 37 | * Enhance test output by not aborting too early on process termination [#128] 38 | * Set proper names for python logger [#127] 39 | 40 | .. rubric:: Documentation 41 | 42 | * Corrected retry_wait default value in reference [#118] 43 | * Suggest using systemd-tmpfiles for creating and managing tmp directories 44 | as storage location for plain bundles [#124] (by Jonas Licht) 45 | * Update and clarify python3 venv usage and dependencies for testing [#125] 46 | 47 | Release 1.1 (released Nov 15, 2021) 48 | ----------------------------------- 49 | 50 | .. rubric:: Enhancements 51 | 52 | * RAUC hawkBit Updater does now handle hawkBit cancellation requests. 53 | This allows to cancel deployments that were not yet 54 | received/downloaded/installed. 55 | Once the installation has begun, cancellations are rejected. [#89] 56 | * RAUC hawkBit Updater now explicitly rejects deployments with multiple 57 | chunks/artifacts as these are conceptually unsupported by RAUC. [#103] 58 | * RAUC hawkBit Updater now implements waiting and retrying when receiving 59 | HTTP errors 409 (Conflict) or 429 (Too Many Requests) on DDI API calls. 60 | [#102] 61 | * Enable TCP keep-alive probing to recognize and deal with connection outages 62 | earlier. [#101] 63 | * New configuration options ``low_speed_time`` and ``low_speed_time`` allow 64 | to adjust the detection of slow connections to match the expected 65 | environmental conditions. [#101] 66 | * A new option ``resume_downloads`` allows to configure RAUC hawkBit Updater 67 | to resume aborted downloads if possible. [#101] 68 | * RAUC hawkBit Updater now evaluates the deployment API's 'skip' options for 69 | download and update (as e.g. used for maintenance window handling). 70 | Depending on what attributes are set, this will skip installation after 71 | download or even the entire update. [#111] 72 | 73 | .. rubric:: Testing 74 | 75 | * replaced manual injection of temporary env modification by monkeypatch 76 | fixture 77 | * test cases for all new features were added 78 | 79 | .. rubric:: Documentation 80 | 81 | * Added note on requirements for storage location when using plain bundle 82 | format 83 | 84 | Release 1.0 (released Sep 15, 2021) 85 | ----------------------------------- 86 | 87 | This is the initial release of RAUC hawkBit Updater. 88 | -------------------------------------------------------------------------------- /Doxyfile.in: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = "RAUC HawkBit updater" 2 | PROJECT_BRIEF = "The RAUC hawkBit updater is a simple commandline tool and daemon." 3 | OUTPUT_DIRECTORY = @DOXYGEN_OUTPUT@ 4 | INPUT = @DOXYGEN_INPUT@ 5 | OPTIMIZE_OUTPUT_FOR_C = YES 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU LESSER GENERAL PUBLIC LICENSE 3 | 4 | Version 2.1, February 1999 5 | 6 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 7 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 8 | Everyone is permitted to copy and distribute verbatim copies 9 | of this license document, but changing it is not allowed. 10 | 11 | [This is the first released version of the Lesser GPL. It also counts 12 | as the successor of the GNU Library Public License, version 2, hence 13 | the version number 2.1.] 14 | Preamble 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | 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. 27 | 28 | 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. 29 | 30 | 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. 31 | 32 | 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. 33 | 34 | 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. 35 | 36 | 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. 37 | 38 | 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. 39 | 40 | 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. 41 | 42 | 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. 43 | 44 | 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. 45 | 46 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 47 | 48 | 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". 49 | 50 | 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. 51 | 52 | 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".) 53 | 54 | "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. 55 | 56 | 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. 57 | 58 | 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. 59 | 60 | 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. 61 | 62 | 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: 63 | 64 | a) The modified work must itself be a software library. 65 | b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. 66 | 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. 67 | 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. 68 | (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.) 69 | 70 | 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. 71 | 72 | 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. 73 | 74 | 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. 75 | 76 | 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. 77 | 78 | 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. 79 | 80 | This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 81 | 82 | 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. 83 | 84 | 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. 85 | 86 | 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. 87 | 88 | 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. 89 | 90 | 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. 91 | 92 | 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.) 93 | 94 | 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. 95 | 96 | 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. 97 | 98 | 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: 99 | 100 | 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.) 101 | 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. 102 | 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. 103 | 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. 104 | e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. 105 | 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. 106 | 107 | 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. 108 | 109 | 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: 110 | 111 | 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. 112 | 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. 113 | 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. 114 | 115 | 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. 116 | 117 | 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. 118 | 119 | 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. 120 | 121 | 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. 122 | 123 | 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. 124 | 125 | This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 126 | 127 | 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. 128 | 129 | 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. 130 | 131 | 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. 132 | 133 | 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. 134 | 135 | NO WARRANTY 136 | 137 | 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. 138 | 139 | 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. 140 | 141 | END OF TERMS AND CONDITIONS 142 | 143 | How to Apply These Terms to Your New Libraries 144 | 145 | 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). 146 | 147 | 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. 148 | 149 | one line to give the library`s name and an idea of what it does. 150 | Copyright (C) year name of author 151 | 152 | This library is free software; you can redistribute it and/or 153 | modify it under the terms of the GNU Lesser General Public 154 | License as published by the Free Software Foundation; either 155 | version 2.1 of the License, or (at your option) any later version. 156 | 157 | This library is distributed in the hope that it will be useful, 158 | but WITHOUT ANY WARRANTY; without even the implied warranty of 159 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 160 | Lesser General Public License for more details. 161 | 162 | You should have received a copy of the GNU Lesser General Public 163 | License along with this library; if not, write to the Free Software 164 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 165 | Also add information on how to contact you by electronic and paper mail. 166 | 167 | 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: 168 | 169 | Yoyodyne, Inc., hereby disclaims all copyright interest in 170 | the library `Frob` (a library for tweaking knobs) written 171 | by James Random Hacker. 172 | 173 | signature of Ty Coon, 1 April 1990 174 | Ty Coon, President of Vice 175 | That`s all there is to it! 176 | 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RAUC hawkBit Updater 2 | ==================== 3 | 4 | [![Build Status](https://github.com/rauc/rauc-hawkbit-updater/workflows/tests/badge.svg)](https://github.com/rauc/rauc-hawkbit-updater/actions) 5 | [![License](https://img.shields.io/badge/license-LGPLv2.1-blue.svg)](https://raw.githubusercontent.com/rauc/rauc-hawkbit-updater/master/LICENSE) 6 | [![CodeQL](https://github.com/rauc/rauc-hawkbit-updater/workflows/CodeQL/badge.svg)](https://github.com/rauc/rauc-hawkbit-updater/actions/workflows/codeql.yml) 7 | [![Documentation](https://readthedocs.org/projects/rauc-hawkbit-updater/badge/?version=latest)](https://rauc-hawkbit-updater.readthedocs.io/en/latest/?badge=latest) 8 | [![Matrix](https://img.shields.io/matrix/rauc:matrix.org?label=matrix%20chat)](https://app.element.io/#/room/#rauc:matrix.org) 9 | 10 | The RAUC hawkBit updater is a simple command-line tool/daemon written in C (glib). 11 | It is a port of the RAUC hawkBit Client written in Python. 12 | The daemon runs on your target and operates as an interface between the 13 | [RAUC D-Bus API](https://github.com/rauc/rauc) 14 | and the [hawkBit DDI API](https://github.com/eclipse/hawkbit). 15 | 16 | Quickstart 17 | ---------- 18 | 19 | The RAUC hawkBit updater is primarily meant to be used as a daemon, 20 | but it also allows you to do a one-shot instantly checking and install 21 | new software. 22 | 23 | To quickly get started with hawkBit server, follow 24 | [this](https://github.com/eclipse/hawkbit#getting-started) 25 | instruction. 26 | 27 | Setup target (device) configuration file: 28 | 29 | ```ini 30 | [client] 31 | hawkbit_server = 127.0.0.1:8080 32 | ssl = false 33 | ssl_verify = false 34 | tenant_id = DEFAULT 35 | target_name = test-target 36 | auth_token = bhVahL1Il1shie2aj2poojeChee6ahShu 37 | #gateway_token = bhVahL1Il1shie2aj2poojeChee6ahShu 38 | bundle_download_location = /tmp/bundle.raucb 39 | retry_wait = 60 40 | connect_timeout = 20 41 | timeout = 60 42 | log_level = debug 43 | post_update_reboot = false 44 | #stream_bundle = true 45 | 46 | [device] 47 | product = Terminator 48 | model = T-1000 49 | serialnumber = 8922673153 50 | hw_revision = 2 51 | key1 = value 52 | key2 = value 53 | ``` 54 | 55 | All key/values under [device] group are sent to hawkBit as data (attributes). 56 | The attributes in hawkBit can be used in target filters. 57 | 58 | Finally start the updater as daemon: 59 | 60 | ```shell 61 | $ ./rauc-hawkbit-updater -c config.conf 62 | ``` 63 | 64 | 65 | Debugging 66 | --------- 67 | 68 | When setting the log level to 'debug' the RAUC hawkBit client will print 69 | JSON payload sent and received. This can be done by using option -d. 70 | 71 | ```shell 72 | $ ./rauc-hawkbit-updater -d -c config.conf 73 | ``` 74 | 75 | 76 | Compile 77 | ------- 78 | 79 | Install build pre-requisites: 80 | 81 | * meson 82 | * libcurl 83 | * libjson-glib 84 | 85 | ```shell 86 | $ sudo apt-get update 87 | $ sudo apt-get install meson libcurl4-openssl-dev libjson-glib-dev 88 | ``` 89 | 90 | ```shell 91 | $ meson setup build 92 | $ ninja -C build 93 | ``` 94 | 95 | Test Suite 96 | ---------- 97 | 98 | Prepare test suite: 99 | 100 | ```shell 101 | $ sudo apt install libcairo2-dev libgirepository1.0-dev nginx-full 102 | $ python3 -m venv venv 103 | $ source venv/bin/activate 104 | (venv) $ pip install --upgrade pip 105 | (venv) $ pip install -r test-requirements.txt 106 | ``` 107 | 108 | Run hawkBit docker container: 109 | 110 | ```shell 111 | $ docker pull hawkbit/hawkbit-update-server 112 | $ docker run -d --name hawkbit -p ::1:8080:8080 -p 127.0.0.1:8080:8080 \ 113 | hawkbit/hawkbit-update-server \ 114 | --hawkbit.server.security.dos.filter.enabled=false \ 115 | --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 116 | ``` 117 | 118 | Run test suite: 119 | 120 | ```shell 121 | (venv) $ ./test/wait-for-hawkbit-online && dbus-run-session -- pytest -v 122 | ``` 123 | 124 | Pass `-o log_cli=true` to pytest in order to enable live logging for all test cases. 125 | 126 | Usage / Options 127 | --------------- 128 | 129 | ```shell 130 | $ /usr/bin/rauc-hawkbit-updater --help 131 | Usage: 132 | rauc-hawkbit-updater [OPTION?] 133 | 134 | Help Options: 135 | -h, --help Show help options 136 | 137 | Application Options: 138 | -c, --config-file Configuration file 139 | -v, --version Version information 140 | -d, --debug Enable debug output 141 | -r, --run-once Check and install new software and exit 142 | -s, --output-systemd Enable output to systemd 143 | ``` 144 | -------------------------------------------------------------------------------- /build-uncrustify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | cd `dirname $0` 6 | 7 | git clone https://github.com/uncrustify/uncrustify.git --branch uncrustify-0.68.1 .uncrustify 8 | cd .uncrustify 9 | mkdir build 10 | cd build 11 | cmake -DCMAKE_BUILD_TYPE=Release .. 12 | make 13 | -------------------------------------------------------------------------------- /config.conf.example: -------------------------------------------------------------------------------- 1 | [client] 2 | # host or IP and optional port 3 | hawkbit_server = 10.10.0.254:8080 4 | 5 | # true = HTTPS, false = HTTP 6 | ssl = false 7 | 8 | # validate ssl certificate (only use if ssl is true) 9 | ssl_verify = false 10 | 11 | # Tenant id 12 | tenant_id = DEFAULT 13 | 14 | # Target name (controller id) 15 | target_name = test-target 16 | 17 | # Security token 18 | auth_token = cb115a721af28f781b493fa467819ef5 19 | 20 | # Or gateway_token can be used instead of auth_token 21 | #gateway_token = cb115a721af28f781b493fa467819ef5 22 | 23 | # Temporay file RAUC bundle should be downloaded to 24 | bundle_download_location = /tmp/bundle.raucb 25 | 26 | # Do not download bundle, let RAUC use its HTTP streaming feature instead 27 | #stream_bundle = true 28 | 29 | # time in seconds to wait before retrying 30 | retry_wait = 60 31 | 32 | # connection timeout in seconds 33 | connect_timeout = 20 34 | 35 | # request timeout in seconds 36 | timeout = 60 37 | 38 | # time to be below "low_speed_rate" to trigger the low speed abort 39 | low_speed_time = 0 40 | 41 | # average transfer speed to be below during "low_speed_time" seconds 42 | low_speed_rate = 0 43 | 44 | # reboot after a successful update 45 | post_update_reboot = false 46 | 47 | # debug, info, message, critical, error, fatal 48 | log_level = message 49 | 50 | # Every key / value under [device] is sent to HawkBit (target attributes), 51 | # and can be used in target filter. 52 | [device] 53 | mac_address = ff:ff:ff:ff:ff:ff 54 | hw_revision = 2 55 | model = T1 56 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | .. _changes: 4 | 5 | Changes in RAUC hawkBit Updater 6 | =============================== 7 | 8 | .. include:: ../CHANGES 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'RAUC hawkBit Updater' 21 | copyright = '2018-2022, Lasse Klok Mikkelsen, Enrico Jörns, Bastian Krause' 22 | author = 'Lasse Klok Mikkelsen, Enrico Jörns, Bastian Krause' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '1.3' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # The master toctree document. 31 | master_doc = 'index' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | #templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'sphinx_rtd_theme' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | #html_static_path = ['_static'] 59 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thank you for thinking about contributing to RAUC hawkBit Updater! 5 | Various backgrounds and use-cases are essential for making RAUC hawkBit Updater 6 | work well for all users. 7 | 8 | The following should help you with submitting your changes, but don't let these 9 | guidelines keep you from opening a pull request. 10 | If in doubt, we'd prefer to see the code earlier as a work-in-progress PR and 11 | help you with the submission process. 12 | 13 | Workflow 14 | -------- 15 | 16 | - Changes should be submitted via a `GitHub pull request 17 | `_. 18 | - Try to limit each commit to a single conceptual change. 19 | - Add a signed-of-by line to your commits according to the `Developer's 20 | Certificate of Origin` (see below). 21 | - Check that the tests still work before submitting the pull request. Also 22 | check the CI's feedback on the pull request after submission. 23 | - When adding new features, please also add the corresponding 24 | documentation and test code. 25 | - If your change affects backward compatibility, describe the necessary changes 26 | in the commit message and update the examples where needed. 27 | 28 | Code 29 | ---- 30 | 31 | - Basically follow the Linux kernel coding style 32 | 33 | Documentation 34 | ------------- 35 | 36 | - Use `semantic linefeeds 37 | `_ in .rst files. 38 | 39 | Developer's Certificate of Origin 40 | --------------------------------- 41 | 42 | RAUC hawkBit Updater uses the `Developer's Certificate of Origin 1.1 43 | `_ with the same `process 44 | `_ 45 | as used for the Linux kernel: 46 | 47 | Developer's Certificate of Origin 1.1 48 | 49 | By making a contribution to this project, I certify that: 50 | 51 | (a) The contribution was created in whole or in part by me and I 52 | have the right to submit it under the open source license 53 | indicated in the file; or 54 | 55 | (b) The contribution is based upon previous work that, to the best 56 | of my knowledge, is covered under an appropriate open source 57 | license and I have the right under that license to submit that 58 | work with modifications, whether created in whole or in part 59 | by me, under the same open source license (unless I am 60 | permitted to submit under a different license), as indicated 61 | in the file; or 62 | 63 | (c) The contribution was provided directly to me by some other 64 | person who certified (a), (b) or (c) and I have not modified 65 | it. 66 | 67 | (d) I understand and agree that this project and the contribution 68 | are public and that a record of the contribution (including all 69 | personal information I submit with it, including my sign-off) is 70 | maintained indefinitely and may be redistributed consistent with 71 | this project or the open source license(s) involved. 72 | 73 | Then you just add a line (using ``git commit -s``) saying: 74 | 75 | Signed-off-by: Random J Developer 76 | 77 | using your real name (sorry, no pseudonyms or anonymous contributions). 78 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. RAUC hawkBit Updater documentation master file, created by 2 | sphinx-quickstart on Thu Feb 4 07:40:09 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to RAUC hawkBit Updater's documentation! 7 | ================================================ 8 | 9 | .. toctree:: 10 | :glob: 11 | :numbered: 12 | :maxdepth: 1 13 | 14 | using 15 | reference 16 | contributing 17 | changes 18 | 19 | The RAUC hawkBit updater is a simple commandline tool / daemon written in C (glib). 20 | The daemon runs on your target and operates as an interface between the 21 | `RAUC D-Bus API `_ 22 | and the `hawkBit DDI API `_. 23 | 24 | .. image:: media/rauc-hawkbit-updater-scheme.png 25 | :height: 300 26 | :align: center 27 | -------------------------------------------------------------------------------- /docs/media/rauc-hawkbit-scheme.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 27 | 33 | 34 | 42 | 48 | 49 | 57 | 63 | 64 | 72 | 78 | 79 | 87 | 93 | 94 | 102 | 108 | 109 | 117 | 123 | 124 | 132 | 138 | 139 | 140 | 164 | 166 | 167 | 169 | image/svg+xml 170 | 172 | 173 | 174 | 175 | 176 | 181 | 188 | DDI-API(REST) 204 | 211 | 213 | 220 | RAUC API(D-Bus) 236 | 243 | RAUC 256 | rauc-hawkbit-updater 267 | 273 | 274 | 280 | 282 | 289 | hawkbit 300 | 305 | 310 | 311 | device 323 | server 335 | 336 | 337 | -------------------------------------------------------------------------------- /docs/media/rauc-hawkbit-updater-scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rauc/rauc-hawkbit-updater/b8f1131cdb6485d39acc66915b84bd09b6941964/docs/media/rauc-hawkbit-updater-scheme.png -------------------------------------------------------------------------------- /docs/meson.build: -------------------------------------------------------------------------------- 1 | sphinx = find_program('sphinx-build', required: get_option('doc')) 2 | 3 | if not sphinx.found() 4 | subdir_done() 5 | endif 6 | 7 | doc_src = [ 8 | 'conf.py', 9 | 'changes.rst', 10 | 'contributing.rst', 11 | 'index.rst', 12 | 'reference.rst', 13 | 'using.rst', 14 | ] 15 | 16 | custom_target('doc', 17 | output: 'html', 18 | input: doc_src, 19 | command: [sphinx, '-b', 'html', meson.current_source_dir(), meson.current_build_dir() / 'html'], 20 | build_by_default: get_option('doc').enabled(), 21 | install: get_option('doc').enabled(), 22 | install_dir: get_option('datadir') / 'doc' / meson.project_name(), 23 | ) 24 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | .. _sec_ref: 2 | 3 | Reference 4 | ========= 5 | 6 | .. contents:: 7 | :local: 8 | :depth: 1 9 | 10 | .. _sec_ref_config_file: 11 | 12 | Configuration File 13 | ------------------ 14 | 15 | Example configuration: 16 | 17 | .. code-block:: cfg 18 | 19 | [client] 20 | hawkbit_server = 127.0.0.1:8080 21 | ssl = false 22 | ssl_verify = false 23 | tenant_id = DEFAULT 24 | target_name = test-target 25 | auth_token = bhVahL1Il1shie2aj2poojeChee6ahShu 26 | bundle_download_location = /tmp/bundle.raucb 27 | 28 | [device] 29 | key1 = valueA 30 | key2 = valueB 31 | 32 | **[client] section** 33 | 34 | Configures how to connect to a hawkBit server, etc. 35 | 36 | Mandatory options: 37 | 38 | ``hawkbit_server=[:]`` 39 | The IP or hostname of the hawkbit server to connect to 40 | (Punycode representation must be used for host names containing Unicode 41 | characters). 42 | The ``port`` can be provided optionally, separated by a colon. 43 | 44 | ``target_name=`` 45 | Unique ``name`` string to identify controller. 46 | 47 | ``auth_token=`` 48 | Controller-specific authentication token. 49 | This is set for each device individually. 50 | For details, refer to https://eclipse.dev/hawkbit/concepts/authentication/. 51 | 52 | .. note:: Either ``auth_token`` or ``gateway_token`` must be provided 53 | 54 | ``gateway_token=`` 55 | Gateway authentication token. 56 | This is a tenant-wide token and must explicitly be enabled in hakwBit first. 57 | It is actually meant to authenticate a gateway that itself 58 | manages/authenticates multiple targets, thus use with care. 59 | For details, refer to https://eclipse.dev/hawkbit/concepts/authentication/. 60 | 61 | .. note:: Either ``auth_token`` or ``gateway_token`` must be provided 62 | 63 | ``bundle_download_location=`` 64 | Full path to where the bundle should be downloaded to. 65 | E.g. set to ``/tmp/_bundle.raucb`` to let rauc-hawkbit-updater use this 66 | location within ``/tmp``. 67 | 68 | .. note:: Option can be ommited if ``stream_bundle`` is enabled. 69 | 70 | Optional options: 71 | 72 | ``tenant_id=`` 73 | ID of the tenant to connect to. Defaults to ``DEFAULT``. 74 | 75 | ``ssl=`` 76 | Whether to use SSL connections (``https``) or not (``http``). 77 | Defaults to ``true``. 78 | 79 | ``ssl_verify=`` 80 | Whether to enforce SSL verification or not. 81 | Defaults to ``true``. 82 | 83 | ``connect_timeout=`` 84 | HTTP connection setup timeout [seconds]. 85 | Defaults to ``20`` seconds. 86 | Has no effect on bundle downloading when used with ``stream_bundle=true``. 87 | 88 | ``timeout=`` 89 | HTTP request timeout [seconds]. 90 | Defaults to ``60`` seconds. 91 | 92 | ``retry_wait=`` 93 | Time to wait before retrying in case an error occurred [seconds]. 94 | Defaults to ``300`` seconds. 95 | 96 | ``low_speed_time=`` 97 | Time to be below ``low_speed_rate`` to trigger the low speed abort. 98 | Defaults to ``60``. 99 | See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_TIME.html. 100 | Has no effect when used with ``stream_bundle=true``. 101 | 102 | ``low_speed_rate=`` 103 | Average transfer speed to be below during ``low_speed_time`` seconds to 104 | consider transfer as "too slow" and abort it. 105 | Defaults to ``100``. 106 | See https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html. 107 | Has no effect when used with ``stream_bundle=true``. 108 | 109 | ``resume_downloads=`` 110 | Whether to resume aborted downloads or not. 111 | Defaults to ``false``. 112 | Has no effect when used with ``stream_bundle=true``. 113 | 114 | ``stream_bundle=`` 115 | Whether to install bundles via 116 | `RAUC's HTTP streaming installation support `_. 117 | Defaults to ``false``. 118 | rauc-hawkbit-updater does not download the bundle in this case, but rather 119 | hands the hawkBit bundle URL and the :ref:`authentication header ` to RAUC. 120 | 121 | .. important:: 122 | hawkBit's default configuration limits the number of HTTP range requests to 123 | ~1000 per action and 200 per second. 124 | Depending on the bundle size and bandwidth available, streaming a bundle 125 | might exceed these limitations. 126 | Starting hawkBit with ``--hawkbit.server.security.dos.filter.enabled=false`` 127 | ``--hawkbit.server.security.dos.maxStatusEntriesPerAction=-1`` disables 128 | these limitations. 129 | 130 | .. note:: 131 | hawkBit generates an "ActionStatus" for each range request, see 132 | `this hawkBit issue `_. 133 | 134 | ``post_update_reboot=`` 135 | Whether to reboot the system after a successful update. 136 | Defaults to ``false``. 137 | 138 | .. important:: 139 | Note that this results in an immediate reboot without contacting the system 140 | manager and without terminating any processes or unmounting any file systems. 141 | This may result in data loss. 142 | 143 | ``log_level=`` 144 | Log level to print, where ``level`` is a string of 145 | 146 | * ``debug`` 147 | * ``info`` 148 | * ``message`` 149 | * ``critical`` 150 | * ``error`` 151 | * ``fatal`` 152 | 153 | Defaults to ``message``. 154 | 155 | .. _keyring-section: 156 | 157 | **[device] section** 158 | 159 | This section allows to set a custom list of key-value pairs that will be used 160 | as config data target attribute for device registration. 161 | They can be used for target filtering. 162 | 163 | .. important:: 164 | The [device] section is mandatory and at least one key-value pair must be 165 | configured. 166 | -------------------------------------------------------------------------------- /docs/release-checklist.txt: -------------------------------------------------------------------------------- 1 | Release Process rauc-hawkbit-updater 2 | ==================================== 3 | 4 | Preparation 5 | ----------- 6 | - check for GitHub milestone to be completed 7 | - review & merge open PRs if necessary 8 | - update CHANGES 9 | - update version in docs/conf.py and meson.build 10 | - create preparation PR, merge PR 11 | 12 | Release 13 | ------- 14 | - update release date in CHANGES and commit 15 | - create signed git tag:: 16 | 17 | git tag -m 'release v1.0' -s -u 925F79DAA74AF221 v1.0 18 | 19 | - create release tar archive:: 20 | 21 | meson setup build 22 | ninja -C build dist 23 | 24 | The resulting archive will be placed at build/meson-dist/rauc-hawkbit-updater-.tar.xz 25 | 26 | - sign (and verify) source archive:: 27 | 28 | gpg --detach-sign -u 925F79DAA74AF221 --armor build/meson-dist/rauc-hawkbit-updater-.tar.xz 29 | gpg --verify build/meson-dist/rauc-hawkbit-updater-.tar.xz.asc 30 | 31 | - push master commit (if necessary) 32 | - push signed git tag 33 | - Creating GitHub release 34 | - Start creating release from git tag 35 | - upload source archive and signature 36 | - add release text using CHANGES:: 37 | 38 | pandoc -f rst -t markdown_github CHANGES 39 | 40 | - Submit release button 41 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | # use pip-compile requirements.in to update requirements.txt 2 | sphinx 3 | sphinx-rtd-theme 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | alabaster==0.7.16 8 | # via sphinx 9 | babel==2.14.0 10 | # via sphinx 11 | certifi==2024.7.4 12 | # via requests 13 | charset-normalizer==3.3.2 14 | # via requests 15 | docutils==0.20.1 16 | # via 17 | # sphinx 18 | # sphinx-rtd-theme 19 | idna==3.7 20 | # via requests 21 | imagesize==1.4.1 22 | # via sphinx 23 | jinja2==3.1.6 24 | # via sphinx 25 | markupsafe==2.1.5 26 | # via jinja2 27 | packaging==24.0 28 | # via sphinx 29 | pygments==2.17.2 30 | # via sphinx 31 | requests==2.32.0 32 | # via sphinx 33 | snowballstemmer==2.2.0 34 | # via sphinx 35 | sphinx==7.2.6 36 | # via 37 | # -r requirements.in 38 | # sphinx-rtd-theme 39 | # sphinxcontrib-jquery 40 | sphinx-rtd-theme==2.0.0 41 | # via -r requirements.in 42 | sphinxcontrib-applehelp==1.0.8 43 | # via sphinx 44 | sphinxcontrib-devhelp==1.0.6 45 | # via sphinx 46 | sphinxcontrib-htmlhelp==2.0.5 47 | # via sphinx 48 | sphinxcontrib-jquery==4.1 49 | # via sphinx-rtd-theme 50 | sphinxcontrib-jsmath==1.0.1 51 | # via sphinx 52 | sphinxcontrib-qthelp==1.0.7 53 | # via sphinx 54 | sphinxcontrib-serializinghtml==1.1.10 55 | # via sphinx 56 | urllib3==2.2.2 57 | # via requests 58 | -------------------------------------------------------------------------------- /docs/using.rst: -------------------------------------------------------------------------------- 1 | Using the RAUC hawkbit Updater 2 | ============================== 3 | 4 | .. _authentication-section: 5 | 6 | Authentication 7 | -------------- 8 | 9 | As described on the `hawkBit Authentication page `_ 10 | in the "DDI API Authentication Modes" section, a device can be authenticated 11 | with a security token. A security token can be either a "Target" token or a 12 | "Gateway" token. The "Target" security token is specific to a single target 13 | defined in hawkBit. In the RAUC hawkBit updater's configuration file it's 14 | referred to as ``auth_token``. 15 | 16 | Targets can also be connected through a gateway which manages the targets 17 | directly and as a result these targets are indirectly connected to the hawkBit 18 | update server. The "Gateway" token is used to authenticate this gateway and 19 | allow it to manage all the targets under its tenant. With RAUC hawkBit updater 20 | such token can be used to authenticate all targets on the server. I.e. same 21 | gateway token can be used in a configuration file replicated on many targets. 22 | In the RAUC hawkBit updater's configuration file it's called ``gateway_token``. 23 | Although gateway token is very handy during development or testing, it's 24 | recommended to use this token with care because it can be used to 25 | authenticate any device. 26 | 27 | Streaming Support 28 | ----------------- 29 | 30 | By default, rauc-hawkbit-updater downloads the bundle to a temporary 31 | storage location and then invokes RAUC to install the bundle. 32 | In order to save bundle storage and also potentially download bandwidth 33 | (when combined with adaptive updates), rauc-hawkbit-updater can also leverage 34 | `RAUC's built-in HTTP streaming support `_. 35 | 36 | To enable it, set ``stream_bundle=true`` in the :ref:`sec_ref_config_file`. 37 | 38 | .. note:: rauc-hawkbit-updater will add required authentication headers and 39 | options to its RAUC D-Bus `InstallBundle API call `_. 40 | 41 | Plain Bundle Support 42 | -------------------- 43 | 44 | RAUC takes ownership of `plain format bundles `_ 45 | during installation. 46 | Thus rauc-hawkbit-updater can remove these bundles after installation only if 47 | it they are located in a directory belonging to the user executing 48 | rauc-hawkbit-updater. 49 | 50 | systemd Example 51 | ^^^^^^^^^^^^^^^ 52 | 53 | To store the bundle in such a directory, a configuration file for 54 | systemd-tmpfiles can be created and placed in 55 | ``/usr/lib/tmpfiles.d/rauc-hawkbit-updater.conf``. 56 | This tells systemd-tmpfiles to create a directory in ``/tmp`` with proper 57 | ownership: 58 | 59 | .. code-block:: cfg 60 | 61 | d /tmp/rauc-hawkbit-updater - rauc-hawkbit rauc-hawkbit - - 62 | 63 | The bundle location needs to be set in rauc-hawkbit-updater's config: 64 | 65 | .. code-block:: cfg 66 | 67 | bundle_download_location = /tmp/rauc-hawkbit-updater/bundle.raucb 68 | -------------------------------------------------------------------------------- /include/config-file.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __CONFIG_FILE_H__ 7 | #define __CONFIG_FILE_H__ 8 | 9 | #include 10 | 11 | /** 12 | * @brief struct that contains the Rauc HawkBit configuration. 13 | */ 14 | typedef struct Config_ { 15 | gchar* hawkbit_server; /**< hawkBit host or IP and port */ 16 | gboolean ssl; /**< use https or http */ 17 | gboolean ssl_verify; /**< verify https certificate */ 18 | gboolean post_update_reboot; /**< reboot system after successful update */ 19 | gboolean resume_downloads; /**< resume downloads or not */ 20 | gboolean stream_bundle; /**< streaming installation or not */ 21 | gchar* auth_token; /**< hawkBit target security token */ 22 | gchar* gateway_token; /**< hawkBit gateway security token */ 23 | gchar* tenant_id; /**< hawkBit tenant id */ 24 | gchar* controller_id; /**< hawkBit controller id*/ 25 | gchar* bundle_download_location; /**< file to download rauc bundle to */ 26 | int connect_timeout; /**< connection timeout */ 27 | int timeout; /**< reply timeout */ 28 | int retry_wait; /**< wait between retries */ 29 | int low_speed_time; /**< time to be below the speed to trigger low speed abort */ 30 | int low_speed_rate; /**< low speed limit to abort transfer */ 31 | GLogLevelFlags log_level; /**< log level */ 32 | GHashTable* device; /**< Additional attributes sent to hawkBit */ 33 | } Config; 34 | 35 | /** 36 | * @brief Get Config for config_file. 37 | * 38 | * @param[in] config_file String value containing path to config file 39 | * @param[out] error Error 40 | * @return Config on success, NULL otherwise (error is set) 41 | */ 42 | Config* load_config_file(const gchar *config_file, GError **error); 43 | 44 | /** 45 | * @brief Frees the memory allocated by a Config 46 | * 47 | * @param[in] config Config to free 48 | */ 49 | void config_file_free(Config *config); 50 | 51 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(Config, config_file_free) 52 | 53 | #endif // __CONFIG_FILE_H__ 54 | -------------------------------------------------------------------------------- /include/hawkbit-client.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __HAWKBIT_CLIENT_H__ 7 | #define __HAWKBIT_CLIENT_H__ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config-file.h" 14 | 15 | #define RHU_HAWKBIT_CLIENT_ERROR rhu_hawkbit_client_error_quark() 16 | GQuark rhu_hawkbit_client_error_quark(void); 17 | 18 | typedef enum { 19 | RHU_HAWKBIT_CLIENT_ERROR_ALREADY_IN_PROGRESS, 20 | RHU_HAWKBIT_CLIENT_ERROR_JSON_RESPONSE_PARSE, 21 | RHU_HAWKBIT_CLIENT_ERROR_MULTI_CHUNKS, 22 | RHU_HAWKBIT_CLIENT_ERROR_MULTI_ARTIFACTS, 23 | RHU_HAWKBIT_CLIENT_ERROR_DOWNLOAD, 24 | RHU_HAWKBIT_CLIENT_ERROR_STREAM_INSTALL, 25 | RHU_HAWKBIT_CLIENT_ERROR_CANCELATION, 26 | } RHUHawkbitClientError; 27 | 28 | // uses CURLcode as error codes 29 | #define RHU_HAWKBIT_CLIENT_CURL_ERROR rhu_hawkbit_client_curl_error_quark() 30 | GQuark rhu_hawkbit_client_curl_error_quark(void); 31 | 32 | // uses HTTP codes as error codes 33 | #define RHU_HAWKBIT_CLIENT_HTTP_ERROR rhu_hawkbit_client_http_error_quark() 34 | GQuark rhu_hawkbit_client_http_error_quark(void); 35 | 36 | #define HAWKBIT_USERAGENT "rauc-hawkbit-c-agent/1.0" 37 | #define DEFAULT_CURL_REQUEST_BUFFER_SIZE 512 38 | #define DEFAULT_CURL_DOWNLOAD_BUFFER_SIZE 64 * 1024 // 64KB 39 | 40 | extern gboolean run_once; /**< only run software check once and exit */ 41 | 42 | /** 43 | * @brief HTTP methods. 44 | */ 45 | enum HTTPMethod { 46 | GET, 47 | HEAD, 48 | PUT, 49 | POST, 50 | PATCH, 51 | DELETE 52 | }; 53 | 54 | enum ActionState { 55 | ACTION_STATE_NONE, 56 | ACTION_STATE_CANCELED, 57 | ACTION_STATE_ERROR, 58 | ACTION_STATE_SUCCESS, 59 | ACTION_STATE_PROCESSING, 60 | ACTION_STATE_DOWNLOADING, 61 | ACTION_STATE_INSTALLING, 62 | ACTION_STATE_CANCEL_REQUESTED, 63 | }; 64 | 65 | /** 66 | * @brief struct that contains the context of an HawkBit action. 67 | */ 68 | struct HawkbitAction { 69 | gchar *id; /**< HawkBit action id */ 70 | GMutex mutex; /**< mutex used for accessing all other members */ 71 | enum ActionState state; /**< state of this action */ 72 | GCond cond; /**< condition on state */ 73 | }; 74 | 75 | /** 76 | * @brief struct containing the payload and size of REST body. 77 | */ 78 | typedef struct RestPayload_ { 79 | gchar *payload; /**< string representation of payload */ 80 | size_t size; /**< size of payload */ 81 | } RestPayload; 82 | 83 | /** 84 | * @brief struct containing data about an artifact that is currently being deployed. 85 | */ 86 | typedef struct Artifact_ { 87 | gchar *name; /**< name of software */ 88 | gchar *version; /**< software version */ 89 | gint64 size; /**< size of software bundle file */ 90 | gchar *download_url; /**< download URL of software bundle file */ 91 | gchar *feedback_url; /**< URL status feedback should be sent to */ 92 | gchar *sha1; /**< sha1 checksum of software bundle file */ 93 | gchar *maintenance_window; /**< maintenance flag, possible values: available, unavailable, null */ 94 | gboolean do_install; /**< whether the installation should be started or not */ 95 | } Artifact; 96 | 97 | /** 98 | * @brief struct containing the new downloaded file. 99 | */ 100 | struct on_new_software_userdata { 101 | GSourceFunc install_progress_callback; /**< callback function to be called when new progress */ 102 | GSourceFunc install_complete_callback; /**< callback function to be called when installation is complete */ 103 | gchar *file; /**< downloaded new software file */ 104 | gchar *auth_header; /**< authentication header for bundle streaming */ 105 | gboolean ssl_verify; /**< whether to ignore server cert verification errors */ 106 | gboolean install_success; /**< whether the installation succeeded or not (only meaningful for run_once mode!) */ 107 | }; 108 | 109 | /** 110 | * @brief struct containing the result of the installation. 111 | */ 112 | struct on_install_complete_userdata { 113 | gboolean install_success; /**< status of installation */ 114 | }; 115 | 116 | /** 117 | * @brief Pass config, callback for installation ready and initialize libcurl. 118 | * Intended to be called from program's main(). 119 | * 120 | * @param[in] config Config* to make global 121 | * @param[in] on_install_ready GSourceFunc to call after artifact download, to 122 | * trigger RAUC installation 123 | */ 124 | void hawkbit_init(Config *config, GSourceFunc on_install_ready); 125 | 126 | /** 127 | * @brief Sets up timeout and event sources, initializes and runs main loop. 128 | * 129 | * @return numeric return code, to be returned by main() 130 | */ 131 | int hawkbit_start_service_sync(); 132 | 133 | /** 134 | * @brief Callback for install thread, sends msg as progress feedback to 135 | * hawkBit. 136 | * 137 | * @param[in] msg Progress message 138 | * @return G_SOURCE_REMOVE is always returned 139 | */ 140 | gboolean hawkbit_progress(const gchar *msg); 141 | 142 | /** 143 | * @brief Callback for install thread, sends installation feedback to hawkBit. 144 | * 145 | * @param[in] ptr on_install_complete_userdata* containing set install_success 146 | * @return G_SOURCE_REMOVE is always returned 147 | */ 148 | gboolean install_complete_cb(gpointer ptr); 149 | 150 | /** 151 | * @brief Frees the memory allocated by a RestPayload 152 | * 153 | * @param[in] payload RestPayload to free 154 | */ 155 | void rest_payload_free(RestPayload *payload); 156 | 157 | /** 158 | * @brief Frees the memory allocated by an Artifact 159 | * 160 | * @param[in] artifact Artifact to free 161 | */ 162 | void artifact_free(Artifact *artifact); 163 | 164 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(RestPayload, rest_payload_free) 165 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(Artifact, artifact_free) 166 | 167 | #endif // __HAWKBIT_CLIENT_H__ 168 | -------------------------------------------------------------------------------- /include/json-helper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __JSON_HELPER_H__ 7 | #define __JSON_HELPER_H__ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | /** 14 | * @brief Get the string inside the first JsonNode element matching path in json_node. 15 | * 16 | * @param[in] json_node JsonNode to evaluate expression on 17 | * @param[in] path JSONPath expression 18 | * @param[out] error Error 19 | * @return gchar*, string value (must be freed), NULL on error (error set) 20 | */ 21 | gchar* json_get_string(JsonNode *json_node, const gchar *path, GError **error); 22 | 23 | /** 24 | * @brief Get the integer inside the first JsonNode element matching path in json_node. 25 | * 26 | * @param[in] json_node JsonNode to evaluate expression on 27 | * @param[in] path JSONPath expression 28 | * @param[out] error Error 29 | * @return gint64, integer value, 0 on error (error set) 30 | */ 31 | gint64 json_get_int(JsonNode *json_node, const gchar *path, GError **error); 32 | 33 | /** 34 | * @brief Get the JsonArray inside the first JsonNode element matching path in json_node. 35 | * 36 | * @param[in] json_node JsonNode to evaluate expression on 37 | * @param[in] path JSONPath expression 38 | * @param[out] error Error 39 | * @return JsonArray*, array (must be freed), NULL on error (error set) 40 | */ 41 | JsonArray* json_get_array(JsonNode *json_node, const gchar *path, GError **error); 42 | 43 | /** 44 | * @brief Check if the given path matches an element in json_node. 45 | * 46 | * @param[in] json_node JsonNode to evaluate expression on 47 | * @param[in] path JSONPath expression 48 | * @return gboolean, TRUE if path matches an element, FALSE otherwise 49 | */ 50 | gboolean json_contains(JsonNode *root, gchar *key); 51 | 52 | #endif // __JSON_HELPER_H__ 53 | -------------------------------------------------------------------------------- /include/log.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __LOG_H__ 7 | #define __LOG_H__ 8 | 9 | #include 10 | #ifdef WITH_SYSTEMD 11 | #include 12 | #endif 13 | 14 | /** 15 | * @brief Setup Glib log handler 16 | * 17 | * @param[in] domain Log domain 18 | * @param[in] level Log level 19 | * @param[in] p_output_to_systemd output to systemd journal 20 | */ 21 | void setup_logging(const gchar *domain, GLogLevelFlags level, gboolean output_to_systemd); 22 | 23 | #endif // __LOG_H__ 24 | -------------------------------------------------------------------------------- /include/rauc-installer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __RAUC_INSTALLER_H__ 7 | #define __RAUC_INSTALLER_H__ 8 | 9 | #include 10 | 11 | /** 12 | * @brief struct that contains the context of an Rauc installation. 13 | */ 14 | struct install_context { 15 | gchar *bundle; /**< Rauc bundle file to install */ 16 | gchar *auth_header; /**< Authentication header for bundle streaming */ 17 | gboolean ssl_verify; /**< Whether to ignore server cert verification errors */ 18 | GSourceFunc notify_event; /**< Callback function */ 19 | GSourceFunc notify_complete; /**< Callback function */ 20 | GMutex status_mutex; /**< Mutex used for accessing status_messages */ 21 | GQueue status_messages; /**< Queue of status messages from Rauc DBUS */ 22 | gint status_result; /**< The result of the installation */ 23 | GMainLoop *mainloop; /**< The installation GMainLoop */ 24 | GMainContext *loop_context; /**< GMainContext for the GMainLoop */ 25 | gboolean keep_install_context; /**< Whether the installation thread should free this struct or keep it */ 26 | }; 27 | 28 | /** 29 | * @brief RAUC install bundle 30 | * 31 | * @param[in] bundle RAUC bundle file (.raucb) to install. 32 | * @param[in] auth_header Authentication header on HTTP streaming installation or NULL on normal 33 | * installation. 34 | * @param[in] ssl_verify Whether to ignore server cert verification errors. 35 | * @param[in] on_install_notify Callback function to be called with status info during 36 | * installation. 37 | * @param[in] on_install_complete Callback function to be called with the result of the 38 | * installation. 39 | * @param[in] wait Whether to wait until install thread finished or not. 40 | * @return for wait=TRUE, TRUE if installation succeeded, FALSE otherwise; for 41 | * wait=FALSE TRUE is always returned immediately 42 | */ 43 | gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ssl_verify, 44 | GSourceFunc on_install_notify, GSourceFunc on_install_complete, gboolean wait); 45 | 46 | #endif // __RAUC_INSTALLER_H__ 47 | -------------------------------------------------------------------------------- /include/sd-helper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | */ 5 | 6 | #ifndef __SD_HELPER_H__ 7 | #define __SD_HELPER_H__ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | /** 14 | * @brief Binding GSource and sd_event together. 15 | */ 16 | struct SDSource 17 | { 18 | GSource source; 19 | sd_event *event; 20 | GPollFD pollfd; 21 | }; 22 | 23 | /** 24 | * @brief Attach GSource to GMainLoop 25 | * 26 | * @param[in] source Glib GSource 27 | * @param[in] loop GMainLoop the GSource should be attached to. 28 | * @return 0 on success, value != 0 on error 29 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GMainLoop 30 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 31 | */ 32 | int sd_source_attach(GSource *source, GMainLoop *loop); 33 | 34 | /** 35 | * @brief Create GSource from a sd_event 36 | * 37 | * @param[in] event Systemd event that should be converted to a Glib GSource 38 | * @return the newly-created GSource 39 | * @see https://www.freedesktop.org/software/systemd/man/sd-event.html 40 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 41 | */ 42 | GSource * sd_source_new(sd_event *event); 43 | 44 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(sd_event, sd_event_unref) 45 | 46 | #endif // __SD_HELPER_H__ 47 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'rauc-hawkbit-updater', 3 | 'c', 4 | version : '1.3', 5 | meson_version : '>=0.50', 6 | default_options: [ 7 | 'warning_level=2', 8 | ], 9 | license : 'LGPL-2.1-only', 10 | ) 11 | 12 | conf = configuration_data() 13 | conf.set_quoted('PROJECT_VERSION', meson.project_version()) 14 | 15 | libcurldep = dependency('libcurl', version : '>=7.47.0') 16 | giodep = dependency('gio-2.0', version : '>=2.26.0') 17 | giounixdep = dependency('gio-unix-2.0', version : '>=2.26.0') 18 | jsonglibdep = dependency('json-glib-1.0') 19 | 20 | incdir = include_directories('include') 21 | 22 | sources_updater = [ 23 | 'src/rauc-hawkbit-updater.c', 24 | 'src/rauc-installer.c', 25 | 'src/config-file.c', 26 | 'src/hawkbit-client.c', 27 | 'src/json-helper.c', 28 | 'src/log.c', 29 | ] 30 | 31 | c_args = ''' 32 | -Wbad-function-cast 33 | -Wcast-align 34 | -Wdeclaration-after-statement 35 | -Wformat=2 36 | -Wshadow 37 | -Wno-unused-parameter 38 | -Wno-missing-field-initializers 39 | '''.split() 40 | add_project_arguments(c_args, language : 'c') 41 | 42 | systemddep = dependency('systemd', required : get_option('systemd')) 43 | libsystemddep = dependency('libsystemd', required : get_option('systemd')) 44 | 45 | if systemddep.found() 46 | conf.set('WITH_SYSTEMD', '1') 47 | sources_updater += 'src/sd-helper.c' 48 | systemdsystemunitdir = get_option('systemdsystemunitdir') 49 | if systemdsystemunitdir == '' 50 | systemdsystemunitdir = systemddep.get_pkgconfig_variable('systemdsystemunitdir') 51 | endif 52 | install_data('script/rauc-hawkbit-updater.service', install_dir : systemdsystemunitdir) 53 | endif 54 | 55 | gnome = import('gnome') 56 | dbus = 'rauc-installer-gen' 57 | dbus_ifaces = files('src/rauc-installer.xml') 58 | dbus_sources = gnome.gdbus_codegen( 59 | dbus, 60 | sources : dbus_ifaces, 61 | interface_prefix : 'de.pengutronix.rauc.', 62 | namespace: 'R', 63 | ) 64 | 65 | config_h = configure_file( 66 | output : 'config.h', 67 | configuration : conf 68 | ) 69 | add_project_arguments('-include' + meson.current_build_dir() / 'config.h', language: 'c') 70 | 71 | doxygen = find_program('doxygen', required : get_option('apidoc')) 72 | 73 | if doxygen.found() 74 | doc_config = configuration_data() 75 | doc_config.set('DOXYGEN_OUTPUT', meson.current_build_dir() / 'doxygen') 76 | doc_config.set('DOXYGEN_INPUT', meson.current_source_dir() / 'src' + ' ' + meson.current_source_dir() / 'include') 77 | 78 | doxyfile = configure_file(input : 'Doxyfile.in', 79 | output : 'Doxyfile', 80 | configuration : doc_config, 81 | install : false) 82 | custom_target('doxygen', 83 | output : 'doxygen', 84 | input : doxyfile, 85 | command : [doxygen, '@INPUT@'], 86 | depend_files : sources_updater, 87 | build_by_default : get_option('apidoc').enabled(), 88 | ) 89 | endif 90 | 91 | subdir('docs') 92 | 93 | executable('rauc-hawkbit-updater', 94 | sources_updater, 95 | dbus_sources, 96 | config_h, 97 | dependencies : [libcurldep, giodep, giounixdep, jsonglibdep, libsystemddep], 98 | include_directories : incdir, 99 | install: true) 100 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | # feature options 2 | option( 3 | 'systemd', 4 | type : 'feature', 5 | value : 'disabled', 6 | description : 'Build for systemd (sd-notify support)') 7 | option( 8 | 'doc', 9 | type : 'feature', 10 | value : 'auto', 11 | description : 'Build user documentation') 12 | option( 13 | 'apidoc', 14 | type : 'feature', 15 | value : 'auto', 16 | description : 'Build API documentation (doxygen)') 17 | 18 | # path options 19 | option( 20 | 'systemdsystemunitdir', 21 | type : 'string', 22 | value : '', 23 | description : 'Directory for systemd service files') 24 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_file_level = INFO 3 | log_format = %(levelname)s %(name)s %(message)s 4 | addopts = --show-capture=log -rs 5 | -------------------------------------------------------------------------------- /script/LICENSE.0BSD: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any 2 | purpose with or without fee is hereby granted. 3 | 4 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 5 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 6 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 7 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 8 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 9 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 10 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 11 | -------------------------------------------------------------------------------- /script/hawkbit_mgmt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: 0BSD 3 | # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix 4 | # SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix 5 | 6 | import time 7 | 8 | import attr 9 | import requests as r 10 | 11 | 12 | class HawkbitError(Exception): 13 | pass 14 | 15 | class HawkbitIdStore(dict): 16 | """dict raising a HawkbitMgmtTestClient related error on KeyError.""" 17 | def __getitem__(self, key): 18 | try: 19 | return super().__getitem__(key) 20 | except KeyError: 21 | raise HawkbitError(f'{key} not yet created via HawkbitMgmtTestClient') 22 | 23 | @attr.s(eq=False) 24 | class HawkbitMgmtTestClient: 25 | """ 26 | Test oriented client for hawkBit's Management API. 27 | Does not cover the whole Management API, only the parts required for the rauc-hawkbit-updater 28 | test suite. 29 | 30 | https://eclipse.dev/hawkbit/apis/management_api/ 31 | """ 32 | host = attr.ib(validator=attr.validators.instance_of(str)) 33 | port = attr.ib(validator=attr.validators.instance_of(int)) 34 | username = attr.ib(default='admin', validator=attr.validators.instance_of(str)) 35 | password = attr.ib(default='admin', validator=attr.validators.instance_of(str)) 36 | version = attr.ib(default=1.0, validator=attr.validators.instance_of(float)) 37 | 38 | def __attrs_post_init__(self): 39 | self.url = f'http://{self.host}:{self.port}/rest/v1/{{endpoint}}' 40 | self.id = HawkbitIdStore() 41 | 42 | def get(self, endpoint: str): 43 | """ 44 | Performs an authenticated HTTP GET request on `endpoint`. 45 | Endpoint can either be a full URL or a path relative to /rest/v1/. Expects and returns the 46 | JSON response. 47 | """ 48 | url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) 49 | req = r.get( 50 | url, 51 | headers={'Content-Type': 'application/json;charset=UTF-8'}, 52 | auth=(self.username, self.password) 53 | ) 54 | if req.status_code != 200: 55 | try: 56 | raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') 57 | except: 58 | raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') 59 | 60 | return req.json() 61 | 62 | def post(self, endpoint: str, json_data: dict = None, file_name: str = None): 63 | """ 64 | Performs an authenticated HTTP POST request on `endpoint`. 65 | If `json_data` is given, it is sent along with the request and JSON data is expected in the 66 | response, which is in that case returned. 67 | If `file_name` is given, the file's content is sent along with the request and JSON data is 68 | expected in the response, which is in that case returned. 69 | json_data and file_name must not be specified in the same call. 70 | Endpoint can either be a full URL or a path relative to /rest/v1/. 71 | """ 72 | assert not (json_data and file_name) 73 | 74 | url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) 75 | files = {'file': open(file_name, 'rb')} if file_name else None 76 | headers = {'Content-Type': 'application/json;charset=UTF-8'} if json_data else None 77 | 78 | req = r.post( 79 | url, 80 | headers=headers, 81 | auth=(self.username, self.password), 82 | json=json_data, 83 | files=files 84 | ) 85 | 86 | if not 200 <= req.status_code < 300: 87 | try: 88 | raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') 89 | except: 90 | raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') 91 | 92 | if json_data or file_name: 93 | return req.json() 94 | 95 | return None 96 | 97 | def put(self, endpoint: str, json_data: dict): 98 | """ 99 | Performs an authenticated HTTP PUT request on `endpoint`. `json_data` is sent along with 100 | the request. 101 | `endpoint` can either be a full URL or a path relative to /rest/v1/. 102 | """ 103 | url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) 104 | 105 | req = r.put( 106 | url, 107 | auth=(self.username, self.password), 108 | json=json_data 109 | ) 110 | if not 200 <= req.status_code < 300: 111 | try: 112 | raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') 113 | except: 114 | raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') 115 | 116 | def delete(self, endpoint: str): 117 | """ 118 | Performs an authenticated HTTP DELETE request on endpoint. 119 | Endpoint can either be a full URL or a path relative to /rest/v1/. 120 | """ 121 | url = endpoint if endpoint.startswith('http') else self.url.format(endpoint=endpoint) 122 | 123 | req = r.delete( 124 | url, 125 | auth=(self.username, self.password) 126 | ) 127 | if not 200 <= req.status_code < 300: 128 | try: 129 | raise HawkbitError(f'HTTP error {req.status_code}: {req.json()}') 130 | except: 131 | raise HawkbitError(f'HTTP error {req.status_code}: {req.content.decode()}') 132 | 133 | def set_config(self, key: str, value: str): 134 | """ 135 | Changes a configuration `value` of a specific configuration `key`. 136 | 137 | https://eclipse.dev/hawkbit/rest-api/tenant-api-guide.html#_put_restv1systemconfigskeyname 138 | """ 139 | self.put(f'system/configs/{key}', {'value' : value}) 140 | 141 | def get_config(self, key: str): 142 | """ 143 | Returns the configuration value of a specific configuration `key`. 144 | 145 | https://eclipse.dev/hawkbit/rest-api/tenant-api-guide.html#_get_restv1systemconfigskeyname 146 | """ 147 | return self.get(f'system/configs/{key}')['value'] 148 | 149 | def add_target(self, target_id: str = None, token: str = None): 150 | """ 151 | Adds a new target with id and name `target_id`. 152 | If `target_id` is not given, a generic id is made up. 153 | If `token` is given, set it as target's token, otherwise hawkBit sets a random token 154 | itself. 155 | Stores the id of the created target for future use by other methods. 156 | Returns the target's id. 157 | 158 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_post_restv1targets 159 | """ 160 | target_id = target_id or f'test-{time.monotonic()}' 161 | testdata = { 162 | 'controllerId': target_id, 163 | 'name': target_id, 164 | } 165 | 166 | if token: 167 | testdata['securityToken'] = token 168 | 169 | self.post('targets', [testdata]) 170 | 171 | self.id['target'] = target_id 172 | return self.id['target'] 173 | 174 | def get_target(self, target_id: str = None): 175 | """ 176 | Returns the target matching `target_id`. 177 | If `target_id` is not given, returns the target created by the most recent `add_target()` 178 | call. 179 | 180 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetid 181 | """ 182 | target_id = target_id or self.id['target'] 183 | 184 | return self.get(f'targets/{target_id}') 185 | 186 | def delete_target(self, target_id: str = None): 187 | """ 188 | Deletes the target matching `target_id`. 189 | If target_id is not given, deletes the target created by the most recent add_target() call. 190 | 191 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_delete_restv1targetstargetid 192 | """ 193 | target_id = target_id or self.id['target'] 194 | self.delete(f'targets/{target_id}') 195 | 196 | if 'target' in self.id and target_id == self.id['target']: 197 | del self.id['target'] 198 | 199 | def get_attributes(self, target_id: str = None): 200 | """ 201 | Returns the attributes of the target matching `target_id`. 202 | If `target_id` is not given, uses the target created by the most recent `add_target()` 203 | call. 204 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidattributes 205 | """ 206 | target_id = target_id or self.id['target'] 207 | 208 | return self.get(f'targets/{target_id}/attributes') 209 | 210 | def add_softwaremodule(self, name: str = None, module_type: str = 'os'): 211 | """ 212 | Adds a new software module with `name`. 213 | If `name` is not given, a generic name is made up. 214 | Stores the id of the created software module for future use by other methods. 215 | Returns the id of the created software module. 216 | 217 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_post_restv1softwaremodules 218 | """ 219 | name = name or f'software module {time.monotonic()}' 220 | data = [{ 221 | 'name': name, 222 | 'version': str(self.version), 223 | 'type': module_type, 224 | }] 225 | 226 | self.id['softwaremodule'] = self.post('softwaremodules', data)[0]['id'] 227 | return self.id['softwaremodule'] 228 | 229 | def get_softwaremodule(self, module_id: str = None): 230 | """ 231 | Returns the sotware module matching `module_id`. 232 | If `module_id` is not given, returns the software module created by the most recent 233 | `add_softwaremodule()` call. 234 | 235 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_get_restv1softwaremodulessoftwaremoduleid 236 | """ 237 | module_id = module_id or self.id['softwaremodule'] 238 | 239 | return self.get(f'softwaremodules/{module_id}') 240 | 241 | def delete_softwaremodule(self, module_id: str = None): 242 | """ 243 | Deletes the software module matching `module_id`. 244 | 245 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_delete_restv1softwaremodulessoftwaremoduleid 246 | """ 247 | module_id = module_id or self.id['softwaremodule'] 248 | self.delete(f'softwaremodules/{module_id}') 249 | 250 | if 'softwaremodule' in self.id and module_id == self.id['softwaremodule']: 251 | del self.id['softwaremodule'] 252 | 253 | def add_distributionset(self, name: str = None, module_ids: list = [], dist_type: str = 'os'): 254 | """ 255 | Adds a new distribution set with `name` containing the software modules matching `module_ids`. 256 | If `name` is not given, a generic name is made up. 257 | If `module_ids` is not given, uses the software module created by the most recent 258 | `add_softwaremodule()` call. 259 | Stores the id of the created distribution set for future use by other methods. 260 | Returns the id of the created distribution set. 261 | 262 | https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_post_restv1distributionsets 263 | """ 264 | assert isinstance(module_ids, list) 265 | 266 | name = name or f'distribution {self.version} ({time.monotonic()})' 267 | module_ids = module_ids or [self.id['softwaremodule']] 268 | data = [{ 269 | 'name': name, 270 | 'description': 'Test distribution', 271 | 'version': str(self.version), 272 | 'modules': [], 273 | 'type': dist_type, 274 | }] 275 | for module_id in module_ids: 276 | data[0]['modules'].append({'id': module_id}) 277 | 278 | self.id['distributionset'] = self.post('distributionsets', data)[0]['id'] 279 | return self.id['distributionset'] 280 | 281 | def get_distributionset(self, dist_id: str = None): 282 | """ 283 | Returns the distribution set matching `dist_id`. 284 | If `dist_id` is not given, returns the distribution set created by the most recent 285 | `add_distributionset()` call. 286 | 287 | https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_get_restv1distributionsetsdistributionsetid 288 | """ 289 | dist_id = dist_id or self.id['distributionset'] 290 | 291 | return self.get(f'distributionsets/{dist_id}') 292 | 293 | def delete_distributionset(self, dist_id: str = None): 294 | """ 295 | Deletes the distrubition set matching `dist_id`. 296 | If `dist_id` is not given, deletes the distribution set created by the most recent 297 | `add_distributionset()` call. 298 | 299 | https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_delete_restv1distributionsetsdistributionsetid 300 | """ 301 | dist_id = dist_id or self.id['distributionset'] 302 | 303 | self.delete(f'distributionsets/{dist_id}') 304 | 305 | if 'distributionset' in self.id and dist_id == self.id['distributionset']: 306 | del self.id['distributionset'] 307 | 308 | def add_artifact(self, file_name: str, module_id: str = None): 309 | """ 310 | Adds a new artifact specified by `file_name` to the software module matching `module_id`. 311 | If `module_id` is not given, adds the artifact to the software module created by the most 312 | recent `add_softwaremodule()` call. 313 | Stores the id of the created artifact for future use by other methods. 314 | Returns the id of the created artifact. 315 | 316 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_post_restv1softwaremodulessoftwaremoduleidartifacts 317 | """ 318 | module_id = module_id or self.id['softwaremodule'] 319 | 320 | self.id['artifact'] = self.post(f'softwaremodules/{module_id}/artifacts', 321 | file_name=file_name)['id'] 322 | return self.id['artifact'] 323 | 324 | def get_artifact(self, artifact_id: str = None, module_id: str = None): 325 | """ 326 | Returns the artifact matching `artifact_id` from the software module matching `module_id`. 327 | If `artifact_id` is not given, returns the artifact created by the most recent 328 | `add_artifact()` call. 329 | If `module_id` is not given, uses the software module created by the most recent 330 | `add_softwaremodule()` call. 331 | 332 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_get_restv1softwaremodulessoftwaremoduleidartifactsartifactid 333 | """ 334 | module_id = module_id or self.id['softwaremodule'] 335 | artifact_id = artifact_id or self.id['artifact'] 336 | 337 | return self.get(f'softwaremodules/{module_id}/artifacts/{artifact_id}')['id'] 338 | 339 | def delete_artifact(self, artifact_id: str = None, module_id: str = None): 340 | """ 341 | Deletes the artifact matching `artifact_id` from the software module matching `module_id`. 342 | If `artifact_id` is not given, deletes the artifact created by the most recent 343 | `add_artifact()` call. 344 | If `module_id` is not given, uses the software module created by the most recent 345 | `add_softwaremodule()` call. 346 | 347 | https://eclipse.dev/hawkbit/rest-api/softwaremodules-api-guide.html#_delete_restv1softwaremodulessoftwaremoduleidartifactsartifactid 348 | """ 349 | module_id = module_id or self.id['softwaremodule'] 350 | artifact_id = artifact_id or self.id['artifact'] 351 | 352 | self.delete(f'softwaremodules/{module_id}/artifacts/{artifact_id}') 353 | 354 | if 'artifact' in self.id and artifact_id == self.id['artifact']: 355 | del self.id['artifact'] 356 | 357 | def assign_target(self, dist_id: str = None, target_id: str = None, params: dict = None): 358 | """ 359 | Assigns the distribution set matching `dist_id` to a target matching `target_id`. 360 | If `dist_id` is not given, uses the distribution set created by the most recent 361 | `add_distributionset()` call. 362 | If `target_id` is not given, uses the target created by the most recent `add_target()` 363 | call. 364 | Stores the id of the assignment action for future use by other methods. 365 | 366 | https://eclipse.dev/hawkbit/rest-api/distributionsets-api-guide.html#_post_restv1distributionsetsdistributionsetidassignedtargets 367 | """ 368 | dist_id = dist_id or self.id['distributionset'] 369 | target_id = target_id or self.id['target'] 370 | testdata = [{'id': target_id}] 371 | 372 | if params: 373 | testdata[0].update(params) 374 | 375 | response = self.post(f'distributionsets/{dist_id}/assignedTargets', testdata) 376 | 377 | # Increment version to be able to flash over an already deployed distribution 378 | self.version += 0.1 379 | 380 | self.id['action'] = response.get('assignedActions')[-1].get('id') 381 | return self.id['action'] 382 | 383 | def get_action(self, action_id: str = None, target_id: str = None): 384 | """ 385 | Returns the action matching `action_id` on the target matching `target_id`. 386 | If `action_id` is not given, returns the action created by the most recent 387 | `assign_target()` call. 388 | If `target_id` is not given, uses the target created by the most recent `add_target()` 389 | call. 390 | 391 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidactionsactionid 392 | """ 393 | action_id = action_id or self.id['action'] 394 | target_id = target_id or self.id['target'] 395 | 396 | return self.get(f'targets/{target_id}/actions/{action_id}') 397 | 398 | def get_action_status(self, action_id: str = None, target_id: str = None): 399 | """ 400 | Returns the first (max.) 50 action states of the action matching `action_id` of the target 401 | matching `target_id` sorted by id. 402 | If `action_id` is not given, uses the action created by the most recent `assign_target()` 403 | call. 404 | If `target_id` is not given, uses the target created by the most recent `add_target()` 405 | call. 406 | 407 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_get_restv1targetstargetidactionsactionidstatus 408 | """ 409 | action_id = action_id or self.id['action'] 410 | target_id = target_id or self.id['target'] 411 | 412 | req = self.get(f'targets/{target_id}/actions/{action_id}/status?offset=0&limit=50&sort=id:DESC') 413 | return req['content'] 414 | 415 | def cancel_action(self, action_id: str = None, target_id: str = None, *, force: bool = False): 416 | """ 417 | Cancels the action matching `action_id` of the target matching `target_id`. 418 | If `force=True` is given, cancels the action without telling the target. 419 | If `action_id` is not given, uses the action created by the most recent `assign_target()` 420 | call. 421 | If `target_id` is not given, uses the target created by the most recent `add_target()` 422 | call. 423 | 424 | https://eclipse.dev/hawkbit/rest-api/targets-api-guide.html#_delete_restv1targetstargetidactionsactionid 425 | """ 426 | action_id = action_id or self.id['action'] 427 | target_id = target_id or self.id['target'] 428 | 429 | self.delete(f'targets/{target_id}/actions/{action_id}') 430 | 431 | if force: 432 | self.delete(f'targets/{target_id}/actions/{action_id}?force=true') 433 | 434 | 435 | if __name__ == '__main__': 436 | import argparse 437 | 438 | parser = argparse.ArgumentParser() 439 | parser.add_argument('bundle', help='RAUC bundle to add as artifact') 440 | args = parser.parse_args() 441 | 442 | client = HawkbitMgmtTestClient('localhost', 8080) 443 | 444 | client.set_config('pollingTime', '00:00:30') 445 | client.set_config('pollingOverdueTime', '00:03:00') 446 | client.set_config('authentication.targettoken.enabled', True) 447 | 448 | client.add_target('test', 'ieHai3du7gee7aPhojeth4ong') 449 | client.add_softwaremodule() 450 | client.add_artifact(args.bundle) 451 | client.add_distributionset() 452 | client.assign_target() 453 | 454 | try: 455 | target = client.get_target() 456 | print(f'Created target (target_name={target["controllerId"]}, auth_token={target["securityToken"]}) assigned distribution containing {args.bundle} to it') 457 | print('Clean quit with a single ctrl-c') 458 | 459 | while True: 460 | time.sleep(1) 461 | except KeyboardInterrupt: 462 | print('Cleaning up..') 463 | finally: 464 | try: 465 | client.cancel_action(force=True) 466 | except: 467 | pass 468 | 469 | client.delete_distributionset() 470 | client.delete_artifact() 471 | client.delete_softwaremodule() 472 | client.delete_target() 473 | print('Done') 474 | -------------------------------------------------------------------------------- /script/rauc-hawkbit-updater.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=HawkBit client for Rauc 3 | After=network-online.target rauc.service 4 | Wants=network-online.target 5 | 6 | [Service] 7 | User=rauc-hawkbit 8 | Group=rauc-hawkbit 9 | AmbientCapabilities=CAP_SYS_BOOT 10 | ExecStart=/usr/bin/rauc-hawkbit-updater -s -c /etc/rauc-hawkbit-updater/config.conf 11 | TimeoutSec=60s 12 | WatchdogSec=5m 13 | Restart=on-failure 14 | RestartSec=1m 15 | NotifyAccess=main 16 | ProtectSystem=full 17 | Nice=10 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /src/config-file.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 5 | * 6 | * @file 7 | * @brief Configuration file parser 8 | */ 9 | 10 | #include "config-file.h" 11 | #include 12 | #include 13 | 14 | 15 | static const gint DEFAULT_CONNECTTIMEOUT = 20; // 20 sec. 16 | static const gint DEFAULT_TIMEOUT = 60; // 1 min. 17 | static const gint DEFAULT_RETRY_WAIT = 5 * 60; // 5 min. 18 | static const gboolean DEFAULT_SSL = TRUE; 19 | static const gboolean DEFAULT_SSL_VERIFY = TRUE; 20 | static const gboolean DEFAULT_REBOOT = FALSE; 21 | static const gchar* DEFAULT_LOG_LEVEL = "message"; 22 | 23 | /** 24 | * @brief Get string value from key_file for key in group, optional default_value can be specified 25 | * that will be used in case key is not found in group. 26 | * 27 | * @param[in] key_file GKeyFile to look value up 28 | * @param[in] group A group name 29 | * @param[in] key A key 30 | * @param[out] value Output string value 31 | * @param[in] default_value String value to return in case no value found, or NULL (not found 32 | * leads to error) 33 | * @param[out] error Error 34 | * @return TRUE if found, TRUE if not found and default_value given, FALSE otherwise (error is set) 35 | */ 36 | static gboolean get_key_string(GKeyFile *key_file, const gchar *group, const gchar *key, 37 | gchar **value, const gchar *default_value, GError **error) 38 | { 39 | g_autofree gchar *val = NULL; 40 | 41 | g_return_val_if_fail(key_file, FALSE); 42 | g_return_val_if_fail(group, FALSE); 43 | g_return_val_if_fail(key, FALSE); 44 | g_return_val_if_fail(value && *value == NULL, FALSE); 45 | g_return_val_if_fail(error == NULL || *error == NULL, FALSE); 46 | 47 | val = g_key_file_get_string(key_file, group, key, error); 48 | if (!val) { 49 | if (default_value) { 50 | *value = g_strdup(default_value); 51 | g_clear_error(error); 52 | return TRUE; 53 | } 54 | 55 | return FALSE; 56 | } 57 | 58 | val = g_strchomp(val); 59 | *value = g_steal_pointer(&val); 60 | return TRUE; 61 | } 62 | 63 | /** 64 | * @brief Get gboolean value from key_file for key in group, default_value must be specified, 65 | * returned in case key not found in group. 66 | * 67 | * @param[in] key_file GKeyFile to look value up 68 | * @param[in] group A group name 69 | * @param[in] key A key 70 | * @param[out] value Output gboolean value 71 | * @param[in] default_value Return this value in case no value found 72 | * @param[out] error Error 73 | * @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in 74 | * group is not found, value is set to default_value in this case. 75 | */ 76 | static gboolean get_key_bool(GKeyFile *key_file, const gchar *group, const gchar *key, 77 | gboolean *value, const gboolean default_value, GError **error) 78 | { 79 | g_autofree gchar *val = NULL; 80 | 81 | g_return_val_if_fail(key_file, FALSE); 82 | g_return_val_if_fail(group, FALSE); 83 | g_return_val_if_fail(key, FALSE); 84 | g_return_val_if_fail(value, FALSE); 85 | g_return_val_if_fail(error == NULL || *error == NULL, FALSE); 86 | 87 | val = g_key_file_get_string(key_file, group, key, NULL); 88 | if (!val) { 89 | *value = default_value; 90 | return TRUE; 91 | } 92 | 93 | val = g_strchomp(val); 94 | 95 | if (g_strcmp0(val, "0") == 0 || g_ascii_strcasecmp(val, "no") == 0 || 96 | g_ascii_strcasecmp(val, "false") == 0) { 97 | *value = FALSE; 98 | return TRUE; 99 | } 100 | 101 | if (g_strcmp0(val, "1") == 0 || g_ascii_strcasecmp(val, "yes") == 0 || 102 | g_ascii_strcasecmp(val, "true") == 0) { 103 | *value = TRUE; 104 | return TRUE; 105 | } 106 | 107 | g_set_error(error, G_KEY_FILE_ERROR, 108 | G_KEY_FILE_ERROR_INVALID_VALUE, 109 | "Value '%s' cannot be interpreted as a boolean.", val); 110 | 111 | return FALSE; 112 | } 113 | 114 | /** 115 | * @brief Get integer value from key_file for key in group, default_value must be specified, 116 | * returned in case key not found in group. 117 | * 118 | * @param[in] key_file GKeyFile to look value up 119 | * @param[in] group A group name 120 | * @param[in] key A key 121 | * @param[out] value Output integer value 122 | * @param[in] default_value Return this value in case no value found 123 | * @param[out] error Error 124 | * @return FALSE on error (error is set), TRUE otherwise. Note that TRUE is returned if key in 125 | * group is not found, value is set to default_value in this case. 126 | */ 127 | static gboolean get_key_int(GKeyFile *key_file, const gchar *group, const gchar *key, gint *value, 128 | const gint default_value, GError **error) 129 | { 130 | GError *ierror = NULL; 131 | gint val; 132 | 133 | g_return_val_if_fail(key_file, FALSE); 134 | g_return_val_if_fail(group, FALSE); 135 | g_return_val_if_fail(key, FALSE); 136 | g_return_val_if_fail(value, FALSE); 137 | g_return_val_if_fail(error == NULL || *error == NULL, FALSE); 138 | 139 | val = g_key_file_get_integer(key_file, group, key, &ierror); 140 | 141 | if (g_error_matches(ierror, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { 142 | g_clear_error(&ierror); 143 | *value = default_value; 144 | return TRUE; 145 | } else if (ierror) { 146 | g_propagate_error(error, ierror); 147 | return FALSE; 148 | } 149 | 150 | *value = val; 151 | return TRUE; 152 | } 153 | 154 | /** 155 | * @brief Get GHashTable containing keys/values from group in key_file. 156 | * 157 | * @param[in] key_file GKeyFile to look value up 158 | * @param[in] group A group name 159 | * @param[out] hash Output GHashTable 160 | * @param[out] error Error 161 | * @return TRUE on keys/values stored successfully, FALSE on empty group/value or on other errors 162 | * (error set) 163 | */ 164 | static gboolean get_group(GKeyFile *key_file, const gchar *group, GHashTable **hash, 165 | GError **error) 166 | { 167 | g_autoptr(GHashTable) tmp_hash = NULL; 168 | guint key; 169 | gsize num_keys; 170 | g_auto(GStrv) keys = NULL; 171 | 172 | g_return_val_if_fail(key_file, FALSE); 173 | g_return_val_if_fail(group, FALSE); 174 | g_return_val_if_fail(hash && *hash == NULL, FALSE); 175 | g_return_val_if_fail(error == NULL || *error == NULL, FALSE); 176 | 177 | tmp_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); 178 | keys = g_key_file_get_keys(key_file, group, &num_keys, error); 179 | if (!keys) 180 | return FALSE; 181 | 182 | if (!num_keys) { 183 | g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, 184 | "Group '%s' has no keys set", group); 185 | return FALSE; 186 | } 187 | 188 | for (key = 0; key < num_keys; key++) { 189 | g_autofree gchar *value = g_key_file_get_value(key_file, group, keys[key], error); 190 | if (!value) 191 | return FALSE; 192 | 193 | value = g_strchomp(value); 194 | g_hash_table_insert(tmp_hash, g_strdup(keys[key]), g_steal_pointer(&value)); 195 | } 196 | 197 | *hash = g_steal_pointer(&tmp_hash); 198 | return TRUE; 199 | } 200 | 201 | /** 202 | * @brief Get GLogLevelFlags for error string. 203 | * 204 | * @param[in] log_level Log level string 205 | * @return GLogLevelFlags matching error string, else default log level (message) 206 | */ 207 | static GLogLevelFlags log_level_from_string(const gchar *log_level) 208 | { 209 | g_return_val_if_fail(log_level, 0); 210 | 211 | if (g_strcmp0(log_level, "error") == 0) { 212 | return G_LOG_LEVEL_ERROR; 213 | } else if (g_strcmp0(log_level, "critical") == 0) { 214 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL; 215 | } else if (g_strcmp0(log_level, "warning") == 0) { 216 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | 217 | G_LOG_LEVEL_WARNING; 218 | } else if (g_strcmp0(log_level, "message") == 0) { 219 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | 220 | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE; 221 | } else if (g_strcmp0(log_level, "info") == 0) { 222 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | 223 | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | 224 | G_LOG_LEVEL_INFO; 225 | } else if (g_strcmp0(log_level, "debug") == 0) { 226 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | 227 | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | 228 | G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG; 229 | } else { 230 | g_warning("Invalid log level given, defaulting to level \"message\""); 231 | return G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | 232 | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE; 233 | } 234 | } 235 | 236 | Config* load_config_file(const gchar *config_file, GError **error) 237 | { 238 | g_autoptr(Config) config = NULL; 239 | g_autofree gchar *val = NULL; 240 | g_autoptr(GKeyFile) ini_file = NULL; 241 | gboolean key_auth_token_exists = FALSE; 242 | gboolean key_gateway_token_exists = FALSE; 243 | gboolean bundle_location_given = FALSE; 244 | 245 | g_return_val_if_fail(config_file, NULL); 246 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); 247 | 248 | config = g_new0(Config, 1); 249 | ini_file = g_key_file_new(); 250 | 251 | if (!g_key_file_load_from_file(ini_file, config_file, G_KEY_FILE_NONE, error)) 252 | return NULL; 253 | 254 | if (!get_key_string(ini_file, "client", "hawkbit_server", &config->hawkbit_server, NULL, 255 | error)) 256 | return NULL; 257 | 258 | key_auth_token_exists = get_key_string(ini_file, "client", "auth_token", 259 | &config->auth_token, NULL, NULL); 260 | key_gateway_token_exists = get_key_string(ini_file, "client", "gateway_token", 261 | &config->gateway_token, NULL, NULL); 262 | if (!key_auth_token_exists && !key_gateway_token_exists) { 263 | g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, 264 | "Neither 'auth_token' nor 'gateway_token' set"); 265 | return NULL; 266 | } 267 | if (key_auth_token_exists && key_gateway_token_exists) { 268 | g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, 269 | "Both 'auth_token' and 'gateway_token' set"); 270 | return NULL; 271 | } 272 | 273 | if (!get_key_string(ini_file, "client", "target_name", &config->controller_id, NULL, 274 | error)) 275 | return NULL; 276 | if (!get_key_string(ini_file, "client", "tenant_id", &config->tenant_id, "DEFAULT", error)) 277 | return NULL; 278 | bundle_location_given = get_key_string(ini_file, "client", "bundle_download_location", 279 | &config->bundle_download_location, NULL, NULL); 280 | if (!get_key_bool(ini_file, "client", "ssl", &config->ssl, DEFAULT_SSL, error)) 281 | return NULL; 282 | if (!get_key_bool(ini_file, "client", "ssl_verify", &config->ssl_verify, 283 | DEFAULT_SSL_VERIFY, error)) 284 | return NULL; 285 | if (!get_group(ini_file, "device", &config->device, error)) 286 | return NULL; 287 | if (!get_key_int(ini_file, "client", "connect_timeout", &config->connect_timeout, 288 | DEFAULT_CONNECTTIMEOUT, error)) 289 | return NULL; 290 | if (!get_key_int(ini_file, "client", "timeout", &config->timeout, DEFAULT_TIMEOUT, error)) 291 | return NULL; 292 | if (!get_key_int(ini_file, "client", "retry_wait", &config->retry_wait, DEFAULT_RETRY_WAIT, 293 | error)) 294 | return NULL; 295 | if (!get_key_int(ini_file, "client", "low_speed_rate", &config->low_speed_rate, 100, 296 | error)) 297 | return NULL; 298 | if (!get_key_int(ini_file, "client", "low_speed_time", &config->low_speed_time, 60, error)) 299 | return NULL; 300 | if (!get_key_bool(ini_file, "client", "resume_downloads", &config->resume_downloads, FALSE, 301 | error)) 302 | return NULL; 303 | if (!get_key_bool(ini_file, "client", "stream_bundle", &config->stream_bundle, FALSE, 304 | error)) 305 | return NULL; 306 | if (!get_key_string(ini_file, "client", "log_level", &val, DEFAULT_LOG_LEVEL, error)) 307 | return NULL; 308 | config->log_level = log_level_from_string(val); 309 | 310 | if (!get_key_bool(ini_file, "client", "post_update_reboot", &config->post_update_reboot, DEFAULT_REBOOT, error)) 311 | return NULL; 312 | 313 | if (config->timeout > 0 && config->connect_timeout > 0 && 314 | config->timeout < config->connect_timeout) { 315 | g_set_error(error, 316 | G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, 317 | "'timeout' (%d) must be greater than 'connect_timeout' (%d)", 318 | config->timeout, config->connect_timeout); 319 | return NULL; 320 | } 321 | 322 | if (!bundle_location_given && !config->stream_bundle) { 323 | g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND, 324 | "'bundle_download_location' is required if 'stream_bundle' is disabled"); 325 | return NULL; 326 | } 327 | 328 | return g_steal_pointer(&config); 329 | } 330 | 331 | void config_file_free(Config *config) 332 | { 333 | if (!config) 334 | return; 335 | 336 | g_free(config->hawkbit_server); 337 | g_free(config->controller_id); 338 | g_free(config->tenant_id); 339 | g_free(config->auth_token); 340 | g_free(config->gateway_token); 341 | g_free(config->bundle_download_location); 342 | if (config->device) 343 | g_hash_table_destroy(config->device); 344 | g_free(config); 345 | } 346 | -------------------------------------------------------------------------------- /src/json-helper.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | * 5 | * @file 6 | * @brief JSON helper functions 7 | */ 8 | 9 | #include "json-helper.h" 10 | #include 11 | 12 | 13 | /** 14 | * @brief Get the first JsonNode element matching path in json_node. 15 | * 16 | * @param[in] json_node JsonNode to query 17 | * @param[in] path Query path 18 | * @param[out] error Error 19 | * @return JsonNode*, matching JsonNode element (must be freed), NULL on error 20 | */ 21 | static JsonNode* json_get_first_matching_element(JsonNode *json_node, const gchar *path, 22 | GError **error) 23 | { 24 | g_autoptr(JsonNode) match = NULL, node = NULL; 25 | JsonArray *arr = NULL; 26 | 27 | g_return_val_if_fail(json_node, NULL); 28 | g_return_val_if_fail(path, NULL); 29 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); 30 | 31 | match = json_path_query(path, json_node, error); 32 | if (!match) 33 | return NULL; 34 | 35 | arr = json_node_get_array(match); 36 | if (!arr) { 37 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 38 | "Failed to retrieve array from node for path %s", path); 39 | return NULL; 40 | } 41 | 42 | if (json_array_get_length(arr) > 0) 43 | node = json_array_dup_element(arr, 0); 44 | 45 | if (!node) { 46 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 47 | "Failed to retrieve element from array for path %s", path); 48 | return NULL; 49 | } 50 | 51 | return g_steal_pointer(&node); 52 | } 53 | 54 | gchar* json_get_string(JsonNode *json_node, const gchar *path, GError **error) 55 | { 56 | g_autofree gchar *res_str = NULL; 57 | g_autoptr(JsonNode) result = NULL; 58 | 59 | g_return_val_if_fail(json_node, NULL); 60 | g_return_val_if_fail(path, NULL); 61 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); 62 | 63 | result = json_get_first_matching_element(json_node, path, error); 64 | if (!result) 65 | return NULL; 66 | 67 | res_str = json_node_dup_string(result); 68 | if (!res_str) { 69 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 70 | "Failed to retrieve string element from array for path %s", path); 71 | return NULL; 72 | } 73 | 74 | return g_steal_pointer(&res_str); 75 | } 76 | 77 | gint64 json_get_int(JsonNode *json_node, const gchar *path, GError **error) 78 | { 79 | g_autoptr(JsonNode) result = NULL; 80 | 81 | g_return_val_if_fail(json_node, 0); 82 | g_return_val_if_fail(path, 0); 83 | g_return_val_if_fail(error == NULL || *error == NULL, 0); 84 | 85 | result = json_get_first_matching_element(json_node, path, error); 86 | if (!result) 87 | return 0; 88 | 89 | if (!JSON_NODE_HOLDS_VALUE(result)) { 90 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 91 | "Failed to retrieve value from node for path %s", path); 92 | return 0; 93 | } 94 | 95 | return json_node_get_int(result); 96 | } 97 | 98 | JsonArray* json_get_array(JsonNode *json_node, const gchar *path, GError **error) 99 | { 100 | g_autoptr(JsonArray) res_arr = NULL; 101 | g_autoptr(JsonNode) result = NULL; 102 | 103 | g_return_val_if_fail(error == NULL || *error == NULL, NULL); 104 | g_return_val_if_fail(json_node, NULL); 105 | g_return_val_if_fail(path, NULL); 106 | 107 | result = json_get_first_matching_element(json_node, path, error); 108 | if (!result) 109 | return NULL; 110 | 111 | if (!JSON_NODE_HOLDS_ARRAY(result)) { 112 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 113 | "Failed to retrieve value from node for path %s", path); 114 | return NULL; 115 | } 116 | 117 | res_arr = json_node_dup_array(result); 118 | if (!res_arr || !json_array_get_length(res_arr)) { 119 | g_set_error(error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE, 120 | "Empty JSON array for path %s", path); 121 | return NULL; 122 | } 123 | 124 | return g_steal_pointer(&res_arr); 125 | } 126 | 127 | gboolean json_contains(JsonNode *json_node, gchar *path) 128 | { 129 | g_autoptr(GError) error = NULL; 130 | g_autoptr(JsonNode) node = NULL; 131 | 132 | g_return_val_if_fail(json_node, FALSE); 133 | g_return_val_if_fail(path, FALSE); 134 | 135 | node = json_path_query(path, json_node, &error); 136 | if (!node) { 137 | // failed to compile expression to JSONPath 138 | g_warning("%s", error->message); 139 | return FALSE; 140 | } 141 | 142 | if (json_array_get_length(json_node_get_array(node)) > 0) 143 | return TRUE; 144 | 145 | return FALSE; 146 | } 147 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | * 5 | * @file 6 | * @brief Log handling 7 | */ 8 | 9 | #include "log.h" 10 | #include 11 | 12 | static gboolean output_to_systemd = FALSE; 13 | 14 | /** 15 | * @brief convert GLogLevelFlags to string 16 | * 17 | * @param[in] level Log level that should be returned as string. 18 | * @return log level string 19 | */ 20 | static const gchar *log_level_to_string(GLogLevelFlags level) 21 | { 22 | switch (level) { 23 | case G_LOG_LEVEL_ERROR: 24 | return "ERROR"; 25 | case G_LOG_LEVEL_CRITICAL: 26 | return "CRITICAL"; 27 | case G_LOG_LEVEL_WARNING: 28 | return "WARNING"; 29 | case G_LOG_LEVEL_MESSAGE: 30 | return "MESSAGE"; 31 | case G_LOG_LEVEL_INFO: 32 | return "INFO"; 33 | case G_LOG_LEVEL_DEBUG: 34 | return "DEBUG"; 35 | default: 36 | return "UNKNOWN"; 37 | } 38 | } 39 | 40 | /** 41 | * @brief map glib log level to syslog 42 | * 43 | * @param[in] level Log level that should be returned as string. 44 | * @return syslog level 45 | */ 46 | #ifdef WITH_SYSTEMD 47 | static int log_level_to_int(GLogLevelFlags level) 48 | { 49 | switch (level) { 50 | case G_LOG_LEVEL_ERROR: 51 | return LOG_ERR; 52 | case G_LOG_LEVEL_CRITICAL: 53 | return LOG_CRIT; 54 | case G_LOG_LEVEL_WARNING: 55 | return LOG_WARNING; 56 | case G_LOG_LEVEL_MESSAGE: 57 | return LOG_NOTICE; 58 | case G_LOG_LEVEL_INFO: 59 | return LOG_INFO; 60 | case G_LOG_LEVEL_DEBUG: 61 | return LOG_DEBUG; 62 | default: 63 | return LOG_INFO; 64 | } 65 | } 66 | #endif 67 | 68 | /** 69 | * @brief Glib log handler callback 70 | * 71 | * @param[in] log_domain Log domain 72 | * @param[in] log_level Log level 73 | * @param[in] message Log message 74 | * @param[in] user_data Not used 75 | */ 76 | static void log_handler_cb(const gchar *log_domain, 77 | GLogLevelFlags log_level, 78 | const gchar *message, 79 | gpointer user_data) 80 | { 81 | const gchar *log_level_str; 82 | #ifdef WITH_SYSTEMD 83 | if (output_to_systemd) { 84 | int log_level_int = log_level_to_int(log_level & G_LOG_LEVEL_MASK); 85 | sd_journal_print(log_level_int, "%s", message); 86 | } else { 87 | #endif 88 | log_level_str = log_level_to_string(log_level & G_LOG_LEVEL_MASK); 89 | if (log_level <= G_LOG_LEVEL_WARNING) { 90 | g_printerr("%s: %s\n", log_level_str, message); 91 | } else { 92 | g_print("%s: %s\n", log_level_str, message); 93 | } 94 | #ifdef WITH_SYSTEMD 95 | } 96 | #endif 97 | } 98 | 99 | void setup_logging(const gchar *domain, GLogLevelFlags level, gboolean p_output_to_systemd) 100 | { 101 | output_to_systemd = p_output_to_systemd; 102 | g_log_set_handler(NULL, 103 | level | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION, 104 | log_handler_cb, NULL); 105 | } 106 | -------------------------------------------------------------------------------- /src/rauc-hawkbit-updater.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 5 | * 6 | * @file 7 | * @brief RAUC HawkBit updater daemon 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include "rauc-installer.h" 14 | #include "hawkbit-client.h" 15 | #include "config-file.h" 16 | #include "log.h" 17 | 18 | #define PROGRAM "rauc-hawkbit-updater" 19 | #define VERSION PROJECT_VERSION 20 | 21 | // program arguments 22 | static gchar *config_file = NULL; 23 | static gboolean opt_version = FALSE; 24 | static gboolean opt_debug = FALSE; 25 | static gboolean opt_run_once = FALSE; 26 | static gboolean opt_output_systemd = FALSE; 27 | 28 | // Commandline options 29 | static GOptionEntry entries[] = 30 | { 31 | { "config-file", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &config_file, "Configuration file", NULL }, 32 | { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version, "Version information", NULL }, 33 | { "debug", 'd', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_debug, "Enable debug output", NULL }, 34 | { "run-once", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_run_once, "Check and install new software and exit", NULL }, 35 | #ifdef WITH_SYSTEMD 36 | { "output-systemd", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_output_systemd, "Enable output to systemd", NULL }, 37 | #endif 38 | { NULL } 39 | }; 40 | 41 | // hawkbit callbacks 42 | static GSourceFunc notify_hawkbit_install_progress; 43 | static GSourceFunc notify_hawkbit_install_complete; 44 | 45 | 46 | /** 47 | * @brief GSourceFunc callback for install thread, consumes RAUC progress messages, logs them and 48 | * passes them on to notify_hawkbit_install_progress(). 49 | * 50 | * @param[in] data install_context pointer allowing access to received status messages 51 | * @return G_SOURCE_REMOVE is always returned 52 | */ 53 | static gboolean on_rauc_install_progress_cb(gpointer data) 54 | { 55 | struct install_context *context = data; 56 | 57 | g_return_val_if_fail(data, G_SOURCE_REMOVE); 58 | 59 | g_mutex_lock(&context->status_mutex); 60 | while (!g_queue_is_empty(&context->status_messages)) { 61 | g_autofree gchar *msg = g_queue_pop_head(&context->status_messages); 62 | g_message("Installing: %s : %s", context->bundle, msg); 63 | // notify hawkbit server about progress 64 | notify_hawkbit_install_progress(msg); 65 | } 66 | g_mutex_unlock(&context->status_mutex); 67 | 68 | return G_SOURCE_REMOVE; 69 | } 70 | 71 | /** 72 | * @brief GSourceFunc callback for install thread, consumes RAUC installation status result 73 | * (on complete) and passes it on to notify_hawkbit_install_complete(). 74 | * 75 | * @param[in] data install_context pointer allowing access to received status result 76 | * @return G_SOURCE_REMOVE is always returned 77 | */ 78 | static gboolean on_rauc_install_complete_cb(gpointer data) 79 | { 80 | struct install_context *context = data; 81 | struct on_install_complete_userdata userdata; 82 | 83 | g_return_val_if_fail(data, G_SOURCE_REMOVE); 84 | 85 | userdata.install_success = (context->status_result == 0); 86 | 87 | // notify hawkbit about install result 88 | notify_hawkbit_install_complete(&userdata); 89 | 90 | return G_SOURCE_REMOVE; 91 | } 92 | 93 | /** 94 | * @brief GSourceFunc callback for download thread, or main thread in case of HTTP streaming 95 | * installation. Triggers RAUC installation. 96 | * 97 | * @param[in] data on_new_software_userdata pointer 98 | * @return G_SOURCE_REMOVE is always returned 99 | */ 100 | static gboolean on_new_software_ready_cb(gpointer data) 101 | { 102 | struct on_new_software_userdata *userdata = data; 103 | 104 | g_return_val_if_fail(data, G_SOURCE_REMOVE); 105 | 106 | notify_hawkbit_install_progress = userdata->install_progress_callback; 107 | notify_hawkbit_install_complete = userdata->install_complete_callback; 108 | userdata->install_success = rauc_install(userdata->file, userdata->auth_header, 109 | userdata->ssl_verify, 110 | on_rauc_install_progress_cb, 111 | on_rauc_install_complete_cb, run_once); 112 | 113 | return G_SOURCE_REMOVE; 114 | } 115 | 116 | int main(int argc, char **argv) 117 | { 118 | g_autoptr(GError) error = NULL; 119 | g_autoptr(GOptionContext) context = NULL; 120 | g_auto(GStrv) args = NULL; 121 | GLogLevelFlags log_level; 122 | g_autoptr(Config) config = NULL; 123 | GLogLevelFlags fatal_mask; 124 | 125 | fatal_mask = g_log_set_always_fatal(G_LOG_FATAL_MASK); 126 | fatal_mask |= G_LOG_LEVEL_CRITICAL; 127 | g_log_set_always_fatal(fatal_mask); 128 | 129 | args = g_strdupv(argv); 130 | 131 | context = g_option_context_new(""); 132 | g_option_context_add_main_entries(context, entries, NULL); 133 | if (!g_option_context_parse_strv(context, &args, &error)) { 134 | g_printerr("option parsing failed: %s\n", error->message); 135 | return 1; 136 | } 137 | 138 | if (opt_version) { 139 | g_printf("Version %s\n", PROJECT_VERSION); 140 | return 0; 141 | } 142 | 143 | if (!config_file) { 144 | g_printerr("No configuration file given\n"); 145 | return 2; 146 | } 147 | 148 | if (!g_file_test(config_file, G_FILE_TEST_EXISTS)) { 149 | g_printerr("No such configuration file: %s\n", config_file); 150 | return 3; 151 | } 152 | 153 | run_once = opt_run_once; 154 | 155 | config = load_config_file(config_file, &error); 156 | if (!config) { 157 | g_printerr("Loading config file failed: %s\n", error->message); 158 | return 4; 159 | } 160 | 161 | log_level = (opt_debug) ? G_LOG_LEVEL_MASK : config->log_level; 162 | 163 | setup_logging(PROGRAM, log_level, opt_output_systemd); 164 | hawkbit_init(config, on_new_software_ready_cb); 165 | 166 | return hawkbit_start_service_sync(); 167 | } 168 | -------------------------------------------------------------------------------- /src/rauc-installer.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 5 | * 6 | * @file 7 | * @brief RAUC client 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "gobject/gclosure.h" 15 | #include "rauc-installer.h" 16 | #include "rauc-installer-gen.h" 17 | 18 | static GThread *thread_install = NULL; 19 | 20 | /** 21 | * @brief RAUC DBUS property changed callback 22 | * 23 | * @see https://github.com/rauc/rauc/blob/master/src/de.pengutronix.rauc.Installer.xml 24 | */ 25 | static void on_installer_status(GDBusProxy *proxy, GVariant *changed, 26 | const gchar* const *invalidated, gpointer data) 27 | { 28 | struct install_context *context = data; 29 | gint32 percentage; 30 | g_autofree gchar *message = NULL; 31 | 32 | g_return_if_fail(changed); 33 | g_return_if_fail(context); 34 | 35 | if (invalidated && invalidated[0]) { 36 | g_warning("RAUC DBUS service disappeared"); 37 | g_mutex_lock(&context->status_mutex); 38 | context->status_result = 2; 39 | g_mutex_unlock(&context->status_mutex); 40 | g_main_loop_quit(context->mainloop); 41 | return; 42 | } 43 | 44 | if (context->notify_event) { 45 | gboolean status_received = FALSE; 46 | 47 | g_mutex_lock(&context->status_mutex); 48 | if (g_variant_lookup(changed, "Operation", "s", &message)) 49 | g_queue_push_tail(&context->status_messages, g_steal_pointer(&message)); 50 | else if (g_variant_lookup(changed, "Progress", "(isi)", &percentage, &message, 51 | NULL)) 52 | g_queue_push_tail(&context->status_messages, 53 | g_strdup_printf("%3" G_GINT32_FORMAT "%% %s", percentage, 54 | message)); 55 | else if (g_variant_lookup(changed, "LastError", "s", &message) && message[0] != 0) 56 | g_queue_push_tail(&context->status_messages, 57 | g_strdup_printf("LastError: %s", message)); 58 | 59 | status_received = !g_queue_is_empty(&context->status_messages); 60 | g_mutex_unlock(&context->status_mutex); 61 | 62 | if (status_received) 63 | g_main_context_invoke(context->loop_context, context->notify_event, 64 | context); 65 | } 66 | } 67 | 68 | /** 69 | * @brief RAUC DBUS complete signal callback 70 | * 71 | * @see https://github.com/rauc/rauc/blob/master/src/de.pengutronix.rauc.Installer.xml 72 | */ 73 | static void on_installer_completed(GDBusProxy *proxy, gint result, gpointer data) 74 | { 75 | struct install_context *context = data; 76 | 77 | g_return_if_fail(context); 78 | 79 | g_mutex_lock(&context->status_mutex); 80 | context->status_result = result; 81 | g_mutex_unlock(&context->status_mutex); 82 | 83 | if (result >= 0) 84 | g_main_loop_quit(context->mainloop); 85 | } 86 | 87 | /** 88 | * @brief Create and init a install_context 89 | * 90 | * @return Pointer to initialized install_context struct. Should be freed by calling 91 | * install_context_free(). 92 | */ 93 | static struct install_context *install_context_new(void) 94 | { 95 | struct install_context *context = g_new0(struct install_context, 1); 96 | 97 | g_mutex_init(&context->status_mutex); 98 | g_queue_init(&context->status_messages); 99 | context->status_result = -2; 100 | 101 | return context; 102 | } 103 | 104 | /** 105 | * @brief Free a install_context and its members 106 | * 107 | * @param[in] context the install_context struct that should be freed. 108 | * If NULL 109 | */ 110 | static void install_context_free(struct install_context *context) 111 | { 112 | if (!context) 113 | return; 114 | 115 | g_free(context->bundle); 116 | g_free(context->auth_header); 117 | g_mutex_clear(&context->status_mutex); 118 | 119 | // make sure all pending events are processed 120 | while (g_main_context_iteration(context->loop_context, FALSE)); 121 | g_main_context_unref(context->loop_context); 122 | 123 | g_assert_cmpint(context->status_result, >=, 0); 124 | g_assert_true(g_queue_is_empty(&context->status_messages)); 125 | g_main_loop_unref(context->mainloop); 126 | g_free(context); 127 | } 128 | 129 | /** 130 | * @brief RAUC client mainloop 131 | * 132 | * Install mainloop running until installation completes. 133 | * @param[in] data pointer to a install_context struct. 134 | * @return NULL is always returned. 135 | */ 136 | static gpointer install_loop_thread(gpointer data) 137 | { 138 | GBusType bus_type = (!g_strcmp0(g_getenv("DBUS_STARTER_BUS_TYPE"), "session")) 139 | ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM; 140 | RInstaller *r_installer_proxy = NULL; 141 | g_autoptr(GError) error = NULL; 142 | g_auto(GVariantDict) args = G_VARIANT_DICT_INIT(NULL); 143 | struct install_context *context = NULL; 144 | 145 | g_return_val_if_fail(data, NULL); 146 | 147 | context = data; 148 | g_main_context_push_thread_default(context->loop_context); 149 | 150 | if (context->auth_header) { 151 | gchar *headers[2] = {NULL, NULL}; 152 | headers[0] = context->auth_header; 153 | g_variant_dict_insert(&args, "http-headers", "^as", headers); 154 | 155 | g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify); 156 | } 157 | 158 | g_debug("Creating RAUC DBUS proxy"); 159 | r_installer_proxy = r_installer_proxy_new_for_bus_sync( 160 | bus_type, G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, 161 | "de.pengutronix.rauc", "/", NULL, &error); 162 | if (!r_installer_proxy) { 163 | g_warning("Failed to create RAUC DBUS proxy: %s", error->message); 164 | goto notify_complete; 165 | } 166 | if (g_signal_connect(r_installer_proxy, "g-properties-changed", 167 | G_CALLBACK(on_installer_status), context) <= 0) { 168 | g_warning("Failed to connect properties-changed signal"); 169 | goto out_loop; 170 | } 171 | if (g_signal_connect(r_installer_proxy, "completed", 172 | G_CALLBACK(on_installer_completed), context) <= 0) { 173 | g_warning("Failed to connect completed signal"); 174 | goto out_loop; 175 | } 176 | 177 | g_debug("Trying to contact RAUC DBUS service"); 178 | if (!r_installer_call_install_bundle_sync(r_installer_proxy, context->bundle, 179 | g_variant_dict_end(&args), NULL, &error)) { 180 | g_warning("%s", error->message); 181 | goto out_loop; 182 | } 183 | 184 | g_main_loop_run(context->mainloop); 185 | 186 | out_loop: 187 | g_signal_handlers_disconnect_by_data(r_installer_proxy, context); 188 | 189 | notify_complete: 190 | // Notify the result of the RAUC installation 191 | if (context->notify_complete) 192 | context->notify_complete(context); 193 | 194 | g_clear_pointer(&r_installer_proxy, g_object_unref); 195 | g_main_context_pop_thread_default(context->loop_context); 196 | 197 | // on wait, calling function will take care of freeing after reading context->status_result 198 | if (!context->keep_install_context) 199 | install_context_free(context); 200 | return NULL; 201 | } 202 | 203 | gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ssl_verify, 204 | GSourceFunc on_install_notify, GSourceFunc on_install_complete, 205 | gboolean wait) 206 | { 207 | GMainContext *loop_context = NULL; 208 | struct install_context *context = NULL; 209 | 210 | g_return_val_if_fail(bundle, FALSE); 211 | 212 | loop_context = g_main_context_new(); 213 | context = install_context_new(); 214 | context->bundle = g_strdup(bundle); 215 | context->auth_header = g_strdup(auth_header); 216 | context->ssl_verify = ssl_verify; 217 | context->notify_event = on_install_notify; 218 | context->notify_complete = on_install_complete; 219 | context->mainloop = g_main_loop_new(loop_context, FALSE); 220 | context->loop_context = loop_context; 221 | context->status_result = 2; 222 | context->keep_install_context = wait; 223 | 224 | // unref/free previous install thread by joining it 225 | if (thread_install) 226 | g_thread_join(thread_install); 227 | 228 | // start install thread 229 | thread_install = g_thread_new("installer", install_loop_thread, (gpointer) context); 230 | if (wait) { 231 | gboolean result; 232 | 233 | g_thread_join(thread_install); 234 | result = context->status_result == 0; 235 | 236 | install_context_free(context); 237 | return result; 238 | } 239 | 240 | // return immediately if we did not wait for the install thread 241 | return TRUE; 242 | } 243 | -------------------------------------------------------------------------------- /src/rauc-installer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 92 | 93 | 94 | 95 | 96 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/sd-helper.c: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-License-Identifier: LGPL-2.1-only 3 | * SPDX-FileCopyrightText: 2018-2020 Lasse K. Mikkelsen , Prevas A/S (www.prevas.com) 4 | * 5 | * @file 6 | * @brief Systemd helper 7 | */ 8 | 9 | #include "sd-helper.h" 10 | 11 | /** 12 | * @brief Callback function: prepare GSource 13 | * 14 | * @param[in] source sd_event_source that should be prepared. 15 | * @param[in] timeout not used 16 | * @return gboolean, TRUE on success, FALSE otherwise 17 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 18 | */ 19 | static gboolean sd_source_prepare(GSource *source, gint *timeout) 20 | { 21 | g_return_val_if_fail(source, FALSE); 22 | 23 | return sd_event_prepare(((struct SDSource *) source)->event) > 0 ? TRUE : FALSE; 24 | } 25 | 26 | /** 27 | * @brief Callback function: check GSource 28 | * 29 | * @param[in] source sd_event_source that should be checked 30 | * @return gboolean, TRUE on success, FALSE otherwise 31 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 32 | */ 33 | static gboolean sd_source_check(GSource *source) 34 | { 35 | g_return_val_if_fail(source, FALSE); 36 | 37 | return sd_event_wait(((struct SDSource *) source)->event, 0) > 0 ? TRUE : FALSE; 38 | } 39 | 40 | /** 41 | * @brief Callback function: dispatch 42 | * 43 | * @param[in] source sd_event_source that should be dispatched 44 | * @param[in] callback not used 45 | * @param[in] userdata not used 46 | * @return gboolean, TRUE on success, FALSE otherwise 47 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 48 | */ 49 | static gboolean sd_source_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) 50 | { 51 | g_return_val_if_fail(source, FALSE); 52 | 53 | return sd_event_dispatch(((struct SDSource *) source)->event) >= 0 54 | ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE; 55 | } 56 | 57 | /** 58 | * @brief Callback function: finalize GSource 59 | * 60 | * @param[in] source sd_event_source that should be finalized 61 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GSource 62 | */ 63 | static void sd_source_finalize(GSource *source) 64 | { 65 | g_return_if_fail(source); 66 | 67 | sd_event_unref(((struct SDSource *) source)->event); 68 | } 69 | 70 | /** 71 | * @brief Callback function: when source exits 72 | * 73 | * @param[in] source sd_event_source that exits 74 | * @param[in] userdata the GMainLoop the source is attached to. 75 | * @return always return 0 76 | * @see https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html#GMainLoop 77 | */ 78 | static int sd_source_on_exit(sd_event_source *source, void *userdata) 79 | { 80 | g_return_val_if_fail(source, -1); 81 | g_return_val_if_fail(userdata, -1); 82 | 83 | g_main_loop_quit(userdata); 84 | 85 | sd_event_source_set_enabled(source, FALSE); 86 | sd_event_source_unref(source); 87 | 88 | return 0; 89 | } 90 | 91 | int sd_source_attach(GSource *source, GMainLoop *loop) 92 | { 93 | g_return_val_if_fail(source, -1); 94 | g_return_val_if_fail(loop, -1); 95 | 96 | g_source_set_name(source, "sd-event"); 97 | g_source_add_poll(source, &((struct SDSource *) source)->pollfd); 98 | g_source_attach(source, g_main_loop_get_context(loop)); 99 | 100 | return sd_event_add_exit(((struct SDSource *) source)->event, 101 | NULL, 102 | sd_source_on_exit, 103 | loop); 104 | } 105 | 106 | GSource * sd_source_new(sd_event *event) 107 | { 108 | static GSourceFuncs funcs = { 109 | sd_source_prepare, 110 | sd_source_check, 111 | sd_source_dispatch, 112 | sd_source_finalize, 113 | }; 114 | GSource *s = NULL; 115 | 116 | g_return_val_if_fail(event, NULL); 117 | 118 | s = g_source_new(&funcs, sizeof(struct SDSource)); 119 | if (s) { 120 | ((struct SDSource *) s)->event = sd_event_ref(event); 121 | ((struct SDSource *) s)->pollfd.fd = sd_event_get_fd(event); 122 | ((struct SDSource *) s)->pollfd.events = G_IO_IN | G_IO_HUP; 123 | } 124 | 125 | return s; 126 | } 127 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | attrs 3 | requests 4 | pydbus 5 | pygobject 6 | pexpect 7 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix 3 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | 5 | import os 6 | import sys 7 | from configparser import ConfigParser 8 | 9 | import pytest 10 | 11 | from hawkbit_mgmt import HawkbitMgmtTestClient, HawkbitError 12 | from helper import run_pexpect, available_port 13 | 14 | def pytest_addoption(parser): 15 | """Register custom argparse-style options.""" 16 | parser.addoption( 17 | '--hawkbit-instance', 18 | help='HOST:PORT of hawkBit instance to use (default: %(default)s)', 19 | default='localhost:8080') 20 | 21 | @pytest.fixture(autouse=True) 22 | def env_setup(monkeypatch): 23 | monkeypatch.setenv('PATH', f'{os.path.dirname(os.path.abspath(__file__))}/../build', 24 | prepend=os.pathsep) 25 | monkeypatch.setenv('DBUS_STARTER_BUS_TYPE', 'session') 26 | 27 | @pytest.fixture(scope='session') 28 | def hawkbit(pytestconfig): 29 | """Instance of HawkbitMgmtTestClient connecting to a hawkBit instance.""" 30 | from uuid import uuid4 31 | 32 | host, port = pytestconfig.option.hawkbit_instance.rsplit(':', 1) 33 | client = HawkbitMgmtTestClient(host, int(port)) 34 | 35 | client.set_config('pollingTime', '00:00:30') 36 | client.set_config('pollingOverdueTime', '00:03:00') 37 | client.set_config('authentication.targettoken.enabled', True) 38 | client.set_config('authentication.gatewaytoken.enabled', True) 39 | client.set_config('authentication.gatewaytoken.key', uuid4().hex) 40 | 41 | return client 42 | 43 | @pytest.fixture 44 | def hawkbit_target_added(hawkbit): 45 | """Creates a hawkBit target.""" 46 | target = hawkbit.add_target() 47 | yield target 48 | 49 | hawkbit.delete_target(target) 50 | 51 | @pytest.fixture 52 | def config(tmp_path, hawkbit, hawkbit_target_added): 53 | """ 54 | Creates a temporary rauc-hawkbit-updater configuration matching the hawkBit (target) 55 | configuration of the hawkbit and hawkbit_target_added fixtures. 56 | """ 57 | target = hawkbit.get_target() 58 | target_token = target.get('securityToken') 59 | target_name = target.get('name') 60 | bundle_location = tmp_path / 'bundle.raucb' 61 | 62 | hawkbit_config = ConfigParser() 63 | hawkbit_config['client'] = { 64 | 'hawkbit_server': f'{hawkbit.host}:{hawkbit.port}', 65 | 'ssl': 'false', 66 | 'ssl_verify': 'false', 67 | 'tenant_id': 'DEFAULT', 68 | 'target_name': target_name, 69 | 'auth_token': target_token, 70 | 'bundle_download_location': str(bundle_location), 71 | 'retry_wait': '60', 72 | 'connect_timeout': '20', 73 | 'timeout': '60', 74 | 'log_level': 'debug', 75 | } 76 | hawkbit_config['device'] = { 77 | 'product': 'Terminator', 78 | 'model': 'T-1000', 79 | 'serialnumber': '8922673153', 80 | 'hw_revision': '2', 81 | 'mac_address': 'ff:ff:ff:ff:ff:ff', 82 | } 83 | 84 | tmp_config = tmp_path / 'rauc-hawkbit-updater.conf' 85 | with tmp_config.open('w') as f: 86 | hawkbit_config.write(f) 87 | return tmp_config 88 | 89 | @pytest.fixture 90 | def adjust_config(config): 91 | """ 92 | Adjusts the rauc-hawkbit-updater configuration created by the config fixture by 93 | adding/overwriting or removing options. 94 | """ 95 | def _adjust_config(options={'client': {}}, remove={}, add_trailing_space=False): 96 | adjusted_config = ConfigParser() 97 | adjusted_config.read(config) 98 | 99 | # update 100 | for section, option in options.items(): 101 | for key, value in option.items(): 102 | adjusted_config.set(section, key, value) 103 | 104 | # remove 105 | for section, option in remove.items(): 106 | adjusted_config.remove_option(section, option) 107 | 108 | # add trailing space 109 | if add_trailing_space: 110 | for orig_section, orig_options in adjusted_config.items(): 111 | for orig_option in orig_options.items(): 112 | adjusted_config.set(orig_section, orig_option[0], orig_option[1] + ' ') 113 | 114 | with config.open('w') as f: 115 | adjusted_config.write(f) 116 | return config 117 | 118 | return _adjust_config 119 | 120 | @pytest.fixture(scope='session') 121 | def rauc_bundle(tmp_path_factory): 122 | """Creates a temporary 512 KB file to be used as a dummy RAUC bundle.""" 123 | bundle = tmp_path_factory.mktemp('data') / 'bundle.raucb' 124 | bundle.write_bytes(os.urandom(512)*1024) 125 | return str(bundle) 126 | 127 | @pytest.fixture 128 | def assign_bundle(hawkbit, hawkbit_target_added, rauc_bundle, tmp_path): 129 | """ 130 | Creates a softwaremodule containing the file from the rauc_bundle fixture as an artifact. 131 | Creates a distributionset from this softwaremodule. Assigns this distributionset to the target 132 | created by the hawkbit_target_added fixture. Returns the corresponding action ID of this 133 | assignment. 134 | """ 135 | swmodules = [] 136 | artifacts = [] 137 | distributionsets = [] 138 | actions = [] 139 | 140 | def _assign_bundle(swmodules_num=1, artifacts_num=1, params=None): 141 | for i in range(swmodules_num): 142 | swmodule_type = 'application' if swmodules_num > 1 else 'os' 143 | swmodules.append(hawkbit.add_softwaremodule(module_type=swmodule_type)) 144 | 145 | for k in range(artifacts_num): 146 | # hawkBit will reject files with the same name, so symlink to unique names 147 | symlink_dest = tmp_path / f'{os.path.basename(rauc_bundle)}_{k}' 148 | try: 149 | os.symlink(rauc_bundle, symlink_dest) 150 | except FileExistsError: 151 | pass 152 | 153 | artifacts.append(hawkbit.add_artifact(symlink_dest, swmodules[-1])) 154 | 155 | dist_type = 'app' if swmodules_num > 1 else 'os' 156 | distributionsets.append(hawkbit.add_distributionset(module_ids=swmodules, 157 | dist_type=dist_type)) 158 | actions.append(hawkbit.assign_target(distributionsets[-1], params=params)) 159 | 160 | return actions[-1] 161 | 162 | yield _assign_bundle 163 | 164 | for action in actions: 165 | try: 166 | hawkbit.cancel_action(action, hawkbit_target_added, force=True) 167 | except HawkbitError: 168 | pass 169 | 170 | for distributionset in distributionsets: 171 | hawkbit.delete_distributionset(distributionset) 172 | 173 | for swmodule in swmodules: 174 | for artifact in artifacts: 175 | try: 176 | hawkbit.delete_artifact(artifact, swmodule) 177 | except HawkbitError: # artifact does not necessarily belong to this swmodule 178 | pass 179 | 180 | hawkbit.delete_softwaremodule(swmodule) 181 | 182 | @pytest.fixture 183 | def bundle_assigned(assign_bundle): 184 | """ 185 | Creates a softwaremodule containing the file from the rauc_bundle fixture as an artifact. 186 | Creates a distributionset from this softwaremodule. Assigns this distributionset to the target 187 | created by the hawkbit_target_added fixture. Returns the corresponding action ID of this 188 | assignment. 189 | """ 190 | 191 | assign_bundle() 192 | 193 | @pytest.fixture 194 | def rauc_dbus_install_success(rauc_bundle): 195 | """ 196 | Creates a RAUC D-Bus dummy interface on the SessionBus mimicing a successful installation on 197 | InstallBundle(). 198 | """ 199 | import pexpect 200 | 201 | proc = run_pexpect(f'{sys.executable} -m rauc_dbus_dummy {rauc_bundle}', 202 | cwd=os.path.dirname(__file__)) 203 | proc.expect('Interface published') 204 | 205 | yield 206 | 207 | assert proc.isalive() 208 | assert proc.terminate(force=True) 209 | proc.expect(pexpect.EOF) 210 | 211 | @pytest.fixture 212 | def rauc_dbus_install_failure(rauc_bundle): 213 | """ 214 | Creates a RAUC D-Bus dummy interface on the SessionBus mimicing a failing installation on 215 | InstallBundle(). 216 | """ 217 | proc = run_pexpect(f'{sys.executable} -m rauc_dbus_dummy {rauc_bundle} --completed-code=1', 218 | cwd=os.path.dirname(__file__), timeout=None) 219 | proc.expect('Interface published') 220 | 221 | yield 222 | 223 | assert proc.isalive() 224 | assert proc.terminate(force=True) 225 | 226 | @pytest.fixture(scope='session') 227 | def nginx_config(tmp_path_factory): 228 | """ 229 | Creates a temporary nginx proxy configuration incorporating additional given options to the 230 | location section. 231 | """ 232 | config_template = """ 233 | daemon off; 234 | pid /tmp/hawkbit-nginx-{port}.pid; 235 | 236 | # non-fatal alert for /var/log/nginx/error.log will still be shown 237 | # https://trac.nginx.org/nginx/ticket/147 238 | error_log stderr notice; 239 | 240 | events {{ }} 241 | 242 | http {{ 243 | access_log /dev/null; 244 | 245 | server {{ 246 | listen 127.0.0.1:{port}; 247 | listen [::1]:{port}; 248 | 249 | location / {{ 250 | proxy_pass http://localhost:8080; 251 | {location_options} 252 | 253 | # use proxy URL in JSON responses 254 | sub_filter "localhost:$proxy_port/" "$host:$server_port/"; 255 | sub_filter "$host:$proxy_port/" "$host:$server_port/"; 256 | sub_filter_types application/json; 257 | sub_filter_once off; 258 | }} 259 | }} 260 | }}""" 261 | 262 | def _nginx_config(port, location_options): 263 | proxy_config = tmp_path_factory.mktemp('nginx') / 'nginx.conf' 264 | location_options = ( f'{key} {value};' for key, value in location_options.items()) 265 | proxy_config_str = config_template.format(port=port, location_options=" ".join(location_options)) 266 | proxy_config.write_text(proxy_config_str) 267 | return proxy_config 268 | 269 | return _nginx_config 270 | 271 | @pytest.fixture(scope='session') 272 | def nginx_proxy(nginx_config): 273 | """ 274 | Runs an nginx rate liming proxy, limiting download speeds to 70 KB/s. HTTP requests are 275 | forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the 276 | proxy is running on. This port can be set in the rauc-hawkbit-updater config to rate limit its 277 | HTTP requests. 278 | """ 279 | import pexpect 280 | 281 | procs = [] 282 | 283 | def _nginx_proxy(options): 284 | port = available_port() 285 | proxy_config = nginx_config(port, options) 286 | 287 | try: 288 | proc = run_pexpect(f'nginx -c {proxy_config} -p .', timeout=None) 289 | except (pexpect.exceptions.EOF, pexpect.exceptions.ExceptionPexpect): 290 | pytest.skip('nginx unavailable') 291 | 292 | try: 293 | proc.expect('start worker process ') 294 | except pexpect.exceptions.EOF: 295 | pytest.skip('nginx failed, use -s to see logs') 296 | 297 | procs.append(proc) 298 | 299 | return port 300 | 301 | yield _nginx_proxy 302 | 303 | for proc in procs: 304 | assert proc.isalive() 305 | proc.terminate(force=True) 306 | proc.expect(pexpect.EOF) 307 | 308 | @pytest.fixture(scope='session') 309 | def rate_limited_port(nginx_proxy): 310 | """ 311 | Runs an nginx rate liming proxy, limiting download speeds to 70 KB/s. HTTP requests are 312 | forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the 313 | proxy is running on. This port can be set in the rauc-hawkbit-updater config to rate limit its 314 | HTTP requests. 315 | """ 316 | def _rate_limited_port(rate): 317 | location_options = {'proxy_limit_rate': rate} 318 | return nginx_proxy(location_options) 319 | 320 | return _rate_limited_port 321 | 322 | @pytest.fixture(scope='session') 323 | def partial_download_port(nginx_proxy): 324 | """ 325 | Runs an nginx proxy, forcing partial downloads. HTTP requests are forwarded to port 8080 326 | (default port of the docker hawkBit instance). Returns the port the proxy is running on. This 327 | port can be set in the rauc-hawkbit-updater config to test partial downloads. 328 | """ 329 | location_options = { 330 | 'limit_rate_after': '200k', 331 | 'limit_rate': '70k', 332 | } 333 | return nginx_proxy(location_options) 334 | -------------------------------------------------------------------------------- /test/hawkbit_mgmt.py: -------------------------------------------------------------------------------- 1 | ../script/hawkbit_mgmt.py -------------------------------------------------------------------------------- /test/helper.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix 3 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | 5 | import os 6 | import subprocess 7 | import shlex 8 | import logging 9 | import socket 10 | from contextlib import closing 11 | 12 | 13 | class PExpectLogger: 14 | """ 15 | pexpect Logger, allows to use Python's logging stdlib. To be passed as pexpect 'logfile". 16 | Logs linewise to given logger at given level. 17 | """ 18 | def __init__(self, level=logging.INFO, logger=None): 19 | self.level = level 20 | self.data = b'' 21 | self.logger = logger or logging.getLogger() 22 | 23 | def write(self, data): 24 | self.data += data 25 | 26 | def flush(self): 27 | for line in self.data.splitlines(): 28 | self.logger.log(self.level, line.decode()) 29 | 30 | self.data = b'' 31 | 32 | def logger_from_command(command): 33 | """ 34 | Returns a logger named after the executable, or in case of a python executable, after the 35 | python module, 36 | """ 37 | cmd_parts = command.split() 38 | base_cmd = os.path.basename(cmd_parts[0]) 39 | try: 40 | if base_cmd.startswith('python') and cmd_parts[1] == '-m': 41 | base_cmd = command.split()[2] 42 | except IndexError: 43 | pass 44 | 45 | return logging.getLogger(base_cmd) 46 | 47 | def run_pexpect(command, *, timeout=30, cwd=None): 48 | """ 49 | Runs given command via pexpect with DBUS_STARTER_BUS_TYPE=session and PATH+=./build. Returns 50 | process handle immediately allowing further expect calls. Logs command and its 51 | stdout/stderr/exit code. 52 | """ 53 | import pexpect 54 | logger = logger_from_command(command) 55 | logger.info('running: %s', command) 56 | 57 | pexpect_log = PExpectLogger(logger=logger) 58 | return pexpect.spawn(command, timeout=timeout, cwd=cwd, logfile=pexpect_log) 59 | 60 | def run(command, *, timeout=30): 61 | """ 62 | Runs given command as subprocess with DBUS_STARTER_BUS_TYPE=session and PATH+=./build. Blocks 63 | until command terminates. Logs command and its stdout/stderr/exit code. 64 | Returns tuple (stdout, stderr, exit code). 65 | """ 66 | logger = logger_from_command(command) 67 | logger.info('running: %s', command) 68 | 69 | def stdout_print_helper(logger, prefix, stdout): 70 | if stdout is None: 71 | return 72 | 73 | for line in stdout.splitlines(): 74 | if line: 75 | logger.info(f'{prefix}: %s', line) 76 | 77 | try: 78 | proc = subprocess.run(shlex.split(command), capture_output=True, text=True, check=False, 79 | timeout=timeout) 80 | except subprocess.TimeoutExpired as e: 81 | stdout_print_helper(logger, "stdout", e.stdout) 82 | stdout_print_helper(logger, "stderr", e.stderr) 83 | raise 84 | 85 | stdout_print_helper(logger, "stdout", proc.stdout) 86 | stdout_print_helper(logger, "stderr", proc.stderr) 87 | 88 | logger.info('exitcode: %d', proc.returncode) 89 | 90 | return proc.stdout, proc.stderr, proc.returncode 91 | 92 | def available_port(): 93 | """Returns an available local port.""" 94 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 95 | sock.bind(('localhost', 0)) 96 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 97 | return sock.getsockname()[1] 98 | 99 | def timezone_offset_utc(date): 100 | utc_offset = int(date.astimezone().utcoffset().total_seconds()) 101 | 102 | utc_offset_hours, remainder = divmod(utc_offset, 60*60) 103 | utc_offset_minutes, remainder = divmod(remainder, 60) 104 | sign_offset = '+' if utc_offset >=0 else '-' 105 | 106 | if remainder != 0: 107 | raise Exception('UTC offset contains fraction of a minute') 108 | 109 | return f'{sign_offset}{utc_offset_hours:02}:{utc_offset_minutes:02}' 110 | -------------------------------------------------------------------------------- /test/rauc_dbus_dummy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: LGPL-2.1-only 3 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | 5 | import hashlib 6 | import time 7 | from pathlib import Path 8 | 9 | from gi.repository import GLib 10 | from pydbus.generic import signal 11 | import requests 12 | 13 | 14 | class Installer: 15 | """ 16 | D-Bus interface `de.pengutronix.rauc.Installer`, to be used with 17 | `pydbus.{SessionBus,SystemBus}.publish()` 18 | 19 | The interface is defined via the xml read in the dbus class property. 20 | The relevant D-Bus properties are implemented as Python's @property/@Progress.setter. 21 | The D-Bus methods are Python methods. 22 | 23 | See https://github.com/LEW21/pydbus/blob/master/doc/tutorial.rst#class-preparation 24 | """ 25 | dbus = Path('../src/rauc-installer.xml').read_text() 26 | interface = 'de.pengutronix.rauc.Installer' 27 | 28 | Completed = signal() 29 | PropertiesChanged = signal() 30 | 31 | def __init__(self, bundle, completed_code=0): 32 | self._bundle = bundle 33 | self._completed_code = completed_code 34 | 35 | self._operation = 'idle' 36 | self._last_error = '' 37 | self._progress = 0, '', 1 38 | 39 | def InstallBundle(self, source, args): 40 | def mimic_install(): 41 | """Mimics a sucessful/failing installation, depending on `self._completed_code`.""" 42 | progresses = [ 43 | 'Installing', 44 | 'Determining slot states', 45 | 'Determining slot states done.', 46 | 'Checking bundle', 47 | 'Verifying signature', 48 | 'Verifying signature done.', 49 | 'Checking bundle done.', 50 | 'Loading manifest file', 51 | 'Loading manifest file done.', 52 | 'Determining target install group', 53 | 'Determining target install group done.', 54 | 'Updating slots', 55 | 'Checking slot rootfs.1', 56 | 'Checking slot rootfs.1 done.', 57 | 'Copying image to rootfs.1', 58 | 'Copying image to rootfs.1 done.', 59 | 'Updating slots done.', 60 | 'Install failed.' if self._completed_code else 'Installing done.', 61 | ] 62 | 63 | self.Operation = 'installing' 64 | 65 | for i, progress in enumerate(progresses): 66 | percentage = (i+1)*100 / len(progresses) 67 | self.Progress = percentage, progress, 1 68 | time.sleep(0.1) 69 | 70 | self.Completed(self._completed_code) 71 | 72 | if not self._completed_code: 73 | self.LastError = 'Installation error' 74 | 75 | self.Operation = 'idle' 76 | 77 | # do not call again 78 | return False 79 | 80 | print(f'installing {source}') 81 | try: 82 | self._check_install_requirements(source, args) 83 | except Exception as e: 84 | self.Completed(1) 85 | self.LastError = f'Installation error: {e}' 86 | self.Operation = 'idle' 87 | raise 88 | 89 | GLib.timeout_add_seconds(interval=1, function=mimic_install) 90 | 91 | @staticmethod 92 | def _get_bundle_sha1(bundle): 93 | """Calculates the SHA1 checksum of `self._bundle`.""" 94 | sha1 = hashlib.sha1() 95 | 96 | with open(bundle, 'rb') as f: 97 | while True: 98 | chunk = f.read(sha1.block_size) 99 | if not chunk: 100 | break 101 | sha1.update(chunk) 102 | 103 | return sha1.hexdigest() 104 | 105 | @staticmethod 106 | def _get_http_bundle_sha1(url, auth_header): 107 | """Download file from URL using HTTP range requests and compute its sha1 checksum.""" 108 | sha1 = hashlib.sha1() 109 | headers = auth_header 110 | range_size = 128 * 1024 # default squashfs block size 111 | 112 | offset = 0 113 | while True: 114 | headers['Range'] = f'bytes={offset}-{offset + range_size - 1}' 115 | r = requests.get(url, headers=headers) 116 | try: 117 | r.raise_for_status() 118 | sha1.update(r.content) 119 | except requests.HTTPError: 120 | if r.status_code == 416: # range not satisfiable, assuming download completed 121 | break 122 | raise 123 | 124 | offset += range_size 125 | 126 | return sha1.hexdigest() 127 | 128 | def _check_install_requirements(self, source, args): 129 | """ 130 | Check that required headers are set, bundle is accessible (HTTP or locally) and its 131 | checksum matches. 132 | """ 133 | if 'http-headers' in args: 134 | assert len(args['http-headers']) == 1 135 | 136 | [auth_header] = args['http-headers'] 137 | key, value = auth_header.split(': ', maxsplit=1) 138 | http_bundle_sha1 = self._get_http_bundle_sha1(source, {key: value}) 139 | assert http_bundle_sha1 == self._get_bundle_sha1(self._bundle) 140 | 141 | # assume ssl_verify=false is set in test setup 142 | assert args['tls-no-verify'] is True 143 | 144 | else: 145 | # check bundle checksum matches expected checksum 146 | assert self._get_bundle_sha1(source) == self._get_bundle_sha1(self._bundle) 147 | 148 | @property 149 | def Operation(self): 150 | return self._operation 151 | 152 | @Operation.setter 153 | def Operation(self, value): 154 | self._operation = value 155 | self.PropertiesChanged(Installer.interface, {'Operation': self.Operation}, []) 156 | 157 | @property 158 | def Progress(self): 159 | return self._progress 160 | 161 | @Progress.setter 162 | def Progress(self, value): 163 | self._progress = value 164 | self.PropertiesChanged(Installer.interface, {'Progress': self.Progress}, []) 165 | 166 | @property 167 | def LastError(self): 168 | return self._last_error 169 | 170 | @LastError.setter 171 | def LastError(self, value): 172 | self._last_error = value 173 | self.PropertiesChanged(Installer.interface, {'LastError': self.LastError}, []) 174 | 175 | @property 176 | def Compatible(self): 177 | return "not implemented" 178 | 179 | @property 180 | def Variant(self): 181 | return "not implemented" 182 | 183 | @property 184 | def BootSlot(self): 185 | return "not implemented" 186 | 187 | 188 | if __name__ == '__main__': 189 | import argparse 190 | from pydbus import SessionBus 191 | 192 | parser = argparse.ArgumentParser() 193 | parser.add_argument('bundle', help='Expected RAUC bundle') 194 | parser.add_argument('--completed-code', type=int, default=0, 195 | help='Code to emit as D-Bus Completed signal') 196 | args = parser.parse_args() 197 | 198 | loop = GLib.MainLoop() 199 | bus = SessionBus() 200 | installer = Installer(args.bundle, args.completed_code) 201 | with bus.publish('de.pengutronix.rauc', ('/', installer)): 202 | print('Interface published') 203 | loop.run() 204 | -------------------------------------------------------------------------------- /test/test_basics.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021 Enrico Jörns , Pengutronix 3 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 4 | 5 | import re 6 | from configparser import ConfigParser 7 | 8 | import pytest 9 | 10 | from helper import run 11 | 12 | def test_version(): 13 | """Test version argument.""" 14 | out, err, exitcode = run('rauc-hawkbit-updater -v') 15 | 16 | assert exitcode == 0 17 | assert out.startswith('Version ') 18 | assert err == '' 19 | 20 | def test_invalid_arg(): 21 | """Test invalid argument.""" 22 | out, err, exitcode = run('rauc-hawkbit-updater --invalidarg') 23 | 24 | assert exitcode == 1 25 | assert out == '' 26 | assert err.strip() == 'option parsing failed: Unknown option --invalidarg' 27 | 28 | def test_config_unspecified(): 29 | """Test call without config argument.""" 30 | out, err, exitcode = run('rauc-hawkbit-updater') 31 | 32 | assert exitcode == 2 33 | assert out == '' 34 | assert err.strip() == 'No configuration file given' 35 | 36 | def test_config_file_non_existent(): 37 | """Test call with inexistent config file.""" 38 | out, err, exitcode = run('rauc-hawkbit-updater -c does-not-exist.conf') 39 | 40 | assert exitcode == 3 41 | assert out == '' 42 | assert err.strip() == 'No such configuration file: does-not-exist.conf' 43 | 44 | def test_config_no_auth_token(adjust_config): 45 | """Test config without auth_token option in client section.""" 46 | config = adjust_config(remove={'client': 'auth_token'}) 47 | 48 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 49 | 50 | assert exitcode == 4 51 | assert out == '' 52 | assert err.strip() == \ 53 | "Loading config file failed: Neither 'auth_token' nor 'gateway_token' set" 54 | 55 | def test_config_multiple_auth_methods(adjust_config): 56 | """Test config with auth_token and gateway_token options in client section.""" 57 | config = adjust_config({'client': {'gateway_token': 'wrong-gateway-token'}}) 58 | 59 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 60 | 61 | assert exitcode == 4 62 | assert out == '' 63 | assert err.strip() == \ 64 | "Loading config file failed: Both 'auth_token' and 'gateway_token' set" 65 | 66 | def test_register_and_check_invalid_gateway_token(adjust_config): 67 | """Test config with invalid gateway_token.""" 68 | config = adjust_config( 69 | {'client': {'gateway_token': 'wrong-gateway-token'}}, 70 | remove={'client': 'auth_token'} 71 | ) 72 | 73 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 74 | 75 | assert exitcode == 1 76 | assert 'MESSAGE: Checking for new software...' in out 77 | assert err.strip() == 'WARNING: Failed to authenticate. Check if gateway_token is correct?' 78 | 79 | @pytest.mark.parametrize("trailing_space", ('no_trailing_space', 'trailing_space')) 80 | def test_register_and_check_valid_gateway_token(hawkbit, adjust_config, trailing_space): 81 | """Test config with valid gateway_token.""" 82 | gateway_token = hawkbit.get_config('authentication.gatewaytoken.key') 83 | config = adjust_config( 84 | {'client': {'gateway_token': gateway_token}}, 85 | remove={'client': 'auth_token'}, 86 | add_trailing_space=(trailing_space == 'trailing_space'), 87 | ) 88 | 89 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 90 | 91 | assert exitcode == 0 92 | assert 'MESSAGE: Checking for new software...' in out 93 | assert err == '' 94 | 95 | def test_register_and_check_invalid_auth_token(adjust_config): 96 | """Test config with invalid auth_token.""" 97 | config = adjust_config({'client': {'auth_token': 'wrong-auth-token'}}) 98 | 99 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 100 | 101 | assert exitcode == 1 102 | assert 'MESSAGE: Checking for new software...' in out 103 | assert err.strip() == 'WARNING: Failed to authenticate. Check if auth_token is correct?' 104 | 105 | @pytest.mark.parametrize("trailing_space", ('no_trailing_space', 'trailing_space')) 106 | def test_register_and_check_valid_auth_token(adjust_config, trailing_space): 107 | """Test config with valid auth_token.""" 108 | config = adjust_config( 109 | add_trailing_space=(trailing_space == 'trailing_space'), 110 | ) 111 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 112 | 113 | assert exitcode == 0 114 | assert 'MESSAGE: Checking for new software...' in out 115 | assert err == '' 116 | 117 | def test_register_and_check_no_download_location_no_streaming(adjust_config): 118 | """Test config without bundle_download_location and without stream_bundle.""" 119 | config = adjust_config(remove={'client': 'bundle_download_location'}) 120 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 121 | 122 | assert exitcode == 4 123 | assert out == '' 124 | assert err.strip() == \ 125 | "Loading config file failed: 'bundle_download_location' is required if 'stream_bundle' is disabled" 126 | 127 | def test_identify(hawkbit, config): 128 | """ 129 | Test that supplying target meta information works and information are received correctly by 130 | hawkBit. 131 | """ 132 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 133 | 134 | assert exitcode == 0 135 | assert 'Providing meta information to hawkbit server' in out 136 | assert err == '' 137 | 138 | ref_config = ConfigParser() 139 | ref_config.read(config) 140 | 141 | assert dict(ref_config.items('device')) == hawkbit.get_attributes() 142 | 143 | @pytest.mark.parametrize("multi_object", ('chunks', 'artifacts')) 144 | def test_unsupported_multi_objects(hawkbit, config, assign_bundle, multi_object): 145 | """ 146 | Test that deployments with multiple software modules (called chunks in the DDI API) or multiple 147 | artifacts are rejected. 148 | """ 149 | expected_error = rf'Deployment \d*? unsupported: cannot handle multiple {multi_object}.' 150 | 151 | if multi_object == 'chunks': 152 | assign_param = {'swmodules_num': 2} 153 | elif multi_object == 'artifacts': 154 | assign_param = {'artifacts_num': 2} 155 | 156 | assign_bundle(**assign_param) 157 | 158 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 159 | 160 | assert exitcode == 1 161 | assert re.fullmatch(f'(WARNING: {expected_error}\n){{2}}', err) 162 | 163 | status = hawkbit.get_action_status() 164 | assert status[0]['type'] == 'error' 165 | assert re.fullmatch(expected_error, status[0]['messages'][0]) 166 | -------------------------------------------------------------------------------- /test/test_cancel.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 3 | 4 | from pexpect import TIMEOUT, EOF 5 | import pytest 6 | 7 | from helper import run, run_pexpect 8 | 9 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 10 | def test_cancel_before_poll(hawkbit, adjust_config, bundle_assigned, rauc_dbus_install_success, 11 | mode): 12 | """ 13 | Assign distribution containing bundle to target and cancel it right away. Then run 14 | rauc-hawkbit-updater and make sure it acknowledges the not yet processed action. 15 | """ 16 | hawkbit.cancel_action() 17 | 18 | config_params = {'client': {'stream_bundle': 'true'}} if mode == 'streaming' else {} 19 | config = adjust_config(config_params) 20 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 21 | 22 | assert f'Received cancelation for unprocessed action {hawkbit.id["action"]}, acknowledging.' \ 23 | in out 24 | assert 'Action canceled.' in out 25 | assert err == '' 26 | assert exitcode == 0 27 | 28 | cancel = hawkbit.get_action() 29 | assert cancel['type'] == 'cancel' 30 | assert cancel['status'] == 'finished' 31 | 32 | cancel_status = hawkbit.get_action_status() 33 | assert cancel_status[0]['type'] == 'canceled' 34 | assert 'Action canceled.' in cancel_status[0]['messages'] 35 | 36 | def test_cancel_during_download(hawkbit, adjust_config, bundle_assigned, rate_limited_port): 37 | """ 38 | Assign distribution containing bundle to target. Run rauc-hawkbit-updater configured to 39 | comminucate via rate-limited proxy with hawkBit. Cancel the assignment once the download 40 | started and make sure the cancelation is acknowledged and no installation is started. 41 | """ 42 | port = rate_limited_port('70k') 43 | 44 | config_params = {'client': {'hawkbit_server': f'{hawkbit.host}:{port}'}} 45 | config = adjust_config(config_params) 46 | 47 | # note: we cannot use -r here since that prevents further polling of the base resource 48 | # announcing the cancelation 49 | proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') 50 | proc.expect('Start downloading: ') 51 | proc.expect(TIMEOUT, timeout=1) 52 | 53 | # assuming: 54 | # - rauc-hawkbit-updater polls base resource every 5 s for cancelations during download 55 | # - download of 512 KB bundle @ 70 KB/s takes ~7.3 s 56 | # -> cancelation should be received and processed before download finishes 57 | hawkbit.cancel_action() 58 | 59 | # do not wait longer than 5 s (poll interval) + 3 s (processing margin) 60 | proc.expect(f'Received cancelation for action {hawkbit.id["action"]}', timeout=8) 61 | proc.expect('Action canceled.') 62 | # wait for feedback to arrive at hawkbit server 63 | proc.expect(TIMEOUT, timeout=2) 64 | proc.terminate(force=True) 65 | proc.expect(EOF) 66 | 67 | cancel = hawkbit.get_action() 68 | assert cancel['type'] == 'cancel' 69 | assert cancel['status'] == 'finished' 70 | 71 | cancel_status = hawkbit.get_action_status() 72 | assert cancel_status[0]['type'] == 'canceled' 73 | assert 'Action canceled.' in cancel_status[0]['messages'] 74 | 75 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 76 | def test_cancel_during_install(hawkbit, adjust_config, bundle_assigned, rauc_dbus_install_success, 77 | mode): 78 | """ 79 | Assign distribution containing bundle to target. Run rauc-hawkbit-updater and cancel the 80 | assignment once the installation started. Make sure the cancelation does not disrupt the 81 | installation. 82 | """ 83 | config_params = {'client': {'stream_bundle': 'true'}} if mode == 'streaming' else {} 84 | config = adjust_config(config_params) 85 | 86 | proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') 87 | proc.expect('MESSAGE: Installing: ') 88 | 89 | hawkbit.cancel_action() 90 | 91 | # wait for installation to finish 92 | proc.expect('Software bundle installed successfully.') 93 | # wait for feedback to arrive at hawkbit server 94 | proc.expect(TIMEOUT, timeout=2) 95 | proc.terminate(force=True) 96 | proc.expect(EOF) 97 | 98 | cancel = hawkbit.get_action() 99 | assert cancel['type'] == 'update' 100 | assert cancel['status'] == 'finished' 101 | 102 | cancel_status = hawkbit.get_action_status() 103 | assert cancel_status[0]['type'] == 'finished' 104 | assert 'Software bundle installed successfully.' in cancel_status[0]['messages'] 105 | -------------------------------------------------------------------------------- /test/test_download.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix 3 | 4 | import re 5 | 6 | from helper import run 7 | 8 | def test_download_inexistent_location(hawkbit, bundle_assigned, adjust_config): 9 | """ 10 | Assign bundle to target and test download to an inexistent location specified in config. 11 | """ 12 | location = '/tmp/does_not_exist/foo' 13 | config = adjust_config( 14 | {'client': {'bundle_download_location': location}} 15 | ) 16 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 17 | 18 | assert 'New software ready for download' in out 19 | # same warning from feedback() and from hawkbit_pull_cb() 20 | assert err == \ 21 | f'WARNING: Failed to calculate free space for {location}: No such file or directory\n'*2 22 | assert exitcode == 1 23 | 24 | status = hawkbit.get_action_status() 25 | assert status[0]['type'] == 'error' 26 | assert f'Failed to calculate free space for {location}: No such file or directory' in \ 27 | status[0]['messages'] 28 | 29 | def test_download_unallowed_location(hawkbit, bundle_assigned, adjust_config): 30 | """ 31 | Assign bundle to target and test download to an unallowed location specified in config. 32 | """ 33 | location = '/root/foo' 34 | config = adjust_config( 35 | {'client': {'bundle_download_location': location}} 36 | ) 37 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 38 | 39 | assert 'Start downloading' in out 40 | assert err.strip() == \ 41 | f'WARNING: Download failed: Failed to open {location} for download: Permission denied' 42 | assert exitcode == 1 43 | 44 | status = hawkbit.get_action_status() 45 | assert status[0]['type'] == 'error' 46 | assert f'Download failed: Failed to open {location} for download: Permission denied' in \ 47 | status[0]['messages'] 48 | 49 | def test_download_too_slow(hawkbit, bundle_assigned, adjust_config, rate_limited_port): 50 | """Assign bundle to target and test too slow download of bundle.""" 51 | # limit to 50 bytes/s 52 | port = rate_limited_port(50) 53 | config = adjust_config({ 54 | 'client': { 55 | 'hawkbit_server': f'{hawkbit.host}:{port}', 56 | 'low_speed_time': '3', 57 | 'low_speed_rate': '100', 58 | } 59 | }) 60 | 61 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r', timeout=90) 62 | 63 | assert 'Start downloading: ' in out 64 | assert err.strip() == 'WARNING: Download failed: Timeout was reached' 65 | assert exitcode == 1 66 | 67 | def test_download_partials_without_resume(hawkbit, bundle_assigned, adjust_config, 68 | partial_download_port): 69 | """ 70 | Assign bundle to target and test download of partial bundle parts without having 71 | download resuming configured. 72 | """ 73 | config = adjust_config( 74 | {'client': {'hawkbit_server': f'{hawkbit.host}:{partial_download_port}'}} 75 | ) 76 | 77 | # ignore failing installation 78 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 79 | 80 | assert 'Start downloading: ' in out 81 | assert err.strip() == 'WARNING: Download failed: Transferred a partial file' 82 | assert exitcode == 1 83 | 84 | def test_download_partials_with_resume(hawkbit, bundle_assigned, adjust_config, 85 | partial_download_port): 86 | """ 87 | Assign bundle to target and test download of partial bundle parts with download resuming 88 | configured. 89 | """ 90 | config = adjust_config({ 91 | 'client': { 92 | 'hawkbit_server': f'{hawkbit.host}:{partial_download_port}', 93 | 'resume_downloads': 'true', 94 | } 95 | }) 96 | 97 | # ignore failing installation 98 | out, _, _ = run(f'rauc-hawkbit-updater -c "{config}" -r') 99 | 100 | assert re.findall('Resuming download from offset [1-9]', out) 101 | assert 'Download complete.' in out 102 | assert 'File checksum OK.' in out 103 | 104 | def test_download_slow_with_resume(hawkbit, bundle_assigned, adjust_config, rate_limited_port): 105 | """ 106 | Assign bundle to target and test slow download of bundle with download resuming enabled. That 107 | should lead to resuming downloads. 108 | """ 109 | port = rate_limited_port(50000) 110 | config = adjust_config({ 111 | 'client': { 112 | 'hawkbit_server': f'{hawkbit.host}:{port}', 113 | 'resume_downloads': 'true', 114 | 'low_speed_time': '1', 115 | 'low_speed_rate': '100000', 116 | } 117 | }) 118 | 119 | # ignore failing installation 120 | out, _, _ = run(f'rauc-hawkbit-updater -c "{config}" -r') 121 | 122 | assert 'Timeout was reached, resuming download..' in out 123 | assert 'Resuming download from offset' in out 124 | assert 'Download complete.' in out 125 | assert 'File checksum OK.' in out 126 | 127 | def test_download_only(hawkbit, config, assign_bundle): 128 | """Test "downloadonly" deployment.""" 129 | assign_bundle(params={'type': 'downloadonly'}) 130 | 131 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 132 | assert 'Start downloading' in out 133 | assert 'hawkBit requested to skip installation, not invoking RAUC yet.' in out 134 | assert 'Download complete' in out 135 | assert 'File checksum OK' in out 136 | assert err == '' 137 | assert exitcode == 0 138 | 139 | status = hawkbit.get_action_status() 140 | assert status[0]['type'] == 'downloaded' 141 | 142 | # check last status message 143 | assert 'File checksum OK.' in status[0]['messages'] 144 | -------------------------------------------------------------------------------- /test/test_install.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-2.1-only 2 | # SPDX-FileCopyrightText: 2021-2022 Bastian Krause , Pengutronix 3 | 4 | from datetime import datetime, timedelta 5 | from pathlib import Path 6 | 7 | from pexpect import TIMEOUT, EOF 8 | import pytest 9 | 10 | from helper import run, run_pexpect, timezone_offset_utc 11 | 12 | @pytest.fixture 13 | def install_config(config, adjust_config): 14 | def _install_config(mode): 15 | if mode == 'streaming': 16 | return adjust_config( 17 | {'client': {'stream_bundle': 'true'}}, 18 | remove={'client': 'bundle_download_location'}, 19 | ) 20 | return config 21 | 22 | return _install_config 23 | 24 | 25 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 26 | def test_install_bundle_no_dbus_iface(hawkbit, install_config, bundle_assigned, mode): 27 | """Assign bundle to target and test installation without RAUC D-Bus interface available.""" 28 | config = install_config(mode) 29 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 30 | 31 | err_lines = err.splitlines() 32 | 33 | assert 'New software ready for download' in out 34 | 35 | if mode == 'download': 36 | assert 'Download complete' in out 37 | 38 | assert err_lines.pop(0) == \ 39 | 'WARNING: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name de.pengutronix.rauc was not provided by any .service files' 40 | assert err_lines.pop(0) == 'WARNING: Failed to install software bundle.' 41 | 42 | if mode == 'streaming': 43 | assert err_lines.pop(0) == 'WARNING: Streaming installation failed' 44 | 45 | assert not err_lines 46 | assert exitcode == 1 47 | 48 | status = hawkbit.get_action_status() 49 | assert status[0]['type'] == 'error' 50 | 51 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 52 | def test_install_success(hawkbit, install_config, bundle_assigned, rauc_dbus_install_success, mode): 53 | """ 54 | Assign bundle to target and test successful download and installation. Make sure installation 55 | result is received correctly by hawkBit. 56 | """ 57 | config = install_config(mode) 58 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 59 | 60 | assert 'New software ready for download' in out 61 | 62 | if mode == 'download': 63 | assert 'Download complete' in out 64 | 65 | assert 'Software bundle installed successfully.' in out 66 | assert err == '' 67 | assert exitcode == 0 68 | 69 | status = hawkbit.get_action_status() 70 | assert status[0]['type'] == 'finished' 71 | 72 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 73 | def test_install_failure(hawkbit, install_config, bundle_assigned, rauc_dbus_install_failure, mode): 74 | """ 75 | Assign bundle to target and test successful download and failing installation. Make sure 76 | installation result is received correctly by hawkBit. 77 | """ 78 | config = install_config(mode) 79 | out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') 80 | 81 | assert 'New software ready for download' in out 82 | assert 'WARNING: Failed to install software bundle.' in err 83 | assert exitcode == 1 84 | 85 | status = hawkbit.get_action_status() 86 | assert status[0]['type'] == 'error' 87 | assert 'Failed to install software bundle.' in status[0]['messages'] 88 | 89 | @pytest.mark.parametrize('mode', ('download', 'streaming')) 90 | def test_install_maintenance_window(hawkbit, install_config, rauc_bundle, assign_bundle, 91 | rauc_dbus_install_success, mode): 92 | bundle_size = Path(rauc_bundle).stat().st_size 93 | maintenance_start = datetime.now() + timedelta(seconds=15) 94 | maintenance_window = { 95 | 'maintenanceWindow': { 96 | 'schedule' : maintenance_start.strftime('%-S %-M %-H ? %-m * %-Y'), 97 | 'timezone' : timezone_offset_utc(maintenance_start), 98 | 'duration' : '00:01:00' 99 | } 100 | } 101 | assign_bundle(params=maintenance_window) 102 | 103 | config = install_config(mode) 104 | proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') 105 | proc.expect(r"hawkBit requested to skip installation, not invoking RAUC yet \(maintenance window is 'unavailable'\)") 106 | 107 | if mode == 'download': 108 | proc.expect('Start downloading') 109 | proc.expect('Download complete') 110 | proc.expect('File checksum OK') 111 | 112 | # wait for the maintenance window to become available and the next poll of the base resource 113 | proc.expect(TIMEOUT, timeout=30) 114 | proc.expect(r"Continuing scheduled deployment .* \(maintenance window is 'available'\)") 115 | # RAUC bundle should have been already downloaded completely 116 | if mode == 'download': 117 | proc.expect(f'Resuming download from offset {bundle_size}') 118 | proc.expect('Download complete') 119 | proc.expect('File checksum OK') 120 | 121 | proc.expect('Software bundle installed successfully') 122 | 123 | # let feedback propagate to hawkBit before termination 124 | proc.expect(TIMEOUT, timeout=2) 125 | proc.terminate(force=True) 126 | proc.expect(EOF) 127 | 128 | status = hawkbit.get_action_status() 129 | assert status[0]['type'] == 'finished' 130 | -------------------------------------------------------------------------------- /test/wait-for-hawkbit-online: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Loops with 5 second intervals to Wait for hawkbit server to be ready to 4 | # accept connections. 5 | # This should be run once before continuing with tests and scripts that 6 | # interact with hawkbit. 7 | 8 | printf "Waiting for hawkbit to come up " 9 | cycles=0 10 | until $(curl --output /dev/null --silent --head --fail http://localhost:8080); do 11 | printf '.' 12 | [ $((cycles++)) -gt 10 ] && printf " failed\n" && exit 1 13 | sleep 5 14 | done 15 | 16 | printf '\n' 17 | -------------------------------------------------------------------------------- /uncrustify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | cd `dirname $0` 6 | 7 | if [ ! -e .uncrustify/build/uncrustify ]; then 8 | ./build-uncrustify.sh 9 | fi 10 | 11 | .uncrustify/build/uncrustify -c .uncrustify.cfg -l C --replace src/*.c include/*.h 12 | --------------------------------------------------------------------------------