Microservice Python SDK: Using Notifications2 to listen to all new device registration

Hi there!
We need to do some automatic actions whenever a new device gets registered, which we accomplish by listening to Notifications2 from a microservice. We were able to run it on our local test (with our admin user credentials), but when deploying it to the cumulocity servers, some permissions problems arise:

  • If we use PER_TENANT, the service user automatically created, does not receive the notifications for the new devices. This user also cannot access to any previously created device’ MO. This is because this user does not have access to the inventory root. We wanted to fix it, but it looks like it’s impossible to grant access to the inventory root (0), not from within the microservice nor from outside: the service user does not appear in the Users list, not in the UI or using the c8y CLI.
  • If we use MULTI_TENANT, the bootstrap service user also does not have any permission on the tenant’s managed object and, as before, also cannot modify it’s own inventory assigned roles.

What should be the best practice here?

We have 2 options to test, but not sure if it’s a good practice or not:

  • Dedicated technical user: create a new user and assign the roles and inventory roles it needs to perform the task. In the microservice, only assign the role to access tenant options to get the technical user credentials. Once those are retrieved, logout and login with those credentials and perform the normal activity.
  • Change the design: instead of using Notifications2, provide an endpoint from the microservice, so the device, after registration, asks the microservice to do the initial automatic tasks. This would be sub-optimal: depends on the device to do an immportant job.

Is there any other option we have not yet considered?

Thanks in advance!

This does not seem to be a Python specific problem.

Can you be elaborate a bit on your setup, i.e. what are the roles specified in the manifest, how could you not find the users using the CLI, …?

1 Like

Hi again and thanks for your fast response!
Sure, I might elaborate a little bit more the technical details:

  • About the roles: I started from small (minimal requested roles list) to maximal, just trying to make it work. My current list includes:
    "requiredRoles": [
    "ROLE_AUDIT_ADMIN",
    "ROLE_OPTION_MANAGEMENT_READ",
    "ROLE_NOTIFICATION_2_ADMIN",
    "ROLE_IDENTITY_ADMIN",
    "ROLE_INVENTORY_ADMIN",
    "ROLE_MANAGED_OBJECT_ADMIN",
    "ROLE_TENANT_MANAGEMENT_ADMIN",
    "ROLE_USER_MANAGEMENT_READ",
    "ROLE_USER_MANAGEMENT_ADMIN",
    "ROLE_USER_MANAGEMENT_OWN_ADMIN",
    "ROLE_ACCOUNT_ADMIN",
    "ROLE_BULK_OPERATION_ADMIN"
    ]
  • The Python c8y SDK (and c8y REST under the hood) provides 2 mechanisms to read the current user: by using the /user/currentUser endpoint (CurrentUser class, c8y.users.get_current() API method) or by using the User class (using the API method c8y.users.get(username) ).

The CurrenUser method works, but this class does not have the inventory assignment methods:

AttributeError: ‘CurrentUser’ object has no attribute ‘assign_inventory_roles’

The full User method does not work, failing with an AccessDeniedError.

  • Creating the InventoryRoleAssignment didn’t worked:
    assignment = InventoryRoleAssignment(
    c8y,
    roles=[reader_role],
    user_id=username,
    managed_object=“0”
    )
    assignment.create()
  • Using the inventory role’s assign method neither:
    c8y.inventory_roles.assign(
    user=username,
    roles=reader_role.id,
    object_id='0'
    )

Any clues?

The bootstrap user is not designed to access any data in any tenant, neither in PER_TENANT nor in MULTI_TENANT services. The bootstrap user is designed to perform bootstrap activities. This includes obtaining all subscribed tenants and their respective service users. These service users will receive the permissions from requiredRoles in your manifest and can be used to access Notifications or other APIs

https://cumulocity.com/api/core/#operation/getApplicationUserCollectionRepresentation

1 Like

Hi Philipp and thanks for your help!

We only used the bootstrap user in MULTI_TENANT mode because there is no normal user in this mode. I have just seen your link about how to obtaining the subscribed users. I will try it tomorrow or maybe on Monday. Is there a way of getting this using the Python SDK?

