From a944ab9b7796662efef2b866db56db89e667d603 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Tue, 23 Jan 2024 17:33:34 +0100 Subject: [PATCH 01/11] Deploy: Reorder args --- .vscode/settings.json | 1 + scripts/deploy/deploy_robots.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d36231a82..3143debc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -72,6 +72,7 @@ "Rhoban", "robocup", "RoboCup", + "rory", "rosbag", "rosbags", "rosdep", diff --git a/scripts/deploy/deploy_robots.py b/scripts/deploy/deploy_robots.py index 467e20287..e74ac0e01 100644 --- a/scripts/deploy/deploy_robots.py +++ b/scripts/deploy/deploy_robots.py @@ -91,8 +91,6 @@ def _parse_arguments(self) -> argparse.Namespace: # Optional arguments parser.add_argument("-p", "--package", default="", help="Synchronize and build only the given ROS package") - parser.add_argument("-u", "--user", default="bitbots", help="The user to connect to the target machines with") - parser.add_argument("-w", "--workspace", default="~/colcon_ws", help="The workspace to deploy to") parser.add_argument( "--clean", action="store_true", @@ -110,6 +108,8 @@ def _parse_arguments(self) -> argparse.Namespace: ) parser.add_argument("-v", "--verbose", action="count", default=0, help="More output") parser.add_argument("-q", "--quiet", action="count", default=0, help="Less output") + parser.add_argument("-u", "--user", default="bitbots", help="The user to connect to the target machines with") + parser.add_argument("-w", "--workspace", default="~/colcon_ws", help="The workspace to deploy to") args = parser.parse_args() From ea93ed506aa6e7098a67ba70b5051cdec9c4ff8f Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 18:40:08 +0100 Subject: [PATCH 02/11] Main: Deploy WIP --- scripts/deploy/deploy_robots.py | 2 +- scripts/deploy/known_targets.yaml | 24 +++---- scripts/deploy/misc.py | 108 +++++++++++------------------- 3 files changed, 52 insertions(+), 82 deletions(-) diff --git a/scripts/deploy/deploy_robots.py b/scripts/deploy/deploy_robots.py index eaa28c203..8461dcded 100644 --- a/scripts/deploy/deploy_robots.py +++ b/scripts/deploy/deploy_robots.py @@ -13,7 +13,7 @@ print_known_targets, print_success, ) -from deploy.tasks import AbstractTask, AbstractTaskWhichRequiresSudo, Build, Configure, Install, Launch, Sync +from deploy.tasks import AbstractTask, AbstractTaskWhichRequiresSudo, Build, Configure, Install, Launch, Sync # type: ignore from rich.prompt import Prompt # TODO: Install this script as a command line tool diff --git a/scripts/deploy/known_targets.yaml b/scripts/deploy/known_targets.yaml index dc602711f..7b042d711 100644 --- a/scripts/deploy/known_targets.yaml +++ b/scripts/deploy/known_targets.yaml @@ -1,18 +1,18 @@ -nuc1: - ip: "172.20.1.11" +"172.20.1.11": + hostname: "nuc1" robot_name: "amy" -nuc2: - ip: "172.20.1.12" +"172.20.1.12": + hostname: "nuc2" robot_name: "rory" -nuc3: - ip: "172.20.1.13" +"172.20.1.13": + hostname: "nuc3" robot_name: "jack" -nuc4: - ip: "172.20.1.14" +"172.20.1.14": + hostname: "nuc4" robot_name: "donna" -nuc5: - ip: "172.20.1.15" +"172.20.1.15": + hostname: "nuc5" robot_name: "melody" -nuc6: - ip: "172.20.1.16" +"172.20.1.16": + hostname: "nuc6" robot_name: "rose" diff --git a/scripts/deploy/misc.py b/scripts/deploy/misc.py index 392bad58d..bbdf16149 100644 --- a/scripts/deploy/misc.py +++ b/scripts/deploy/misc.py @@ -103,9 +103,9 @@ def print_known_targets() -> None: known_targets = get_known_targets() table.add_row("ALL", "", "") - for hostname, values in known_targets.items(): - table.add_row(hostname, values.get("robot_name", ""), values.get("ip", "")) - print_info("You can enter the following values as targets:") + for ip, values in known_targets.items(): + table.add_row(values.get("hostname", ""), values.get("robot_name", ""), ip) + print_info(f"You can enter the following values as targets:") CONSOLE.print(table) exit(0) @@ -120,99 +120,69 @@ def get_known_targets() -> dict[str, dict[str, str]]: class Target: - hostname: str - ip: Optional[ipaddress.IPv4Address | ipaddress.IPv6Address] - def __init__(self, identifier: str) -> None: """ Target represents a robot to deploy to. It can be initialized with a hostname, IP address or a robot name. """ - self.hostname, self.ip = self._identify_target(identifier) + self.ip: Optional[ipaddress.IPv4Address | ipaddress.IPv6Address] = self._identify_ip(identifier) + self.hostname: Optional[str] = None # TODO: Get the hostname after we have a connection - def _identify_target(self, identifier: str) -> tuple[str, Optional[ipaddress.IPv4Address | ipaddress.IPv6Address]]: + def _identify_ip(self, identifier: str) -> Optional[ipaddress.IPv4Address | ipaddress.IPv6Address]: """ - Identifies a target from an identifier. + Identifies an IP address from an identifier. The identifier can be a hostname, IP address or a robot name. :param identifier: The identifier to identify the target from. - :return: A tuple containing the hostname and the IP address of the target. + :return: IP address of the identified target. """ - print_debug(f"Identifying target from identifier: {identifier}") - - identified_target: Optional[str] = None # The hostname of the identified target + print_debug(f"Identifying IP address from identifier: '{identifier}'.") - # Iterate over the known targets - for hostname, values in KNOWN_TARGETS.items(): - print_debug(f"Checking if {identifier} is {hostname}") + # Is the identifier an IP address? + try: + print_debug(f"Checking if {identifier} is a IP address") + ip = ipaddress.ip_address(identifier) + print_debug(f"Found {ip} as IP address") + return ip + except ValueError: + print_debug(f"Entered target is not a IP-address.") + # It was not an IP address, so we try to find a known target + for ip, values in KNOWN_TARGETS.items(): # Is the identifier a known hostname? - print_debug(f"Comparing {identifier} with {hostname}") - if hostname == identifier: - identified_target = hostname - break - + known_hostname = values.get("hostname", None) + if known_hostname: + print_debug(f"Comparing {identifier} with {known_hostname}") + if known_hostname.strip() == identifier.strip(): + print_debug(f"Found hostname '{known_hostname}' for identifier '{identifier}'. Using its IP {ip}.") + return ipaddress.ip_address(ip) + else: + print_debug(f"Hostname '{known_hostname}' does not match identifier '{identifier}'.") + # Is the identifier a known robot name? - print_debug(f"Comparing {identifier} with {values['robot_name']}") if "robot_name" in values else None - if values.get("robot_name") == identifier: - identified_target = hostname - break - - # Is the identifier a known IP address? - identifier_ip = None - try: - print_debug(f"Checking if {identifier} is a IP address") - identifier_ip = ipaddress.ip_address(identifier) - except ValueError: - print_debug("Entered target is not a IP-address") - # We accept every IP address, but if we later find an associated hostname, we use that - identified_target = str(identifier_ip) - - if "ip" in values: - try: - known_target_ip = ipaddress.ip_address(values["ip"]) - except ValueError: - print_warn(f"Invalid IP address ('{values['ip']}') defined for known target: {hostname}") - exit(1) - - if identifier_ip is not None and identifier_ip == known_target_ip: - identified_target = hostname - break - - # If no target was identified, exit - if identified_target is None: - print_err( - f"Could not find a known target for the given identifier: {identifier}\nChoose from the known targets" - ) - print_known_targets() - exit(1) - - print_debug(f"Found {identified_target} as known target") - - identified_ip = None - if "ip" in KNOWN_TARGETS[identified_target]: - try: - identified_ip = ipaddress.ip_address(KNOWN_TARGETS[identified_target]["ip"]) - except ValueError: - print_err(f"Invalid IP address defined for known target: {identified_target}") - exit(1) - - return (identified_target, identified_ip) + known_robot_name = values.get("robot_name", None) + if known_robot_name: + print_debug(f"Comparing '{identifier}' with '{known_robot_name}'.") + if known_robot_name.strip() == identifier.strip(): + print_debug(f"Found robot name '{known_robot_name}' for identifier '{identifier}'. Using its IP {ip}.") + return ipaddress.ip_address(ip) + else: + print_debug(f"Robot name '{known_robot_name}' does not match identifier '{identifier}'.") def __str__(self) -> str: """Returns the target's hostname if available or IP address.""" return self.hostname if self.hostname is not None else str(self.ip) def get_connection_identifier(self) -> str: - """Returns the target's IP address if available or the hostname.""" - return str(self.ip) if self.ip is not None else self.hostname + """Returns the target's IP address.""" + return str(self.ip) def _parse_targets(input_targets: str) -> list[Target]: """ Parse target input into usable Targets. - :param input_targets: The input string of targets as a comma separated string of either hostnames, robot names or IPs. 'ALL' is a valid argument and will be expanded to all known targets. + :param input_targets: The input string of targets as a comma separated string of either hostnames, robot names or IPs. :return: List of Targets """ targets: list[Target] = [] From 4368185ca07c4bd5dcea171ddddb3bb9032b3966 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 20:20:37 +0100 Subject: [PATCH 03/11] Deploy: Improve error logs for connection issues --- requirements/dev.txt | 1 + scripts/deploy/deploy_robots.py | 12 ++++++-- scripts/deploy/misc.py | 49 +++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 921427219..e7caa998d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,6 +3,7 @@ black # Auto-formatting for python exhale # Necessary for rst rendering fabric # Manages SSH sessions for the deploy tool +paramiko # Necessary for fabric pre-commit # Installs and runs pre-commit hooks for git rich # Rich terminal output ruff # Python linting diff --git a/scripts/deploy/deploy_robots.py b/scripts/deploy/deploy_robots.py index 8461dcded..1d6b567f0 100644 --- a/scripts/deploy/deploy_robots.py +++ b/scripts/deploy/deploy_robots.py @@ -13,7 +13,15 @@ print_known_targets, print_success, ) -from deploy.tasks import AbstractTask, AbstractTaskWhichRequiresSudo, Build, Configure, Install, Launch, Sync # type: ignore +from deploy.tasks import ( + AbstractTask, + AbstractTaskWhichRequiresSudo, + Build, + Configure, + Install, + Launch, + Sync, +) from rich.prompt import Prompt # TODO: Install this script as a command line tool @@ -47,7 +55,7 @@ def _parse_arguments(self) -> argparse.Namespace: parser = ArgumentParserShowTargets( description="Deploy the Bit-Bots software on a robot. " "This script provides 5 tasks: sync, install, configure, build, launch. " - "By default, it runs all tasks. You can select a subset of tasks by using the corresponding flags." + "By default, it runs all tasks. You can select a subset of tasks by using the corresponding flags. " "For example, to only run the sync and build task, use the -sb." ) diff --git a/scripts/deploy/misc.py b/scripts/deploy/misc.py index bbdf16149..1993c5b2e 100644 --- a/scripts/deploy/misc.py +++ b/scripts/deploy/misc.py @@ -6,6 +6,7 @@ import yaml from fabric import Connection, GroupResult, ThreadingGroup +from paramiko import AuthenticationException from rich import box from rich.console import Console from rich.panel import Panel @@ -105,7 +106,7 @@ def print_known_targets() -> None: table.add_row("ALL", "", "") for ip, values in known_targets.items(): table.add_row(values.get("hostname", ""), values.get("robot_name", ""), ip) - print_info(f"You can enter the following values as targets:") + print_info("You can enter the following values as targets:") CONSOLE.print(table) exit(0) @@ -145,7 +146,7 @@ def _identify_ip(self, identifier: str) -> Optional[ipaddress.IPv4Address | ipad print_debug(f"Found {ip} as IP address") return ip except ValueError: - print_debug(f"Entered target is not a IP-address.") + print_debug("Entered target is not a IP-address.") # It was not an IP address, so we try to find a known target for ip, values in KNOWN_TARGETS.items(): @@ -158,13 +159,15 @@ def _identify_ip(self, identifier: str) -> Optional[ipaddress.IPv4Address | ipad return ipaddress.ip_address(ip) else: print_debug(f"Hostname '{known_hostname}' does not match identifier '{identifier}'.") - + # Is the identifier a known robot name? known_robot_name = values.get("robot_name", None) if known_robot_name: print_debug(f"Comparing '{identifier}' with '{known_robot_name}'.") if known_robot_name.strip() == identifier.strip(): - print_debug(f"Found robot name '{known_robot_name}' for identifier '{identifier}'. Using its IP {ip}.") + print_debug( + f"Found robot name '{known_robot_name}' for identifier '{identifier}'. Using its IP {ip}." + ) return ipaddress.ip_address(ip) else: print_debug(f"Robot name '{known_robot_name}' does not match identifier '{identifier}'.") @@ -200,23 +203,47 @@ def _get_connections_from_targets( targets: list[Target], user: str, connection_timeout: Optional[int] = 10 ) -> ThreadingGroup: """ - Get connections to the given Targets using the 'bitbots' username. + Get connections to the given Targets using the given username. :param targets: The Targets to connect to :param user: The username to connect with :param connection_timeout: Timeout for establishing the connection :return: The connections """ + + def _concat_exception_args(e: Exception) -> str: + """Concatenate all arguments of an exception into a string.""" + reason = "" + for arg in e.args: + if arg: + reason += f"{arg} " + return reason + hosts: list[str] = [target.get_connection_identifier() for target in targets] - try: - connections = ThreadingGroup(*hosts, user=user, connect_timeout=connection_timeout) - for connection in connections: + connections = ThreadingGroup(*hosts, user=user, connect_timeout=connection_timeout) + failures: list[tuple[Connection, str]] = [] # List of tuples of failed connections and their error message + for connection in connections: + try: print_debug(f"Connecting to {connection.host}...") connection.open() print_debug(f"Connected to {connection.host}...") - except Exception as e: - print_err(f"Could not establish all required connections: {hosts}") - print_debug(e) + except AuthenticationException as e: + failures.append( + ( + connection, + _concat_exception_args(e) + + f"Did you add your SSH key to the target? Run '[bold blue]ssh-copy-id {user}@{connection.host}[/bold blue]' manually to do so.", + ) + ) + except Exception as e: + failures.append((connection, _concat_exception_args(e))) + if failures: + print_err("Could not connect to the following hosts:") + failure_table = Table(style="bold red", box=box.HEAVY) + failure_table.add_column("Host") + failure_table.add_column("Reason") + [failure_table.add_row(connection.host, reason) for connection, reason in failures] + CONSOLE.print(failure_table) exit(1) return connections From 6992b5a80399d814d517f68eb6c5e33a5930874c Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 20:42:57 +0100 Subject: [PATCH 04/11] Deploy: replacing Target with IP,set original_host --- scripts/deploy/misc.py | 147 ++++++++++++-------------- scripts/deploy/tasks/abstract_task.py | 2 +- 2 files changed, 69 insertions(+), 80 deletions(-) diff --git a/scripts/deploy/misc.py b/scripts/deploy/misc.py index 1993c5b2e..156d5ba62 100644 --- a/scripts/deploy/misc.py +++ b/scripts/deploy/misc.py @@ -120,92 +120,76 @@ def get_known_targets() -> dict[str, dict[str, str]]: return KNOWN_TARGETS -class Target: - def __init__(self, identifier: str) -> None: - """ - Target represents a robot to deploy to. - It can be initialized with a hostname, IP address or a robot name. - """ - self.ip: Optional[ipaddress.IPv4Address | ipaddress.IPv6Address] = self._identify_ip(identifier) - self.hostname: Optional[str] = None # TODO: Get the hostname after we have a connection - - def _identify_ip(self, identifier: str) -> Optional[ipaddress.IPv4Address | ipaddress.IPv6Address]: - """ - Identifies an IP address from an identifier. - The identifier can be a hostname, IP address or a robot name. - - :param identifier: The identifier to identify the target from. - :return: IP address of the identified target. - """ - print_debug(f"Identifying IP address from identifier: '{identifier}'.") - - # Is the identifier an IP address? - try: - print_debug(f"Checking if {identifier} is a IP address") - ip = ipaddress.ip_address(identifier) - print_debug(f"Found {ip} as IP address") - return ip - except ValueError: - print_debug("Entered target is not a IP-address.") - - # It was not an IP address, so we try to find a known target - for ip, values in KNOWN_TARGETS.items(): - # Is the identifier a known hostname? - known_hostname = values.get("hostname", None) - if known_hostname: - print_debug(f"Comparing {identifier} with {known_hostname}") - if known_hostname.strip() == identifier.strip(): - print_debug(f"Found hostname '{known_hostname}' for identifier '{identifier}'. Using its IP {ip}.") - return ipaddress.ip_address(ip) - else: - print_debug(f"Hostname '{known_hostname}' does not match identifier '{identifier}'.") - - # Is the identifier a known robot name? - known_robot_name = values.get("robot_name", None) - if known_robot_name: - print_debug(f"Comparing '{identifier}' with '{known_robot_name}'.") - if known_robot_name.strip() == identifier.strip(): - print_debug( - f"Found robot name '{known_robot_name}' for identifier '{identifier}'. Using its IP {ip}." - ) - return ipaddress.ip_address(ip) - else: - print_debug(f"Robot name '{known_robot_name}' does not match identifier '{identifier}'.") - - def __str__(self) -> str: - """Returns the target's hostname if available or IP address.""" - return self.hostname if self.hostname is not None else str(self.ip) - - def get_connection_identifier(self) -> str: - """Returns the target's IP address.""" - return str(self.ip) - - -def _parse_targets(input_targets: str) -> list[Target]: +def _identify_ip(identifier: str) -> str | None: + """ + Identifies an IP address from an identifier. + The identifier can be a hostname, IP address or a robot name. + + :param identifier: The identifier to identify the target from. + :return: IP address of the identified target. + """ + print_debug(f"Identifying IP address from identifier: '{identifier}'.") + + # Is the identifier an IP address? + try: + print_debug(f"Checking if {identifier} is an IP address") + ip = ipaddress.ip_address(identifier) + print_debug(f"Identified {ip} as an IP address") + return str(ip) + except ValueError: + print_debug("Entered target is not an IP-address.") + + # It was not an IP address, so we try to find a known target + for ip, values in KNOWN_TARGETS.items(): + # Is the identifier a known hostname? + known_hostname = values.get("hostname", None) + if known_hostname: + print_debug(f"Comparing {identifier} with {known_hostname}") + if known_hostname.strip() == identifier.strip(): + print_debug(f"Identified hostname '{known_hostname}' for '{identifier}'. Using its IP {ip}.") + return str(ipaddress.ip_address(ip)) + else: + print_debug(f"Hostname '{known_hostname}' does not match identifier '{identifier}'.") + + # Is the identifier a known robot name? + known_robot_name = values.get("robot_name", None) + if known_robot_name: + print_debug(f"Comparing '{identifier}' with '{known_robot_name}'.") + if known_robot_name.strip() == identifier.strip(): + print_debug(f"Identified robot name '{known_robot_name}' for '{identifier}'. Using its IP {ip}.") + return str(ipaddress.ip_address(ip)) + else: + print_debug(f"Robot name '{known_robot_name}' does not match '{identifier}'.") + + +def _parse_targets(input_targets: str) -> list[str]: """ - Parse target input into usable Targets. + Parse target input into usable target IP addresses. :param input_targets: The input string of targets as a comma separated string of either hostnames, robot names or IPs. - :return: List of Targets + :return: List of target IP addresses. """ - targets: list[Target] = [] + target_ips: list[str] = [] for input_target in input_targets.split(","): try: - target = Target(input_target) + target_ip = _identify_ip(input_target) except ValueError: - print_err(f"Could not determine hostname or IP from input: '{input_target}'") + print_err(f"Could not determine IP address from input: '{input_target}'") + exit(1) + if target_ip is None: + print_err(f"Could not determine IP address from input:' {input_target}'") exit(1) - targets.append(target) - return targets + target_ips.append(target_ip) + return target_ips def _get_connections_from_targets( - targets: list[Target], user: str, connection_timeout: Optional[int] = 10 + target_ips: list[str], user: str, connection_timeout: Optional[int] = 10 ) -> ThreadingGroup: """ - Get connections to the given Targets using the given username. + Get connections to the given target IP addresses using the given username. - :param targets: The Targets to connect to + :param target_ips: The target IP addresses to connect to :param user: The username to connect with :param connection_timeout: Timeout for establishing the connection :return: The connections @@ -219,14 +203,17 @@ def _concat_exception_args(e: Exception) -> str: reason += f"{arg} " return reason - hosts: list[str] = [target.get_connection_identifier() for target in targets] - connections = ThreadingGroup(*hosts, user=user, connect_timeout=connection_timeout) + connections = ThreadingGroup(*target_ips, user=user, connect_timeout=connection_timeout) failures: list[tuple[Connection, str]] = [] # List of tuples of failed connections and their error message for connection in connections: try: print_debug(f"Connecting to {connection.host}...") connection.open() print_debug(f"Connected to {connection.host}...") + print_debug(f"Getting hostname of {connection.host}...") + hostname: str = connection.run("hostname", hide=hide_output()).stdout.strip() + print_debug(f"Got hostname of {connection.host}: {hostname}. Setting is as original hostname.") + connection.original_host = hostname except AuthenticationException as e: failures.append( ( @@ -257,11 +244,13 @@ def _get_connections_from_all_known_targets(user: str, connection_timeout: Optio :param connection_timeout: Timeout for establishing the connection :return: The connections """ - # Get hosts from all known targets - hosts: list[str] = [Target(hostname).get_connection_identifier() for hostname in KNOWN_TARGETS.keys()] + # Get all known target IP addresses + target_ips: list[str] = list(KNOWN_TARGETS.keys()) # Create connections - connections: list[Connection] = [Connection(host, user=user, connect_timeout=connection_timeout) for host in hosts] + connections: list[Connection] = [ + Connection(host, user=user, connect_timeout=connection_timeout) for host in target_ips + ] # Connect to all hosts open_connections: list[Connection] = [] @@ -293,11 +282,11 @@ def get_connections_from_targets( :return: The connections to the targets """ if input_targets == "ALL": - print_info(f"Connecting to all known Targets: {KNOWN_TARGETS.keys()}") + print_info(f"Connecting to all known targets: {KNOWN_TARGETS.keys()}") return _get_connections_from_all_known_targets(user=user, connection_timeout=connection_timeout) return _get_connections_from_targets( - targets=_parse_targets(input_targets), user=user, connection_timeout=connection_timeout + target_ips=_parse_targets(input_targets), user=user, connection_timeout=connection_timeout ) diff --git a/scripts/deploy/tasks/abstract_task.py b/scripts/deploy/tasks/abstract_task.py index 5aefcd3bd..8e584c1ec 100644 --- a/scripts/deploy/tasks/abstract_task.py +++ b/scripts/deploy/tasks/abstract_task.py @@ -43,7 +43,7 @@ def _results_hosts(self, results) -> list[str]: :param results: The results of the task. :return: The list of hosts that have results. """ - return [connection.host for connection in results.keys()] + return [connection.original_host for connection in results.keys()] def _succeeded_hosts(self, results: GroupResult) -> list[str]: """ From 72c143df7032c8e224be90e11886bfbf4017e26e Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 21:37:25 +0100 Subject: [PATCH 05/11] Deploy: install apt upgrades --- scripts/deploy/tasks/install.py | 42 +++++++++++++++++++++++++++++---- sync_includes_wolfgang_nuc.yaml | 1 + 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/scripts/deploy/tasks/install.py b/scripts/deploy/tasks/install.py index 24ded836c..6d847fc25 100644 --- a/scripts/deploy/tasks/install.py +++ b/scripts/deploy/tasks/install.py @@ -20,7 +20,7 @@ def __init__(self, remote_workspace: str) -> None: self._remote_workspace = remote_workspace # TODO: also install pip upgrades - # TODO: sudo apt update && sudo apt upgrade -y + # TODO: run yes | scripts/make_basler.sh def _run(self, connections: Group) -> GroupResult: """ @@ -34,8 +34,9 @@ def _run(self, connections: Group) -> GroupResult: if not internet_available_results.succeeded: return internet_available_results - # Some hosts have an internet connection, install rosdeps - install_results = self._install_rosdeps(get_connections_from_succeeded(internet_available_results)) + # Some hosts have an internet connection, make updates and installs + apt_upgrade_results = self._apt_upgrade(get_connections_from_succeeded(internet_available_results)) + install_results = self._install_rosdeps(get_connections_from_succeeded(apt_upgrade_results)) return install_results def _internet_available_on_target(self, connections: Group) -> GroupResult: @@ -60,6 +61,37 @@ def _internet_available_on_target(self, connections: Group) -> GroupResult: results = e.result return results + def _apt_upgrade(self, connections: Group) -> GroupResult: + """ + Upgrade all apt packages on the target. + Runs apt update and apt upgrade -y. + + :param connections: The connections to remote servers. + :return: Results, with success if the upgrade succeeded on the target + """ + print_debug("Updating apt") + + cmd = "apt update" + print_debug(f"Calling {cmd}") + try: + update_results = connections.sudo(cmd, hide=hide_output(), password=self._sudo_password) + print_debug(f"Updated apt on the following hosts: {self._succeeded_hosts(update_results)}") + except GroupException as e: + print_err(f"Failed to update apt on the following hosts: {self._failed_hosts(e.result)}") + update_results = e.result + + print_debug("Upgrading apt packages") + + cmd = "apt upgrade -y" + print_debug(f"Calling {cmd}") + try: + upgrade_results = connections.sudo(cmd, hide=hide_output(), password=self._sudo_password) + print_debug(f"Upgraded apt packages on the following hosts: {self._succeeded_hosts(upgrade_results)}") + except GroupException as e: + print_err(f"Failed to upgrade apt packages on the following hosts: {self._failed_hosts(e.result)}") + upgrade_results = e.result + return update_results + def _install_rosdeps(self, connections: Group) -> GroupResult: """ Install ROS dependencies. @@ -71,12 +103,12 @@ def _install_rosdeps(self, connections: Group) -> GroupResult: The "sudo" functionality provided by fabric is not able to autofill in this case. :param connections: The connections to remote servers. - :return: Results, with success if the Target has an internet connection + :return: Results, with success if the install commands succeeded on the target """ remote_src_path = os.path.join(self._remote_workspace, "src") print_debug(f"Gathering rosdep install commands in {remote_src_path}") - cmd = f"rosdep install --simulate --default-yes --ignore-src --from-paths {remote_src_path}" + cmd = f"rosdep update && rosdep install --simulate --default-yes --ignore-src --from-paths {remote_src_path}" print_debug(f"Calling {cmd}") try: gather_results = connections.run(cmd, hide=hide_output()) diff --git a/sync_includes_wolfgang_nuc.yaml b/sync_includes_wolfgang_nuc.yaml index 1b25d2834..ea67cbefd 100644 --- a/sync_includes_wolfgang_nuc.yaml +++ b/sync_includes_wolfgang_nuc.yaml @@ -63,6 +63,7 @@ include: - ros2_python_extension - soccer_ipm - udp_bridge + - requirements - scripts exclude: - "*.bag" From 703e0b70b9295fa8d6bcc09c843bd86c798c3571 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 21:44:50 +0100 Subject: [PATCH 06/11] Deploy: install pip upgrades --- scripts/deploy/tasks/install.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/tasks/install.py b/scripts/deploy/tasks/install.py index 6d847fc25..4e4646e01 100644 --- a/scripts/deploy/tasks/install.py +++ b/scripts/deploy/tasks/install.py @@ -13,13 +13,12 @@ def __init__(self, remote_workspace: str) -> None: """ Task to install and update all dependencies. - :param remote_workspace: Path to the remote workspace to run rosdep in + :param remote_workspace: Path to the remote workspace """ super().__init__() self._remote_workspace = remote_workspace - # TODO: also install pip upgrades # TODO: run yes | scripts/make_basler.sh def _run(self, connections: Group) -> GroupResult: @@ -36,8 +35,9 @@ def _run(self, connections: Group) -> GroupResult: # Some hosts have an internet connection, make updates and installs apt_upgrade_results = self._apt_upgrade(get_connections_from_succeeded(internet_available_results)) - install_results = self._install_rosdeps(get_connections_from_succeeded(apt_upgrade_results)) - return install_results + rosdep_results = self._install_rosdeps(get_connections_from_succeeded(apt_upgrade_results)) + pip_upgrade_results = self._pip_upgrade(get_connections_from_succeeded(rosdep_results)) + return pip_upgrade_results def _internet_available_on_target(self, connections: Group) -> GroupResult: """ @@ -197,3 +197,22 @@ def _install_commands_on_single_host(connection: Connection, result: Result) -> installs_results.succeeded[key] = value return installs_results + + def _pip_upgrade(self, connections: Group) -> GroupResult: + """ + Install and upgrade all pip robot requirements on the target. + + :param connections: The connections to remote servers. + :return: Results, with success if the upgrade succeeded on the target + """ + print_debug("Upgrading pip packages") + + cmd = f"pip3 install --upgrade -r {self._remote_workspace}/src/requirements/robot.txt" + print_debug(f"Calling {cmd}") + try: + upgrade_results = connections.run(cmd, hide=hide_output()) + print_debug(f"Upgraded pip packages on the following hosts: {self._succeeded_hosts(upgrade_results)}") + except GroupException as e: + print_err(f"Failed to upgrade pip packages on the following hosts: {self._failed_hosts(e.result)}") + upgrade_results = e.result + return upgrade_results From 5fe7a4cc90335a0c87df9db897c2bfe035fef874 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 21:56:13 +0100 Subject: [PATCH 07/11] Deploy: Install basler drivers --- scripts/deploy/tasks/install.py | 24 +++++++++++++++++++++--- scripts/make_basler.sh | 4 ++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/deploy/tasks/install.py b/scripts/deploy/tasks/install.py index 4e4646e01..1e2a64bbb 100644 --- a/scripts/deploy/tasks/install.py +++ b/scripts/deploy/tasks/install.py @@ -19,8 +19,6 @@ def __init__(self, remote_workspace: str) -> None: self._remote_workspace = remote_workspace - # TODO: run yes | scripts/make_basler.sh - def _run(self, connections: Group) -> GroupResult: """ Install and update all dependencies, if internet is available. @@ -35,7 +33,8 @@ def _run(self, connections: Group) -> GroupResult: # Some hosts have an internet connection, make updates and installs apt_upgrade_results = self._apt_upgrade(get_connections_from_succeeded(internet_available_results)) - rosdep_results = self._install_rosdeps(get_connections_from_succeeded(apt_upgrade_results)) + basler_install_results = self._install_basler(get_connections_from_succeeded(apt_upgrade_results)) + rosdep_results = self._install_rosdeps(get_connections_from_succeeded(basler_install_results)) pip_upgrade_results = self._pip_upgrade(get_connections_from_succeeded(rosdep_results)) return pip_upgrade_results @@ -92,6 +91,25 @@ def _apt_upgrade(self, connections: Group) -> GroupResult: upgrade_results = e.result return update_results + def _install_basler(self, connections: Group) -> GroupResult: + """ + Installs the basler camera drivers on the targets. + + :param connections: The connections to remote servers. + :return: Results, with success if the install succeeded on the target + """ + print_debug("Installing basler drivers") + + cmd = f"{self._remote_workspace}/src/scripts/make_basler.sh -ci" + print_debug(f"Calling {cmd}") + try: + install_results = connections.sudo(cmd, hide=hide_output(), password=self._sudo_password) + print_debug(f"Installed basler drivers on the following hosts: {self._succeeded_hosts(install_results)}") + except GroupException as e: + print_err(f"Failed to install basler drivers on the following hosts: {self._failed_hosts(e.result)}") + install_results = e.result + return install_results + def _install_rosdeps(self, connections: Group) -> GroupResult: """ Install ROS dependencies. diff --git a/scripts/make_basler.sh b/scripts/make_basler.sh index e4d7dcaad..ea8e46c32 100755 --- a/scripts/make_basler.sh +++ b/scripts/make_basler.sh @@ -1,6 +1,6 @@ -#!/bin/bash +#!/bin/bash -# You need to fill out a form to download the pylon driver. +# You need to fill out a form to download the pylon driver. # The pylon driver can be found in the download section of the following link: # https://www.baslerweb.com/en/downloads/software-downloads/ # Go to the download button and copy the link address. From f4933279286aeb36be19cb6a0d21d8f69dc0bc11 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Thu, 25 Jan 2024 21:56:48 +0100 Subject: [PATCH 08/11] Readme: Improve formatting --- README.md | 10 ++++------ scripts/README.md | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5aced6870..2cf830496 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - - # Bit-Bots Software Stack [![Test if all packages build](https://github.com/bit-bots/bitbots_main/actions/workflows/build.yml/badge.svg)](https://github.com/bit-bots/bitbots_main/actions/workflows/build.yml) @@ -20,15 +18,15 @@ Full step-by-step instructions for installing the Bit-Bots software stack and RO If you want to update this repo, all third party source files as well as the supplementing files, run -``` bash +``` shell make pull-all ``` If you encounter any problems consider cleaning the third party source files (the `lib` folder) first: -**THIS DELETES ALL CHANGES YOU MADE TO THE THIRD PARTY SOURCE FILES** +**THIS DELETES ALL CHANGES YOU MADE TO THE THIRD PARTY SOURCE FILES!** -``` bash +``` shell make fresh-libs ``` @@ -36,7 +34,7 @@ make fresh-libs To format all code in the repository, run -``` bash +``` shell make format ``` diff --git a/scripts/README.md b/scripts/README.md index 7ee054b60..11f3eca30 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -18,31 +18,31 @@ Five different tasks can be performed: - Get help and list all arguments: - ```bash + ```shell ./deploy_robots.py --help ``` - Default usage: Run all tasks on the `nuc1` host: - ```bash + ```shell ./deploy_robots.py nuc1 ``` - Make all robots ready for games. This also launch the teamplayer software on all robots: - ```bash + ```shell ./deploy_robots.py ALL ``` - Only run the sync and build tasks on the `nuc1` and `nuc2` hosts: - ```bash + ```shell ./deploy_robots.py --sync --build nuc1 nuc2 ``` - Only build the `bitbots_utils` ROS package on the `nuc1` host: - ```bash + ```shell ./deploy_robots.py --package bitbots_utils nuc1 ``` From 1c567e157911a461cb4dfa2cff2b9252fd3284c4 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Fri, 26 Jan 2024 19:02:25 +0100 Subject: [PATCH 09/11] Make_basler: make progress optional and fix flag --- .github/workflows/build.yml | 2 +- scripts/deploy/tasks/install.py | 12 ++++++++++-- scripts/make_basler.sh | 10 ++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 298a97411..9abf5f019 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: run: git config --global --add safe.directory /__w/bitbots_main/bitbots_main - name: Pull source code for libraries and install dependencies - run: make install HTTPS=true ARGS="-ci" + run: make install HTTPS=true ARGS="--ci" - name: Set up colcon workspace run: | diff --git a/scripts/deploy/tasks/install.py b/scripts/deploy/tasks/install.py index 1e2a64bbb..7e026b6d6 100644 --- a/scripts/deploy/tasks/install.py +++ b/scripts/deploy/tasks/install.py @@ -27,14 +27,22 @@ def _run(self, connections: Group) -> GroupResult: :return: The results of the task. """ internet_available_results = self._internet_available_on_target(connections) - if not internet_available_results.succeeded: return internet_available_results # Some hosts have an internet connection, make updates and installs apt_upgrade_results = self._apt_upgrade(get_connections_from_succeeded(internet_available_results)) + if not apt_upgrade_results.succeeded: + return apt_upgrade_results + basler_install_results = self._install_basler(get_connections_from_succeeded(apt_upgrade_results)) + if not basler_install_results.succeeded: + return basler_install_results + rosdep_results = self._install_rosdeps(get_connections_from_succeeded(basler_install_results)) + if not rosdep_results.succeeded: + return rosdep_results + pip_upgrade_results = self._pip_upgrade(get_connections_from_succeeded(rosdep_results)) return pip_upgrade_results @@ -100,7 +108,7 @@ def _install_basler(self, connections: Group) -> GroupResult: """ print_debug("Installing basler drivers") - cmd = f"{self._remote_workspace}/src/scripts/make_basler.sh -ci" + cmd = f"{self._remote_workspace}/src/scripts/make_basler.sh --ci" print_debug(f"Calling {cmd}") try: install_results = connections.sudo(cmd, hide=hide_output(), password=self._sudo_password) diff --git a/scripts/make_basler.sh b/scripts/make_basler.sh index ea8e46c32..c595a225d 100755 --- a/scripts/make_basler.sh +++ b/scripts/make_basler.sh @@ -14,9 +14,10 @@ BLAZE_VERSION="1.5.0" # Check let the user confirm that they read the license agreement on the basler website and agree with it. echo "You need to confirm that you read the license agreements for pylon $PYLON_VERSION and the blaze supplementary package $BLAZE_VERSION on the basler download page (https://www.baslerweb.com/en/downloads/software-downloads/) and agree with it." -# Check -ci flag for automatic confirmation in the ci -if [[ $1 == "-ci" ]]; then +# Check --ci flag for automatic confirmation in the ci +if [[ $1 == "--ci" ]]; then echo "Running in a CI environment, continuing..." + SHOW_PROGRESS="" else # Ask the user if they want to continue and break if they don't read -p "Do you want to continue? [y/N] " -n 1 -r @@ -25,6 +26,7 @@ else echo "Aborting..." exit 1 fi + SHOW_PROGRESS="--show-progress" fi # Create function to check if we have an internet connection @@ -49,7 +51,7 @@ else exit 1 fi # Download the pylon driver to temp folder - wget --no-verbose --show-progress $PYLON_DOWNLOAD_URL -O /tmp/pylon_${PYLON_VERSION}.tar.gz + wget --no-verbose $SHOW_PROGRESS $PYLON_DOWNLOAD_URL -O /tmp/pylon_${PYLON_VERSION}.tar.gz # Extract the pylon driver tar -xzf /tmp/pylon_${PYLON_VERSION}.tar.gz -C /tmp # Install the pylon driver @@ -69,7 +71,7 @@ else exit 1 fi # Download the blaze supplementary package to temp folder - wget --no-verbose --show-progress $BLAZE_DOWNLOAD_URL -O /tmp/pylon-blaze-supplementary-package_${BLAZE_VERSION}.deb + wget --no-verbose $SHOW_PROGRESS $BLAZE_DOWNLOAD_URL -O /tmp/pylon-blaze-supplementary-package_${BLAZE_VERSION}.deb # Install the blaze supplementary package sudo apt install /tmp/pylon-blaze-supplementary-package_${BLAZE_VERSION}*.deb -y fi From eb033c4bccb88aee2480fff9e705a051d3e72b57 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Fri, 26 Jan 2024 19:10:44 +0100 Subject: [PATCH 10/11] Fix make_basler --ci flag --- scripts/deploy/tasks/install.py | 2 -- scripts/make_basler.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/deploy/tasks/install.py b/scripts/deploy/tasks/install.py index 7e026b6d6..b5226801d 100644 --- a/scripts/deploy/tasks/install.py +++ b/scripts/deploy/tasks/install.py @@ -175,8 +175,6 @@ def _install_commands_on_single_host(connection: Connection, result: Result) -> # Define command prefixes to search for apt_command_prefix = "sudo -H apt-get install -y " apt_packages: list[str] = [] - # pip_command_prefix = "" # TODO what is it? - # pip_packages: list[str] = [] install_result: Optional[ Result diff --git a/scripts/make_basler.sh b/scripts/make_basler.sh index c595a225d..7fa301a36 100755 --- a/scripts/make_basler.sh +++ b/scripts/make_basler.sh @@ -32,7 +32,7 @@ fi # Create function to check if we have an internet connection function check_internet_connection () { # Check if we have an internet connection, except in the ci as azure does not support ping by design - if [[ $1 != "-ci" ]] && ! ping -q -c 1 -W 1 google.com >/dev/null; then + if [[ $1 != "--ci" ]] && ! ping -q -c 1 -W 1 google.com >/dev/null; then echo "No internet connection. Please check your internet connection to install the basler drivers." exit 1 fi From 17fd700cedda00c01921af821a294ad1bb7b3e50 Mon Sep 17 00:00:00 2001 From: Jan Gutsche Date: Fri, 26 Jan 2024 19:17:40 +0100 Subject: [PATCH 11/11] Deploy: unify argparse descriptions --- scripts/deploy/deploy_robots.py | 35 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/scripts/deploy/deploy_robots.py b/scripts/deploy/deploy_robots.py index 1d6b567f0..faa3dcb6f 100644 --- a/scripts/deploy/deploy_robots.py +++ b/scripts/deploy/deploy_robots.py @@ -74,50 +74,57 @@ def _parse_arguments(self) -> argparse.Namespace: "--sync", dest="only_sync", action="store_true", - help="Only synchronize (copy) files from you to the target machine", + help="Only synchronize (copy) files from you to the target machine.", ) parser.add_argument( "-i", "--install", dest="only_install", action="store_true", - help="Only install ROS dependencies on the target", + help="Only install ROS dependencies on the targets.", ) parser.add_argument( - "-c", "--configure", dest="only_configure", action="store_true", help="Only configure the target machine" + "-c", "--configure", dest="only_configure", action="store_true", help="Only configure the target machines." ) parser.add_argument( - "-b", "--build", dest="only_build", action="store_true", help="Only build on the target machine" + "-b", "--build", dest="only_build", action="store_true", help="Only build/compile on the target machines." ) parser.add_argument( "-l", "--launch", dest="only_launch", action="store_true", - help="Only launch teamplayer software on the target", + help="Only launch teamplayer software on the targets.", ) # Optional arguments - parser.add_argument("-p", "--package", default="", help="Synchronize and build only the given ROS package") + parser.add_argument("-p", "--package", default="", help="Synchronize and build only the given ROS package.") parser.add_argument( "--clean", action="store_true", - help="Clean complete workspace (source and install, ...) before syncing and building", + help="Clean complete workspace (source and install, ...) before syncing and building.", ) - parser.add_argument("--clean-src", action="store_true", help="Clean source directory before syncing") + parser.add_argument("--clean-src", action="store_true", help="Clean source directory before syncing.") parser.add_argument( "--clean-build", action="store_true", - help="Clean workspace before building. If --package is given, clean only that package", + help="Clean workspace before building. If --package is given, clean only that package.", ) parser.add_argument("--connection-timeout", default=10, help="Timeout to establish SSH connections in seconds.") parser.add_argument( - "--print-bit-bot", action="store_true", default=False, help="Print our logo at script start" + "--print-bit-bot", action="store_true", default=False, help="Print our logo at script start." + ) + parser.add_argument("-v", "--verbose", action="count", default=0, help="More output.") + parser.add_argument("-q", "--quiet", action="count", default=0, help="Less output.") + parser.add_argument( + "-u", "--user", default="bitbots", help="The SSH user to connect to the target machines with" + ) + parser.add_argument( + "-w", + "--workspace", + default="~/colcon_ws", + help="Path to the workspace directory to deploy to. Defaults to '~/colcon_ws'", ) - parser.add_argument("-v", "--verbose", action="count", default=0, help="More output") - parser.add_argument("-q", "--quiet", action="count", default=0, help="Less output") - parser.add_argument("-u", "--user", default="bitbots", help="The user to connect to the target machines with") - parser.add_argument("-w", "--workspace", default="~/colcon_ws", help="The workspace to deploy to") args = parser.parse_args()