Skip to content

Commit

Permalink
[nvidia-bluefield] add sonic-byo python script (#19774)
Browse files Browse the repository at this point in the history
- Why I did it
To add a possibility to disable SONiC containers and run a user-provided data-plane application.

- How I did it
Added Nvidia platform-specific script sonic-byo.py

- How to verify it
Manual test

---------

Signed-off-by: Yakiv Huryk <yhuryk@nvidia.com>
  • Loading branch information
Yakiv-Huryk committed Sep 26, 2024
1 parent dbd6f5e commit bd2d4c3
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
2 changes: 2 additions & 0 deletions files/build_templates/sonic_debian_extension.j2
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,8 @@ for fw_file_name in ${!FW_FILE_MAP[@]}; do
sudo ln -s /host/image-$SONIC_IMAGE_VERSION/$PLATFORM_DIR/fw/dpu/${FW_FILE_MAP[$fw_file_name]} $FILESYSTEM_ROOT/etc/bluefield/${FW_FILE_MAP[$fw_file_name]}
done

sudo install -m 755 platform/nvidia-bluefield/byo/sonic-byo.py $FILESYSTEM_ROOT/usr/bin/sonic-byo.py

SONIC_PLATFORM_PY3_WHEEL_NAME=$(basename {{platform_api_py3_wheel_path}})
sudo cp {{platform_api_py3_wheel_path}} $FILESYSTEM_ROOT/$SONIC_PLATFORM_PY3_WHEEL_NAME
sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip3 install $SONIC_PLATFORM_PY3_WHEEL_NAME
Expand Down
192 changes: 192 additions & 0 deletions platform/nvidia-bluefield/byo/sonic-byo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/python
#
# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES.
# Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import docker
import subprocess
import os
import sys

DOCKER_TIMEOUT = 600
CONTAINER_NAME = 'byo-app-container'

client = docker.DockerClient(timeout=DOCKER_TIMEOUT)
api_client = docker.APIClient(timeout=DOCKER_TIMEOUT)


def get_args():
parser = argparse.ArgumentParser(description='Utility for SONiC BYO configuration')
mode = parser.add_subparsers(dest='mode')
mode.required = True

mode.add_parser('disable', help='Stop BYO application, restart SONiC services')

enable_parser = mode.add_parser('enable', help='Stop SONiC services and run BYO application')
group = enable_parser.add_mutually_exclusive_group(required=True)
group.add_argument('--pull', help='Docker image URL to pull')
group.add_argument('--file', help='Docker image gz file to load')
group.add_argument('--image', help='Docker image name to run')

return parser.parse_args()


def run_cmd(cmd, verbose=False):
if verbose:
print(' '.join(cmd))
subprocess.run(cmd)


def sonic_services_ctl(start, verbose):
services = [
'featured',
'swss',
'syncd',
'pmon',
'snmp',
'lldp',
'gnmi',
'bgp',
'eventd'
]

systemctlops_start = ['unmask', 'enable', 'start']
systemctlops_stop = ['stop', 'disable', 'mask']

# Start featured last
if start:
services = services[1:] + services[:1]

print('#', 'Starting' if start else 'Stopping', ', '.join(services))

ops = systemctlops_start if start else systemctlops_stop
for op in ops:
run_cmd(["systemctl", op] + services, verbose=verbose)


def prepare_sonic(verbose=False):
print('# Preparing sonic..')

sonic_services_ctl(start=False, verbose=verbose)

print('# Loading mlx5_core driver')
run_cmd(["modprobe", "mlx5_core"], verbose=verbose)


def restore_sonic(verbose=False):
print('# Restoring sonic..')
sonic_services_ctl(start=True, verbose=verbose)


def pull_image(name):
try:
print(f'# Pulling image {name}')
for line in api_client.pull(name, stream=True, decode=True):
status = line.get('status', '')
progress = line.get('progress', '')
if progress:
sys.stdout.write(f'\r{status}: {progress}')
else:
sys.stdout.write(f'\r{status}')
sys.stdout.flush()
print()
return api_client.inspect_image(name)['Id']
except docker.errors.APIError as e:
print(f'Error pulling image: {e}')
return None


def load_gz(file):
def chunked_file(f):
loaded = 0
total = os.path.getsize(file)
chunk_size = max(8192, int(total / 1000))
with open(file, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
print()
break
loaded += len(chunk)
progress = loaded / total * 100
sys.stdout.write(f'\rLoading.. {progress:.2f}%')
sys.stdout.flush()
yield chunk

try:
print(f'# Loading image {file}')
return client.images.load(chunked_file(file))[0].id

except Exception as e:
print(f'Failed to load: {e}')
return None


def run_container(image):
print(f'# Running image {image}')
config = {
'image': image,
'name': CONTAINER_NAME,
'detach': True,
'tty': True,
'privileged': True,
'network_mode': 'host',
'auto_remove': True
}

container = client.containers.run(**config) # nosemgrep: python.docker.security.audit.docker-arbitrary-container-run.docker-arbitrary-container-run
print(f'Container name: {container.name}')


def stop_container():
try:
container = client.containers.get(CONTAINER_NAME)
container.stop()
print(f'Container {CONTAINER_NAME} stopped and removed successfully')

except docker.errors.NotFound:
print(f'Container {CONTAINER_NAME} not found')
except Exception as e:
print(f'Docker error occurred: {str(e)}')


def byo_enable(args):
prepare_sonic(verbose=True)
if args.pull:
image_name = pull_image(args.pull)
elif args.file:
image_name = load_gz(args.file)
else:
image_name = args.image
run_container(image_name)


def byo_disable():
stop_container()
restore_sonic(verbose=True)


def main():
args = get_args()
if args.mode == 'enable':
byo_enable(args)
if args.mode == 'disable':
byo_disable()


if __name__ == "__main__":
main()

0 comments on commit bd2d4c3

Please sign in to comment.