In PER_TENANT mode, the service user did got the Notifications and other APIs permissions, but not to all the objects in the database (specifically, not to the newly registered devices, so it worked but did not got any notification, as opposed to what happened when using my personal credentials in local). It looks like it only has access to the objects owned by itself. So trying to auto-assign the tenant inventory root role to it would look like a good idea, but didn’t work, as mentioned before. Also, trying to manually assign the inventory role to the service using the UI also didn’t work, because the service user does not appear in the users list, neither using the c8y cli.

Again, thanks for your help!
emilio

Hi again @Philipp_Emmel !
Thanks for your help! I finally could use the bootstrap user to get the subscription credentials and use those for the rest of the operation:

baseurl = os.getenv(“C8Y_BASEURL“)
c8y_bootstrap = CumulocityApi(
base_url=baseurl,
tenant_id=os.getenv(“C8Y_BOOTSTRAP_TENANT“),
username=os.getenv(“C8Y_BOOTSTRAP_USER“),
password=os.getenv(“C8Y_BOOTSTRAP_PASSWORD”),
)
current_subs = c8y_bootstrap.applications.get_current_subscriptions()
c8y = CumulocityApi(
base_url=baseurl,
tenant_id=current_subs[0].tenant,
username=current_subs[0].username,
password=current_subs[0].password,
)

# Now c8y can be used for normal operations

Unfortunately, this user does not fix my original problem: I cannot self assign the tenant inventory root role, and cannot neither find this user(s) in the users table (from UI nor using c8y CLI).

Any clue?

Hm maybe I misunderstand your requirements but here are my suggestions.
First of all there are global roles and inventory roles. Global roles are used by service users and provide access on tenant level while inventory roles are assigned to users on device / group level.

What you should use are global roles and not inventory roles.

The service users permission is defined via the cumulocity microservice manifest file. So if you want the user to have global access to all inventory objects you need to assign the role
ROLE_INVENTORY_READ and ROLE_DEVICE_CONTROL_READ to read all device requests. Cumulocity - OpenAPI

Currently you only have ROLE_INVENTORY_ADMIN which DOES NOT contain any read permissions and is the main reason you don’t receive something from the inventory. You should add the according READ roles. Afterwards you should be able to query the data and (if available) get pushed via Notification2.0 if Managed Object is created/updated.

I’m not sure if device requests are covered by Notification 2.0. I think currently you need to poll. Cumulocity - OpenAPI.
Still, a device request does not mean that the device is successfully onboarded. So the managed object create with a defined filter is the right API to subscribe on in my opinion.

1 Like

Hi @Stefan_Witschel and thanks for your help!

Adding the roles ROLE_INVENTORY_READ and ROLE_DEVICE_CONTROL_READ indeed added permissions to the service user to access all the devices. Thanks a lot!

Now, the problem about NOT getting the notification on the device registration still remains :confused: . When executing it as my user (when testing it in local), it gets the notifications, but not when it’s deployed as a microservice in the c8y cloud.

This confuses me, because it looks like it should work with whatever mechanism it uses under the hood (WebSockets?). Maybe are there other missing ROLEs I should add to the microservice?

If not, what do you mean with:

  • I’m not sure if device requests are covered by Notification 2.0. I think currently you need to poll

    Any idea/tip/direction on how do I “poll” using Python SDK?

  • So the managed object create with a defined filter is the right API to subscribe on in my opinion“.

    How would I filter on device creation only? It should filter on:

    • action == “CREATE”
    • Having c8y_IsDevice

Thanks again for your help!
emilio

Hi emilio,

great to hear that worked!

I’m not aware that this is propagated to notification 2.0 in any way. What exactly did you get when you are testing with your user locally? I don’t think that a device registration is a good trigger that a new device is onboarded, as already mentioned.

The Inventory / Managed Object Create is a better trigger.
For Notification 2.0 you need to create a subscription with context=TENANT, action=CREATE api=Inventory and a filter if you like.

It is all documented here Cumulocity - OpenAPI and here Cumulocity - OpenAPI

