Skip to content

Commit

Permalink
Merge commit '25bd8ff548' into use-chrony-ntp
Browse files Browse the repository at this point in the history
  • Loading branch information
saiarcot895 committed Jan 2, 2025
2 parents fe83265 + 25bd8ff commit 636c1dc
Show file tree
Hide file tree
Showing 17 changed files with 2,096 additions and 203 deletions.
17 changes: 9 additions & 8 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ stages:
vmImage: ubuntu-20.04

container:
image: sonicdev-microsoft.azurecr.io:443/sonic-slave-bullseye:$(BUILD_BRANCH)
image: sonicdev-microsoft.azurecr.io:443/sonic-slave-bookworm:$(BUILD_BRANCH)

steps:
- checkout: self
Expand Down Expand Up @@ -58,7 +58,7 @@ stages:
sudo dpkg -i libyang_1.0.73_*.deb
sudo dpkg -i libswsscommon_1.0.0_amd64.deb
sudo dpkg -i python3-swsscommon_1.0.0_amd64.deb
workingDirectory: $(Pipeline.Workspace)/target/debs/bullseye/
workingDirectory: $(Pipeline.Workspace)/target/debs/bookworm/
displayName: 'Install Debian dependencies'
- script: |
Expand All @@ -71,20 +71,22 @@ stages:
sudo pip3 install sonic_config_engine-1.0-py3-none-any.whl
sudo pip3 install sonic_platform_common-1.0-py3-none-any.whl
sudo pip3 install sonic_utilities-1.2-py3-none-any.whl
workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bullseye/
workingDirectory: $(Pipeline.Workspace)/target/python-wheels/bookworm/
displayName: 'Install Python dependencies'
- script: |
set -ex
# Install .NET CORE
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
sudo apt-add-repository https://packages.microsoft.com/debian/11/prod
sudo apt-add-repository https://packages.microsoft.com/debian/12/prod
sudo apt-get update
sudo apt-get install -y dotnet-sdk-5.0
sudo apt-get install -y dotnet-sdk-8.0
displayName: "Install .NET CORE"
- script: |
python3 setup.py test
pip3 install ".[testing]"
pip3 uninstall --yes sonic-host-services
pytest
displayName: 'Test Python 3'
- task: PublishTestResults@2
Expand All @@ -103,8 +105,7 @@ stages:
displayName: 'Publish Python 3 test coverage'

