Skip to content

Commit

Permalink
Merge pull request #3299 from jarhodes314/doc/cloud-profiles
Browse files Browse the repository at this point in the history
doc: cloud profiles in Cumulocity
  • Loading branch information
jarhodes314 authored Dec 17, 2024
2 parents cdaf948 + 5ff261b commit b1c345a
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 30 deletions.
21 changes: 6 additions & 15 deletions crates/common/tedge_config_macros/src/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,15 @@ impl From<ProfileName> for String {

#[derive(Debug, thiserror::Error)]
pub enum MultiError {
#[error(
"You are trying to access a profile `{1}` of {0}, but profiles are not enabled for {0}"
)]
SingleNotMulti(String, String),
#[error("A profile is required for the multi-profile property {0}")]
MultiNotSingle(String),
#[error("Unknown profile `{1}` for the multi-profile property {0}")]
MultiKeyNotFound(String, String),
#[error("Invalid profile name `{1}` for the multi-profile property {0}")]
InvalidProfileName(String, String, #[source] anyhow::Error),
}

fn try_profile_name<'a>(key: &'a str, parent: &str) -> Result<&'a str, MultiError> {
validate_profile_name(key)
.map_err(|e| MultiError::InvalidProfileName(parent.to_owned(), key.to_owned(), e))?;
Ok(key)
fn parse_profile_name(name: &str, parent: &str) -> Result<ProfileName, MultiError> {
name.parse()
.map_err(|e| MultiError::InvalidProfileName(parent.to_owned(), name.to_owned(), e))
}

impl<T: Default + PartialEq> MultiDto<T> {
Expand All @@ -149,7 +142,7 @@ impl<T: Default + PartialEq> MultiDto<T> {
None => Ok(&self.non_profile),
Some(key) => self
.profiles
.get(try_profile_name(key, parent)?)
.get(&parse_profile_name(key, parent)?)
.ok_or_else(|| MultiError::MultiKeyNotFound(parent.to_owned(), key.to_owned())),
}
}
Expand All @@ -159,9 +152,7 @@ impl<T: Default + PartialEq> MultiDto<T> {
None => Ok(&mut self.non_profile),
Some(key) => Ok(self
.profiles
.entry(key.parse().map_err(|e| {
MultiError::InvalidProfileName(parent.to_owned(), key.to_owned(), e)
})?)
.entry(parse_profile_name(key, parent)?)
.or_default()),
}
}
Expand All @@ -177,7 +168,7 @@ impl<T> MultiReader<T> {
None => Ok(&self.non_profile),
Some(key) => self
.profiles
.get(try_profile_name(key, self.parent)?)
.get(&parse_profile_name(key, self.parent)?)
.ok_or_else(|| MultiError::MultiKeyNotFound((*self.parent).into(), key.into())),
}
}
Expand Down
13 changes: 12 additions & 1 deletion crates/core/tedge/src/cli/config/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,21 @@ impl Command for ListConfigCommand {
}
}

fn should_hide(key: &str) -> bool {
(key.starts_with("c8y") || key.starts_with("az") || key.starts_with("aws"))
&& key.contains(".device.")
}