You can also leave the subscription filter empty and just filter after receiving the message within your code. Be aware that you will receive also non-device managed objects like assets, dashboards, groups etc. you need to filter out.

If you still want to do something with device registrations you can poll this API Cumulocity - OpenAPI with just a GET HTTP request periodically and check if there is something new. Again, a device requests does not mean that this is either a valid device nor it is something onboarded already. Only when the device managed object is created you can be sure that there is a new device in cumulocity created.

1 Like

Hi again @Stefan_Witschel and, again, thanks for your help and patience!

What exactly did you get when you are testing with your user locally?

You can also leave the subscription filter empty and just filter after receiving the message within your code. Be aware that you will receive also non-device managed objects like assets, dashboards, groups etc. you need to filter out.

I was getting messages about every Managed Object just created. During the processing of each message, I “discarded” all messages which did not match my criteria. My criteria was:

action: str = msg.action
data: dict = msg.json
if action == ‘CREATE’ and ‘c8y_IsDevice’ in data:

I don’t think that a device registration is a good trigger that a new device is onboarded, as already mentioned.

The Inventory / Managed Object Create is a better trigger.

In fact, I am using this, I think. And that’s fine (when it works…).

For Notification 2.0 you need to create a subscription with context=TENANT, action=CREATE api=Inventory and a filter if you like.

I can see an API for MANAGED_OBJECTS, but not for INVENTORY:

class ApiFilter(object):
    """Notification API filter types."""
    ANY = '*'
    ALARMS = 'alarms'
    ALARMS_WITH_CHILDREN = 'alarmsWithChildren'
    EVENTS = 'events'
    EVENTS_WITH_CHILDREN = 'eventsWithChildren'
    MANAGED_OBJECTS = 'managedobjects'
    MEASUREMENTS = 'measurements'
    OPERATIONS = 'operations'

Also, I cannot find how to filter by action:

def __init__(self, c8y: CumulocityRestApi = None, name: str = None, context: str = None, source_id: str = None,
             api_filter: List[str] = None, type_filter: str = None,
             fragments: List[str] = None, non_persistent: bool = None):

My actual code creating the subscription is:

sub = Subscription(
        c8y,
        name=sub_name,
        non_persistent=False,
        context=Subscription.Context.TENANT,
        api_filter=[Subscription.ApiFilter.MANAGED_OBJECTS],
    ).create()

And for creating and starting the listener:

def process_messages(msg: AsyncListener.Message):
    logger.info(...)
    # etc.

listener = AsyncListener(
    c8y,
    subscription_name=sub_name,
    auto_ack=False,
)

listener.start(process_messages)

I am currently logging all the messages before filtering them (for debugging purposes). I get zero messages when deploying the microservice into Cumulocity servers.

Any clues on what could I be missing?

That’s look all pretty fine to me and should also work in the cloud.

What am I missing is the subscriber ID. If you are using the same locally and in the cloud you would “steal” yourself the messsages what could explain that you don’t receive anything in the cloud but locally only. If you are not providing it I think the subscription_name is used.
@Christoph_Souris Is my assumption correct?

Make sure your subscriber ID is unique per tenant and client.

Try this for testing purpose and deploy it directly to the cloud:

listener = AsyncListener(
    c8y,
    subscription_name=sub_name,
    subscriber_name="MyTestSubscriber123",
    auto_ack=False,
)

If you want to test it locally you should change your subscriber_name to another one.

Can you trigger a creation of a new device by using the Inventory API to create a device and check if you receive a message in the cloud now?

Hi again @Stefan_Witschel ,

Try this for testing purpose and deploy it directly to the cloud:

I tested that and directly deployed to the cloud (without testing in local). Same results.

Just note: I only have the cloud running during these days, as the local I already tested it works. I just run it locally once when there are lots of changes to test if it still works in local. Before and after testing it locally, I manually remove the Subscription using the c8y CLI, and then redeploy the microservice to the cloud. And I have just tested the local version and it does get the notifications, while the cloud one does not.

Best regards,
emilio

