Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions editor-server/device_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DeviceInstallError(RuntimeError):
_HARDWARE_PAIRING_INSPECTION_MARKER = "__BLACKNODE_HARDWARE_PAIRINGS__="
_HARDWARE_ADOPTION_MARKER = "__BLACKNODE_HARDWARE_ADOPTION__="
_HARDWARE_CONFIGURATION_MARKER = "__BLACKNODE_HARDWARE_CONFIGURATION__="
_HARDWARE_INSTALL_MARKER = "__BLACKNODE_HARDWARE_INSTALL__="
_UPDATE_REPORT_MARKER = "__BLACKNODE_UPDATE_REPORT__="
_OFFLINE_BUNDLE_SCHEMA_VERSION = 2
_OFFLINE_BUNDLE_LOCK = threading.Lock()
Expand Down Expand Up @@ -2080,16 +2081,35 @@ def report(percent: int, message: str) -> None:
exit 2
}
stack_root="$HOME/Blacknode/devices/$instance"
runtime_dir="$stack_root/runtime"
hardware_dir="$stack_root/hardware"
organized_runtime_dir="$stack_root/runtime"
organized_hardware_dir="$stack_root/hardware"
service_instance=""
[[ "$instance" == "default" ]] || service_instance="$instance"
if [[ "$instance" == "default" ]]; then
legacy_runtime_dir="$HOME/blacknode-runtime"
legacy_hardware_dir="$HOME/blacknode-hardware"
else
legacy_runtime_dir="$HOME/blacknode-runtimes/$instance"
legacy_hardware_dir="$HOME/blacknode-hardware-instances/$instance"
fi
runtime_dir="$organized_runtime_dir"
hardware_dir="$organized_hardware_dir"
layout="organized"
if [[ ! -d "$runtime_dir/.git" || ! -f "$runtime_dir/pyproject.toml" ]]; then
runtime_dir="$legacy_runtime_dir"
hardware_dir="$legacy_hardware_dir"
layout="legacy"
fi
case "$hardware_dir" in
"$HOME/Blacknode/devices/"*/hardware) ;;
"$HOME/Blacknode/devices/"*/hardware|\
"$HOME/blacknode-hardware"|\
"$HOME/blacknode-hardware-instances/"*) ;;
*) echo "Unsafe Hardware directory."; exit 2 ;;
esac
[[ -d "$runtime_dir/.git" && -f "$runtime_dir/pyproject.toml" ]] || {
echo "The organized Runtime installation is missing: $runtime_dir"
echo "No valid Runtime installation was found. Checked:"
echo " $organized_runtime_dir"
echo " $legacy_runtime_dir"
exit 3
}
created=false
Expand Down Expand Up @@ -2154,6 +2174,8 @@ def report(percent: int, message: str) -> None:
)
PY
created=false
printf '__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":"%s","layout":"%s","stack_mode":"isolated"}\n' \
"$hardware_dir" "$layout"
progress 100 "Robot Hardware package installed"
"""
remote_script_path = (
Expand All @@ -2167,7 +2189,7 @@ def report(percent: int, message: str) -> None:
sftp.chmod(remote_script_path, 0o700)
finally:
sftp.close()
_run(
output = _run(
connection,
f"bash {remote_script_path} {selected_instance}",
stdin_text=_sudo_input(password, attempts=8),
Expand All @@ -2184,13 +2206,33 @@ def report(percent: int, message: str) -> None:
else None
),
)
marker_line = next(
(
line[len(_HARDWARE_INSTALL_MARKER):]
for line in output.splitlines()
if line.startswith(_HARDWARE_INSTALL_MARKER)
),
"",
)
if not marker_line:
raise DeviceInstallError(
"The device did not confirm the Robot Hardware installation path."
)
try:
installation = json.loads(marker_line)
hardware_dir = str(installation["hardware_dir"])
stack_mode = str(installation.get("stack_mode") or "isolated")
layout = str(installation.get("layout") or "organized")
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
raise DeviceInstallError(
"The device returned invalid Robot Hardware installation information."
) from exc
return {
"ok": True,
"instance_id": selected_instance,
"hardware_dir": (
f"~/Blacknode/devices/{selected_instance}/hardware"
),
"stack_mode": "isolated",
"hardware_dir": hardware_dir,
"stack_mode": stack_mode,
"layout": layout,
}
finally:
try:
Expand Down
2 changes: 1 addition & 1 deletion editor-server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7400,7 +7400,7 @@ def _install_device_host_hardware_payload(
updated_management = {
**managed,
"hardware_dir": str(installed["hardware_dir"]),
"stack_mode": "isolated",
"stack_mode": str(installed.get("stack_mode") or "isolated"),
}
device = _device_registry.set_host_management(
host_id,
Expand Down
64 changes: 62 additions & 2 deletions tests/test_editor_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,12 @@ def fake_run(_connection, command, **kwargs):
"__BLACKNODE_HARDWARE_INSTALL_PROGRESS__=50|"
"Setting up the Robot Hardware environment"
)
if "blacknode-hardware-install-" in command and command.startswith("bash "):
return (
'__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":'
'"/home/alex/Blacknode/devices/default/hardware",'
'"layout":"organized","stack_mode":"isolated"}\n'
)
return ""

with (
Expand All @@ -1146,17 +1152,21 @@ def fake_run(_connection, command, **kwargs):

self.assertEqual(
result["hardware_dir"],
"~/Blacknode/devices/default/hardware",
"/home/alex/Blacknode/devices/default/hardware",
)
self.assertEqual(result["stack_mode"], "isolated")
self.assertEqual(result["layout"], "organized")
self.assertTrue(any(item["progress"] == 50 for item in progress))
self.assertIn("bash /tmp/blacknode-hardware-install-", commands[0])
self.assertEqual(stdin_values[0], "ssh-password\n" * 8)
self.assertNotIn("ssh-password", uploaded[0])
self.assertIn(
'hardware_dir="$stack_root/hardware"',
'organized_hardware_dir="$stack_root/hardware"',
uploaded[0],
)
self.assertIn('legacy_runtime_dir="$HOME/blacknode-runtime"', uploaded[0])
self.assertIn('legacy_hardware_dir="$HOME/blacknode-hardware"', uploaded[0])
self.assertIn('layout="legacy"', uploaded[0])
self.assertIn(
"git clone https://github.com/temiroff/blacknode-robot.git",
uploaded[0],
Expand All @@ -1172,6 +1182,56 @@ def fake_run(_connection, command, **kwargs):
remote_python = uploaded[0].split("<<'PY'\n", 1)[1].rsplit("\nPY", 1)[0]
compile(remote_python, "<hardware-environment-install>", "exec")

def test_hardware_environment_reports_legacy_layout_without_moving_runtime(self):
class RemoteFile(io.StringIO):
def __enter__(self):
return self

def __exit__(self, *_args):
self.close()
return False

class Sftp:
def file(self, _path, _mode):
return RemoteFile()

def chmod(self, _path, _mode):
return None

def close(self):
return None

connection = SimpleNamespace(
client=SimpleNamespace(open_sftp=lambda: Sftp()),
fingerprint="SHA256:trusted-device-key",
close=lambda: None,
)

def fake_run(_connection, command, **_kwargs):
if command.startswith("bash "):
return (
'__BLACKNODE_HARDWARE_INSTALL__={"hardware_dir":'
'"/home/ubuntu/blacknode-hardware","layout":"legacy",'
'"stack_mode":"isolated"}\n'
)
return ""

with (
patch.object(device_installer, "_connect", return_value=connection),
patch.object(device_installer, "_run", side_effect=fake_run),
):
result = device_installer.install_hardware_environment(
host="192.168.1.87",
port=22,
username="ubuntu",
password="ssh-password",
host_fingerprint="SHA256:trusted-device-key",
instance_id="default",
)

self.assertEqual(result["hardware_dir"], "/home/ubuntu/blacknode-hardware")
self.assertEqual(result["layout"], "legacy")

def test_default_stack_can_adopt_recognized_legacy_hardware_services(self):
uploaded = []

Expand Down