- script: |
set -e
python3 setup.py bdist_wheel
python3 -m build -n
displayName: 'Build Python 3 wheel'
- publish: '$(System.DefaultWorkingDirectory)/dist/'
Expand Down
253 changes: 253 additions & 0 deletions host_modules/docker_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""Docker service handler"""

from host_modules import host_service
import docker
import signal
import errno
import logging

MOD_NAME = "docker_service"

# The set of allowed containers that can be managed by this service.
ALLOWED_CONTAINERS = {
"bgp",
"bmp",
"database",
"dhcp_relay",
"eventd",
"gnmi",
"lldp",
"pmon",
"radv",
"restapi",
"snmp",
"swss",
"syncd",
"teamd",
"telemetry",
}

# The set of allowed images that can be managed by this service.
ALLOWED_IMAGES = {
"docker-database",
"docker-dhcp-relay",
"docker-eventd",
"docker-fpm-frr",
"docker-lldp",
"docker-orchagent",
"docker-platform-monitor",
"docker-router-advertiser",
"docker-snmp",
"docker-sonic-bmp",
"docker-sonic-gnmi",
"docker-sonic-restapi",
"docker-sonic-telemetry",
"docker-syncd-brcm",
"docker-syncd-cisco",
"docker-teamd",
}


def is_allowed_image(image):
"""
Check if the image is allowed to be managed by this service.
Args:
image (str): The image name.
Returns:
bool: True if the image is allowed, False otherwise.
"""
image_name = image.split(":")[0] # Remove tag if present
return image_name in ALLOWED_IMAGES


def get_sonic_container(container_id):
"""
Get a Sonic docker container by name. If the container is not a Sonic container, raise PermissionError.
"""
client = docker.from_env()
if container_id not in ALLOWED_CONTAINERS:
raise PermissionError(
"Container {} is not allowed to be managed by this service.".format(
container_id
)
)
container = client.containers.get(container_id)
return container


def validate_docker_run_options(kwargs):
"""
Validate the keyword arguments passed to the Docker container run API.
"""
# Validate the keyword arguments here if needed
# Disallow priviledge mode for security reasons
if kwargs.get("privileged", False):
raise ValueError("Privileged mode is not allowed for security reasons.")
# Disallow sensitive directories to be mounted.
sensitive_dirs = ["/etc", "/var", "/usr"]
for bind in kwargs.get("volumes", {}).keys():
for sensitive_dir in sensitive_dirs:
if bind.startswith(sensitive_dir):
raise ValueError(
"Mounting sensitive directories is not allowed for security reasons."
)
# Disallow running containers as root.
if kwargs.get("user", None) == "root":
raise ValueError(
"Running containers as root is not allowed for security reasons."
)
# Disallow cap_add for security reasons.
if kwargs.get("cap_add", None):
raise ValueError(
"Adding capabilities to containers is not allowed for security reasons."
)
# Disallow access to sensitive devices.
if kwargs.get("devices", None):
raise ValueError("Access to devices is not allowed for security reasons.")


class DockerService(host_service.HostModule):
"""
DBus endpoint that executes the docker command
"""

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
)
def stop(self, container_id):
"""
Stop a running Docker container.
Args:
container_id (str): The name of the Docker container.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
container = get_sonic_container(container_id)
container.stop()
return 0, "Container {} has been stopped.".format(container.name)
except PermissionError:
msg = "Container {} is not allowed to be managed by this service.".format(
container_id
)
logging.error(msg)
return errno.EPERM, msg
except docker.errors.NotFound:
msg = "Container {} does not exist.".format(container_id)
logging.error(msg)
return errno.ENOENT, msg
except Exception as e:
msg = "Failed to stop container {}: {}".format(container_id, str(e))
logging.error(msg)
return 1, msg

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="si", out_signature="is"
)
def kill(self, container_id, signal=signal.SIGKILL):
"""
Kill or send a signal to a running Docker container.
Args:
container_id (str): The name or ID of the Docker container.
signal (int): The signal to send. Defaults to SIGKILL.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
container = get_sonic_container(container_id)
container.kill(signal=signal)
return 0, "Container {} has been killed with signal {}.".format(
container.name, signal
)
except PermissionError:
msg = "Container {} is not allowed to be managed by this service.".format(
container_id
)
logging.error(msg)
return errno.EPERM, msg
except docker.errors.NotFound:
msg = "Container {} does not exist.".format(container_id)
logging.error(msg)
return errno.ENOENT, msg
except Exception as e:
return 1, "Failed to kill container {}: {}".format(container_id, str(e))

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="s", out_signature="is"
)
def restart(self, container_id):
"""
Restart a running Docker container.
Args:
container_id (str): The name or ID of the Docker container.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
container = get_sonic_container(container_id)
container.restart()
return 0, "Container {} has been restarted.".format(container.name)
except PermissionError:
return (
errno.EPERM,
"Container {} is not allowed to be managed by this service.".format(
container_id
),
)
except docker.errors.NotFound:
return errno.ENOENT, "Container {} does not exist.".format(container_id)
except Exception as e:
return 1, "Failed to restart container {}: {}".format(container_id, str(e))

@host_service.method(
host_service.bus_name(MOD_NAME), in_signature="ssa{sv}", out_signature="is"
)
def run(self, image, command, kwargs):
"""
Run a Docker container.
Args:
image (str): The name of the Docker image to run.
command (str): The command to run in the container
kwargs (dict): Additional keyword arguments to pass to the Docker API.
Returns:
tuple: A tuple containing the exit code (int) and a message indicating the result of the operation.
"""
try:
client = docker.from_env()

if not is_allowed_image(image):
return (
errno.EPERM,
"Image {} is not allowed to be managed by this service.".format(
image
),
)

if command:
return (
errno.EPERM,
"Only an empty string command is allowed. Non-empty commands are not permitted by this service.",
)

validate_docker_run_options(kwargs)

# Semgrep cannot detect codes for validating image and command.
# nosemgrep: python.docker.security.audit.docker-arbitrary-container-run.docker-arbitrary-container-run
container = client.containers.run(image, command, **kwargs)
return 0, "Container {} has been created.".format(container.name)
except ValueError as e:
return errno.EINVAL, "Invalid argument.".format(str(e))
except docker.errors.ImageNotFound:
return errno.ENOENT, "Image {} not found.".format(image)
except Exception as e:
return 1, "Failed to run image {}: {}".format(image, str(e))
Loading

0 comments on commit 636c1dc

Please sign in to comment.