diff --git a/docs/Manifest Parameters.md b/docs/Manifest Parameters.md index 63428f220..266a689f1 100644 --- a/docs/Manifest Parameters.md +++ b/docs/Manifest Parameters.md @@ -14,6 +14,8 @@ The following outlines the manifest parameters used to perform the supported OTA 9. [Configuration LOAD](#Load) 10. [Configuration APPEND](#Append) 11. [Configuration REMOVE](#Remove) +12. [Source Application](#Application) +13. [Source OS](#OS) ## Manifest Rules @@ -41,18 +43,18 @@ The following outlines the manifest parameters used to perform the supported OTA | Tag | Example | Required /Optional | Notes | |:-----------------------------------------|:----------------------------------------------------|:------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `` | `` | R || -| `` | `` | R || +| `` | `` | R | | +| `` | `` | R | | | `` | `ota` | R | Always OTA | -| `` | `` | R || -| `
` | `
` | R || -| `` | `yourID` | O || +| `` | `` | R | | +| `
` | `
` | R | | +| `` | `yourID` | O | | | `` | `YourName` | O | Endpoint Manufacturer Name | -| `` | `YourDescription | O || -| `` | `fota` | R || +| `` | `YourDescription | O | | +| `` | `fota` | R | | | `` | `remote` | O | [local or remote]. If file is already downloaded on the system, then use _**local**_. If it needs to be fetched from remote repository, use **_remote_**. | -| `
` | `
` | R || -| `` | `` | R || +| `
` | `
` | R | | +| `` | `` | R | | | `` | `` | R | Text must be compliant with XML Standards | | `` | `http://yoururl:80/BIOSUPDATE.tar` | R | FOTA path created in repository | | `` | `ABC123` | O | Digital signature of *.tar file. | @@ -65,10 +67,10 @@ The following outlines the manifest parameters used to perform the supported OTA | `` | `7acbd1a5a-33a4-48c3ab11-a4c33b3d0e56` | O | Check for ‘System Firmware Type’ on running cmd:fwupdate -l | | `` | `user` | O | Username used during fetch from remote repository | | `` | `pwd` | O | Password used during fetch from remote repository | -| `` | `` | R || -| `` | `` | R || -| `
` | `
` | R || -| `
` | `
` | R || +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `
` | `
` | R | | The following table references each XML tag within a manifest that triggers the FOTA update. Using the following XML tags in the order of description will trigger a FOTA update via Manifest. @@ -103,28 +105,28 @@ description will trigger a FOTA update via Manifest. | Tag | Example | Required/Optional | Notes | |:-----------------------------------------|:---------------------------------------------|:-----------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `` | `` | R || -| `` | ` | R || +| `` | `` | R | | +| `` | ` | R | | | `` | `ota` | R | Always OTA | -| `
` | `
` | R || -| `` | `Example` | O || -| `` | `Example` | O || -| `` | `Example` | O || -| `` | `sota` | R || +| `
` | `
` | R | | +| `` | `Example` | O | | +| `` | `Example` | O | | +| `` | `Example` | O | | +| `` | `sota` | R | | | `` | `remote` | R | [local or remote]. If file is already downloaded on the system, then use _**local**_. If it needs to be fetched from remote repository, use **_remote_**. | -| `
` | `
` | R || -| `` | `` | R || -| `` | `` | R || -| `` | `update` | R || -| `` | `full` | O | Valid values: [full, no-download, download-only] +| `
` | `
` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `update` | R | | +| `` | `full` | O | Valid values: [full, no-download, download-only] | | `` | `https://yoururl/file.mender` | O | Used to download mender file from remote repository. (use repo=remote) | | `` | `xx` | O | Username for remote repository | | | `` | `xxx` | O | Password for remote repository | | | `` | `2020-01-01` | O | The release date provided should be in ‘YYYY-MM-DD’ format. | -| `` | `` | R || -| `` | `` | R || -| `` | `` | R || -| `
` | `
| R || +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `
` | ` | R | | ### Sample SOTA Manifest - Ubuntu: ```xml @@ -204,7 +206,7 @@ The POTA manifest is used to perform both a FOTA and SOTA update at the same tim | `` | `` | R | | | `` | `` | R | | | `` | `update` | R | -| `` | `full` | O | Valid values: [full, no-download, download-only] | +| `` | `full` | O | Valid values: [full, no-download, download-only] | | `` | `https://yoururl/file.mender` | O | Used to download mender file from remote repository. (use repo=remote) | | `` | `/var/cache/file.mender` | O | Used to update using a local mender file . (use repo=local) | | `` | `xx` | O | Username for remote repository | | @@ -298,7 +300,7 @@ The POTA manifest is used to perform both a FOTA and SOTA update at the same tim update application - yoururl/package.deb/fetch> + yoururl/package.deb yes @@ -905,3 +907,284 @@ The query command can be used to gather information about the system and the Vis ``` + +## Application + +#### Source Application Add Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:-----------------------------------------------------------------------------------------------|:-----------------:|:----------------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `https://dl-ssl.google.com/linux/linux_signing_key.pub` | R | | +| `` | `google-chrome.gpg` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main` | R | | +| `` | `` | R | | +| `` | `google-chrome.list` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source application add Manifest Example +```xml + + + source + + + + https://dl-ssl.google.com/linux/linux_signing_key.pub + google-chrome.gpg + + + + deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main + deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable second + + google-chrome.list + + + + +``` + +#### Source Application Update Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:-----------------------------------------------------------------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main` | R | | +| `` | `` | R | | +| `` | `google-chrome.list` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source Application Update Manifest Example +```xml + + + source + + + + + deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main + + google-chrome.list + + + + +``` + +#### Source Application Remove Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:-----------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `google-chrome.gpg` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `google-chrom.list` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source Application Remove Manifest Example +```xml + + + source + + + + google-chrome.gpg + + + google-chrome.list + + + + +``` + +#### Source Application List Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:-----------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source application list Manifest Example +```xml + + + source + + + + +``` + +## OS + +#### Source OS Add Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:--------------------------------------------------------------------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted ` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source OS Add Manifest Example +```xml + + + source + + + + deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted + deb http://archive.ubuntu.com/ubuntu/ jammy-security universe + + + + +``` + +#### Source os Update Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:--------------------------------------------------------------------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted ` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source OS Update Manifest Example +```xml + + + source + + + + deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted + deb http://archive.ubuntu.com/ubuntu/ jammy-security universe + + + + +``` + +#### Source OS Remove Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:------------------------------------------------------------------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + +#### Source OS Remove Manifest Example +```xml + + + source + + + + deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted + deb http://archive.ubuntu.com/ubuntu/ jammy-security universe + + + + +``` + +#### Source os List Manifest Parameters +| Tag | Example | Required/Optional | Notes | +|:-----------------------------------------|:-----------------------------------------|:-----------------:|:------| +| `` | `` | R | | +| `` | `` | R | | +| `source` | `source` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | +| `` | `` | R | | + + + + +#### Source OS List Manifest Example +```xml + + + source + + + + +``` \ No newline at end of file diff --git a/autopep8.sh b/format-python.sh similarity index 82% rename from autopep8.sh rename to format-python.sh index a2ee3fff9..e9ef58327 100755 --- a/autopep8.sh +++ b/format-python.sh @@ -4,6 +4,6 @@ set -euxo pipefail for dir in inbc-program inbm inbm-lib ; do ( cd "$dir" - ./autopep8.sh + ./format-python.sh ) done diff --git a/inbc-program/.gitignore b/inbc-program/.gitignore index f98570b2c..8cc02e24f 100644 --- a/inbc-program/.gitignore +++ b/inbc-program/.gitignore @@ -4,3 +4,5 @@ coverage.xml *.cover **/cover/ +inbm_lib +inbm_common_lib diff --git a/inbc-program/README.md b/inbc-program/README.md index 6da6deff0..4737645c6 100644 --- a/inbc-program/README.md +++ b/inbc-program/README.md @@ -6,7 +6,7 @@ 1. [Introduction](#introduction) 2. [Prerequisites](#prerequisites) 3. [Notes](#-notes) -4. [MQTT Communication](#mqtt-communication) +4. [MQTT Communication](#mqtt-communication-) 1. [Publish Channels](#publish-channels) 2. [Subscribe Channels](#subscribe-channels) 5. [Commands](#commands) @@ -19,6 +19,14 @@ 7. [Configuration Set](#set) 8. [Restart](#restart) 9. [Query](#query) + 10. [Source Application Add](#source-application-add) + 11. [Source Application Remove](#source-application-remove) + 12. [Source Application Update](#source-application-update) + 13. [Source Application List](#source-application-list) + 14. [Source OS Add](#source-os-add) + 15. [Source OS Remove](#source-os-remove) + 16. [Source OS Update](#source-os-update) + 15. [Source OS List](#source-os-list) 6. [Status Codes](#status-codes) 7. [Return and Exit Codes](#return-and-exit-codes) @@ -352,7 +360,7 @@ Remove is only applicable to two config tags, which are trustedRepositories and ### Usage ``` -inbc remove +inbc remove {--path, -p KEY_PATH;...} ``` @@ -404,6 +412,152 @@ inbc query --option hw inbc query --option sw ``` +## SOURCE APPLICATION ADD +### Description +Downloads and encrypts GPG key and stores it on the system under /usr/share/keyrings. Creates a file under /etc/apt/sources.list.d to store the update source information. +This list file is used during 'sudo apt update' to update the application + +### Usage +``` +inbc source application add + {--gpgKeyUri, -gku=GPG_KEY_URI} + {--gpgKeyName, -gkn=GPG_KEY_NAME} + {--sources, -s=SOURCES} + {--filename, -f=FILENAME} +``` + +### Example +#### Add an application source +``` +inbc source application add + --gpgKeyUri https://dl-ssl.google.com/linux/linux_signing_key.pub + --gpgKeyName google-chrome.gpg + --sources "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" + --filename google-chrome.list +``` + +## SOURCE APPLICATION REMOVE +### Description +Removes the GPG key file from under /usr/share/keyrings. Removes the source file from under /etc/apt/sources.list.d/. + +### Usage +``` +inbc source application remove + {--gpgKeyName, -gkn=GPG_KEY_NAME} + {--filename, -f=FILE_NAME} +``` + +### Example +#### Remove an application source +```commandline +inbc source application remove + --gpgKeyName google-chrome.gpg + --filename google-chrome.list +``` + +## SOURCE APPLICATION UPDATE +### Description +Updates Application sources that are used to update the system +NOTE: Currently this only works on Ubuntu + +### Usage +``` +inbc source application update + {--filename, -f=FILEPATH} + {--sources, -s=SOURCES} +``` + +### Examples +#### Update an application source file +``` +inbc source application update + --filename google-chrome.list + --sources "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable test" "debsrc [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable test2" +``` + +## SOURCE APPLICATION LIST +### Description +Lists Application sources +NOTE: Currently this only works on Ubuntu + +### Usage +``` +inbc source application list +``` + +### Examples +#### Lists all application source files +``` +inbc source application list +``` + +## SOURCE OS ADD +### Description +Appends new source(s) to the /etc/apt/sources.list file + +### Usage +``` +inbc source os add + {--sources, -s=SOURCES} +``` + +### Example +#### Adds two sources +``` +inbc source os add + --sources="deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted" "deb http://archive.ubuntu.com/ubuntu/ jammy-security universe" +``` + +## SOURCE OS REMOVE +### Description +Removes the provided source(s) from the /etc/apt/sources.list file, if they are present. + +### Usage +``` +inbc source os remove + {--sources, -s=SOURCES} +``` + +### Example +#### Removes the two provided source(s) from the /etc/apt/sources.list file +``` +inbc source os remove + --sources="deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted" "deb http://archive.ubuntu.com/ubuntu/ jammy-security universe" +``` + +## SOURCE OS UPDATE +### Description +Creates a new /etc/apt/sources.list file with only the sources provided + +### Usage +``` +inbc source os update + {--sources, -s=SOURCES} +``` + +### Example +#### Creates a new /etc/apt/sources.list file with only the two provided sources +``` +inbc source os update + --sources="deb http://archive.ubuntu.com/ubuntu/ jammy-security main restricted" "deb http://archive.ubuntu.com/ubuntu/ jammy-security universe" +``` + +## SOURCE OS LIST +### Description +Lists OS sources +NOTE: Currently this only works on Ubuntu + +### Usage +```commandline +inbc source os list +``` + +### Examples +#### Lists all OS source files +``` +inbc source os list +``` + # Status Codes | Message | Description | Result | diff --git a/inbc-program/autopep8.sh b/inbc-program/format-python.sh similarity index 100% rename from inbc-program/autopep8.sh rename to inbc-program/format-python.sh diff --git a/inbc-program/inbc/broker.py b/inbc-program/inbc/broker.py index 5bdb85ed1..0bb4dfc68 100644 --- a/inbc-program/inbc/broker.py +++ b/inbc-program/inbc/broker.py @@ -43,15 +43,20 @@ def __init__(self, cmd_type: str, parsed_args: Any, tls: bool = True) -> None: except ValueError: mqtt_port = DEFAULT_MQTT_PORT - self.mqttc = MQTT(PROG, - MQTT_HOST, - mqtt_port, - MQTT_KEEPALIVE_INTERVAL, - ca_certs=CA_CERTS, - env_config=True, - tls=tls, - client_certs=CLIENT_CERTS, - client_keys=CLIENT_KEYS) + try: + self.mqttc = MQTT(PROG, + MQTT_HOST, + mqtt_port, + MQTT_KEEPALIVE_INTERVAL, + ca_certs=CA_CERTS, + env_config=True, + tls=tls, + client_certs=CLIENT_CERTS, + client_keys=CLIENT_KEYS) + except AttributeError as e: + logger.exception("MQTT error: Check that MQTT Service is running") + raise + self.mqttc.start() self._subscribe() self._command = create_command_factory(cmd_type, self) diff --git a/inbc-program/inbc/command/command.py b/inbc-program/inbc/command/command.py index 6ad76b43f..b03cbcd30 100644 --- a/inbc-program/inbc/command/command.py +++ b/inbc-program/inbc/command/command.py @@ -6,7 +6,6 @@ """ import logging -import time from typing import Any, Optional from abc import ABC, abstractmethod from inbc import shared @@ -48,7 +47,7 @@ def stop_timer(self) -> None: def invoke_update(self, args: Any) -> None: """Trigger the command-line utility tool to invoke update. - Sub-classes will override this method to provide specific implementations. + Subclasses will override this method to provide specific implementations. @param args: arguments passed to command-line tool. """ @@ -58,7 +57,7 @@ def _send_manifest(self, args: Any, topic: str) -> None: """Send a manifest. This is a default concrete implementation that takes in a topic. It is intended - to be called from a sub-class. + to be called from a subclass. @param args: arguments passed to command-line tool. @param topic: topic on which to send the manifest @@ -74,7 +73,7 @@ def search_response(self, payload: Any) -> None: @param payload: payload received in which to search """ if self._cmd_type != "query": - if search_keyword(payload, ["SUCCESSFUL"]): + if search_keyword(payload, ["SUCCESSFUL", '"status": 200']): self.terminate_operation(COMMAND_SUCCESS, InbcCode.SUCCESS.value) else: self.terminate_operation(COMMAND_FAIL, InbcCode.FAIL.value) diff --git a/inbc-program/inbc/command/source_command.py b/inbc-program/inbc/command/source_command.py index 0dd8d9364..8ab3777ca 100644 --- a/inbc-program/inbc/command/source_command.py +++ b/inbc-program/inbc/command/source_command.py @@ -1,7 +1,7 @@ """ Source Command classes to represent command entered by user. - # Copyright (C) 2020-2023 Intel Corporation + # Copyright (C) 2020-2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 """ from typing import Any @@ -33,7 +33,6 @@ def search_response(self, payload: Any) -> None: @param payload: payload received in which to search """ - # TODO: Add responses to wait for super().search_response(payload) def search_event(self, payload: Any, topic: str) -> None: diff --git a/inbc-program/inbc/parser/parser.py b/inbc-program/inbc/parser/parser.py index 599c949d1..97201444b 100644 --- a/inbc-program/inbc/parser/parser.py +++ b/inbc-program/inbc/parser/parser.py @@ -65,19 +65,19 @@ def parse_source_args(self) -> None: # Application Add Command app_add_parser = app_subparsers.add_parser('add') - app_add_parser.add_argument('-gpgKeyPath', '--gkp', required=True, + app_add_parser.add_argument('--gpgKeyUri', '-gku', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 1500), - help='Path to GPG key') - app_add_parser.add_argument('-gpgKeyName', '--gkn', required=True, + help='Uri from which to download GPG key') + app_add_parser.add_argument('--gpgKeyName', '-gkn', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 200), help='Name to store the GPG key information') - app_add_parser.add_argument('-source', '--s', required=True, + app_add_parser.add_argument('--sources', '-s', required=True, nargs="*", default=[], type=lambda x: validate_string_less_than_n_characters( - x, 'str', 1000), - help='Source information to store in the file') - app_add_parser.add_argument('-fileName', '--f', required=True, + x, 'List[str]', 3500), + help='List of source information to store in the file') + app_add_parser.add_argument('--filename', '-f', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 200), help='file name to use when storing the source information') @@ -85,11 +85,11 @@ def parse_source_args(self) -> None: # Application Remove Command app_remove_parser = app_subparsers.add_parser('remove') - app_remove_parser.add_argument('-gpgKeyId', '--gki', required=True, + app_remove_parser.add_argument('--gpgKeyName', '-gkn', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 50), - help='GPG Key ID of the source to remove.') - app_remove_parser.add_argument('-fileName', '--f', required=True, + help='GPG key name of the source to remove.') + app_remove_parser.add_argument('--filename', '-f', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 200), help='file name to use when storing the source information') @@ -97,14 +97,14 @@ def parse_source_args(self) -> None: # Application Update Command app_update_parser = app_subparsers.add_parser('update') - app_update_parser.add_argument('-fileName', '--f', required=True, + app_update_parser.add_argument('--filename', '-f', required=True, type=lambda x: validate_string_less_than_n_characters( x, 'str', 200), help='file name to use when storing the source information') - app_update_parser.add_argument('-source', '--s', required=True, + app_update_parser.add_argument('--sources', '-s', required=True, nargs="*", default=[], type=lambda x: validate_string_less_than_n_characters( - x, 'str', 1000), - help='Source information to store in the file') + x, 'List[str]', 3500), + help='List of source information to store in the file') app_update_parser.set_defaults(func=application_update) # Application List Command @@ -119,7 +119,7 @@ def parse_source_args(self) -> None: # OS Add Command os_add_parser = os_subparsers.add_parser('add') - os_add_parser.add_argument('-sources', '--s', required=True, nargs="*", default=[], + os_add_parser.add_argument('--sources', '-s', required=True, nargs="*", default=[], type=lambda x: validate_string_less_than_n_characters( x, 'List[str]', 3500), help='List of source information to store in the file') @@ -127,7 +127,7 @@ def parse_source_args(self) -> None: # OS Remove Command os_remove_parser = os_subparsers.add_parser('remove') - os_remove_parser.add_argument('-sources', '--s', required=True, nargs="*", default=[], + os_remove_parser.add_argument('--sources', '-s', required=True, nargs="*", default=[], type=lambda x: validate_string_less_than_n_characters( x, 'List[str]', 3500), help='Source information to remove from the file') @@ -135,7 +135,7 @@ def parse_source_args(self) -> None: # OS Update Command os_update_parser = os_subparsers.add_parser('update') - os_update_parser.add_argument('-sources', '--s', required=True, nargs="*", default=[], + os_update_parser.add_argument('--sources', '-s', required=True, nargs="*", default=[], type=lambda x: validate_string_less_than_n_characters( x, 'List[str]', 3500), help='Source information to replace in the file') diff --git a/inbc-program/inbc/parser/source_app_parser.py b/inbc-program/inbc/parser/source_app_parser.py index 1d4c58ed6..680dd4615 100644 --- a/inbc-program/inbc/parser/source_app_parser.py +++ b/inbc-program/inbc/parser/source_app_parser.py @@ -10,10 +10,10 @@ def application_add(args: argparse.Namespace) -> str: arguments = { - 'path': args.gkp, - 'keyname': args.gkn, - 'source': args.s, - 'filename': args.f, + 'uri': args.gpgKeyUri, + 'keyname': args.gpgKeyName, + 'sources': args.sources, + 'filename': args.filename, } manifest = ('' + @@ -23,14 +23,16 @@ def application_add(args: argparse.Namespace) -> str: '' '{0}' + '{1}' - '' + - '{2}' - '{3}' - '' + - '').format(create_xml_tag(arguments, "path"), - create_xml_tag(arguments, "keyname"), - create_xml_tag(arguments, "source"), - create_xml_tag(arguments, "filename")) + '').format(create_xml_tag(arguments, "uri"), + create_xml_tag(arguments, "keyname")) + + for source in args.sources: + manifest += '' + source.strip() + '' + + manifest += ('' + f'{create_xml_tag(arguments, "filename")}' + '' + + '') print("manifest {0}".format(manifest)) return manifest @@ -38,41 +40,43 @@ def application_add(args: argparse.Namespace) -> str: def application_remove(args: argparse.Namespace) -> str: arguments = { - 'keyid': args.gki, - 'filename': args.f + 'keyname': args.gpgKeyName, + 'filename': args.filename } manifest = ('' + 'source' + '' + '' - '{0}' + + f'{create_xml_tag(arguments, "keyname")}' + '' + - '{1}' + f'{create_xml_tag(arguments, "filename")}' '' '' + - '').format(create_xml_tag(arguments, "keyid"), - create_xml_tag(arguments, "filename")) + '') - print("manifest {0}".format(manifest)) + print(f"manifest {manifest}") return manifest def application_update(args: argparse.Namespace) -> str: arguments = { - 'source_pkg': args.s, - 'filename': args.f + 'sources': args.sources, + 'filename': args.filename } manifest = ('' + 'source' + '' + - '' - '{0}{1}' + - '' + - '').format(create_xml_tag(arguments, "source_pkg"), - create_xml_tag(arguments, "filename")) + '') - print("manifest {0}".format(manifest)) + for source in args.sources: + manifest += '' + source.strip() + '' + + manifest += (f'{create_xml_tag(arguments, "filename")}' + + '' + + '') + + print(f"manifest {manifest}") return manifest diff --git a/inbc-program/inbc/parser/source_os_parser.py b/inbc-program/inbc/parser/source_os_parser.py index e7178d95e..0570d0fdc 100644 --- a/inbc-program/inbc/parser/source_os_parser.py +++ b/inbc-program/inbc/parser/source_os_parser.py @@ -9,7 +9,7 @@ def os_add(args: argparse.Namespace) -> str: manifest = 'source' \ '' - for source in args.s: + for source in args.sources: manifest += '' + source.strip() + '' manifest += '' @@ -20,7 +20,7 @@ def os_add(args: argparse.Namespace) -> str: def os_remove(args: argparse.Namespace) -> str: manifest = 'source' \ '' - for source in args.s: + for source in args.sources: manifest += '' + source.strip() + '' manifest += '' @@ -31,7 +31,7 @@ def os_remove(args: argparse.Namespace) -> str: def os_update(args: argparse.Namespace) -> str: manifest = 'source' \ '' - for source in args.s: + for source in args.sources: manifest += '' + source.strip() + '' manifest += '' diff --git a/inbc-program/tests/unit/test_ota_parser.py b/inbc-program/tests/unit/test_ota_parser.py index 06d69f7f6..7d9f1ab25 100644 --- a/inbc-program/tests/unit/test_ota_parser.py +++ b/inbc-program/tests/unit/test_ota_parser.py @@ -1,3 +1,4 @@ +import pytest from datetime import datetime from unittest import TestCase from inbc.parser.parser import ArgsParser @@ -138,22 +139,21 @@ def test_aota_docker_compose_down_manifest_pass(self) -> None: @patch('inbc.command.ota_command.FotaCommand.invoke_update') @patch('sys.stderr', new_callable=StringIO) def test_raise_invalid_fota_date_format(self, mock_stderr, mock_trigger) -> None: - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): self.arg_parser.parse_args( ['fota', '-u', 'https://abc.com/test.tar', '-r', '12-31-2024', '-m', 'Intel', '--target', '123ABC', '456DEF']) - self.assertRegexpMatches(mock_stderr.getvalue(), r"Not a valid date - format YYYY-MM-DD:") + assert "Not a valid date - format YYYY-MM-DD:" in str(mock_stderr.getvalue()) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.reconnect') @patch('sys.stderr', new_callable=StringIO) def test_raise_too_long_fota_signature(self, mock_stderr, mock_reconnect) -> None: - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): self.arg_parser.parse_args(['fota', '-u', 'https://abc.com/test.tar', '-r', '2024-12-31', '-m', 'Intel', '-s', OVER_ONE_THOUSAND_CHARACTER_STRING]) - self.assertRegexpMatches(mock_stderr.getvalue( - ), r"Signature is greater than allowed string size") + assert "Signature is greater than allowed string size" in str(mock_stderr.getvalue()) @patch('threading.Thread.start') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.reconnect') @@ -179,18 +179,18 @@ def test_create_fota_manifest_clean_input(self, mock_start, m_broker, m_pass, m_ @patch('sys.stderr', new_callable=StringIO) def test_raise_invalid_pota_sota_release_date_format(self, mock_stderr) -> None: - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): self.arg_parser.parse_args( ['pota', '-fp', './fip.bin', '-sp', './temp/test.mender', '-r', '2024-12-31', '-sr', '12-31-2024']) - self.assertRegexpMatches(mock_stderr.getvalue(), r"Not a valid date - format YYYY-MM-DD:") + assert "Not a valid date - format YYYY-MM-DD:" in str(mock_stderr.getvalue()) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.reconnect') @patch('sys.stderr', new_callable=StringIO) def test_raise_invalid_pota_fota_release_date_format(self, mock_stderr, mock_reconnect) -> None: - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): self.arg_parser.parse_args( ['pota', '-fp', './fip.bin', '-sp', './temp/test.mender', '-r', '12-25-2021']) - self.assertRegexpMatches(mock_stderr.getvalue(), r"Not a valid date - format YYYY-MM-DD:") + assert "Not a valid date - format YYYY-MM-DD:" in str(mock_stderr.getvalue()) def test_create_ubuntu_update_manifest(self) -> None: s = self.arg_parser.parse_args(['sota']) @@ -252,10 +252,10 @@ def test_create_expected_manifest_from_fota(self, mock_dmi) -> None: @patch('sys.stderr', new_callable=StringIO) def test_raise_invalid_sota_release_date_format(self, mock_stderr) -> None: - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit) : self.arg_parser.parse_args( ['sota', '-u', 'https://abc.com/test.mender', '-r', '12-31-2024']) - self.assertRegexpMatches(mock_stderr.getvalue(), r"Not a valid date - format YYYY-MM-DD:") + assert "Not a valid date - format YYYY-MM-DD:" in str(mock_stderr.getvalue()) @patch('inbc.parser.ota_parser._gather_system_details', return_value=PlatformInformation(datetime(2011, 10, 13), 'Intel Corporation', 'ADLSFWI1.R00', diff --git a/inbc-program/tests/unit/test_source_app_parser.py b/inbc-program/tests/unit/test_source_app_parser.py index 605172092..43cfbaeb8 100644 --- a/inbc-program/tests/unit/test_source_app_parser.py +++ b/inbc-program/tests/unit/test_source_app_parser.py @@ -13,72 +13,77 @@ def setUp(self): def test_parse_add_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'application', 'add', - '-gpgKeyPath', 'https://repositories.intel.com/gpu/intel-graphics.key', - '-gpgKeyName', 'intel-graphics.gpg', - '-source', 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"', - '-fileName', 'intel-gpu-jammy.list']) - self.assertEqual(f.gkp, 'https://repositories.intel.com/gpu/intel-graphics.key') - self.assertEqual(f.gkn, 'intel-graphics.gpg') - self.assertEqual(f.s, 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"') - self.assertEqual(f.f, 'intel-gpu-jammy.list') + '--gpgKeyUri', 'https://repositories.intel.com/gpu/intel-graphics.key', + '--gpgKeyName', 'intel-graphics.gpg', + '--sources', 'deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main', + '--filename', 'intel-gpu-jammy.list']) + self.assertEqual(f.gpgKeyUri, 'https://repositories.intel.com/gpu/intel-graphics.key') + self.assertEqual(f.gpgKeyName, 'intel-graphics.gpg') + self.assertEqual(f.sources, ['deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main']) + self.assertEqual(f.filename, 'intel-gpu-jammy.list') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_add_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'application', 'add', - '-gpgKeyPath', 'https://repositories.intel.com/gpu/intel-graphics.key', - '-gpgKeyName', 'intel-graphics.gpg', - '-source', 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"', - '-fileName', 'intel-gpu-jammy.list']) + '--gpgKeyUri', 'https://repositories.intel.com/gpu/intel-graphics.key', + '--gpgKeyName', 'intel-graphics.gpg', + '--sources', 'deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main', + '--filename', 'intel-gpu-jammy.list']) Inbc(p, 'source', False) expected = 'source' \ - 'https://repositories.intel.com/gpu/intel-graphics.key' \ - 'intel-graphics.gpg' \ - 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"' \ - 'intel-gpu-jammy.list' + 'https://repositories.intel.com/gpu/intel-graphics.key' \ + 'intel-graphics.gpg' \ + 'deb http://example.com/ focal main restricted universe' \ + 'deb-src http://example.com/ focal-security main' \ + 'intel-gpu-jammy.list' self.assertEqual(p.func(p), expected) def test_parse_remove_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'application', 'remove', - '-gpgKeyId', '46C1680FC119E61A501811823A319F932D945953', - '-fileName', 'intel-gpu-jammy.list']) - self.assertEqual(f.gki, '46C1680FC119E61A501811823A319F932D945953') - self.assertEqual(f.f, 'intel-gpu-jammy.list') + '--gpgKeyName', 'intel-gpu-jammy.gpg', + '--filename', 'intel-gpu-jammy.list']) + self.assertEqual(f.gpgKeyName, 'intel-gpu-jammy.gpg') + self.assertEqual(f.filename, 'intel-gpu-jammy.list') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_remove_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'application', 'remove', - '-gpgKeyId', '46C1680FC119E61A501811823A319F932D945953', - '-fileName', 'intel-gpu-jammy.list']) + '--gpgKeyName', 'intel-gpu-jammy.gpg', + '--filename', 'intel-gpu-jammy.list']) Inbc(p, 'source', False) expected = 'source' \ - '46C1680FC119E61A501811823A319F932D945953' \ + 'intel-gpu-jammy.gpg' \ 'intel-gpu-jammy.list' self.assertEqual(p.func(p), expected) def test_parse_update_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'application', 'update', - '-source', - 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"', - '-fileName', 'intel-gpu-jammy.list']) - self.assertEqual(f.s, - 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"') - self.assertEqual(f.f, 'intel-gpu-jammy.list') + '--sources', 'deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main', + '--filename', 'intel-gpu-jammy.list']) + self.assertEqual(f.sources, ['deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main']) + self.assertEqual(f.filename, 'intel-gpu-jammy.list') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_update_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'application', 'update', - '-source', 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"', - '-fileName', 'intel-gpu-jammy.list']) + '--sources', 'deb http://example.com/ focal main restricted universe', + 'deb-src http://example.com/ focal-security main', + '--filename', 'intel-gpu-jammy.list']) Inbc(p, 'source', False) expected = 'source' \ - '' \ - 'echo "deb https://repositories.intel.com/gpu/ubuntu jammy/production/2328 unified"' \ - 'intel-gpu-jammy.list' + 'deb http://example.com/ focal main restricted universe' \ + 'deb-src http://example.com/ focal-security main' \ + 'intel-gpu-jammy.list' self.assertEqual(p.func(p), expected) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') diff --git a/inbc-program/tests/unit/test_source_os_parser.py b/inbc-program/tests/unit/test_source_os_parser.py index 6389cc62e..07f6f0d04 100644 --- a/inbc-program/tests/unit/test_source_os_parser.py +++ b/inbc-program/tests/unit/test_source_os_parser.py @@ -17,16 +17,16 @@ def setUp(self): def test_parse_add_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'os', 'add', - '-sources', 'deb http://example.com/ focal main restricted universe', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) - self.assertEqual(f.s, ['deb http://example.com/ focal main restricted universe', + self.assertEqual(f.sources, ['deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_add_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'os', 'add', - '-sources', 'deb http://example.com/ focal main restricted universe', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) Inbc(p, 'source', False) expected = 'source' \ @@ -39,16 +39,16 @@ def test_create_add_manifest_successfully(self, m_connect): def test_parse_remove_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'os', 'remove', - '-sources', 'deb http://example.com/ focal main restricted universe', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) - self.assertEqual(f.s, ['deb http://example.com/ focal main restricted universe', + self.assertEqual(f.sources, ['deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_remove_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'os', 'remove', - '-sources', 'deb http://example.com/ focal main restricted universe', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) Inbc(p, 'source', False) expected = 'source' \ @@ -61,17 +61,17 @@ def test_create_remove_manifest_successfully(self, m_connect): def test_parse_update_arguments_successfully(self): f = self.arg_parser.parse_args( ['source', 'os', 'update', - '-sources', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) - self.assertEqual(f.s, ['deb http://example.com/ focal main restricted universe', + self.assertEqual(f.sources, ['deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') def test_create_update_manifest_successfully(self, m_connect): p = self.arg_parser.parse_args( ['source', 'os', 'update', - '-sources', 'deb http://example.com/ focal main restricted universe', + '--sources', 'deb http://example.com/ focal main restricted universe', 'deb-src http://example.com/ focal-security main']) Inbc(p, 'source', False) expected = 'source' \ diff --git a/inbm-lib/.flakehell_baseline b/inbm-lib/.flakehell_baseline index 75a5a32a7..32b63ace7 100644 --- a/inbm-lib/.flakehell_baseline +++ b/inbm-lib/.flakehell_baseline @@ -20,4 +20,4 @@ f13954c7604ce46048287bab5ace9189 3b9b31bef09601073051011de3d36394 69d09238e7b96e67f5e4128d10b408f4 4a6598d3ba8e439b240626375d07b6cf -436f2af94a646b36baa3bf9d3a32a1d8 +436f2af94a646b36baa3bf9d3a32a1d8 \ No newline at end of file diff --git a/inbm-lib/autopep8.sh b/inbm-lib/format-python.sh similarity index 100% rename from inbm-lib/autopep8.sh rename to inbm-lib/format-python.sh diff --git a/inbm-lib/inbm_common_lib/utility.py b/inbm-lib/inbm_common_lib/utility.py index 50b7f15af..c2abac78f 100644 --- a/inbm-lib/inbm_common_lib/utility.py +++ b/inbm-lib/inbm_common_lib/utility.py @@ -106,20 +106,36 @@ def _check_paths(src: str, destination: str) -> None: raise IOError("Security error: Destination is a symlink") -def remove_file(path: Union[str, Path]) -> None: +def remove_file(path: Union[str, Path]) -> bool: """ Remove file from the given path @param path: location of file to be removed """ canonical_path = get_canonical_representation_of_path(str(path)) if not os.path.exists(canonical_path): - return + return False if os.path.isfile(canonical_path): logger.debug(f"Removing file at {canonical_path}.") os.remove(canonical_path) + return True else: logger.warning("Failed to remove file. Path is a directory.") + return False + + +def create_file_with_contents(path: Union[str, Path], contents: List[str]) -> None: + """ Create a file and add the contents by line + + @param path: location of file to create + @param contents: each item in the list is a line to add to the file + """ + try: + canonical_path = get_canonical_representation_of_path(str(path)) + with open(canonical_path, 'w') as f: + f.writelines([string + '\n' for string in contents]) + except (PermissionError, IsADirectoryError, OSError) as e: + raise IOError(f"Error while writing file: {str(e)}") def remove_file_list(path: List[str]) -> None: @@ -188,6 +204,7 @@ def is_within_directory(directory: str, target: str) -> bool: return prefix == abs_directory + def safe_extract(tarball: tarfile.TarFile, path: str = ".", members: Optional[Iterable[tarfile.TarInfo]] = None, *, numeric_owner: bool = False) -> None: """Avoid path traversal when extracting tarball diff --git a/inbm-lib/inbm_lib/constants.py b/inbm-lib/inbm_lib/constants.py index bfc9cc9d0..00939634c 100644 --- a/inbm-lib/inbm_lib/constants.py +++ b/inbm-lib/inbm_lib/constants.py @@ -22,7 +22,7 @@ CHROOT_PREFIX = "/usr/sbin/chroot /host " # XML parse time limit -PARSE_TIME_SECS = 5 +PARSE_TIME_SECS = 10 # TRTL install location TRTL_PATH = get_canonical_representation_of_path('/usr/bin/trtl') @@ -82,7 +82,7 @@ # OTA STATUS OTA_SUCCESS = "SUCCESS" -OTA_FAIL = "FAIL" +FAIL = "FAIL" OTA_PENDING = "PENDING" FORMAT_VERSION = "v1" diff --git a/inbm-lib/inbm_lib/trtl.py b/inbm-lib/inbm_lib/trtl.py index ed5b296a2..e0f62abe0 100644 --- a/inbm-lib/inbm_lib/trtl.py +++ b/inbm-lib/inbm_lib/trtl.py @@ -13,7 +13,6 @@ from typing import Optional, Tuple import logging -import pipes from .constants import DOCKER, COMPOSE, TRTL_PATH from subprocess import Popen, PIPE @@ -150,7 +149,7 @@ def execute(self, image: str, version: int, opt: bool = False) -> Tuple[str, Opt ", " + str(version) + ", [" + str(command) + "])") (out, err, code) = self.runner.run(self._boilerplate("exec") + " -in=" + image + " -iv=" + str(version) + - " -ec=" + pipes.quote(command)) + " -ec=" + shlex.quote(command)) if err is None: err = "" logger.debug("(2/2) Stdout: [" + out + "]" + "; stderr: [" + err + diff --git a/inbm-lib/pyproject.toml b/inbm-lib/pyproject.toml index d593e6927..a2aeb4903 100644 --- a/inbm-lib/pyproject.toml +++ b/inbm-lib/pyproject.toml @@ -1,5 +1,4 @@ [tool.flakeheaven] -baseline = ".flakehell_baseline" format = "default" show_source = true statistics = false diff --git a/inbm-lib/tests/unit/inbm_common_lib/test_utility.py b/inbm-lib/tests/unit/inbm_common_lib/test_utility.py index d64d1552d..ec1c8a661 100644 --- a/inbm-lib/tests/unit/inbm_common_lib/test_utility.py +++ b/inbm-lib/tests/unit/inbm_common_lib/test_utility.py @@ -1,10 +1,10 @@ import shutil -from unittest.mock import patch, Mock +from unittest.mock import patch, Mock, mock_open from unittest import TestCase from inbm_common_lib.exceptions import UrlSecurityException from inbm_common_lib.utility import clean_input, get_canonical_representation_of_path, canonicalize_uri, \ - validate_file_type, remove_file, copy_file, move_file + validate_file_type, remove_file, copy_file, move_file, create_file_with_contents class TestUtility(TestCase): @@ -94,3 +94,18 @@ def test_move_file_throw_exception(self, os_path: Mock) -> None: def test_raises_when_move_src_is_symlink(self, mock_is_symlink: Mock) -> None: with self.assertRaises(IOError): move_file('/home/usr', '/etc') + + def test_create_file_with_contents_successfully(self) -> None: + try: + m = mock_open() + lines = ['line1', 'line2'] + with patch('builtins.open', m) as m_open: + create_file_with_contents('/etc/apt/sources.list.d/docker.list', lines) + + m_open.assert_called_once_with('/etc/apt/sources.list.d/docker.list', 'w') + + handle = m() + handle.writelines.assert_called_once_with([line + "\n" for line in lines]) + + except IOError as e: + self.fail(f"Unexpected exception raised during test: {e}") diff --git a/inbm-lib/tests/unit/inbm_lib/manifest_schema.xsd b/inbm-lib/tests/unit/inbm_lib/manifest_schema.xsd deleted file mode 100644 index b30422c24..000000000 --- a/inbm-lib/tests/unit/inbm_lib/manifest_schema.xsd +++ /dev/null @@ -1,467 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/inbm-lib/tests/unit/inbm_lib/test_xmlparser.py b/inbm-lib/tests/unit/inbm_lib/test_xmlparser.py index b783ab35e..c95c95f14 100644 --- a/inbm-lib/tests/unit/inbm_lib/test_xmlparser.py +++ b/inbm-lib/tests/unit/inbm_lib/test_xmlparser.py @@ -4,8 +4,20 @@ from inbm_lib.xmlhandler import XmlHandler, XmlException import os -TEST_SCHEMA_LOCATION = os.path.join(os.path.dirname(__file__), - 'manifest_schema.xsd') +TEST_SCHEMA_LOCATION = os.path.join( + os.path.dirname(__file__), + '..', + '..', + '..', + '..', + 'inbm', + 'dispatcher-agent', + 'fpm-template', + 'usr', + 'share', + 'dispatcher-agent', + 'manifest_schema.xsd', + ) GOOD_XML = '' \ 'ota
sampleIdSample FOTA' \ diff --git a/inbm/Changelog.md b/inbm/Changelog.md index 4a75c6935..8047b01b4 100644 --- a/inbm/Changelog.md +++ b/inbm/Changelog.md @@ -8,8 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Changed - RTC 536078 - Added package list option to inbc, cloud, and internal manifest. This allows SOTA to run an install/upgrade command on a set of individual packages rather than all installed packages. -#Added -- RTC 536603, 536604 -[source] Create os and application based manifest +### Added + - RTC 536601 - Added 'source' command to INBM. This command manages `/etc/apt/sources.list` and `/etc/apt/sources.list.d/*` and associated gpg keys on Ubuntu. ### Fixed - RTC 534426 - Could not write to /var/log/inbm-update-status.log on Yocto due to /var/log being a symlink to /var/volatile/log. diff --git a/inbm/dispatcher-agent/.gitignore b/inbm/dispatcher-agent/.gitignore index ecdaa2e20..516d08c19 100644 --- a/inbm/dispatcher-agent/.gitignore +++ b/inbm/dispatcher-agent/.gitignore @@ -100,3 +100,6 @@ fpm-files/ # test coverage report cover/ + +inbm_lib +inbm_common_lib diff --git a/inbm/dispatcher-agent/dispatcher/dispatcher_class.py b/inbm/dispatcher-agent/dispatcher/dispatcher_class.py index 78ae65b88..a88cc1009 100644 --- a/inbm/dispatcher-agent/dispatcher/dispatcher_class.py +++ b/inbm/dispatcher-agent/dispatcher/dispatcher_class.py @@ -30,12 +30,13 @@ from inbm_common_lib.dmi import is_dmi_path_exists, get_dmi_system_info from inbm_common_lib.device_tree import get_device_tree_system_info from inbm_common_lib.platform_info import PlatformInformation -from inbm_lib.constants import QUERY_CMD_CHANNEL, OTA_SUCCESS, OTA_FAIL +from inbm_lib.constants import QUERY_CMD_CHANNEL, OTA_SUCCESS, FAIL from .aota.aota_error import AotaError +from .source.source_exception import SourceError from .common import dispatcher_state from .common.result_constants import CODE_OK, CODE_BAD_REQUEST, CODE_MULTIPLE, \ - CONFIG_LOAD_FAIL_WRONG_PATH, CODE_FOUND + CODE_FOUND from .config_dbs import ConfigDbs from .constants import * from .device_manager.device_manager import get_device_manager @@ -337,21 +338,26 @@ def do_install(self, xml: str, schema_location: Optional[str] = None) -> Result: except (DispatcherException, UrlSecurityException) as error: logger.error(error) result = Result(CODE_BAD_REQUEST, f'Error during install: {error}') - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL + self._update_logger.error = str(error) + except SourceError as error: + logger.error(error) + result = Result(CODE_BAD_REQUEST, f'Error changing sources files: {error}') + self._update_logger.status = FAIL self._update_logger.error = str(error) except XmlException as error: result = Result(CODE_MULTIPLE, f'Error parsing/validating manifest: {error}') - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL self._update_logger.error = str(error) except (AotaError, FotaError, SotaError) as e: result = Result(CODE_BAD_REQUEST, str(e)) - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL self._update_logger.error = str(e) finally: logger.info('Install result: %s', str(result)) self._send_result(str(result)) if result.status != CODE_OK and parsed_head: - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL self._update_logger.error = str(result) self.invoke_workload_orchestration_check(True, type_of_manifest, parsed_head) @@ -650,8 +656,8 @@ def check_sota_state(self) -> None: self._telemetry( 'On Boot, Diagnostics reports some services not up after previous SOTA') self.invoke_sota(action='diagnostic_system_unhealthy', snapshot=None) - self._update_logger.update_log(OTA_FAIL) - logger.info(OTA_FAIL) + self._update_logger.update_log(FAIL) + logger.info(FAIL) def check_fota_state(self, fota_state: dispatcher_state.DispatcherState) -> None: """This method checks the FOTA info in dispatcher state file and validates the release date @@ -661,7 +667,7 @@ def check_fota_state(self, fota_state: dispatcher_state.DispatcherState) -> None @params fota_state: The consumed information from the dispatcher state file. """ # If all the checks pass, the OTA status changes to SUCCESS at the end. - self._update_logger.update_log(OTA_FAIL) + self._update_logger.update_log(FAIL) os_type = platform.system() platform_info = PlatformInformation() diff --git a/inbm/dispatcher-agent/dispatcher/sota/sota.py b/inbm/dispatcher-agent/dispatcher/sota/sota.py index 1f56ba417..128cd6758 100644 --- a/inbm/dispatcher-agent/dispatcher/sota/sota.py +++ b/inbm/dispatcher-agent/dispatcher/sota/sota.py @@ -7,7 +7,6 @@ import logging import os -import re import time from typing import Any, List, Optional, Union, Mapping @@ -17,7 +16,7 @@ from inbm_common_lib.constants import REMOTE_SOURCE, LOCAL_SOURCE from inbm_lib.validate_package_list import parse_and_validate_package_list from inbm_lib.detect_os import detect_os -from inbm_lib.constants import OTA_PENDING, OTA_FAIL, OTA_SUCCESS +from inbm_lib.constants import OTA_PENDING, FAIL, OTA_SUCCESS from dispatcher.dispatcher_exception import DispatcherException from .command_handler import run_commands, print_execution_summary, get_command_status @@ -244,7 +243,7 @@ def execute(self, proceed_without_rollback: bool, skip_sleeps: bool = False) -> rebooter = self.factory.create_rebooter() if self.sota_state == 'diagnostic_system_unhealthy': - self._update_logger.update_log(OTA_FAIL) + self._update_logger.update_log(FAIL) snapshot.revert(rebooter, time_to_wait_before_reboot) elif self.sota_state == 'diagnostic_system_healthy': try: @@ -257,7 +256,7 @@ def execute(self, proceed_without_rollback: bool, skip_sleeps: bool = False) -> msg = "FAILED INSTALL: System has not been properly updated; reverting." logger.debug(str(e)) self._dispatcher_broker.send_result(msg) - self._update_logger.update_log(OTA_FAIL) + self._update_logger.update_log(FAIL) snapshot.revert(rebooter, time_to_wait_before_reboot) else: self.execute_from_manifest(setup_helper=setup_helper, @@ -340,7 +339,7 @@ def execute_from_manifest(self, '{"status": 400, "message": "SOTA command status: FAILURE"}') if download_success and self.sota_mode != 'download-only': snapshotter.recover(rebooter, time_to_wait_before_reboot) - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL self._update_logger.error = "" self._update_logger.save_log() raise SotaError(str(msg)) @@ -366,7 +365,7 @@ def execute_from_manifest(self, else: # Save the log before reboot - self._update_logger.status = OTA_FAIL + self._update_logger.status = FAIL self._update_logger.error = "" self._update_logger.save_log() self._dispatcher_broker.telemetry(SOTA_FAILURE) diff --git a/inbm/dispatcher-agent/dispatcher/source/constants.py b/inbm/dispatcher-agent/dispatcher/source/constants.py index 5680472fa..3d7d3f94c 100644 --- a/inbm/dispatcher-agent/dispatcher/source/constants.py +++ b/inbm/dispatcher-agent/dispatcher/source/constants.py @@ -1,15 +1,15 @@ """ - Copyright (C) 2023 Intel Corporation + Copyright (C) 2024 Intel Corporation SPDX-License-Identifier: Apache-2.0 """ from enum import Enum, unique from dataclasses import dataclass, field -from typing import List UBUNTU_APT_SOURCES_LIST = "/etc/apt/sources.list" UBUNTU_APT_SOURCES_LIST_D = "/etc/apt/sources.list.d" +LINUX_GPG_KEY_PATH = "/usr/share/keyrings" @dataclass(kw_only=True, frozen=True) @@ -18,27 +18,29 @@ class ApplicationSourceList: sources: list[str] -@dataclass(kw_only=True) +@dataclass(kw_only=True, frozen=True) class SourceParameters: - sources: List[str] = field(default_factory=lambda: []) + sources: list[str] = field(default_factory=lambda: []) -@dataclass(kw_only=True) -class ApplicationAddSourceParameters(SourceParameters): - gpg_key_path: str +@dataclass(kw_only=True, frozen=True) +class ApplicationAddSourceParameters: + gpg_key_uri: str gpg_key_name: str file_name: str + sources: list[str] = field(default_factory=lambda: []) -@dataclass(kw_only=True) +@dataclass(kw_only=True, frozen=True) class ApplicationRemoveSourceParameters: - gpg_key_id: str + gpg_key_name: str file_name: str -@dataclass(kw_only=True) -class ApplicationUpdateSourceParameters(SourceParameters): +@dataclass(kw_only=True, frozen=True) +class ApplicationUpdateSourceParameters: file_name: str + sources: list[str] = field(default_factory=lambda: []) @unique diff --git a/inbm/dispatcher-agent/dispatcher/source/linux_gpg_key.py b/inbm/dispatcher-agent/dispatcher/source/linux_gpg_key.py new file mode 100644 index 000000000..ba1fe4be1 --- /dev/null +++ b/inbm/dispatcher-agent/dispatcher/source/linux_gpg_key.py @@ -0,0 +1,64 @@ +""" + Copyright (C) 2024 Intel Corporation + SPDX-License-Identifier: Apache-2.0 +""" +import logging +import subprocess +import os +import requests + +from .source_exception import SourceError +from .constants import LINUX_GPG_KEY_PATH + +logger = logging.getLogger(__name__) + + +def remove_gpg_key_if_exists(gpg_key_name: str) -> None: + """Linux - Removes a GPG key file if it exists + + @param gpg_key_name: name of GPG key file to remove (file under LINUX_GPG_KEY_PATH) + """ + try: + key_path = os.path.join(LINUX_GPG_KEY_PATH, gpg_key_name) + if os.path.exists(key_path): + os.remove(key_path) + # it's OK if the key is not there + + except OSError as e: + raise SourceError(f"Error checking or deleting GPG key: {gpg_key_name}") from e + + +def add_gpg_key(remote_key_path: str, key_store_name: str) -> None: + """Linux - Adds a GPG key from a remote source + + Raises SourceError if there are any problems. + + @param remote_key_path: Remote location of the GPG key to download + @param key_store_name: Name to use to store the GPG under LINUX_GPG_KEY_PATH + """ + + try: + # Download the GPG key from the remote source + response = requests.get(remote_key_path) + response.raise_for_status() + + decoded_ascii = response.content.decode("utf-8", errors="strict") + + key_path = os.path.join(LINUX_GPG_KEY_PATH, key_store_name) + + # Use gpg to dearmor the key and save it to the key store path + subprocess.run( + ["/usr/bin/gpg", "--dearmor", "--output", key_path], + input=decoded_ascii, + check=True, + text=True, + shell=False, + ) + + logger.info(f"GPG key added to {key_store_name}") + + except requests.exceptions.RequestException as e: + raise SourceError(f"Error getting GPG key from remote source: {e}") + + except subprocess.CalledProcessError as e: + raise SourceError(f"Error running gpg command to dearmor key: {e}") diff --git a/inbm/dispatcher-agent/dispatcher/source/source_command.py b/inbm/dispatcher-agent/dispatcher/source/source_command.py index 51c6b7aa6..2f90e0e7e 100644 --- a/inbm/dispatcher-agent/dispatcher/source/source_command.py +++ b/inbm/dispatcher-agent/dispatcher/source/source_command.py @@ -1,12 +1,20 @@ """ - Copyright (C) 2023 Intel Corporation + Copyright (C) 2024 Intel Corporation SPDX-License-Identifier: Apache-2.0 """ +from dataclasses import asdict import logging import json from dispatcher.common.result_constants import Result -from dispatcher.source.constants import ApplicationRemoveSourceParameters, OsType, SourceParameters +from dispatcher.source.constants import ( + ApplicationAddSourceParameters, + ApplicationRemoveSourceParameters, + ApplicationUpdateSourceParameters, + OsType, + SourceParameters, +) + from dispatcher.source.source_manager_factory import create_os_source_manager from dispatcher.source.source_manager_factory import create_application_source_manager from inbm_lib.xmlhandler import XmlException, XmlHandler @@ -25,14 +33,14 @@ def do_source_command(parsed_head: XmlHandler, os_type: OsType) -> Result: logger.debug(f"do_source_command: {parsed_head}") try: - os_action = parsed_head.get_children('osSource') + os_action = parsed_head.get_children("osSource") if os_action: return _handle_os_source_command(parsed_head, os_type, os_action) except XmlException: pass # If we get an XmlException here we still want to try applicationSource try: - app_action = parsed_head.get_children('applicationSource') + app_action = parsed_head.get_children("applicationSource") if app_action: return _handle_app_source_command(parsed_head, os_type, app_action) except XmlException as e: @@ -52,19 +60,42 @@ def _handle_os_source_command(parsed_head: XmlHandler, os_type: OsType, os_actio """ os_source_manager = create_os_source_manager(os_type) - if os_action == {'list': ''}: + if "list" in os_action: return Result(status=200, message=json.dumps(os_source_manager.list())) - if os_action == {'remove': 'remove'}: - source_pkgs = _get_source_packages(parsed_head) - remove_parameters = SourceParameters(sources=source_pkgs) + if "remove" in os_action: + remove_source_pkgs: list[str] = [] + for key, value in parsed_head.get_children_tuples("osSource/remove/repos"): + if key == "source_pkg": + remove_source_pkgs.append(value) + remove_parameters = SourceParameters(sources=remove_source_pkgs) os_source_manager.remove(remove_parameters) return Result(status=200, message="SUCCESS") + if "add" in os_action: + add_source_pkgs: list[str] = [] + for key, value in parsed_head.get_children_tuples("osSource/add/repos"): + if key == "source_pkg": + add_source_pkgs.append(value) + add_parameters = SourceParameters(sources=add_source_pkgs) + os_source_manager.add(add_parameters) + return Result(status=200, message="SUCCESS") + + if "update" in os_action: + update_source_pkgs: list[str] = [] + for key, value in parsed_head.get_children_tuples("osSource/update/repos"): + if key == "source_pkg": + update_source_pkgs.append(value) + update_parameters = SourceParameters(sources=update_source_pkgs) + os_source_manager.update(update_parameters) + return Result(status=200, message="SUCCESS") + return Result(status=400, message="unknown os source command") -def _handle_app_source_command(parsed_head: XmlHandler, os_type: OsType, app_action: dict) -> Result: +def _handle_app_source_command( + parsed_head: XmlHandler, os_type: OsType, app_action: dict +) -> Result: """ Handle the application source commands. @@ -75,28 +106,53 @@ def _handle_app_source_command(parsed_head: XmlHandler, os_type: OsType, app_act """ application_source_manager = create_application_source_manager(os_type) - if app_action == {'list': ''}: - return Result(status=200, message=json.dumps(application_source_manager.list())) + if "list" in app_action: + serialized_list = json.dumps( + [asdict(app_source) for app_source in application_source_manager.list()] + ) + return Result(status=200, message=serialized_list) - if app_action == {'remove': 'remove'}: - keyid = parsed_head.get_children('applicationSource/remove/gpg')['keyid'] - filename = parsed_head.get_children('applicationSource/remove/repo')['filename'] + if "remove" in app_action: + keyname = parsed_head.get_children("applicationSource/remove/gpg")["keyname"] + filename = parsed_head.get_children("applicationSource/remove/repo")["filename"] application_source_manager.remove( - ApplicationRemoveSourceParameters(file_name=filename, gpg_key_id=keyid)) + ApplicationRemoveSourceParameters(file_name=filename, gpg_key_name=keyname) + ) return Result(status=200, message="SUCCESS") - return Result(status=400, message="unknown application source command") - + if "add" in app_action: + gpg_key_uri = parsed_head.get_children("applicationSource/add/gpg")["uri"] + gpg_key_name = parsed_head.get_children("applicationSource/add/gpg")["keyname"] + repo_filename = parsed_head.get_children("applicationSource/add/repo")["filename"] + + add_source_pkgs: list[str] = [] + for key, value in parsed_head.get_children_tuples("applicationSource/add/repo/repos"): + if key == "source_pkg": + add_source_pkgs.append(value) + + application_source_manager.add( + ApplicationAddSourceParameters( + file_name=repo_filename, + gpg_key_name=gpg_key_name, + gpg_key_uri=gpg_key_uri, + sources=add_source_pkgs, + ) + ) + return Result(status=200, message="SUCCESS") -def _get_source_packages(parsed_head: XmlHandler) -> list[str]: - """ - Extract source packages from XML command. + if "update" in app_action: + repo_filename = parsed_head.get_children("applicationSource/update/repo")["filename"] + update_source_pkgs: list[str] = [] + for key, value in parsed_head.get_children_tuples("applicationSource/update/repo/repos"): + if key == "source_pkg": + update_source_pkgs.append(value) + + application_source_manager.update( + ApplicationUpdateSourceParameters( + file_name=repo_filename, + sources=update_source_pkgs, + ) + ) + return Result(status=200, message="SUCCESS") - @param parsed_head: XmlHandler containing the remove source commands - @return List of packages to remove - """ - source_pkgs: list[str] = [] - for key, value in parsed_head.get_children_tuples('osSource/remove/repos'): - if key == 'source_pkg': - source_pkgs.append(value) - return source_pkgs + return Result(status=400, message="unknown application source command") diff --git a/inbm/dispatcher-agent/dispatcher/source/source_exception.py b/inbm/dispatcher-agent/dispatcher/source/source_exception.py new file mode 100644 index 000000000..66279e786 --- /dev/null +++ b/inbm/dispatcher-agent/dispatcher/source/source_exception.py @@ -0,0 +1,13 @@ +""" + Central communication agent in the manageability framework responsible + for issuing commands and signals to other tools/agents + + Copyright (C) 2024 Intel Corporation + SPDX-License-Identifier: Apache-2.0 +""" + + +class SourceError(Exception): + """Class exception Module""" + + pass diff --git a/inbm/dispatcher-agent/dispatcher/source/source_manager.py b/inbm/dispatcher-agent/dispatcher/source/source_manager.py index 8d825d0d0..58025fda4 100644 --- a/inbm/dispatcher-agent/dispatcher/source/source_manager.py +++ b/inbm/dispatcher-agent/dispatcher/source/source_manager.py @@ -1,5 +1,5 @@ """ - Copyright (C) 2023 Intel Corporation + Copyright (C) 2024 Intel Corporation SPDX-License-Identifier: Apache-2.0 """ diff --git a/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py b/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py index 54d1f1db2..0f11c4b55 100644 --- a/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py +++ b/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py @@ -1,7 +1,7 @@ """ Creates concrete classes based on OS Type and type of source file being manipulated. - Copyright (C) 2023 Intel Corporation + Copyright (C) 2024 Intel Corporation SPDX-License-Identifier: Apache-2.0 """ import logging diff --git a/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py b/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py index fbece955d..220b8be3f 100644 --- a/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py +++ b/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py @@ -1,12 +1,13 @@ """ - Copyright (C) 2023 Intel Corporation + Copyright (C) 2024 Intel Corporation SPDX-License-Identifier: Apache-2.0 """ import glob import logging import os -from dispatcher.dispatcher_exception import DispatcherException + +from dispatcher.source.source_exception import SourceError from dispatcher.source.constants import ( UBUNTU_APT_SOURCES_LIST, UBUNTU_APT_SOURCES_LIST_D, @@ -17,7 +18,14 @@ SourceParameters, ) from dispatcher.source.source_manager import ApplicationSourceManager, OsSourceManager -from inbm_common_lib.shell_runner import PseudoShellRunner +from dispatcher.source.linux_gpg_key import remove_gpg_key_if_exists, add_gpg_key + +from inbm_common_lib.utility import ( + get_canonical_representation_of_path, + remove_file, + move_file, + create_file_with_contents, +) logger = logging.getLogger(__name__) @@ -27,9 +35,14 @@ def __init__(self) -> None: pass def add(self, parameters: SourceParameters) -> None: - """Adds a source in the Ubuntu OS source file /etc/apt/sources.list""" - # TODO: Add functionality to add a source file in Ubuntu to /etc/apt/sources.list file - logger.debug(f"sources: {parameters.sources}") + """Adds sources in the Ubuntu OS source file /etc/apt/sources.list""" + + try: + with open(UBUNTU_APT_SOURCES_LIST, "a") as file: + for source in parameters.sources: + file.write(f"{source}\n") + except OSError as e: + raise SourceError(f"Error adding sources: {e}") from e def list(self) -> list[str]: """List deb and deb-src lines in /etc/apt/sources.list""" @@ -44,8 +57,7 @@ def list(self) -> list[str]: line for line in lines if line.startswith("deb ") or line.startswith("deb-src ") ] except OSError as e: - logger.error(f"Error opening source file: {e}") - raise DispatcherException(f"Error opening source file: {e}") from e + raise SourceError(f"Error opening source file: {e}") from e def remove(self, parameters: SourceParameters) -> None: """Removes a source in the Ubuntu OS source file /etc/apt/sources.list""" @@ -66,14 +78,19 @@ def remove(self, parameters: SourceParameters) -> None: logger.debug(f"Removed source: {line}") except OSError as e: - # Wrap any OSError exceptions in a DispatcherException and re-raise. - logger.error(f"Error occurred while trying to remove sources: {e}") - raise DispatcherException(f"Error occurred while trying to remove sources: {e}") from e + raise SourceError(f"Error occurred while trying to remove sources: {e}") from e def update(self, parameters: SourceParameters) -> None: - """Updates a source in the Ubuntu OS source file /etc/apt/sources.list""" - # TODO: Add functionality to update a source in Ubuntu file under /etc/apt/sources.list file - logger.debug(f"sources: {parameters.sources}") + """Updates a source in the Ubuntu OS source file /etc/apt/sources.list + + This will overwrite the file and add the listed sources.""" + + try: + with open(UBUNTU_APT_SOURCES_LIST, "w") as file: + for source in parameters.sources: + file.write(f"{source}\n") + except OSError as e: + raise SourceError(f"Error adding sources: {e}") from e class UbuntuApplicationSourceManager(ApplicationSourceManager): @@ -81,8 +98,16 @@ def __init__(self) -> None: pass def add(self, parameters: ApplicationAddSourceParameters) -> None: - """Adds new application source along with its key""" - pass + # Step 1: Add key + add_gpg_key(parameters.gpg_key_uri, parameters.gpg_key_name) + + # Step 2: Add the source + try: + create_file_with_contents( + os.path.join(UBUNTU_APT_SOURCES_LIST_D, parameters.file_name), parameters.sources + ) + except (IOError, OSError) as e: + raise SourceError(f"Error adding application source list: {e}") def list(self) -> list[ApplicationSourceList]: """List Ubuntu Application source lists under /etc/apt/sources.list.d""" @@ -106,36 +131,38 @@ def list(self) -> list[ApplicationSourceList]: sources.append(new_source) return sources except OSError as e: - logger.error(f"Error listing application sources: {e}") - raise DispatcherException(f"Error listing application sources: {e}") from e + raise SourceError(f"Error listing application sources: {e}") from e def remove(self, parameters: ApplicationRemoveSourceParameters) -> None: - """Removes a source file from the Ubuntu source file list under /etc/apt/sources.list.d""" + """Removes a source file from the Ubuntu source file list under /etc/apt/sources.list.d + @parameters: dataclass parameters for ApplicationRemoveSourceParameters + """ # Remove the GPG key - try: - stdout, stderr, exit_code = PseudoShellRunner().run( - f"gpg --list-keys {parameters.gpg_key_id}" - ) - - # If the key exists, try to remove it - if exit_code == 0: - stdout, stderr, exit_code = PseudoShellRunner().run( - f"gpg --delete-key {parameters.gpg_key_id}" - ) - if exit_code != 0: - raise DispatcherException("Error deleting GPG key: " + (stderr or stdout)) - - except OSError as e: - logger.error(f"Error checking or deleting GPG key: {e}") - raise DispatcherException(f"Error checking or deleting GPG key: {e}") from e + remove_gpg_key_if_exists(parameters.gpg_key_name) # Remove the file under /etc/apt/sources.list.d try: - os.remove(UBUNTU_APT_SOURCES_LIST_D + "/" + parameters.file_name) + if ( + os.path.sep in parameters.file_name + or parameters.file_name == ".." + or parameters.file_name == "." + ): + raise SourceError(f"Invalid file name: {parameters.file_name}") + + if not remove_file( + get_canonical_representation_of_path( + os.path.join(UBUNTU_APT_SOURCES_LIST_D, parameters.file_name) + ) + ): + raise SourceError(f"Error removing file: {parameters.file_name}") except OSError as e: - raise DispatcherException(f"Error removing file: {e}") from e + raise SourceError(f"Error removing file: {e}") from e def update(self, parameters: ApplicationUpdateSourceParameters) -> None: """Updates a source file in Ubuntu OS source file list under /etc/apt/sources.list.d""" - # TODO: Add functionality to update a Ubuntu source file under /etc/apt/sources.list.d - logger.debug(f"file_name: {parameters.file_name}, source: {parameters.sources[0]}") + try: + create_file_with_contents( + os.path.join(UBUNTU_APT_SOURCES_LIST_D, parameters.file_name), parameters.sources + ) + except IOError as e: + raise SourceError(f"Error occurred while trying to update sources: {e}") from e diff --git a/inbm/dispatcher-agent/fpm-template/etc/apparmor.d/usr.bin.inbm-dispatcher b/inbm/dispatcher-agent/fpm-template/etc/apparmor.d/usr.bin.inbm-dispatcher index 7ee31bc6b..6582d7c8e 100644 --- a/inbm/dispatcher-agent/fpm-template/etc/apparmor.d/usr.bin.inbm-dispatcher +++ b/inbm/dispatcher-agent/fpm-template/etc/apparmor.d/usr.bin.inbm-dispatcher @@ -59,7 +59,8 @@ /etc/apt/sources.list.bak rw, /etc/apt/sources.list.d/* rw, /etc/apt/sources.list.d/ rw, - /usr/bin/gpg px, + /usr/bin/gpg rUx, + /bin/gpg rUx, /etc/dispatcher_state rw, /etc/lsb-release r, /etc/magic r, @@ -151,6 +152,7 @@ /usr/share/configuration-agent/iotg_inb_schema.xsd l, /usr/share/configuration-agent/iotg_inb_schema.xsd.dpkg-tmp l, /usr/share/diagnostic-agent/ w, + /usr/share/keyrings/*.gpg w, /usr/share/telemetry-agent/ w, /usr/share/dispatcher-agent/*.json r, /usr/share/dispatcher-agent/manifest_schema.xsd r, @@ -178,6 +180,8 @@ /usr/share/trtl/trtl.xsd r, /usr/share/trtl/ w, /usr/share/trtl/* rw, + /usr/share/keyrings/ rw, + /usr/share/keyrings/* rw, /var/cache/manageability/ rw, /var/cache/manageability/** rw, /etc/intel-manageability/public/configuration-agent/ w, diff --git a/inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd b/inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd index 21c326407..6a8fad59b 100644 --- a/inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd +++ b/inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd @@ -322,16 +322,22 @@ - - + + - - + + + + + + + + @@ -341,10 +347,17 @@ - + - + + + + + + + + @@ -357,14 +370,14 @@ - + - + @@ -375,7 +388,6 @@ - diff --git a/inbm/dispatcher-agent/pyproject.toml b/inbm/dispatcher-agent/pyproject.toml index 705eac0b5..3e82c3d9e 100644 --- a/inbm/dispatcher-agent/pyproject.toml +++ b/inbm/dispatcher-agent/pyproject.toml @@ -40,7 +40,7 @@ max_tuple_unpack_length = 4 [tool.flakeheaven.plugins] "flake8-annotations" = ["-*", "+ANN001", "+ANN2??", "+ANN301"] -"wemake-python-styleguide" = ["-*", "+WPS2??", "-WPS220", "-WPS223", "-WPS222", "-WPS236", "-WPS237", "-WPS238"] # WPS2?? is complexity; 220 is deep nesting; 223 is elifs; 222 is logic; 236 is # of variables unpacked in a tuple; 237 is complex format string; 238 is # of raises in a function +"wemake-python-styleguide" = ["-*", "+WPS2??", "-WPS220", "-WPS223", "-WPS222", "-WPS225", "-WPS236", "-WPS237", "-WPS238"] # WPS2?? is complexity; 220 is deep nesting; 223 is elifs; 222 is logic; 236 is # of variables unpacked in a tuple; 237 is complex format string; 238 is # of raises in a function "flake8-bandit" = ["+*", "-S404", "-S603"] [tool.pytest.ini_options] diff --git a/inbm/dispatcher-agent/tests/unit/sota/test_mender_util.py b/inbm/dispatcher-agent/tests/unit/sota/test_mender_util.py index 1c7dd4e53..c17df2687 100644 --- a/inbm/dispatcher-agent/tests/unit/sota/test_mender_util.py +++ b/inbm/dispatcher-agent/tests/unit/sota/test_mender_util.py @@ -1,8 +1,5 @@ -import os -import tempfile import unittest -import fixtures -from unittest.mock import patch, mock_open +from unittest.mock import patch from dispatcher.sota.mender_util import * diff --git a/inbm/dispatcher-agent/tests/unit/source/test_linux_gpg_key.py b/inbm/dispatcher-agent/tests/unit/source/test_linux_gpg_key.py new file mode 100644 index 000000000..011ae5c58 --- /dev/null +++ b/inbm/dispatcher-agent/tests/unit/source/test_linux_gpg_key.py @@ -0,0 +1,75 @@ +import pytest +from unittest.mock import mock_open, patch + +from requests import RequestException +from dispatcher.dispatcher_exception import DispatcherException +from dispatcher.source.linux_gpg_key import add_gpg_key, remove_gpg_key_if_exists +from dispatcher.source.source_exception import SourceError + + +class TestLinuxGpgKey: + def test_remove_gpg_key_success(self, mocker): + mocker.patch("os.path.join", return_value="dummy/path/to/key") + mocker.patch("os.path.exists", return_value=True) + mock_remove = mocker.patch("os.remove") + + remove_gpg_key_if_exists("mock_key_name") + + mock_remove.assert_called_once_with("dummy/path/to/key") + + def test_remove_gpg_key_os_error(self, mocker): + mocker.patch("os.path.join", return_value="dummy/path/to/key") + mocker.patch("os.path.exists", return_value=True) + + mocker.patch("os.remove", side_effect=OSError("Mock OS Error")) + + with pytest.raises(SourceError) as e: + remove_gpg_key_if_exists("mock_key_name") + + assert "Error checking or deleting GPG key: mock_key_name" in str(e.value) + + def test_add_gpg_key_success(self, mocker): + mock_get = mocker.patch("dispatcher.source.linux_gpg_key.requests.get", autospec=True) + mocker.patch( + "dispatcher.source.linux_gpg_key.requests.Response.raise_for_status", autospec=True + ) + mock_get.return_value.content = b"some-gpg-key-data" + + mock_run = mocker.patch("dispatcher.source.linux_gpg_key.subprocess.run", autospec=True) + + remote_key_path = "https://example.com/key.gpg" + key_store_path = "/etc/apt/trusted.gpg.d/my_key.gpg" + add_gpg_key(remote_key_path, key_store_path) + + mock_get.assert_called_once_with(remote_key_path) + mock_run.assert_called_once_with( + ["/usr/bin/gpg", "--dearmor", "--output", key_store_path], + input="some-gpg-key-data", + check=True, + text=True, + shell=False, + ) + + def test_add_gpg_key_http_error(self, mocker): + # Mock requests.get() to raise an HTTP error + mocker.patch("dispatcher.source.linux_gpg_key.requests.get", side_effect=RequestException) + + # Execute the function and assert that SourceError is raised + with pytest.raises(SourceError): + add_gpg_key("https://example.com/key.gpg", "/etc/apt/trusted.gpg.d/my_key.gpg") + + # @patch("dispatcher.source.linux_gpg_key.PseudoShellRunner.run", + # return_value=[{"", "", 0}, {"", "GPG Delete Error", 1}]) + # def test_raise_when_unable_delete_gpg_key(self, mocker): + # shell_runner_mock = mocker.patch( + # "dispatcher.source.linux_gpg_key.PseudoShellRunner") + # shell_runner_instance = shell_runner_mock.return_value + # if isinstance([{"", "", 0}, {"", "GPG Delete Error", 1}], list): # Simulate sequence of command runs + # shell_runner_instance.run.side_effect = [ + # (stdout, stderr, code) for stdout, stderr, code in [{"", "", 0}, {"", "GPG Delete Error", 1}] + # ] + # with patch('dispatcher.source.linux_gpg_key.PseudoShellRunner.run') as mock_run: + # mock_run.return_value.json.side_effect = [{"", "", 0}, {"", "GPG Delete Error", 1}] + # with pytest.raises(DispatcherException) as ex: + # remove_gpg_key("123456A0") + # assert str(ex.value) == "Error deleting GPG key: GPG Delete Error" diff --git a/inbm/dispatcher-agent/tests/unit/source/test_source_command.py b/inbm/dispatcher-agent/tests/unit/source/test_source_command.py index 28676a20e..a0885ee4f 100644 --- a/inbm/dispatcher-agent/tests/unit/source/test_source_command.py +++ b/inbm/dispatcher-agent/tests/unit/source/test_source_command.py @@ -7,7 +7,14 @@ import pytest from dispatcher.common.result_constants import Result -from dispatcher.source.constants import ApplicationRemoveSourceParameters, OsType, SourceParameters +from dispatcher.source.constants import ( + ApplicationAddSourceParameters, + ApplicationRemoveSourceParameters, + ApplicationSourceList, + ApplicationUpdateSourceParameters, + OsType, + SourceParameters, +) from dispatcher.source.source_command import do_source_command from inbm_lib.xmlhandler import XmlHandler @@ -45,8 +52,8 @@ def _factory(xml): """, "dispatcher.source.source_command.create_application_source_manager", - {"foo.list": ["source1", "source2"]}, - '{"foo.list": ["source1", "source2"]}', + [ApplicationSourceList(name="source_name", sources=["line1", "line2"])], + '[{"name": "source_name", "sources": ["line1", "line2"]}]', ), ], ) @@ -84,14 +91,14 @@ def test_do_source_command_list( """ source - 46C1680FC119E61A501811823A319F932D945953 + intel-gpu-jammy.gpg intel-gpu-jammy.list """, "dispatcher.source.source_command.create_application_source_manager", OsType.Ubuntu, ApplicationRemoveSourceParameters( - gpg_key_id="46C1680FC119E61A501811823A319F932D945953", + gpg_key_name="intel-gpu-jammy.gpg", file_name="intel-gpu-jammy.list", ), ), @@ -111,3 +118,130 @@ def test_do_source_command_remove( mock_manager.remove.assert_called_once_with(expected_call) assert result == Result(status=200, message="SUCCESS") + + +@pytest.mark.parametrize( + "xml, manager_mock, os_type, expected_call", + [ + ( + """ + + source + + + + sourceA + sourceB + + + + """, + "dispatcher.source.source_command.create_os_source_manager", + OsType.Ubuntu, + SourceParameters(sources=["sourceA", "sourceB"]), + ), + ( + """ + + source + + + + gpguri + keyname + + + + sourceA + sourceB + + repofilename + + + + """, + "dispatcher.source.source_command.create_application_source_manager", + OsType.Ubuntu, + ApplicationAddSourceParameters( + file_name="repofilename", + gpg_key_name="keyname", + gpg_key_uri="gpguri", + sources=["sourceA", "sourceB"], + ), + ), + ], +) +def test_do_source_command_add( + mocker, xml_handler_factory, xml, manager_mock, os_type, expected_call +): + xml_handler = xml_handler_factory(xml) + + mock_manager = mocker.Mock() + mock_manager.add.return_value = None + + mocker.patch(manager_mock, return_value=mock_manager) + + result = do_source_command(xml_handler, os_type) + + mock_manager.add.assert_called_once_with(expected_call) + assert result == Result(status=200, message="SUCCESS") + + +@pytest.mark.parametrize( + "xml, manager_mock, os_type, expected_call", + [ + ( + """ + + source + + + + source1 + source2 + + + + """, + "dispatcher.source.source_command.create_os_source_manager", + OsType.Ubuntu, + SourceParameters(sources=["source1", "source2"]), + ), + ( + """ + + source + + + + + source1 + source2 + + filename + + + + """, + "dispatcher.source.source_command.create_application_source_manager", + OsType.Ubuntu, + ApplicationUpdateSourceParameters( + file_name="filename", sources=["source1", "source2"] + ), + ), + ], +) +def test_do_source_command_update( + mocker, xml_handler_factory, xml, manager_mock, os_type, expected_call +): + xml_handler = xml_handler_factory(xml) + + mock_manager = mocker.Mock() + mock_manager.update.return_value = None + + mocker.patch(manager_mock, return_value=mock_manager) + + result = do_source_command(xml_handler, os_type) + + mock_manager.update.assert_called_once_with(expected_call) + assert result == Result(status=200, message="SUCCESS") diff --git a/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py b/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py index 23dfd0c53..9206154f9 100644 --- a/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py +++ b/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py @@ -1,17 +1,25 @@ from unittest import mock import pytest from unittest.mock import mock_open, patch -from dispatcher.dispatcher_exception import DispatcherException +from dispatcher.source.source_exception import SourceError from dispatcher.source.constants import ( UBUNTU_APT_SOURCES_LIST_D, + UBUNTU_APT_SOURCES_LIST, ApplicationRemoveSourceParameters, SourceParameters, + ApplicationUpdateSourceParameters, ) from dispatcher.source.ubuntu_source_manager import ( UbuntuApplicationSourceManager, UbuntuOsSourceManager, ) +APP_SOURCE = [ + "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] " + "https://repositories.intel.com/gpu/ubuntu jammy unified", + "deb-src https://repo.zabbix.com/zabbix/5.0/ubuntu jammy main", +] + @pytest.fixture def sources_list_content(): @@ -45,7 +53,7 @@ def test_list(self, sources_list_content): def test_list_with_oserror_exception(self): with patch("builtins.open", side_effect=OSError): command = UbuntuOsSourceManager() - with pytest.raises(DispatcherException) as exc_info: + with pytest.raises(SourceError) as exc_info: command.list() assert "Error opening source file" in str(exc_info.value) @@ -111,14 +119,100 @@ def write_side_effect(*args, **kwargs): mo.return_value.write.side_effect = write_side_effect - with patch("builtins.open", mo), pytest.raises(DispatcherException) as exc_info: + with patch("builtins.open", mo), pytest.raises(SourceError) as exc_info: manager = UbuntuOsSourceManager() manager.remove(sources_to_remove) assert "Error occurred while trying to remove sources" in str(exc_info.value) + def test_ubuntu_os_source_manager_add_success(self): + test_sources = [ + "deb http://archive.ubuntu.com/ubuntu focal main", + "deb-src http://archive.ubuntu.com/ubuntu focal main", + ] + parameters = SourceParameters(sources=test_sources) + + manager = UbuntuOsSourceManager() + + m = mock_open() + with patch("builtins.open", m): + manager.add(parameters) + + m.assert_called_once_with(UBUNTU_APT_SOURCES_LIST, "a") + m().write.assert_any_call(f"{test_sources[0]}\n") + m().write.assert_any_call(f"{test_sources[1]}\n") + + def test_ubuntu_os_source_manager_add_error(self): + test_sources = [ + "deb http://archive.ubuntu.com/ubuntu focal main", + "deb-src http://archive.ubuntu.com/ubuntu focal main", + ] + parameters = SourceParameters(sources=test_sources) + + manager = UbuntuOsSourceManager() + + m = mock_open() + m.side_effect = OSError("Permission denied") + with patch("builtins.open", m): + with pytest.raises(SourceError) as e: + manager.add(parameters) + assert str(e.value) == "Error adding sources: Permission denied" + + def test_update_sources_success(self): + mock_sources = [ + "deb http://archive.ubuntu.com/ubuntu/ bionic universe", + "deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe", + ] + parameters = SourceParameters(sources=mock_sources) + manager = UbuntuOsSourceManager() + mock_file = mock_open() + + # Act & Assert + with patch("builtins.open", mock_file): + manager.update(parameters) + mock_file.assert_called_once_with(UBUNTU_APT_SOURCES_LIST, "w") + mock_file().write.assert_has_calls( + [mock.call(f"{source}\n") for source in mock_sources] + ) + + def test_update_sources_os_error(self): + # Arrange + parameters = SourceParameters(sources=["source"]) + manager = UbuntuOsSourceManager() + mock_file = mock_open() + + # Simulate an OSError + mock_file.side_effect = OSError("Mocked error") + + # Act & Assert + with patch("builtins.open", mock_file): + with pytest.raises(SourceError) as excinfo: + manager.update(parameters) + assert "Error adding sources: Mocked error" in str(excinfo.value) + class TestUbuntuApplicationSourceManager: + @patch("dispatcher.source.ubuntu_source_manager.move_file") + def test_update_app_source_successfully(self, mock_move): + try: + params = ApplicationUpdateSourceParameters( + file_name="intel-gpu-jammy.list", sources=APP_SOURCE + ) + command = UbuntuApplicationSourceManager() + with patch("builtins.open", new_callable=mock_open()) as m: + command.update(params) + except SourceError as err: + assert False, f"'UbuntuApplicationSourceManager.update' raised an exception {err}" + + # def test_raises_exception_on_io_error_during_update_app_source_ubuntu(self): + # params = ApplicationUpdateSourceParameters(file_name="intel-gpu-jammy.list", + # sources=APP_SOURCE) + # command = UbuntuApplicationSourceManager() + # with pytest.raises(SourceError) as exc_info: + # with patch('builtins.open', new_callable=mock_open(), side_effect=OSError): + # command.update(params) + # assert "Error while writing file: " in str(exc_info.value) + def test_list(self, sources_list_d_content): with patch("glob.glob", return_value=["/etc/apt/sources.list.d/example.list"]), patch( "builtins.open", mock_open(read_data=sources_list_d_content) @@ -136,74 +230,56 @@ def test_list_raises_exception(self): "builtins.open", side_effect=OSError ): command = UbuntuApplicationSourceManager() - with pytest.raises(DispatcherException) as exc_info: + with pytest.raises(SourceError) as exc_info: command.list() assert "Error listing application sources" in str(exc_info.value) - @pytest.mark.parametrize( - "gpg_key_id, file_name, gpg_run_side_effect, gpg_key_exists, expected_except", - [ - # Case: Successful removal of GPG key and source file - ("123456A0", "example_source.list", [("", "", 0), ("", "", 0)], True, None), - # Case: GPG key does not exist, but the source file is still removed - ("123456A1", "example_source.list", [("", "No such key", 1)], False, None), - # Case: GPG exists but fails to delete, expect DispatcherException - ( - "123456A2", - "example_source.list", - [("", "", 0), ("", "GPG Delete Error", 1)], - True, - DispatcherException, - ), - # Case: OSError on checking GPG key, expect DispatcherException - ("123456A3", "example_source.list", OSError("GPG Error"), True, DispatcherException), - # Case: OSError on removing the file, expect DispatcherException - ( - "123456A4", - "example_source.list", - [("", "", 0), ("", "", 0)], - True, - DispatcherException, - ), - ], - ) - def test_ubuntu_application_source_manager_remove( - self, gpg_key_id, file_name, gpg_run_side_effect, gpg_key_exists, expected_except, mocker + @patch("dispatcher.source.ubuntu_source_manager.remove_file", return_value=True) + @patch("dispatcher.source.ubuntu_source_manager.remove_gpg_key_if_exists") + def test_successfully_remove_gpg_key_and_source_list( + self, mock_remove_gpg_key, mock_remove_file ): - parameters = ApplicationRemoveSourceParameters(gpg_key_id=gpg_key_id, file_name=file_name) + parameters = ApplicationRemoveSourceParameters( + gpg_key_name="example_source.gpg", file_name="example_source.list" + ) + command = UbuntuApplicationSourceManager() + try: + command.remove(parameters) + except SourceError: + self.fail("Remove GPG key raised DispatcherException unexpectedly!") - shell_runner_mock = mocker.patch( - "dispatcher.source.ubuntu_source_manager.PseudoShellRunner" + @patch("dispatcher.source.ubuntu_source_manager.remove_gpg_key_if_exists") + def test_raises_when_space_check_fails(self, mock_remove_gpg_key): + parameters = ApplicationRemoveSourceParameters( + gpg_key_name="example_source.gpg", file_name="../example_source.list" ) - shell_runner_instance = shell_runner_mock.return_value - if isinstance(gpg_run_side_effect, list): # Simulate sequence of command runs - shell_runner_instance.run.side_effect = [ - (stdout, stderr, code) for stdout, stderr, code in gpg_run_side_effect - ] - else: # Directly raise OSError - shell_runner_instance.run.side_effect = gpg_run_side_effect - - # Mock os.remove based on whether we expect an exception for file removal or not - os_remove_mock = mocker.patch("os.remove") - if expected_except is DispatcherException and gpg_run_side_effect == [ - ("", "", 0), - ("", "", 0), - ]: - os_remove_mock.side_effect = OSError("File could not be removed") - - if expected_except: - # If we expect an exception, check that it is raised - with pytest.raises(expected_except): - command = UbuntuApplicationSourceManager() - command.remove(parameters) - else: - # If no exception is expected, perform the operation and assert mocks are called as expected - command = UbuntuApplicationSourceManager() + command = UbuntuApplicationSourceManager() + with pytest.raises(SourceError) as ex: command.remove(parameters) + assert str(ex.value) == "Invalid file name: ../example_source.list" - expected_gpg_calls = [mocker.call(f"gpg --list-keys {gpg_key_id}")] - if gpg_key_exists: - expected_gpg_calls.append(mocker.call(f"gpg --delete-key {gpg_key_id}")) - shell_runner_instance.run.assert_has_calls(expected_gpg_calls) + @patch("dispatcher.source.ubuntu_source_manager.remove_file", return_value=False) + @patch("dispatcher.source.ubuntu_source_manager.remove_gpg_key_if_exists") + def test_raises_when_unable_to_remove_file(self, mock_remove_gpg_key, mock_remove_file): + parameters = ApplicationRemoveSourceParameters( + gpg_key_name="example_source.gpg", file_name="example_source.list" + ) + command = UbuntuApplicationSourceManager() + with pytest.raises(SourceError) as ex: + command.remove(parameters) + assert str(ex.value) == "Error removing file: example_source.list" - os_remove_mock.assert_called_once_with(UBUNTU_APT_SOURCES_LIST_D + "/" + file_name) + @patch( + "dispatcher.source.ubuntu_source_manager.os.path.join", + side_effect=OSError("unable to join path"), + ) + @patch("dispatcher.source.ubuntu_source_manager.remove_file", return_value=False) + @patch("dispatcher.source.ubuntu_source_manager.remove_gpg_key_if_exists") + def test_raises_on_os_error(self, mock_remove_gpg_key, mock_remove_file, mock_os_error): + parameters = ApplicationRemoveSourceParameters( + gpg_key_name="example_source.gpg", file_name="example_source.list" + ) + command = UbuntuApplicationSourceManager() + with pytest.raises(SourceError) as ex: + command.remove(parameters) + assert str(ex.value) == "Error removing file: unable to join path" diff --git a/inbm/dispatcher-agent/tests/unit/test_configuration_helper.py b/inbm/dispatcher-agent/tests/unit/test_configuration_helper.py index 5e33a5cbc..2342d3742 100644 --- a/inbm/dispatcher-agent/tests/unit/test_configuration_helper.py +++ b/inbm/dispatcher-agent/tests/unit/test_configuration_helper.py @@ -4,11 +4,8 @@ from dispatcher.packagemanager import memory_repo from dispatcher.dispatcher_exception import DispatcherException from inbm_lib.xmlhandler import XmlHandler -from unittest.mock import patch, MagicMock import os -from typing import Any - TEST_SCHEMA_LOCATION = os.path.join(os.path.dirname(__file__), '../../fpm-template/usr/share/dispatcher-agent/' 'manifest_schema.xsd') diff --git a/inbm/dispatcher-agent/tests/unit/test_dispatcher.py b/inbm/dispatcher-agent/tests/unit/test_dispatcher.py index 881ec3d8c..914448f74 100644 --- a/inbm/dispatcher-agent/tests/unit/test_dispatcher.py +++ b/inbm/dispatcher-agent/tests/unit/test_dispatcher.py @@ -384,6 +384,17 @@ def test_do_install_can_call_do_source_command(self, mock_workload_orchestration_func.assert_called() mock_do_source_command.assert_called_once() + def test_abc(self, mock_logging: Any): + xml = """\ + + + source + + """ + + d = TestDispatcher._build_dispatcher() + d.do_install(xml=xml, schema_location=TEST_SCHEMA_LOCATION) + @patch('dispatcher.config.config_operation.ConfigOperation._do_config_install_load') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.connect') @patch('inbm_lib.mqttclient.mqtt.mqtt.Client.subscribe') diff --git a/inbm/dispatcher-agent/tests/unit/test_update_logger.py b/inbm/dispatcher-agent/tests/unit/test_update_logger.py index e19d5bf9e..a12c6170e 100644 --- a/inbm/dispatcher-agent/tests/unit/test_update_logger.py +++ b/inbm/dispatcher-agent/tests/unit/test_update_logger.py @@ -4,8 +4,7 @@ from unittest.mock import patch from dispatcher.update_logger import UpdateLogger -from inbm_lib.constants import LOG_FILE, OTA_PENDING, OTA_SUCCESS, OTA_FAIL, FORMAT_VERSION -from inbm_lib.path_prefixes import INTEL_MANAGEABILITY_CACHE_PATH_PREFIX +from inbm_lib.constants import LOG_FILE, OTA_PENDING, OTA_SUCCESS, FAIL, FORMAT_VERSION SOTA_MANIFEST = 'ota
sotaremote
update
' @@ -22,7 +21,7 @@ def test_set_time(self) -> None: @patch('dispatcher.update_logger.UpdateLogger.write_log_file') def test_save_log(self, mock_write_log_file) -> None: - expected_status = OTA_FAIL + expected_status = FAIL self.update_logger._time = datetime.datetime(2023, 12, 25, 00, 00, 00, 000000) expected_error = '{"status": 302, "message": "OTA FAILURE"}' expected_metadata = 'ota
aotaremote
updateapplicationhttp://security.ubuntu.com/ubuntu/pool/main/n/net-tools/net-tools_1.60-25ubuntu2.1_amd64.debno
' diff --git a/inbm/dispatcher-agent/tests/unit/test_xmlparser.py b/inbm/dispatcher-agent/tests/unit/test_xmlparser.py index 961cee599..ec9ed840f 100644 --- a/inbm/dispatcher-agent/tests/unit/test_xmlparser.py +++ b/inbm/dispatcher-agent/tests/unit/test_xmlparser.py @@ -103,11 +103,13 @@ def test_application_source_with_add_passes_validation(self) -> None: - + - + + + @@ -124,10 +126,12 @@ def test_application_source_with_update_passes_validation(self) -> None: source - - - - + + + foo + + bar + @@ -143,10 +147,10 @@ def test_application_source_with_remove_passes_validation(self) -> None: - + name.gpg - + name.list diff --git a/inbm/dockerfiles/Dockerfile-check.m4 b/inbm/dockerfiles/Dockerfile-check.m4 index 86f507320..bd929afa0 100644 --- a/inbm/dockerfiles/Dockerfile-check.m4 +++ b/inbm/dockerfiles/Dockerfile-check.m4 @@ -56,6 +56,8 @@ RUN source /venv-py3/bin/activate && \ FROM venv-py3 as test-inbm-lib WORKDIR /src/inbm-lib +# for unit test +COPY inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd /src/inbm/dispatcher-agent/fpm-template/usr/share/dispatcher-agent/manifest_schema.xsd RUN source /venv-py3/bin/activate && \ cd /src/inbm-lib && \ set -o pipefail && \ diff --git a/inbm/autopep8.sh b/inbm/format-python.sh similarity index 100% rename from inbm/autopep8.sh rename to inbm/format-python.sh diff --git a/inbm/integration-reloaded/launchers/source-test.sh b/inbm/integration-reloaded/launchers/source-test.sh new file mode 100755 index 000000000..8acca7bd1 --- /dev/null +++ b/inbm/integration-reloaded/launchers/source-test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euxo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +. "$DIR"/../../integration-common/util/tc-messages.sh + +run_vagrant_provision_test() { + test_with_command "$1" vagrant provision --provision-with \""$1"\" +} + +vagrant_provision() { + vagrant provision --provision-with "$1" +} + +suite_started SOURCE + +cleanup() { + suite_finished SOURCE +} +trap cleanup 0 + +"$DIR"/vagrant-up.sh +"$DIR"/vagrant-wait-for-up.sh + +test_started "SOURCE test" +vagrant ssh -c "sudo /test/source/SOURCE.sh" diff --git a/inbm/integration-reloaded/run-quick.sh b/inbm/integration-reloaded/run-quick.sh index eb564e6a6..df6d4ca2c 100755 --- a/inbm/integration-reloaded/run-quick.sh +++ b/inbm/integration-reloaded/run-quick.sh @@ -18,6 +18,7 @@ trap general 0 launchers/install-framework-quick.sh launchers/update-system.sh +launchers/source-test.sh launchers/aota-test.sh launchers/sota-quick-tests.sh launchers/fota-test.sh diff --git a/inbm/integration-reloaded/run-slow.sh b/inbm/integration-reloaded/run-slow.sh index 1a2d565b0..112a86203 100755 --- a/inbm/integration-reloaded/run-slow.sh +++ b/inbm/integration-reloaded/run-slow.sh @@ -20,6 +20,7 @@ launchers/update-system.sh launchers/install-framework-slow.sh launchers/setup-servers.sh # This should happen after any docker uninstalls. +launchers/source-test.sh launchers/aota-test.sh launchers/sota-quick-tests.sh launchers/fota-test.sh diff --git a/inbm/integration-reloaded/test/source/SOURCE.sh b/inbm/integration-reloaded/test/source/SOURCE.sh new file mode 100755 index 000000000..c19cef04d --- /dev/null +++ b/inbm/integration-reloaded/test/source/SOURCE.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e +set -x + +source /scripts/test_util.sh + +trap 'kill -9 $(jobs -p) || true' EXIT + +APT_SOURCES="/etc/apt/sources.list" +BAK_APT_SOURCES="$APT_SOURCES.bak" +FAKE_SOURCE="deb test123" +UPDATE_SOURCE1="deb test456" +UPDATE_SOURCE2="deb test789" +OPERA_KEY_URI="https://deb.opera.com/archive.key" +OPERA_KEY_NAME="opera.gpg" +OPERA_SOURCES="deb [arch=amd64 signed-by=/usr/share/keyrings/$OPERA_KEY_NAME] https://deb.opera.com/opera-stable/ stable non-free" +OPERA_LIST="opera.list" +NEW_APP_SOURCE="deb newsource" + +cp "$APT_SOURCES" "$BAK_APT_SOURCES" + +test_failed() { + cp "$BAK_APT_SOURCES" "$APT_SOURCES" + rm -f "/usr/share/keyrings/$OPERA_KEY_NAME" + rm -f "/etc/apt/sources.list.d/$OPERA_LIST" + echo "Return code: $?" + echo "TEST FAILED!!!" +} +trap test_failed ERR + +echo "Starting source test." | systemd-cat + +# OS tests +inbc source os add --sources "$FAKE_SOURCE" +grep "$FAKE_SOURCE" "$APT_SOURCES" +inbc source os list 2>&1 | grep "$FAKE_SOURCE" +inbc source os remove --sources "$FAKE_SOURCE" +grep "$FAKE_SOURCE" "$APT_SOURCES" && exit 1 + +inbc source os add --sources "$FAKE_SOURCE" +inbc source os update --sources "$UPDATE_SOURCE1" "$UPDATE_SOURCE2" +grep "$FAKE_SOURCE" "$APT_SOURCES" && exit 1 +grep "$UPDATE_SOURCE1" "$APT_SOURCES" +grep "$UPDATE_SOURCE2" "$APT_SOURCES" + +cp "$BAK_APT_SOURCES" "$APT_SOURCES" + +# Application tests +rm -f "/usr/share/keyrings/$OPERA_KEY_NAME" +rm -f "/etc/apt/sources.list.d/$OPERA_LIST" +inbc source application add --gpgKeyUri "$OPERA_KEY_URI" --gpgKeyName "$OPERA_KEY_NAME" --sources "$OPERA_SOURCES" --filename "$OPERA_LIST" +if [ ! -e "/usr/share/keyrings/$OPERA_KEY_NAME" ]; then + echo "Error: The file '/usr/share/keyrings/$OPERA_KEY_NAME' does not exist!" + exit 1 +fi +inbc source application list 2>&1 | grep "$OPERA_KEY_NAME" +inbc source application remove --gpgKeyName "$OPERA_KEY_NAME" --filename "$OPERA_LIST" + +if inbc source application list 2>&1 | grep -q "$OPERA_KEY_NAME"; then + echo "Error: $OPERA_KEY_NAME should not be present in the application list after removal" + exit 1 +fi + +inbc source application add --gpgKeyUri "$OPERA_KEY_URI" --gpgKeyName "$OPERA_KEY_NAME" --sources "$OPERA_SOURCES" --filename "$OPERA_LIST" +inbc source application update --sources "$NEW_APP_SOURCE" --filename "$OPERA_LIST" +inbc source application list 2>&1 | grep "$NEW_APP_SOURCE" + +if inbc source application list 2>&1 | grep -q "$OPERA_KEY_NAME"; then + echo "Error: $OPERA_KEY_NAME should not be present in the application list after update" + exit 1 +fi +inbc source application remove --gpgKeyName "$OPERA_KEY_NAME" --filename "$OPERA_LIST" +rm -f "/usr/share/keyrings/$OPERA_KEY_NAME" +rm -f "/etc/apt/sources.list.d/$OPERA_LIST" diff --git a/pull_request_template.md b/pull_request_template.md index 64099daf5..94f4fd216 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -30,7 +30,7 @@ Reference URL for issue tracking (JIRA/HSD/Github): **\** - [ ] PR change contains code related to security - [ ] PR introduces changes that breaks compatibility with other modules (If YES, please provide description) - [ ] Specific instructions or information for code reviewers (If any): -- [ ] Run 'go fmt' or autopep8 as applicable. +- [ ] Run 'go fmt' or format-python.sh as applicable. - [ ] New/modified methods and functions should have type annotations on signatures as applicable - [ ] New/modified methods must have appropriate doc strings (language dependent)