|
| 1 | +# Copyright (c) 2004-present, Facebook, Inc. |
| 2 | +# All rights reserved. |
| 3 | +# |
| 4 | +# This source code is licensed under the BSD-style license found in the |
| 5 | +# LICENSE file in the root directory of this source tree. An additional grant |
| 6 | +# of patent rights can be found in the PATENTS file in the same directory. |
| 7 | + |
| 8 | +"""Device update logic for updating FBOSS services on devices.""" |
| 9 | + |
| 10 | +import logging |
| 11 | +from pathlib import Path |
| 12 | + |
| 13 | +from distro_cli.lib.exceptions import DistroInfraError |
| 14 | +from distro_cli.lib.manifest import ImageManifest |
| 15 | + |
| 16 | +logger = logging.getLogger(__name__) |
| 17 | + |
| 18 | +# Component to systemd services mapping. |
| 19 | +COMPONENT_SERVICES: dict[str, list[str]] = { |
| 20 | + "fboss-forwarding-stack": ["wedge_agent", "fsdb", "qsfp_service"], |
| 21 | + "fboss-platform-stack": [ |
| 22 | + "platform_manager", |
| 23 | + "sensor_service", |
| 24 | + "fan_service", |
| 25 | + "data_corral_service", |
| 26 | + ], |
| 27 | +} |
| 28 | + |
| 29 | + |
| 30 | +class DeviceUpdateError(DistroInfraError): |
| 31 | + """Error during device update.""" |
| 32 | + |
| 33 | + |
| 34 | +class DeviceUpdater: |
| 35 | + """Handles updating FBOSS services on a device. |
| 36 | +
|
| 37 | + Workflow: |
| 38 | + 1. Validate component is supported for update |
| 39 | + 2. Acquire artifacts (build OR download) |
| 40 | + 3. Create update package (artifacts + service_update.sh) to scp to the device |
| 41 | + 4. Get device IP |
| 42 | + 5. SCP update package to device |
| 43 | + 6. SSH: extract and run service_update.sh |
| 44 | + """ |
| 45 | + |
| 46 | + def __init__( |
| 47 | + self, |
| 48 | + mac: str, |
| 49 | + manifest: ImageManifest, |
| 50 | + component: str, |
| 51 | + device_ip: str | None = None, |
| 52 | + ): |
| 53 | + """Initialize the DeviceUpdater. |
| 54 | +
|
| 55 | + Args: |
| 56 | + mac: Device MAC address |
| 57 | + manifest: Parsed image manifest |
| 58 | + component: Component name to update |
| 59 | + device_ip: Optional device IP (if already known) |
| 60 | + """ |
| 61 | + self.mac = mac |
| 62 | + self.manifest = manifest |
| 63 | + self.component = component |
| 64 | + self.device_ip = device_ip |
| 65 | + |
| 66 | + def _get_services(self) -> list[str]: |
| 67 | + """Get systemd services for the component.""" |
| 68 | + return COMPONENT_SERVICES.get(self.component, []) |
| 69 | + |
| 70 | + def validate(self) -> None: |
| 71 | + """Validate the update request. |
| 72 | +
|
| 73 | + Raises: |
| 74 | + DeviceUpdateError: If validation fails |
| 75 | + """ |
| 76 | + if self.component not in COMPONENT_SERVICES: |
| 77 | + raise DeviceUpdateError( |
| 78 | + f"Component '{self.component}' is not updatable. " |
| 79 | + f"Updatable components: {', '.join(COMPONENT_SERVICES.keys())}" |
| 80 | + ) |
| 81 | + |
| 82 | + if not self.manifest.has_component(self.component): |
| 83 | + raise DeviceUpdateError( |
| 84 | + f"Component '{self.component}' not found in manifest" |
| 85 | + ) |
| 86 | + |
| 87 | + services = self._get_services() |
| 88 | + if not services: |
| 89 | + raise DeviceUpdateError( |
| 90 | + f"Component '{self.component}' has no services defined in COMPONENT_SERVICES" |
| 91 | + ) |
| 92 | + |
| 93 | + component_data = self.manifest.get_component(self.component) |
| 94 | + has_download = "download" in component_data |
| 95 | + has_execute = "execute" in component_data |
| 96 | + |
| 97 | + if not has_download and not has_execute: |
| 98 | + raise DeviceUpdateError( |
| 99 | + f"Component '{self.component}' has neither 'download' nor 'execute'" |
| 100 | + ) |
| 101 | + |
| 102 | + def _acquire_artifacts(self) -> Path: |
| 103 | + """Acquire component artifacts via build or download. |
| 104 | +
|
| 105 | + Returns: |
| 106 | + Path to the component artifact (tarball) |
| 107 | +
|
| 108 | + Raises: |
| 109 | + DeviceUpdateError: If artifact acquisition fails |
| 110 | + """ |
| 111 | + raise NotImplementedError("Stub") |
| 112 | + |
| 113 | + def _create_update_package(self, artifact_path: Path) -> Path: |
| 114 | + """Create update package with artifacts and update_service.sh script. |
| 115 | +
|
| 116 | + Args: |
| 117 | + artifact_path: Path to the component artifact tarball |
| 118 | +
|
| 119 | + Returns: |
| 120 | + Path to the created update package |
| 121 | +
|
| 122 | + Raises: |
| 123 | + DeviceUpdateError: If package creation fails |
| 124 | + """ |
| 125 | + raise NotImplementedError("Stub") |
| 126 | + |
| 127 | + def _transfer_and_execute(self, package_path: Path, services: list[str]) -> None: |
| 128 | + """Transfer update package to device and execute update_service.sh. |
| 129 | +
|
| 130 | + Args: |
| 131 | + package_path: Path to the update package tarball |
| 132 | + services: List of services to update |
| 133 | +
|
| 134 | + Raises: |
| 135 | + DeviceUpdateError: If transfer or execution fails |
| 136 | + """ |
| 137 | + raise NotImplementedError("Stub") |
| 138 | + |
| 139 | + def update(self) -> bool: |
| 140 | + """Execute the update workflow. |
| 141 | +
|
| 142 | + Returns: |
| 143 | + True if update succeeded |
| 144 | +
|
| 145 | + Raises: |
| 146 | + DeviceUpdateError: If update fails |
| 147 | + """ |
| 148 | + raise NotImplementedError("Stub") |
0 commit comments