Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: [3.7, 3.8, 3.9]
python-version: ['3.10', '3.13', '3.14']

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -37,36 +37,34 @@ jobs:
tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-tests
- name: Test cli
run: |
tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-cli
tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-cli
- name: Check style
if: ${{ matrix.python-version == 3.8 }}
if: ${{ matrix.python-version == '3.13' }}
run: |
tox -e py$(echo ${{ matrix.python-version }} | tr -d .)-lint
tox -e py313-lint
tox -e copying
- name: Upload coverage to Codecov
if: ${{ matrix.python-version == 3.8 }}
uses: codecov/codecov-action@v1
if: ${{ matrix.python-version == '3.13' }}
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
- name: Build a source tarball
if: matrix.python-version == 3.8
run: python setup.py sdist check --strict --metadata
- name: Check package
if: ${{ matrix.python-version == '3.13' }}
run: tox -e py313-checksetup
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
if: >-
matrix.python-version == 3.8 &&
matrix.python-version == '3.13' &&
github.event_name == 'push' &&
startsWith(github.event.ref, 'refs/tags')
with:
user: __token__
password: ${{ secrets.test_pypi_password }}
repository_url: https://test.pypi.org/legacy/
repository-url: https://test.pypi.org/legacy/
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
if: >-
matrix.python-version == 3.8 &&
matrix.python-version == '3.13' &&
github.event_name == 'push' &&
startsWith(github.event.ref, 'refs/tags')
with:
user: __token__
password: ${{ secrets.pypi_password }}
2 changes: 1 addition & 1 deletion iotlabsshcli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.

__version__ = '1.1.0'
__version__ = "1.1.0"
29 changes: 29 additions & 0 deletions iotlabsshcli/_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This file is a part of IoT-LAB ssh-cli-tools
# Copyright (C) 2015 INRIA (Contact: admin@iot-lab.info)
# Contributor(s) : see AUTHORS file
#
# This software is governed by the CeCILL license under French law
# and abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# http://www.cecill.info.
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.

"""Deprecated entry points kept for backward compatibility."""

from iotlabcli.helpers import deprecate_cmd

from iotlabsshcli.parser.open_linux_parser import main as _main


def open_a8_cli():
"""Entry point for the deprecated open-a8-cli command."""
deprecate_cmd(_main, "open-a8-cli", "iotlab-ssh")
85 changes: 40 additions & 45 deletions iotlabsshcli/open_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,44 @@

import os.path
from collections import OrderedDict

from iotlabsshcli.sshlib import OpenLinuxSsh


def _nodes_grouped(nodes):
"""Group nodes per site from a list of nodes.
>>> _nodes_grouped([])
OrderedDict()
>>> _nodes_grouped(['node-a8-1.grenoble.iot-lab.info',
... 'node-a8-2.grenoble.iot-lab.info',
... 'node-a8-2.saclay.iot-lab.info',
... 'node-a8-2.lille.iot-lab.info'])
>>> list(_nodes_grouped(['node-a8-1.grenoble.iot-lab.info',
... 'node-a8-2.grenoble.iot-lab.info',
... 'node-a8-2.saclay.iot-lab.info',
... 'node-a8-2.lille.iot-lab.info']).items())
... # doctest: +NORMALIZE_WHITESPACE
OrderedDict([('grenoble.iot-lab.info',
['node-a8-1.grenoble.iot-lab.info',
'node-a8-2.grenoble.iot-lab.info']),
('saclay.iot-lab.info', ['node-a8-2.saclay.iot-lab.info']),
('lille.iot-lab.info', ['node-a8-2.lille.iot-lab.info'])])
[('grenoble.iot-lab.info',
['node-a8-1.grenoble.iot-lab.info', 'node-a8-2.grenoble.iot-lab.info']),
('saclay.iot-lab.info', ['node-a8-2.saclay.iot-lab.info']),
('lille.iot-lab.info', ['node-a8-2.lille.iot-lab.info'])]
"""
result = OrderedDict()
for host in nodes:
site = host.split('.', 1)[1]
site = host.split(".", 1)[1]
if site not in result:
result.update({site: [host]})
else:
result[site].append(host)
return result


_FLASH_CMD = 'source /etc/profile && /usr/bin/iotlab_flash {fw} {bin}'
_RESET_CMD = 'source /etc/profile && /usr/bin/iotlab_reset'
_MAKE_EXECUTABLE_CMD = 'chmod +x {}'
_RUN_SCRIPT_CMD = 'source /etc/profile && '
_RUN_SCRIPT_CMD += 'screen -S {screen} -dm bash -c \"{path}\"'
_QUIT_SCRIPT_CMD = 'screen -X -S {screen} quit'
_REMOTE_SHARED_DIR = 'shared/.iotlabsshcli'
_FLASH_CMD = "source /etc/profile && /usr/bin/iotlab_flash {fw} {bin}"
_RESET_CMD = "source /etc/profile && /usr/bin/iotlab_reset"
_MAKE_EXECUTABLE_CMD = "chmod +x {}"
_RUN_SCRIPT_CMD = 'source /etc/profile && screen -S {screen} -dm bash -c "{path}"'
_QUIT_SCRIPT_CMD = "screen -X -S {screen} quit"
_REMOTE_SHARED_DIR = "shared/.iotlabsshcli"