Ok that’s weird and might be maybe related to permission issues? It should do actually the same thing but whe deployed to Cumulocity fetch everything from environment variables e.g. service users etc. And when started locally you should use the bootstrap user to do the same (retrieve service user for each tenant).

Hi again @Stefan_Witschel ,
These are the permissions I’m currently setting on the microservice:

"requiredRoles": [
  "ROLE_AUDIT_ADMIN",
  "ROLE_OPTION_MANAGEMENT_READ",
  "ROLE_NOTIFICATION_2_ADMIN",
  "ROLE_IDENTITY_ADMIN",
  "ROLE_INVENTORY_READ",
  "ROLE_INVENTORY_CREATE",
  "ROLE_INVENTORY_ADMIN",
  "ROLE_DEVICE_CONTROL_READ",
  "ROLE_DEVICE_CONTROL_ADMIN",
  "ROLE_MANAGED_OBJECT_READ",
  "ROLE_MANAGED_OBJECT_ADMIN",
  "ROLE_TENANT_MANAGEMENT_ADMIN",
  "ROLE_USER_MANAGEMENT_READ",
  "ROLE_USER_MANAGEMENT_ADMIN",
  "ROLE_USER_MANAGEMENT_OWN_ADMIN",
  "ROLE_ACCOUNT_ADMIN",
  "ROLE_BULK_OPERATION_ADMIN"
],

I will end having them all! :smiley: In fact, I can add them all and then start removing to see what are the ones I need…

If you have any clue on it, I will really appreciate it.

Regards,
emilio

Hi all again,

I have some news: I’ have tested a couple of extreme scenarios: using all the available roles on the microservice and using my own user from the microservice. None of which have worked!

I really don’t know what else can I try :confused:

Any ideas are welcome.

Regards,
emilio

If you send me your current code via PM I can review it and maybe find something.

Sorry for being late to the party, I’m OOO this week.

For now, (a) as Philipp already said, don’t use the bootstrap user. The bootstrap user is to obtain user or tenant scope connections. The MultiTenantCumulocityApp is for just this purpose:

Regarding Notification 2.0 - did you check whether this example works? Not (it is using SimpleCumulocityApp, a PER_TENANT deployment.

1 Like

Hi @Christoph_Souris and thanks for your help!

I have created a service using the code from the synchronous notification. It worked in local but got an Exception when deployed to the cloud:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.12/threading.py", line 1012, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.12/site-packages/c8y_tk/notification2/listener.py", line 557, in listen
    self._event_loop.run_until_complete(self._listener.listen(_callback))
  File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/c8y_tk/notification2/listener.py", line 210, in listen
    self._connection = await self._create_connection()
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/c8y_tk/notification2/listener.py", line 160, in _create_connection
    connection = await ws_client.connect(
                 ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/websockets/asyncio/client.py", line 541, in __await_impl__
    self.connection = await self.create_connection()
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/websockets/asyncio/client.py", line 398, in create_connection
    raise ValueError("ssl argument is incompatible with a ws:// URI")
ValueError: ssl argument is incompatible with a ws:// URI

This looks like it comes from the c8y SDK. I was using the async listener in my PoC, but maybe this error is somehow hidden.

Any clue?

I’m using Python 3.12 and c8y-api==3.6.0.

The error message
ValueError: ssl argument is incompatible with a ws:// URI
indicates that the SDK tries to connect with ws:// URI but either SSL is required or incorrectly provided. The SDK tries to be clever and derive either ws:// or wss:// from the injected base URL.

I think this is a bug in the SDK, can you file an issue?

I’ll try fixing it as soon as possible.

Hi @Christoph_Souris ,

Before your response I was having a thought and I did a test: instead of relying on the provided C8Y_BASEURL, use the FQDN I usually use to access the server (I somehow injected it into my microservice). And voilà!! Everything worked as expected!

So yes, there is a bug in the SDK, where the SSL certificate for the internal name of the server is trusted when doing normal connections (non-WebSockets), but not trusted when doing the WebSocket ones.

I’m filling a bug report right now.

Again, thanks to all for your advice!! I really appreciate it!

emilio