From e7448206058d4b72630643296118312b1e963277 Mon Sep 17 00:00:00 2001 From: Bastian Krause Date: Wed, 11 Aug 2021 16:21:21 +0200 Subject: [PATCH 1/5] hawkbit-client: handle deployment's download attribute skip case Although the deployment's ``deployment.download=skip`` attribute is currently never passed from hawkBit, it is part of the DDI API. So implement it by simply skipping the whole deployment when this attribute is received. Signed-off-by: Bastian Krause --- src/hawkbit-client.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/hawkbit-client.c b/src/hawkbit-client.c index 457fd3b7..0f51941d 100644 --- a/src/hawkbit-client.c +++ b/src/hawkbit-client.c @@ -946,7 +946,8 @@ static gpointer download_thread(gpointer data) static gboolean process_deployment(JsonNode *req_root, GError **error) { g_autoptr(Artifact) artifact = NULL; - g_autofree gchar *deployment = NULL, *feedback_url = NULL; + g_autofree gchar *deployment = NULL, *feedback_url = NULL, *deployment_download = NULL, + *maintenance_window = NULL, *maintenance_msg = NULL; g_autoptr(JsonParser) json_response_parser = NULL; g_autoptr(JsonArray) json_chunks = NULL, json_artifacts = NULL; JsonNode *resp_root = NULL, *json_chunk = NULL, *json_artifact = NULL; @@ -976,6 +977,24 @@ static gboolean process_deployment(JsonNode *req_root, GError **error) resp_root = json_parser_get_root(json_response_parser); + // handle deployment.maintenanceWindow (only available if maintenance window is defined) + maintenance_window = json_get_string(resp_root, "$.deployment.maintenanceWindow", NULL); + maintenance_msg = maintenance_window + ? g_strdup_printf(" (maintenance window is '%s')", maintenance_window) + : g_strdup(""); + + // handle deployment.download=skip + deployment_download = json_get_string(resp_root, "$.deployment.download", error); + if (!deployment_download) + goto error; + + if (!g_strcmp0(deployment_download, "skip")) { + g_message("hawkBit requested to skip download, not downloading yet%s.", + maintenance_msg); + active_action->state = ACTION_STATE_NONE; + return TRUE; + } + // remember deployment's action id g_free(active_action->id); active_action->id = json_get_string(resp_root, "$.id", error); From e58c4c6b8631e4106bd1ee143e4435619471b9f1 Mon Sep 17 00:00:00 2001 From: Bastian Krause Date: Wed, 11 Aug 2021 16:23:09 +0200 Subject: [PATCH 2/5] hawkbit-client: handle deployment's update attribute skip case If a maintenance window is specifed for a deployment, hawkBit will first signal a new deployment with ``deployment.update=skip``. The target is then expected to download the artifact, but skip the actual update for now. When the maintenance window is "available", ``deployment.update={soft,forced}`` is signalled and the target is expected to update. To add support for this, store whether to install the update now or later in a new Artifact member ``do_install`` and act accordingly. To keep things simple, follow the usual code flow for delayed bundle installation and call get_binary(). It will notice that a bundle exists, do a range request to make sure it is complete and install it subsequently. Signed-off-by: Bastian Krause --- include/hawkbit-client.h | 1 + src/hawkbit-client.c | 41 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/include/hawkbit-client.h b/include/hawkbit-client.h index 09721818..c2de550f 100644 --- a/include/hawkbit-client.h +++ b/include/hawkbit-client.h @@ -89,6 +89,7 @@ typedef struct Artifact_ { gchar *download_url; /**< download URL of software bundle file */ gchar *feedback_url; /**< URL status feedback should be sent to */ gchar *sha1; /**< sha1 checksum of software bundle file */ + gboolean do_install; /**< whether the installation should be started or not */ } Artifact; /** diff --git a/src/hawkbit-client.c b/src/hawkbit-client.c index 0f51941d..f7a88861 100644 --- a/src/hawkbit-client.c +++ b/src/hawkbit-client.c @@ -906,6 +906,14 @@ static gpointer download_thread(gpointer data) if (active_action->state == ACTION_STATE_CANCEL_REQUESTED) goto cancel; + // skip installation if hawkBit asked us to do so + if (!artifact->do_install) { + active_action->state = ACTION_STATE_NONE; + g_mutex_unlock(&active_action->mutex); + + return GINT_TO_POINTER(TRUE); + } + // start installation, cancelations are impossible now active_action->state = ACTION_STATE_INSTALLING; g_cond_signal(&active_action->cond); @@ -945,8 +953,9 @@ static gpointer download_thread(gpointer data) */ static gboolean process_deployment(JsonNode *req_root, GError **error) { - g_autoptr(Artifact) artifact = NULL; - g_autofree gchar *deployment = NULL, *feedback_url = NULL, *deployment_download = NULL, + g_autoptr(Artifact) artifact = g_new0(Artifact, 1); + g_autofree gchar *deployment = NULL, *feedback_url = NULL, *temp_id = NULL, + *deployment_download = NULL, *deployment_update = NULL, *maintenance_window = NULL, *maintenance_msg = NULL; g_autoptr(JsonParser) json_response_parser = NULL; g_autoptr(JsonArray) json_chunks = NULL, json_artifacts = NULL; @@ -995,9 +1004,34 @@ static gboolean process_deployment(JsonNode *req_root, GError **error) return TRUE; } + // handle deployment.update=skip + deployment_update = json_get_string(resp_root, "$.deployment.update", error); + if (!deployment_update) + goto error; + + artifact->do_install = g_strcmp0(deployment_update, "skip") != 0; + if (!artifact->do_install) + g_message("hawkBit requested to skip installation, not invoking RAUC yet%s.", + maintenance_msg); + // remember deployment's action id + temp_id = json_get_string(resp_root, "$.id", error); + + if (!artifact->do_install && !g_strcmp0(temp_id, active_action->id)) { + g_debug("Deployment %s is still waiting%s.", active_action->id, maintenance_msg); + active_action->state = ACTION_STATE_NONE; + return TRUE; + } + + // clean up on changed deployment id + if (g_strcmp0(temp_id, active_action->id)) + process_deployment_cleanup(); + else + g_debug("Continuing scheduled deployment %s%s.", active_action->id, + maintenance_msg); + g_free(active_action->id); - active_action->id = json_get_string(resp_root, "$.id", error); + active_action->id = g_steal_pointer(&temp_id); if (!active_action->id) goto error; @@ -1029,7 +1063,6 @@ static gboolean process_deployment(JsonNode *req_root, GError **error) json_artifact = json_array_get_element(json_artifacts, 0); // get artifact information - artifact = g_new0(Artifact, 1); artifact->version = json_get_string(json_chunk, "$.version", error); if (!artifact->version) goto proc_error; From 871458e1ea09c3bfd8c9b5043a90e882b49a4004 Mon Sep 17 00:00:00 2001 From: Bastian Krause Date: Wed, 11 Aug 2021 10:30:13 +0200 Subject: [PATCH 3/5] hawkbit_mgmt/conftest: allow passing additional assignment parameters This will be needed in a future test to specify ``type=downloadonly``. Signed-off-by: Bastian Krause --- script/hawkbit_mgmt.py | 5 ++++- test/conftest.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/script/hawkbit_mgmt.py b/script/hawkbit_mgmt.py index 874e15d1..5d51af5d 100755 --- a/script/hawkbit_mgmt.py +++ b/script/hawkbit_mgmt.py @@ -354,7 +354,7 @@ def delete_artifact(self, artifact_id: str = None, module_id: str = None): if 'artifact' in self.id and artifact_id == self.id['artifact']: del self.id['artifact'] - def assign_target(self, dist_id: str = None, target_id: str = None): + def assign_target(self, dist_id: str = None, target_id: str = None, params: dict = None): """ Assigns the distribution set matching `dist_id` to a target matching `target_id`. If `dist_id` is not given, uses the distribution set created by the most recent @@ -369,6 +369,9 @@ def assign_target(self, dist_id: str = None, target_id: str = None): target_id = target_id or self.id['target'] testdata = [{'id': target_id}] + if params: + testdata[0].update(params) + response = self.post(f'distributionsets/{dist_id}/assignedTargets', testdata) # Increment version to be able to flash over an already deployed distribution diff --git a/test/conftest.py b/test/conftest.py index 0dcad40a..3528d859 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -138,7 +138,7 @@ def assign_bundle(hawkbit, hawkbit_target_added, rauc_bundle, tmp_path): distributionsets = [] actions = [] - def _assign_bundle(swmodules_num=1, artifacts_num=1): + def _assign_bundle(swmodules_num=1, artifacts_num=1, params=None): for i in range(swmodules_num): swmodule_type = 'application' if swmodules_num > 1 else 'os' swmodules.append(hawkbit.add_softwaremodule(module_type=swmodule_type)) @@ -156,7 +156,7 @@ def _assign_bundle(swmodules_num=1, artifacts_num=1): dist_type = 'app' if swmodules_num > 1 else 'os' distributionsets.append(hawkbit.add_distributionset(module_ids=swmodules, dist_type=dist_type)) - actions.append(hawkbit.assign_target(distributionsets[-1])) + actions.append(hawkbit.assign_target(distributionsets[-1], params=params)) return actions[-1] From 3557dbb7f295d81c547f2474d50c7e1270389ccd Mon Sep 17 00:00:00 2001 From: Bastian Krause Date: Wed, 11 Aug 2021 10:34:39 +0200 Subject: [PATCH 4/5] test: download: add test_download_only Signed-off-by: Bastian Krause --- test/test_download.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test_download.py b/test/test_download.py index bb6691c5..697ed143 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -123,3 +123,21 @@ def test_download_slow_with_resume(hawkbit, bundle_assigned, adjust_config, rate assert 'Resuming download from offset' in out assert 'Download complete.' in out assert 'File checksum OK.' in out + +def test_download_only(hawkbit, config, assign_bundle): + """Test "downloadonly" deployment.""" + assign_bundle(params={'type': 'downloadonly'}) + + out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r') + assert 'Start downloading' in out + assert 'hawkBit requested to skip installation, not invoking RAUC yet.' in out + assert 'Download complete' in out + assert 'File checksum OK' in out + assert err == '' + assert exitcode == 0 + + status = hawkbit.get_action_status() + assert any(['download' in s['type'] for s in status]) + + # check last status message + assert 'File checksum OK.' in status[0]['messages'] From c71d62b47fff74af47a9c971ede703b82b6c162f Mon Sep 17 00:00:00 2001 From: Bastian Krause Date: Wed, 11 Aug 2021 10:34:22 +0200 Subject: [PATCH 5/5] test: install: add test_install_maintenance_window Signed-off-by: Bastian Krause --- test/helper.py | 12 ++++++++++++ test/test_install.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/test/helper.py b/test/helper.py index 5ef5e90f..92c6b0c1 100644 --- a/test/helper.py +++ b/test/helper.py @@ -73,3 +73,15 @@ def available_port(): sock.bind(('localhost', 0)) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock.getsockname()[1] + +def timezone_offset_utc(date): + utc_offset = int(date.astimezone().utcoffset().total_seconds()) + + utc_offset_hours, remainder = divmod(utc_offset, 60*60) + utc_offset_minutes, remainder = divmod(remainder, 60) + sign_offset = '+' if utc_offset >=0 else '-' + + if remainder != 0: + raise Exception('UTC offset contains fraction of a minute') + + return f'{sign_offset}{utc_offset_hours:02}:{utc_offset_minutes:02}' diff --git a/test/test_install.py b/test/test_install.py index c27fe389..d9075f43 100644 --- a/test/test_install.py +++ b/test/test_install.py @@ -1,7 +1,12 @@ # SPDX-License-Identifier: LGPL-2.1-only # SPDX-FileCopyrightText: 2021 Bastian Krause , Pengutronix -from helper import run +from datetime import datetime, timedelta +from pathlib import Path + +from pexpect import TIMEOUT + +from helper import run, run_pexpect, timezone_offset_utc def test_install_bundle_no_dbus_iface(hawkbit, bundle_assigned, config): """Assign bundle to target and test installation without RAUC D-Bus interface available.""" @@ -50,3 +55,38 @@ def test_install_failure(hawkbit, config, bundle_assigned, rauc_dbus_install_fai status = hawkbit.get_action_status() assert status[0]['type'] == 'error' assert 'Failed to install software bundle.' in status[0]['messages'] + +def test_install_maintenance_window(hawkbit, config, rauc_bundle, assign_bundle, + rauc_dbus_install_success): + bundle_size = Path(rauc_bundle).stat().st_size + maintenance_start = datetime.now() + timedelta(seconds=15) + maintenance_window = { + 'maintenanceWindow': { + 'schedule' : maintenance_start.strftime('%-S %-M %-H ? %-m * %-Y'), + 'timezone' : timezone_offset_utc(maintenance_start), + 'duration' : '00:01:00' + } + } + assign_bundle(params=maintenance_window) + + proc = run_pexpect(f'rauc-hawkbit-updater -c "{config}"') + proc.expect(r"hawkBit requested to skip installation, not invoking RAUC yet \(maintenance window is 'unavailable'\)") + proc.expect('Start downloading') + proc.expect('Download complete') + proc.expect('File checksum OK') + + # wait for the maintenance window to become available and the next poll of the base resource + proc.expect(TIMEOUT, timeout=30) + proc.expect(r"Continuing scheduled deployment .* \(maintenance window is 'available'\)") + # RAUC bundle should have been already downloaded completely + proc.expect(f'Resuming download from offset {bundle_size}') + proc.expect('Download complete') + proc.expect('File checksum OK') + proc.expect('Software bundle installed successfully') + + # let feedback propagate to hawkBit before termination + proc.expect(TIMEOUT, timeout=2) + proc.terminate(force=True) + + status = hawkbit.get_action_status() + assert status[0]['type'] == 'finished'