def flash(config_ssh, nodes, firmware, verbose=False):
"""Flash the firmware of co-microcontroller """
"""Flash the firmware of co-microcontroller"""
failed_hosts = []
# configure ssh and remote firmware names.
groups = _nodes_grouped(nodes)
Expand All @@ -70,18 +69,18 @@ def flash(config_ssh, nodes, firmware, verbose=False):
# scp create remote directory if it doesn't exist
result = ssh.scp(firmware, remote_fw)
# We delete failed hosts for the next command
if '1' in result:
failed_hosts = [ssh.groups.pop(res) for res in result['1']]
if "1" in result:
failed_hosts = [ssh.groups.pop(res) for res in result["1"]]
# Run firmware update.
bin_opt = "--bin" if remote_fw.endswith(".bin") else ""
result = ssh.run(_FLASH_CMD.format(fw=remote_fw, bin=bin_opt))
for hosts in failed_hosts:
result.setdefault('1', []).extend(hosts)
result.setdefault("1", []).extend(hosts)
return {"flash": result}


def reset(config_ssh, nodes, verbose=False):
"""Reset co-microcontroller """
"""Reset co-microcontroller"""

# Configure ssh
groups = _nodes_grouped(nodes)
Expand All @@ -91,7 +90,7 @@ def reset(config_ssh, nodes, verbose=False):


def wait_for_boot(config_ssh, nodes, max_wait=120, verbose=False):
"""Wait for the open Linux nodes boot """
"""Wait for the open Linux nodes boot"""

# Configure ssh.
groups = _nodes_grouped(nodes)
Expand All @@ -101,7 +100,7 @@ def wait_for_boot(config_ssh, nodes, max_wait=120, verbose=False):


def run_cmd(config_ssh, nodes, cmd, run_on_frontend=False, verbose=False):
"""Run a command on the Linux nodes or SSH frontend servers """
"""Run a command on the Linux nodes or SSH frontend servers"""

# Configure ssh.
groups = _nodes_grouped(nodes)
Expand All @@ -110,7 +109,7 @@ def run_cmd(config_ssh, nodes, cmd, run_on_frontend=False, verbose=False):


def copy_file(config_ssh, nodes, file_path, verbose=False):
"""Copy a file to SSH frontend servers """
"""Copy a file to SSH frontend servers"""

# Configure ssh.
groups = _nodes_grouped(nodes)
Expand All @@ -127,45 +126,41 @@ def _get_failed_result(groups, result, run_on_frontend):
We delete failed hosts for the next commands in the groups
"""
failed = []
if '1' in result and result['1']:
if "1" in result and result["1"]:
if not run_on_frontend:
# nodes list
for site in result['1']:
for site in result["1"]:
failed.extend(groups[site])
del groups[site]
else:
# servers list
failed.extend(result['1'])
all(map(groups.pop, result['1']))
failed.extend(result["1"])
all(map(groups.pop, result["1"]))
return failed


def run_script(config_ssh, nodes, script, run_on_frontend=False,
verbose=False):
"""Run a script in background on Linux nodes or SSH frontend servers """
def run_script(config_ssh, nodes, script, run_on_frontend=False, verbose=False):
"""Run a script in background on Linux nodes or SSH frontend servers"""

# Configure ssh.
failed_hosts = []
groups = _nodes_grouped(nodes)
ssh = OpenLinuxSsh(config_ssh, groups, verbose=verbose)
screen = f'{config_ssh["user"]}-{config_ssh["exp_id"]}'
screen = f"{config_ssh['user']}-{config_ssh['exp_id']}"
remote_script = os.path.join(_REMOTE_SHARED_DIR, os.path.basename(script))
script_data = {'screen': screen, 'path': remote_script}
script_data = {"screen": screen, "path": remote_script}
# Copy script on SSH frontend servers
scp_result = ssh.scp(script, remote_script)
failed_hosts.append(_get_failed_result(ssh.groups, scp_result,
run_on_frontend))
failed_hosts.append(_get_failed_result(ssh.groups, scp_result, run_on_frontend))
# Make script executable
run_result = ssh.run(_MAKE_EXECUTABLE_CMD.format(remote_script),
with_proxy=False)
failed_hosts.append(_get_failed_result(ssh.groups, run_result,
run_on_frontend))
run_result = ssh.run(_MAKE_EXECUTABLE_CMD.format(remote_script), with_proxy=False)
failed_hosts.append(_get_failed_result(ssh.groups, run_result, run_on_frontend))
# Try cleanup and kill any running script (don't check the result)
ssh.run(_QUIT_SCRIPT_CMD.format(**script_data),
with_proxy=not run_on_frontend)
ssh.run(_QUIT_SCRIPT_CMD.format(**script_data), with_proxy=not run_on_frontend)
# Run script
result = ssh.run(_RUN_SCRIPT_CMD.format(**script_data),
with_proxy=not run_on_frontend, use_pty=False)
result = ssh.run(
_RUN_SCRIPT_CMD.format(**script_data), with_proxy=not run_on_frontend, use_pty=False
)
for hosts in filter(None, failed_hosts):
result.setdefault('1', []).extend(hosts)
result.setdefault("1", []).extend(hosts)
return {"run-script": result}
Loading
Loading