Controller API¶
Orchestration helpers for remote runs, Ansible execution, and run catalogs.
Public surface¶
api ¶
Public controller API surface.
Classes¶
AnsibleRunnerExecutor ¶
AnsibleRunnerExecutor(private_data_dir=None, runner_fn=None, stream_output=False, output_callback=None, stop_token=None)
Bases: RemoteExecutor
Remote executor implemented with ansible-runner.
Initialize the executor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
private_data_dir
|
Optional[Path]
|
Directory used by ansible-runner. |
None
|
runner_fn
|
Optional[Callable[..., Any]]
|
Optional runner callable for testing. Defaults to ansible_runner.run when not provided. |
None
|
stream_output
|
bool
|
When True, stream Ansible stdout events to the local process (useful for visibility in long-running tasks). |
False
|
output_callback
|
Optional[Callable[[str, str], None]]
|
Optional callback to handle stdout stream. Signature: (text: str, end: str) -> None |
None
|
Source code in lb_controller/adapters/ansible_runner.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
Attributes¶
private_data_dir
instance-attribute
¶
private_data_dir = private_data_dir or Path('.ansible_runner')
Functions¶
interrupt ¶
interrupt()
Request interruption of the current playbook execution.
Source code in lb_controller/adapters/ansible_runner.py
177 178 179 180 | |
run_playbook ¶
run_playbook(playbook_path, inventory, extravars=None, tags=None, limit_hosts=None, *, cancellable=True)
Execute a playbook using ansible-runner.
Source code in lb_controller/adapters/ansible_runner.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
BenchmarkController ¶
BenchmarkController(config, options=None)
Controller coordinating remote benchmark runs.
Source code in lb_controller/engine/controller.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
Attributes¶
services
instance-attribute
¶
services = ControllerServices(config=config, executor=executor, output_formatter=output_formatter, stop_token=stop_token, lifecycle=lifecycle, journal_refresh=_journal_refresh, use_progress_stream=_use_progress_stream)
workload_runner
instance-attribute
¶
workload_runner = WorkloadRunner(config=config, ui_notifier=_ui)
Functions¶
on_event ¶
on_event(event)
Process an event for stop coordination.
Source code in lb_controller/engine/controller.py
87 88 89 90 | |
run ¶
run(test_types, run_id=None, journal=None, resume=False, journal_path=None)
Execute the configured benchmarks on remote hosts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test_types
|
List[str]
|
List of benchmark identifiers to execute. |
required |
run_id
|
Optional[str]
|
Optional run identifier. If not provided, a timestamp-based id is generated. |
None
|
journal
|
Optional[RunJournal]
|
Optional pre-loaded journal used for resume flows. |
None
|
resume
|
bool
|
When True, reuse the provided journal instead of creating a new one. |
False
|
journal_path
|
Optional[Path]
|
Optional override for where the journal is persisted. |
None
|
Source code in lb_controller/engine/controller.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
ConnectivityReport
dataclass
¶
ConnectivityReport(results=list(), timeout_seconds=10)
Aggregated report of connectivity checks for multiple hosts.
Attributes¶
ConnectivityService ¶
ConnectivityService(timeout_seconds=None)
Service for checking SSH connectivity to remote hosts.
Initialize the connectivity service.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_seconds
|
int | None
|
Default timeout for connectivity checks. Defaults to 10 seconds. |
None
|
Source code in lb_controller/services/connectivity_service.py
63 64 65 66 67 68 69 70 | |
Attributes¶
Functions¶
check_hosts ¶
check_hosts(hosts, timeout_seconds=None)
Check connectivity to all specified hosts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hosts
|
list[RemoteHostConfig]
|
List of remote host configurations to check. |
required |
timeout_seconds
|
int | None
|
Optional timeout override for this check. |
None
|
Returns:
| Type | Description |
|---|---|
ConnectivityReport
|
ConnectivityReport with results for each host. |
Source code in lb_controller/services/connectivity_service.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
ControllerOptions
dataclass
¶
ControllerOptions(executor=None, output_callback=None, output_formatter=None, journal_refresh=None, stop_token=None, stop_timeout_s=30.0, state_machine=None)
Optional dependencies and hooks for BenchmarkController.
Attributes¶
Functions¶
build_executor ¶
build_executor()
Source code in lb_controller/models/controller_options.py
26 27 28 29 30 31 32 33 34 | |
ControllerRunner ¶
ControllerRunner(run_callable, stop_token=None, on_state_change=None, state_machine=None)
Run a BenchmarkController in a dedicated thread with state tracking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_callable
|
Callable[[], Any]
|
A callable that executes the controller and returns a summary. |
required |
stop_token
|
StopToken | None
|
Optional stop token to request graceful termination. |
None
|
on_state_change
|
Optional[StateCallback]
|
Optional callback invoked on every state transition. |
None
|
Source code in lb_controller/adapters/remote_runner.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
Attributes¶
Functions¶
arm_stop ¶
arm_stop(reason=None)
Signal the controller to stop gracefully.
Source code in lb_controller/adapters/remote_runner.py
74 75 76 77 78 79 80 81 82 83 84 85 | |
start ¶
start()
Start the controller thread.
Source code in lb_controller/adapters/remote_runner.py
54 55 56 57 58 59 60 61 62 63 | |
wait ¶
wait(timeout=None)
Block until completion or timeout; re-raise exceptions from the worker.
Source code in lb_controller/adapters/remote_runner.py
65 66 67 68 69 70 71 72 | |
ControllerState ¶
Bases: str, Enum
Phase-aware controller lifecycle states.
Attributes¶
RUNNING_GLOBAL_SETUP
class-attribute
instance-attribute
¶
RUNNING_GLOBAL_SETUP = 'running_global_setup'
RUNNING_GLOBAL_TEARDOWN
class-attribute
instance-attribute
¶
RUNNING_GLOBAL_TEARDOWN = 'running_global_teardown'
STOPPING_INTERRUPT_SETUP
class-attribute
instance-attribute
¶
STOPPING_INTERRUPT_SETUP = 'stopping_interrupt_setup'
STOPPING_INTERRUPT_TEARDOWN
class-attribute
instance-attribute
¶
STOPPING_INTERRUPT_TEARDOWN = 'stopping_interrupt_teardown'
STOPPING_WAIT_RUNNERS
class-attribute
instance-attribute
¶
STOPPING_WAIT_RUNNERS = 'stopping_wait_runners'
ControllerStateMachine ¶
ControllerStateMachine()
Thread-safe controller state tracker.
Source code in lb_controller/models/state.py
104 105 106 107 108 | |
Attributes¶
Functions¶
allows_cleanup ¶
allows_cleanup()
Source code in lb_controller/models/state.py
124 125 126 127 128 129 | |
is_terminal ¶
is_terminal()
Source code in lb_controller/models/state.py
120 121 122 | |
register_callback ¶
register_callback(callback)
Register a callback invoked on every transition.
Source code in lb_controller/models/state.py
131 132 133 134 135 | |
snapshot ¶
snapshot()
Source code in lb_controller/models/state.py
158 159 160 | |
transition ¶
transition(new_state, reason=None)
Attempt a state transition; raise ValueError if invalid.
Source code in lb_controller/models/state.py
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
DoubleCtrlCStateMachine
dataclass
¶
DoubleCtrlCStateMachine(state=RunInterruptState.RUNNING)
Double-press Ctrl+C confirmation state machine.
Policy: the second Ctrl+C confirms stop at any time after the first press, until the run finishes (no timeout window).
Attributes¶
Functions¶
mark_finished ¶
mark_finished()
Transition to FINISHED and disable further confirmation handling.
Source code in lb_controller/engine/interrupts.py
60 61 62 | |
on_sigint ¶
on_sigint(*, run_active)
Process a SIGINT and return what the caller should do next.
Source code in lb_controller/engine/interrupts.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
reset_arm ¶
reset_arm()
Return to RUNNING from STOP_ARMED after a timeout window.
Source code in lb_controller/engine/interrupts.py
64 65 66 67 | |
ExecutionResult
dataclass
¶
ExecutionResult(rc, status, stats=dict())
HostConnectivityResult
dataclass
¶
HostConnectivityResult(name, address, reachable, latency_ms=None, error_message=None)
Result of a connectivity check for a single host.
InventorySpec
dataclass
¶
InventorySpec(hosts, inventory_path=None)
LogSink ¶
LogSink(journal, journal_path, log_file=None)
Persist events and mirror them to the run journal and optional log file.
Source code in lb_controller/services/journal.py
181 182 183 184 185 186 187 188 189 190 | |
Attributes¶
Functions¶
close ¶
close()
Source code in lb_controller/services/journal.py
201 202 203 204 205 206 207 | |
emit ¶
emit(event)
Handle a single event.
Source code in lb_controller/services/journal.py
192 193 194 195 | |
emit_many ¶
emit_many(events)
Source code in lb_controller/services/journal.py
197 198 199 | |
RemoteExecutor ¶
Bases: Protocol
Protocol for remote execution engines.
Functions¶
run_playbook ¶
run_playbook(playbook_path, inventory, extravars=None, tags=None, limit_hosts=None, *, cancellable=True)
Execute a playbook and return the result.
Source code in lb_controller/models/types.py
54 55 56 57 58 59 60 61 62 63 64 65 | |
RunCatalogService ¶
RunCatalogService(output_dir, report_dir=None, data_export_dir=None)
Discover run directories and their basic metadata.
Source code in lb_controller/services/run_catalog_service.py
20 21 22 23 24 25 26 27 28 | |
Attributes¶
Functions¶
get_run ¶
get_run(run_id)
Return RunInfo for the given run_id if present.
Source code in lb_controller/services/run_catalog_service.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
list_runs ¶
list_runs()
Return all runs found under output_dir, newest first when possible.
Source code in lb_controller/services/run_catalog_service.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
RunExecutionSummary
dataclass
¶
RunExecutionSummary(run_id, per_host_output, phases, success, output_root, report_root, data_export_root, controller_state=None, cleanup_allowed=False)
Summary of a complete controller run.
Attributes¶
RunInterruptState ¶
Bases: str, Enum
Explicit run/interrupt lifecycle states.
RunJournal
dataclass
¶
RunJournal(run_id, tasks=dict(), metadata=dict())
Contains the entire execution plan and state.
Attributes¶
Functions¶
add_task ¶
add_task(task)
Source code in lb_controller/services/journal.py
64 65 | |
get_task ¶
get_task(host, workload, rep)
Return a specific task or None when absent.
Source code in lb_controller/services/journal.py
73 74 75 76 | |
get_tasks_by_host ¶
get_tasks_by_host(host)
Source code in lb_controller/services/journal.py
67 68 69 70 71 | |
initialize
classmethod
¶
initialize(run_id, config, test_types)
Factory to create a new journal based on configuration.
Source code in lb_controller/services/journal.py
54 55 56 57 58 59 60 61 62 | |
load
classmethod
¶
load(path, config=None)
Load journal from disk, optionally validating against a config.
Source code in lb_controller/services/journal.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
rehydrate_config ¶
rehydrate_config()
Return a BenchmarkConfig reconstructed from the stored config_dump.
Source code in lb_controller/services/journal.py
165 166 167 168 169 170 171 172 173 174 175 | |
save ¶
save(path)
Persist journal to disk.
Source code in lb_controller/services/journal.py
136 137 138 139 140 141 142 143 144 145 146 147 148 | |
should_run ¶
should_run(host, workload, rep, *, allow_skipped=False)
Determines if a task should be executed. Returns True if task is PENDING or FAILED (and we want to retry). For now, we skip COMPLETED tasks.
Source code in lb_controller/services/journal.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
update_task ¶
update_task(host, workload, rep, status, action='', error=None, error_type=None, error_context=None)
Source code in lb_controller/services/journal.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
RunLifecycle
dataclass
¶
RunLifecycle(phase=RunPhase.IDLE, stop_stage=StopStage.IDLE)
Tracks the lifecycle phase and stop intent.
Attributes¶
Functions¶
arm_stop ¶
arm_stop()
Source code in lb_controller/engine/lifecycle.py
50 51 52 | |
finish ¶
finish()
Source code in lb_controller/engine/lifecycle.py
42 43 44 45 46 47 48 | |
mark_failed ¶
mark_failed()
Source code in lb_controller/engine/lifecycle.py
66 67 | |
mark_interrupting_setup ¶
mark_interrupting_setup()
Source code in lb_controller/engine/lifecycle.py
54 55 | |
mark_interrupting_teardown ¶
mark_interrupting_teardown()
Source code in lb_controller/engine/lifecycle.py
63 64 | |
mark_stopped ¶
mark_stopped()
Source code in lb_controller/engine/lifecycle.py
69 70 | |
mark_teardown ¶
mark_teardown()
Source code in lb_controller/engine/lifecycle.py
60 61 | |
mark_waiting_runners ¶
mark_waiting_runners()
Source code in lb_controller/engine/lifecycle.py
57 58 | |
start_phase ¶
start_phase(phase)
Source code in lb_controller/engine/lifecycle.py
39 40 | |
SigintDecision ¶
Bases: str, Enum
Decision returned by the SIGINT state machine.
SigintDoublePressHandler ¶
SigintDoublePressHandler(*, state_machine, run_active, on_first_sigint, on_confirmed_sigint)
Bases: AbstractContextManager['SigintDoublePressHandler']
Installs a SIGINT handler that implements double-press confirmation.
Source code in lb_controller/engine/interrupts.py
73 74 75 76 77 78 79 80 81 82 83 84 85 | |
StopCoordinator ¶
StopCoordinator(expected_runners, stop_timeout=30.0, run_id=None)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_runners
|
Set[str]
|
Set of hostnames expected to confirm stop. |
required |
stop_timeout
|
float
|
Seconds to wait for confirmations. |
30.0
|
run_id
|
str | None
|
Run identifier used to correlate stop events. |
None
|
Source code in lb_controller/engine/stops.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
Attributes¶
Functions¶
can_proceed_to_teardown ¶
can_proceed_to_teardown()
Source code in lb_controller/engine/stops.py
108 109 | |
check_timeout ¶
check_timeout()
Check if the stop protocol has timed out.
Source code in lb_controller/engine/stops.py
95 96 97 98 99 100 101 102 103 104 105 106 | |
initiate_stop ¶
initiate_stop()
Transition to STOPPING_WORKLOADS.
Source code in lb_controller/engine/stops.py
47 48 49 50 51 52 53 | |
process_event ¶
process_event(event)
Process incoming events to check for stop confirmation.
We accept 'stopped', 'failed', or 'cancelled' as confirmation that the runner has ceased execution for the current workload.
Source code in lb_controller/engine/stops.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
StopState ¶
Bases: Enum
TaskState
dataclass
¶
TaskState(host, workload, repetition, status=RunStatus.PENDING, current_action='', timestamp=(lambda: datetime.now().timestamp())(), error=None, error_type=None, error_context=None, started_at=None, finished_at=None, duration_seconds=None)
Functions¶
apply_playbook_defaults ¶
apply_playbook_defaults(config)
Ensure remote_execution playbook paths point to controller Ansible assets.
Source code in lb_controller/services/paths.py
55 56 57 58 59 60 61 62 63 | |
backfill_timings_from_results ¶
backfill_timings_from_results(journal, journal_path, hosts, workload, per_host_output, refresh=None)
Backfill per-repetition timing data from all *_results.json artifacts.
Source code in lb_controller/services/journal_sync.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
pending_exists ¶
pending_exists(journal, tests, hosts, repetitions, *, allow_skipped=False)
Return True if any repetition remains to run.
Source code in lb_controller/models/pending.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
prepare_run_dirs ¶
prepare_run_dirs(config, run_id)
Create base directories for a run.
Source code in lb_controller/services/paths.py
26 27 28 29 30 31 32 33 34 35 36 37 38 | |
Benchmark controller¶
BenchmarkController ¶
BenchmarkController(config, options=None)
Controller coordinating remote benchmark runs.
Source code in lb_controller/engine/controller.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
Attributes¶
services
instance-attribute
¶
services = ControllerServices(config=config, executor=executor, output_formatter=output_formatter, stop_token=stop_token, lifecycle=lifecycle, journal_refresh=_journal_refresh, use_progress_stream=_use_progress_stream)
workload_runner
instance-attribute
¶
workload_runner = WorkloadRunner(config=config, ui_notifier=_ui)
Functions¶
on_event ¶
on_event(event)
Process an event for stop coordination.
Source code in lb_controller/engine/controller.py
87 88 89 90 | |
run ¶
run(test_types, run_id=None, journal=None, resume=False, journal_path=None)
Execute the configured benchmarks on remote hosts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test_types
|
List[str]
|
List of benchmark identifiers to execute. |
required |
run_id
|
Optional[str]
|
Optional run identifier. If not provided, a timestamp-based id is generated. |
None
|
journal
|
Optional[RunJournal]
|
Optional pre-loaded journal used for resume flows. |
None
|
resume
|
bool
|
When True, reuse the provided journal instead of creating a new one. |
False
|
journal_path
|
Optional[Path]
|
Optional override for where the journal is persisted. |
None
|
Source code in lb_controller/engine/controller.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
Ansible executor¶
AnsibleRunnerExecutor ¶
AnsibleRunnerExecutor(private_data_dir=None, runner_fn=None, stream_output=False, output_callback=None, stop_token=None)
Bases: RemoteExecutor
Remote executor implemented with ansible-runner.
Initialize the executor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
private_data_dir
|
Optional[Path]
|
Directory used by ansible-runner. |
None
|
runner_fn
|
Optional[Callable[..., Any]]
|
Optional runner callable for testing. Defaults to ansible_runner.run when not provided. |
None
|
stream_output
|
bool
|
When True, stream Ansible stdout events to the local process (useful for visibility in long-running tasks). |
False
|
output_callback
|
Optional[Callable[[str, str], None]]
|
Optional callback to handle stdout stream. Signature: (text: str, end: str) -> None |
None
|
Source code in lb_controller/adapters/ansible_runner.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
Attributes¶
private_data_dir
instance-attribute
¶
private_data_dir = private_data_dir or Path('.ansible_runner')
Functions¶
interrupt ¶
interrupt()
Request interruption of the current playbook execution.
Source code in lb_controller/adapters/ansible_runner.py
177 178 179 180 | |
run_playbook ¶
run_playbook(playbook_path, inventory, extravars=None, tags=None, limit_hosts=None, *, cancellable=True)
Execute a playbook using ansible-runner.
Source code in lb_controller/adapters/ansible_runner.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
Catalog services¶
RunCatalogService ¶
RunCatalogService(output_dir, report_dir=None, data_export_dir=None)
Discover run directories and their basic metadata.
Source code in lb_controller/services/run_catalog_service.py
20 21 22 23 24 25 26 27 28 | |
Attributes¶
Functions¶
get_run ¶
get_run(run_id)
Return RunInfo for the given run_id if present.
Source code in lb_controller/services/run_catalog_service.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
list_runs ¶
list_runs()
Return all runs found under output_dir, newest first when possible.
Source code in lb_controller/services/run_catalog_service.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
Journals and execution types¶
RunJournal
dataclass
¶
RunJournal(run_id, tasks=dict(), metadata=dict())
Contains the entire execution plan and state.
Attributes¶
Functions¶
add_task ¶
add_task(task)
Source code in lb_controller/services/journal.py
64 65 | |
get_task ¶
get_task(host, workload, rep)
Return a specific task or None when absent.
Source code in lb_controller/services/journal.py
73 74 75 76 | |
get_tasks_by_host ¶
get_tasks_by_host(host)
Source code in lb_controller/services/journal.py
67 68 69 70 71 | |
initialize
classmethod
¶
initialize(run_id, config, test_types)
Factory to create a new journal based on configuration.
Source code in lb_controller/services/journal.py
54 55 56 57 58 59 60 61 62 | |
load
classmethod
¶
load(path, config=None)
Load journal from disk, optionally validating against a config.
Source code in lb_controller/services/journal.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
rehydrate_config ¶
rehydrate_config()
Return a BenchmarkConfig reconstructed from the stored config_dump.
Source code in lb_controller/services/journal.py
165 166 167 168 169 170 171 172 173 174 175 | |
save ¶
save(path)
Persist journal to disk.
Source code in lb_controller/services/journal.py
136 137 138 139 140 141 142 143 144 145 146 147 148 | |
should_run ¶
should_run(host, workload, rep, *, allow_skipped=False)
Determines if a task should be executed. Returns True if task is PENDING or FAILED (and we want to retry). For now, we skip COMPLETED tasks.
Source code in lb_controller/services/journal.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
update_task ¶
update_task(host, workload, rep, status, action='', error=None, error_type=None, error_context=None)
Source code in lb_controller/services/journal.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |