Skip to content

Commit

Permalink
Merge pull request #111 from Bastian-Krause/bst/maintenance-window
Browse files Browse the repository at this point in the history
Handle Deployments Maintenance Windows
  • Loading branch information
ejoerns authored Oct 15, 2021
2 parents 97034e2 + c71d62b commit 2ba3490
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 8 deletions.
1 change: 1 addition & 0 deletions include/hawkbit-client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
5 changes: 4 additions & 1 deletion script/hawkbit_mgmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
60 changes: 56 additions & 4 deletions src/hawkbit-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -945,8 +953,10 @@ 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_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;
JsonNode *resp_root = NULL, *json_chunk = NULL, *json_artifact = NULL;
Expand Down Expand Up @@ -976,9 +986,52 @@ 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;
}

// 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;

Expand Down Expand Up @@ -1010,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;
Expand Down
4 changes: 2 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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]

Expand Down
12 changes: 12 additions & 0 deletions test/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
18 changes: 18 additions & 0 deletions test/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
42 changes: 41 additions & 1 deletion test/test_install.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# SPDX-License-Identifier: LGPL-2.1-only
# SPDX-FileCopyrightText: 2021 Bastian Krause <[email protected]>, 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."""
Expand Down Expand Up @@ -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'

0 comments on commit 2ba3490

Please sign in to comment.