Skip to content
Draft
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ chi-edge device register \

After this stage, the device will appear with (`2/4` checks) passing.

> **Note:** newly registered devices are restricted to your current Chameleon project by default — only reservations from that project can use them. See [Access control](#access-control) to broaden or review access.

### 2. Bake the OS image

Download the appropriate [balenaOS image](https://chameleoncloud.gitbook.io/chi-edge/edge-sdk#download-balena-os-image) for your device, then configure it for the testbed:
Expand All @@ -54,11 +56,42 @@ Write the baked image to your device's storage (microSD or eMMC) using [balenaEt
| Command | Description |
|---------|-------------|
| `chi-edge device list` | List your registered devices |
| `chi-edge device list --long` | Include type, project restrictions, contact, and local egress columns |
| `chi-edge device show <name>` | Show device details and health |
| `chi-edge device set` | Update device configuration |
| `chi-edge device delete <name>` | Remove a device |
| `chi-edge device sync <name>` | Force device re-sync |

## Access control

Every device has an `authorized_projects` list that controls which Chameleon projects can reserve it. A fresh registration sets this to your current project only.

### See the current state

```
chi-edge device list --long
```

The `Restricted to` column shows the authorized projects, or `public` if the device has no restrictions.

### Add or change authorized projects

```
chi-edge device set <name> --authorized-projects proj-a,proj-b,proj-c
```

The list is replaced wholesale — include every project that should retain access.

### Make a device public

Before removing project restrictions, reserve and use the device from your own project to confirm it's working as expected. Once you're satisfied:

```
chi-edge device set <name> --unset authorized_projects
```

The device will now accept reservations from any Chameleon project.

## Configuration

Uses OpenStack [clouds.yaml](https://docs.openstack.org/python-openstackclient/latest/configuration/index.html) or environment variables for authentication. Specify the cloud with `--os-cloud` or set `OS_CLOUD`.
Expand Down
6 changes: 6 additions & 0 deletions chi_edge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def register(
ctx = click.get_current_context()
conn = openstack.connect(cloud=ctx.obj.get("os_cloud"))

if not conn.current_project_id:
raise click.ClickException(
"cannot register device: no project scope on current auth session"
)

if bool(application_credential_id) != bool(application_credential_secret):
raise click.ClickException(
"--application-credential-id and --application-credential-secret must be provided together"
Expand Down Expand Up @@ -194,6 +199,7 @@ def register(
"application_credential_secret": application_credential_secret,
"contact_email": contact_email,
"machine_name": machine_name,
"authorized_projects": [conn.current_project_id],
},
},
)
Expand Down
76 changes: 76 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,79 @@ def test_device_set_rejects_set_and_unset_of_same_property():
assert result.exit_code != 0
assert "Cannot both set and unset --contact-email" in result.output
mock_adapter.patch.assert_not_called()


def _register_mocks(project_id: "str | None" = "proj-abc"):
"""Return (mock_conn, mock_adapter) wired for a successful register."""
mock_conn = MagicMock()
mock_conn.current_project_id = project_id
mock_conn.current_user_id = "user-xyz"
mock_conn.identity.application_credentials.return_value = []
mock_conn.identity.create_application_credential.return_value = MagicMock(
id="cred-id", secret="cred-secret"
)
mock_adapter = MagicMock()
mock_adapter.post.return_value.json.return_value = FAKE_DEVICE
return mock_conn, mock_adapter


def test_register_auto_authorizes_owning_project():
mock_conn, mock_adapter = _register_mocks(project_id="proj-abc")

runner = CliRunner()
with (
patch("chi_edge.cli.openstack") as mock_os,
patch("chi_edge.cli.doni_client", return_value=mock_adapter),
patch("chi_edge.cli.utils.validate_rfc1123_name", return_value=True),
):
mock_os.connect.return_value = mock_conn
result = runner.invoke(
cli,
[
"device",
"register",
"iot-rpi4-01",
"--machine-name",
"raspberrypi4-64",
"--contact-email",
"test@example.com",
"--application-credential-id",
"cid",
"--application-credential-secret",
"csec",
],
)
assert result.exit_code == 0, result.output
((_, kwargs),) = mock_adapter.post.call_args_list
assert kwargs["json"]["properties"]["authorized_projects"] == ["proj-abc"]


def test_register_fails_without_project_scope():
mock_conn, mock_adapter = _register_mocks(project_id=None)

runner = CliRunner()
with (
patch("chi_edge.cli.openstack") as mock_os,
patch("chi_edge.cli.doni_client", return_value=mock_adapter),
patch("chi_edge.cli.utils.validate_rfc1123_name", return_value=True),
):
mock_os.connect.return_value = mock_conn
result = runner.invoke(
cli,
[
"device",
"register",
"iot-rpi4-01",
"--machine-name",
"raspberrypi4-64",
"--contact-email",
"test@example.com",
"--application-credential-id",
"cid",
"--application-credential-secret",
"csec",
],
)
assert result.exit_code != 0
assert "no project scope" in result.output
mock_adapter.post.assert_not_called()