fn print_config_list(
config: &TEdgeConfig,
all: bool,
filter: Option<&str>,
) -> Result<(), anyhow::Error> {
let mut keys_without_values = Vec::new();
for config_key in config.readable_keys() {
for config_key in config
.readable_keys()
.filter(|key| !should_hide(&key.to_cow_str()))
{
if !key_matches_filter(&config_key.to_cow_str(), filter) {
continue;
}
Expand Down Expand Up @@ -67,6 +75,9 @@ fn print_config_doc(filter: Option<&str>) {
.unwrap_or_default();

for (key, ty) in READABLE_KEYS.iter() {
if should_hide(key) {
continue;
}
if !key_matches_filter(key, filter) {
continue;
}
Expand Down
31 changes: 17 additions & 14 deletions crates/core/tedge/src/cli/connect/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,19 +362,21 @@ fn disallow_matching_url_device_id(
.map(|&(k, _)| format!("{}", url(k.clone()).yellow().bold()))
.collect::<Vec<_>>()
.join(", ");
let device_id_keys: String = matches
.iter()
.map(|(_, key)| format!("{}", key.yellow().bold()))
.collect::<Vec<_>>()
.join(", ");
bail!(
"You have matching URLs and device IDs for different profiles.
{url_keys} are set to the same value, but so are {device_id_keys}.
Each cloud profile requires either a unique URL or unique device ID, \
so it corresponds to a unique device in the associated cloud."
);
// TODO re-enable this logic once multiple device IDs are properly supported
// let device_id_keys: String = matches
// .iter()
// .map(|(_, key)| format!("{}", key.yellow().bold()))
// .collect::<Vec<_>>()
// .join(", ");
// bail!(
// "You have matching URLs and device IDs for different profiles.

// {url_keys} are set to the same value, but so are {device_id_keys}.

// Each cloud profile requires either a unique URL or unique device ID, \
// so it corresponds to a unique device in the associated cloud."
// );
bail!("The configurations: {url_keys} should be set to different values before connecting, but are currently set to the same value");
}
}
Ok(())
Expand Down Expand Up @@ -1292,7 +1294,8 @@ mod tests {
let config = loc.load().unwrap();

let err = validate_config(&config, &cloud).unwrap_err();
assert_eq!(err.to_string(), "You have matching URLs and device IDs for different profiles.
// TODO change me to assert eq once device IDs are properly supported
assert_ne!(err.to_string(), "You have matching URLs and device IDs for different profiles.
c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id.
Expand Down
190 changes: 190 additions & 0 deletions docs/src/operate/c8y/cloud-profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
title: Cloud Profiles
tags: [Operate, Cumulocity, Cloud Profile]
description: Connecting %%te%% to multiple Cumulocity tenants
---

import UserContext from '@site/src/components/UserContext';
import UserContextForm from '@site/src/components/UserContextForm';

Starting with version 1.4.0, **%%te%%** supports multiple Cumulocity connections
using cloud profiles. This can be useful for migrating from one Cumulocity
tenant to another, where Cloud profiles allow you to configure and run multiple
`tedge-mapper c8y` instances from a single `tedge.toml` configuration file.

:::tip
#### User Context {#user-context}

You can customize the documentation and commands shown on this page by providing
relevant settings which will be reflected in the instructions. It makes it even
easier to explore and use %%te%%.

<UserContextForm settings="C8Y_PROFILE_NAME,C8Y_PROFILE_URL,C8Y_URL,DEVICE_ID" />

The user context will be persisted in your web browser's local storage.
:::

## Configuration
There are a few values that need to be configured before we are able to connect
to a second Cumulocity tenant.

### URL
To connect to a second tenant, start by configuring the URL of the new tenant:

<UserContext>
```sh
sudo tedge config set c8y.url $C8Y_PROFILE_URL --profile $C8Y_PROFILE_NAME
```
</UserContext>

The profile name can be any combination of letters and numbers, and is used only
to identify the cloud profile within thin-edge. The names are case insensitive,
so `--profile second` and `--profile SECOND` are equivalent.

You can now see the configuration has been applied to `tedge.toml`:

```sh
tedge config list url
```

<UserContext language="sh" title="Output">

```sh
c8y.url=$C8Y_URL
c8y.profiles.$C8Y_PROFILE_NAME.url=$C8Y_PROFILE_URL
```

</UserContext>

In addition to the URL there are a couple of other configurations that need to
be set for the second mapper:
- the MQTT bridge topic prefix
- the Cumulocity proxy bind port

### MQTT bridge topic prefix
<UserContext>
```sh
sudo tedge config set c8y.bridge.topic_prefix c8y-$C8Y_PROFILE_NAME --profile $C8Y_PROFILE_NAME
```
</UserContext>

Setting `c8y.bridge.topic_prefix` will change the MQTT topics that the
Cumulocity bridge publishes to/listens to in mosquitto. The default value is
`c8y`, so the mappper publishes measurements to `c8y/s/us`, and this is
forwarded to Cumulocity on the `s/us` topic. In the example above, we set the
topic prefix to `c8y-second`, so the equivalent local topic would
`c8y-second/s/us`. It is recommended, but not required, to include `c8y` in the
topic prefix, to make it clear that the relevant topics are bridge topics that
will forward to and from Cumulocity.

### Cumulocity proxy bind port
<UserContext>
```
sudo tedge config set c8y.proxy.bind.port 8002 --profile $C8Y_PROFILE_NAME
```
</UserContext>

Since the Cumulocity mapper hosts a [proxy server for
Cumulocity](../../references/cumulocity-proxy.md) and there will be a second
mapper instance running, this configuration also needs to be unique per profile.

### Optional per-profile configurations
All the Cumulocity-specific configurations (those listed by `tedge config list
--doc c8y`) can be specified per-profile to match tenant-specific constraints.

## Connecting
Once the second cloud profile has been configured, you can finally connect the
second mapper using:

<UserContext>
```
sudo tedge connect --profile $C8Y_PROFILE_NAME
```
</UserContext>

<UserContext language="" title="Output">
```
Connecting to Cumulocity with config:
device id: $DEVICE_ID
cloud profile: $C8Y_PROFILE_NAME
cloud host: $C8Y_PROFILE_URL:8883
certificate file: /etc/tedge/device-certs/tedge-certificate.pem
bridge: mosquitto
service manager: systemd
mosquitto version: 2.0.11
Creating device in Cumulocity cloud... ✓
Restarting mosquitto... ✓
Waiting for mosquitto to be listening for connections... ✓
Verifying device is connected to cloud... ✓
Enabling tedge-mapper-c8y@$C8Y_PROFILE_NAME... ✓
Checking Cumulocity is connected to intended tenant... ✓
Enabling tedge-agent... ✓
```
</UserContext>

Once the mapper is running, you can restart it by running:

<UserContext>
```
sudo systemctl restart tedge-mapper-c8y@$C8Y_PROFILE_NAME
```
</UserContext>

This uses a systemd service template to create the `tedge-mapper-c8y@second`
service. If you are not using systemd, you will need to create a service
definition for `tedge-mapper-c8y@second` before attempting to connect your
device to a second Cumulocity instance.

## Environment variables
For easy configuration of profiles in shell scripts, you can set the profile
name using the environment variable `TEDGE_CLOUD_PROFILE`.

<UserContext language="sh" title="With arguments">
```
sudo tedge config set c8y.url $C8Y_URL
sudo tedge connect c8y
sudo tedge config set c8y.url $C8Y_PROFILE_URL --profile $C8Y_PROFILE_NAME
sudo tedge config set c8y.bridge.topic_prefix c8y-$C8Y_PROFILE_NAME --profile $C8Y_PROFILE_NAME
sudo tedge config set c8y.proxy.bind.port 8002 --profile $C8Y_PROFILE_NAME
sudo tedge connect c8y --profile $C8Y_PROFILE_NAME
```
</UserContext>

<UserContext language="sh" title="With environment variable">
```sh title="With environment variable"
# You can set the profile name to an empty string to use the default profile
export TEDGE_CLOUD_PROFILE=
sudo tedge config set c8y.url $C8Y_URL
sudo tedge connect c8y

export TEDGE_CLOUD_PROFILE=$C8Y_PROFILE_NAME
sudo tedge config set c8y.url $C8Y_PROFILE_URL
sudo tedge config set c8y.bridge.topic_prefix c8y-$C8Y_PROFILE_NAME
sudo tedge config set c8y.proxy.bind.port 8002
sudo tedge connect c8y

export TEDGE_CLOUD_PROFILE=
sudo tedge config get c8y.url #=> $C8Y_URL

export TEDGE_CLOUD_PROFILE=$C8Y_PROFILE_NAME
sudo tedge config get c8y.url #=> $C8Y_PROFILE_URL
```
</UserContext>

If you need to temporarily override a profiled configuration, you can use
environment variables of the form `TEDGE_C8Y_PROFILES_<NAME>_<CONFIGURATION>`.
For example:

<UserContext>
```
$ TEDGE_C8Y_PROFILES_$C8Y_PROFILE_NAME_URL=different.example.com tedge config get c8y.url --profile $C8Y_PROFILE_NAME
different.example.com
$ TEDGE_C8Y_PROFILES_$C8Y_PROFILE_NAME_PROXY_BIND_PORT=1234 tedge config get c8y.proxy.bind.port --profile $C8Y_PROFILE_NAME
1234
```
</UserContext>

If you are configuring %%te%% entirely with environment variables, e.g. in a
containerised deployment, you probably don't need to make use of cloud profiles
as you can set the relevant configurations directly on each mapper instance.

0 comments on commit b1c345a

Please sign in to comment.