-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: Replace 'screen' use with Docker. WIP #54
Draft
figuernd
wants to merge
5
commits into
main
Choose a base branch
from
nathan.figueroa/config_consolidation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c5e605d
refactor: Replace 'screen' use with Docker. WIP
figuernd 1e79f38
Fix: Configuration read and apply bugs
figuernd c905400
Reverting phyto-arm script changes in favor of new CLI wrapper: pa
figuernd 3e8939f
fix: Standardize on /data instead of /mnt/data
figuernd 8530ef9
feat: Use static volume mount destinations instead of parameters. Hav…
figuernd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
PhytO-ARM Container CLI (pa) | ||
|
||
This script manages Docker containers for PhytO-ARM processes, using the original | ||
phyto-arm script within each container to manage ROS nodes. | ||
""" | ||
import argparse | ||
import os | ||
import sys | ||
import time | ||
from pathlib import Path | ||
from typing import Dict, Optional | ||
|
||
import docker | ||
import yaml | ||
from docker.models.containers import Container | ||
|
||
def create_network(client: docker.DockerClient) -> docker.models.networks.Network: | ||
"""Create or get the Docker network for PhytO-ARM containers""" | ||
try: | ||
return client.networks.get("phyto-arm-net") | ||
except docker.errors.NotFound: | ||
return client.networks.create("phyto-arm-net", driver="bridge") | ||
|
||
def get_running_containers(client: docker.DockerClient) -> Dict[str, Container]: | ||
"""Get all running PhytO-ARM containers""" | ||
containers = client.containers.list(filters={"name": "phyto-arm-"}) | ||
return {c.name.replace('phyto-arm-', ''): c for c in containers} | ||
|
||
# This is here to wire up arguments to volume mounts to ROS launch parameters. | ||
# The alternative is to generate a mount automatically and pass the paths as | ||
# arguments to their respective launch files. | ||
def handle_special_volumes_by_process(process_name: str, process_config: dict) -> dict: | ||
volumes = {} | ||
if process_name == "main": | ||
volumes[process_config['launch_args']['rosbag_dir']] = { | ||
"bind": "/app/volumes/rosbags", # Must match launch file | ||
"mode": "rw" | ||
} | ||
if process_name == "arm_ifcb": | ||
volumes[process_config['launch_args']['routines_dir']] = { | ||
"bind": "/app/volumes/routines", # Must match launch file | ||
"mode": "ro" | ||
} | ||
volumes[process_config['launch_args']['data_dir']] = { | ||
"bind": "/app/volumes/ifcbdata", # Must match launch file | ||
"mode": "rw" | ||
} | ||
return volumes | ||
|
||
def start_container( | ||
client: docker.DockerClient, | ||
network: docker.models.networks.Network, | ||
config_path: Path, | ||
process_name: str, | ||
config: dict | ||
) -> Container: | ||
"""Start a single PhytO-ARM container""" | ||
|
||
process_config = config['processes'][process_name] | ||
image_name = config['docker_image'] | ||
container_name = f"phyto-arm-{process_name}" | ||
log_dir = config['log_dir'] | ||
log_mount = "/app/volumes/roslogs" | ||
|
||
# Base volumes that all containers need | ||
volumes = { | ||
str(config_path.absolute()): { | ||
"bind": "/app/config.yaml", | ||
"mode": "ro" | ||
}, | ||
str(config_path.parent.absolute()): { | ||
"bind": "/app/configs", | ||
"mode": "ro" | ||
}, | ||
log_dir: { | ||
"bind": log_mount, | ||
"mode": "rw" | ||
} | ||
} | ||
|
||
# Add process-specific volumes | ||
special_volumes = handle_special_volumes_by_process(process_name, process_config) | ||
volumes.update(special_volumes) | ||
|
||
# Handle devices | ||
devices = [ | ||
f"{path}:{path}" | ||
for path in process_config.get('devices', {}).values() | ||
] | ||
|
||
# Handle ports | ||
ports = {} | ||
for port in process_config.get('tcp_ports', {}).values(): | ||
ports[f'{port}/tcp'] = port | ||
for port in process_config.get('udp_ports', {}).values(): | ||
ports[f'{port}/udp'] = port | ||
|
||
# Container configuration | ||
container_config = { | ||
"image": image_name, | ||
"name": container_name, | ||
"command": f"phyto-arm start {process_name} /app/config.yaml", | ||
"detach": True, | ||
"remove": True, | ||
"volumes": volumes, | ||
"network": network.name, | ||
"devices": devices, | ||
"ports": ports, | ||
"environment": { | ||
"DONT_SCREEN": "1", # screen is not needed within containers | ||
"ROS_MASTER_URI": "http://phyto-arm-main:11311", | ||
"ROS_LOG_DIR": log_mount | ||
} | ||
} | ||
|
||
if process_name == "main": | ||
container_config["environment"].pop("ROS_MASTER_URI") | ||
|
||
try: | ||
container = client.containers.run(**container_config) | ||
print(f"Started container {container_name}") | ||
return container | ||
except docker.errors.APIError as e: | ||
print(f"Failed to start container {container_name}: {e}") | ||
raise | ||
|
||
def start_processes(config_path: Path, process_names: Optional[list[str]] = None) -> None: | ||
"""Start PhytO-ARM containers based on config""" | ||
client = docker.DockerClient.from_env() | ||
|
||
# Check if any containers are already running | ||
running = get_running_containers(client) | ||
if running and process_names: | ||
|
||
# Check for conflicts with requested processes | ||
conflicts = set(process_names) & set(running.keys()) | ||
if conflicts: | ||
print("Some requested processes are already running:") | ||
for name in conflicts: | ||
print(f" - {name}") | ||
sys.exit(1) | ||
elif running: | ||
print("Some PhytO-ARM containers are already running:") | ||
for name in running: | ||
print(f" - {name}") | ||
print("Specify process names to start additional containers") | ||
sys.exit(1) | ||
|
||
# Load config | ||
with open(config_path) as f: | ||
config = yaml.safe_load(f) | ||
|
||
# Create network | ||
network = create_network(client) | ||
|
||
# Determine which processes to start | ||
processes = config['processes'] | ||
if process_names: | ||
# Validate requested processes exist | ||
invalid = set(process_names) - set(processes.keys()) | ||
if invalid: | ||
print(f"Processes not found in config: {', '.join(invalid)}") | ||
sys.exit(1) | ||
# Filter to only requested processes | ||
processes = {k: v for k, v in processes.items() if k in process_names} | ||
|
||
# Always start main first if it's in the list | ||
if 'main' in processes: | ||
start_container( | ||
client, network, config_path, 'main', config | ||
) | ||
|
||
# Wait for ROS master to start | ||
time.sleep(10) | ||
|
||
# Remove main so we don't start it again | ||
processes.pop('main') | ||
|
||
# Start remaining processes | ||
for name, process_config in processes.items(): | ||
if process_config.get('enabled', True): | ||
start_container( | ||
client, network, config_path, name, config | ||
) | ||
|
||
# If process is not enabled in the config, but was requested, print a warning | ||
elif process_names: | ||
print(f"Process {name} is not enabled in the config; skipping") | ||
|
||
def stop_processes(process_names: Optional[list[str]] = None) -> None: | ||
"""Stop PhytO-ARM containers""" | ||
client = docker.DockerClient.from_env() | ||
stoppable = get_running_containers(client) | ||
|
||
if not stoppable: | ||
print("No PhytO-ARM containers are running") | ||
return | ||
|
||
if process_names: | ||
|
||
# Validate requested processes exist | ||
invalid = set(process_names) - set(stoppable.keys()) | ||
if invalid: | ||
print(f"Processes not running: {', '.join(invalid)}") | ||
sys.exit(1) | ||
|
||
# Filter to only requested processes | ||
stoppable = {k: v for k, v in stoppable.items() if k in process_names} | ||
|
||
for name, container in stoppable.items(): | ||
try: | ||
container.stop() | ||
print(f"Stopped {name}") | ||
except docker.errors.APIError as e: | ||
print(f"Failed to stop {name}: {e}") | ||
|
||
def list_processes() -> None: | ||
"""List running PhytO-ARM containers""" | ||
client = docker.DockerClient.from_env() | ||
running = get_running_containers(client) | ||
|
||
if not running: | ||
print("No PhytO-ARM containers are running") | ||
return | ||
|
||
print("Running processes:") | ||
for name in running: | ||
print(f" - {name}") | ||
|
||
def attach_process(process_name: str) -> None: | ||
"""Attach to a running PhytO-ARM container""" | ||
client = docker.DockerClient.from_env() | ||
running = get_running_containers(client) | ||
|
||
if not running: | ||
print("No PhytO-ARM containers are running") | ||
return | ||
|
||
if process_name not in running: | ||
print(f"Process '{process_name}' is not running") | ||
print("Running processes:", ", ".join(running.keys())) | ||
return | ||
|
||
os.system(f"docker attach phyto-arm-{process_name}") | ||
|
||
def cleanup() -> None: | ||
"""Remove PhytO-ARM network and any stopped containers""" | ||
client = docker.DockerClient.from_env() | ||
|
||
# Remove network | ||
try: | ||
network = client.networks.get("phyto-arm-net") | ||
network.remove() | ||
print("Removed PhytO-ARM network") | ||
except docker.errors.NotFound: | ||
pass | ||
|
||
# Remove any stopped containers | ||
containers = client.containers.list( | ||
all=True, | ||
filters={"name": "phyto-arm-"} | ||
) | ||
for container in containers: | ||
try: | ||
container.remove() | ||
print(f"Removed container {container.name}") | ||
except docker.errors.APIError: | ||
pass | ||
|
||
def main(): | ||
parser = argparse.ArgumentParser(description=__doc__) | ||
subparsers = parser.add_subparsers(dest='command', required=True) | ||
|
||
# Start command | ||
start_parser = subparsers.add_parser('start') | ||
start_parser.add_argument('config', type=Path) | ||
start_parser.add_argument('processes', nargs='*', | ||
help='Specific processes to start (default: all enabled)') | ||
|
||
# Stop command | ||
stop_parser = subparsers.add_parser('stop') | ||
stop_parser.add_argument('processes', nargs='*', | ||
help='Specific processes to stop (default: all)') | ||
|
||
# List command | ||
subparsers.add_parser('list') | ||
|
||
# Attach command | ||
attach_parser = subparsers.add_parser('attach') | ||
attach_parser.add_argument('process') | ||
|
||
# Cleanup command | ||
subparsers.add_parser('cleanup') | ||
|
||
args = parser.parse_args() | ||
|
||
try: | ||
if args.command == 'start': | ||
start_processes(args.config, args.processes) | ||
elif args.command == 'stop': | ||
stop_processes(args.processes) | ||
elif args.command == 'list': | ||
list_processes() | ||
elif args.command == 'attach': | ||
attach_process(args.process) | ||
elif args.command == 'cleanup': | ||
cleanup() | ||
except docker.errors.APIError as e: | ||
print(f"Docker error: {e}") | ||
sys.exit(1) | ||
except KeyboardInterrupt: | ||
print("\nOperation interrupted") | ||
sys.exit(1) | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to find a more elegant way of expressing this. Here, it is attached to the "process" structure, which is pretty far from the actual node configuration some 250 lines later (and called just
web
).