diff --git a/.gitignore b/.gitignore index 6ac9b00..001d99a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ vscode-bitbake-build/ documentation/_build/ documentation/oe-logs documentation/oe-workdir - +__pycache__ \ No newline at end of file diff --git a/documentation/quick-build.rst b/documentation/quick-build.rst index 4731281..ed4abfe 100644 --- a/documentation/quick-build.rst +++ b/documentation/quick-build.rst @@ -46,7 +46,8 @@ Theses packages are needed on your build machine: chrpath socat cpio python3 python3-pip python3-pexpect xz-utils \ debianutils iputils-ping python3-git python3-jinja2 libegl1-mesa \ libsdl1.2-dev pylint3 xterm python3-subunit mesa-common-dev zstd \ - liblz4-tool bmap-tools efitools openssl sbsign + liblz4-tool bmap-tools efitools openssl sbsign python3-click \ + python3-aiohttp Use Git to clone CoreOS ######################## diff --git a/layers/meta-belden-coreos/classes/coreos-image.bbclass b/layers/meta-belden-coreos/classes/coreos-image.bbclass index a1d097e..d2898c4 100644 --- a/layers/meta-belden-coreos/classes/coreos-image.bbclass +++ b/layers/meta-belden-coreos/classes/coreos-image.bbclass @@ -14,7 +14,7 @@ FEATURE_PACKAGES_networkmanager = "networkmanager networkmanager-nmcli" FEATURE_PACKAGES_networkmanager-dev-tools = "networkmanager-nmtui" FEATURE_PACKAGES_networkmanager-cockpit = "cockpit-networkmanager" -FEATURE_PACKAGES_swupdate = "swupdate swupdate-client swupdate-lua" +FEATURE_PACKAGES_swupdate = "packagegroup-coreos-swupdate" # The cockpit feature automatically install the corresponding # *-cockpit FEATURES_PACKAGES for any image features diff --git a/layers/meta-belden-coreos/lib/devtool/coreos-device.py b/layers/meta-belden-coreos/lib/devtool/coreos-device.py new file mode 100644 index 0000000..bbcc3ea --- /dev/null +++ b/layers/meta-belden-coreos/lib/devtool/coreos-device.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: GPL-2.0-only +# This scripts use bitbake api, so it has to be under the GPL-2.0 license + +""" + devtool wrapper for the coreos-device script +""" + +import subprocess + +from devtool import setup_tinfoil, parse_recipe + +class InvalidImage(Exception): + pass + +def swupdate_www_push(args, config, basepath, workspace): + """ + devtool update-device command + + Use devtool library to get information about the image from the + bitbake context, then call update_device() + """ + # Only use inside a bitbake environment, we can import some more libs here + + image = args.imagename + tinfoil = setup_tinfoil(basepath=basepath) + + rd = parse_recipe(config, tinfoil, image, True) + if not rd: + # Error already shown + return 1 + + if not bb.data.inherits_class('coreos-image', rd): + raise InvalidImage("Image should be based on the coreos-image class") + + if rd.getVar('COREOS_IMAGE_GENERATE_SWU') != "1": + raise InvalidImage("COREOS_IMAGE_GENERATE_SWU should be set to '1'") + + image_name = rd.getVar('IMAGE_LINK_NAME') + outputdir = rd.getVar('DEPLOY_DIR_IMAGE') + + return run(["coreos-device", "swupdate-www-push", f"{outputdir}/{image_name}.swu", args.target, f"--port={args.port}"]) + + +def register_commands(subparsers, context): + """Register devtool subcommands from this plugin""" + subparsers.add_subparser_group('swupdate', 'Updating device using swupdate') + cmd_push = subparsers.add_parser('swupdate-www-push', help='Update device with image', + description='Deploy an image to a device using swupdate', + group='swupdate') + cmd_push.add_argument('imagename', help='Image recipe to deploy') + cmd_push.add_argument('target', help='IP address of the running target') + cmd_push.add_argument('-P', '--port', default=8080, help='Specify port to use for connection to the target') + cmd_push.set_defaults(func=swupdate_www_push, fixed_setup=context.fixed_setup) + +def run(cmd): + return subprocess.run(cmd).returncode diff --git a/layers/meta-belden-coreos/recipes-core/packagegroups/packagegroup-coreos-swupdate.bb b/layers/meta-belden-coreos/recipes-core/packagegroups/packagegroup-coreos-swupdate.bb new file mode 100644 index 0000000..d8429c4 --- /dev/null +++ b/layers/meta-belden-coreos/recipes-core/packagegroups/packagegroup-coreos-swupdate.bb @@ -0,0 +1,16 @@ +SUMMARY = "Add CoreOS swupdate backends and integrations packages" +DESCRIPTION = "Install swupdate and related components" + +inherit packagegroup + + +PACKAGES = "\ + ${PN} \ +" + +RDEPENDS:${PN} = "\ + swupdate \ + swupdate-progress \ + swupdate-client \ + swupdate-lua \ +" diff --git a/scripts/coreos-device b/scripts/coreos-device new file mode 100755 index 0000000..d5caa1a --- /dev/null +++ b/scripts/coreos-device @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +""" + This is a low level script that communicate with a device over the network. + + Currently only a swupdate-www-push subcommand is available. + + It can be used this way: + coreos-device swupdate-www-push ~/coreos-image-demo-vm-x64.swu 192.168.122.21 + + This script doesn't interact with bitbake so it can't retrieve variables from + bitbake. For a more user friendly usage you can use devtool: + + devtool swupdate-www-push coreos-image-demo 192.168.122.21 +""" + +import os +import sys +import pathlib +import ipaddress +import asyncio +import json + +# We need some modules that are not part of the Python Standard Library +# If they are not found, let's fail with an appropriate error message +try: + import click + import aiohttp +except ModuleNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + print(f"Please install the python3-{e.name} package", file=sys.stderr) + sys.exit(1) + +@click.group() +def cli(): + pass + +@cli.command() +@click.argument( 'image', type=click.Path( + exists=True, resolve_path=True, readable=True, dir_okay=False, + path_type=pathlib.Path, +)) +@click.argument('device', type=ipaddress.ip_address) +@click.option('--port', type=click.IntRange(min=1, max=65535), default=8080) +def swupdate_www_push(image, device, port): + loop = asyncio.new_event_loop() + returncode = loop.run_until_complete(swupdate_www_push_async(image, device, port)) + loop.close() + sys.exit(returncode) + + +async def swupdate_www_push_async(image, device, port): + url = f'http://{device}:{port}' + + async with aiohttp.ClientSession() as session: + tasks = [] + + tasks.append(asyncio.ensure_future(swupdate_www_push_data(session, url, image))) + tasks.append(asyncio.ensure_future(swupdate_www_websocket_logger(session, url))) + + responses = await asyncio.gather(*tasks) + + # Return the sum of return value of all ours tasks + returncode = 0 + for response in responses: + returncode += response + return returncode + + +async def swupdate_www_push_data(session, url, image): + + with open(image, 'rb') as f: + data = {'file': f} + try: + async with session.post(f"{url}/upload", data=data) as resp: + if resp.status == 200: + click.secho("coreos-device: Update was successfull", bg="green") + return 0 + else: + click.secho(f"coreos-device: status {resp.status}, {await resp.text}", bg="red") + return resp.status + except Exception as e: + click.secho(f"coreos-device: ERROR: {e}", bg="red") + return 1 + + +async def swupdate_www_websocket_logger(session, url): + try: + async with session.ws_connect(f"{url}/ws") as ws: + async for msg in ws: + data = json.loads(msg.data) + if data["type"] == "message": + msg = data["text"] + if "Waiting for requests" in msg: + await ws.close() + return 1 + elif "ERROR" in msg: + click.secho("swupdate: " + data["text"], fg='red') + else: + click.secho("swupdate: " + data["text"], fg='green') + + if data["type"] == "status": + if data["status"] == "DONE": + await ws.close() + return 0 + except Exception as e: + click.secho(f"coreos-device: ERROR: {e}", bg="red") + return 1 + +if __name__ == '__main__': + cli()