forked from platypush/platypush
Added an `add_dependencies` plugin to the Sphinx build process that parses the manifest files of the scanned backends and plugins and automatically generates the documentation for the required dependencies and triggered events. This means that those dependencies are no longer required to be listed in the docstring of the class itself. Also in this commit: - Black/LINT for some integrations that hadn't been touched in a long time. - Deleted some leftovers from previous refactors (deprecated `backend.mqtt`, `backend.zwave.mqtt`, `backend.http.request.rss`). - Deleted deprecated `inotify` backend - replaced by `file.monitor` (see #289).
240 lines
8.2 KiB
Python
240 lines
8.2 KiB
Python
from typing import Collection, List, Optional
|
|
|
|
from linode_api4 import LinodeClient, Instance, objects
|
|
|
|
from platypush.context import get_bus
|
|
from platypush.entities.cloud import CloudInstance
|
|
from platypush.entities.managers.cloud import CloudInstanceEntityManager, InstanceId
|
|
from platypush.entities.managers.switches import EnumSwitchEntityManager
|
|
from platypush.entities.switches import EnumSwitch
|
|
from platypush.message.event.linode import LinodeInstanceStatusChanged
|
|
from platypush.schemas.linode import (
|
|
LinodeInstance,
|
|
LinodeInstanceSchema,
|
|
LinodeInstanceStatus,
|
|
)
|
|
from platypush.plugins import RunnablePlugin, action
|
|
|
|
|
|
class LinodePlugin(RunnablePlugin, CloudInstanceEntityManager, EnumSwitchEntityManager):
|
|
"""
|
|
This plugin can interact with a Linode account and manage node and volumes.
|
|
|
|
To get your token:
|
|
|
|
- Login to <https://cloud.linode.com/>.
|
|
- Go to My Profile -> API Tokens -> Add a Personal Access Token.
|
|
- Select the scopes that you want to provide to your new token.
|
|
|
|
"""
|
|
|
|
def __init__(self, token: str, poll_interval: float = 60.0, **kwargs):
|
|
"""
|
|
:param token: Linode API token.
|
|
:param poll_interval: How often to poll the Linode API
|
|
(default: 60 seconds).
|
|
"""
|
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
|
self._token = token
|
|
|
|
def _get_client(self, token: Optional[str] = None) -> LinodeClient:
|
|
"""
|
|
Get a :class:`LinodeClient` instance.
|
|
|
|
:param token: Override the default token.
|
|
"""
|
|
return LinodeClient(token or self._token)
|
|
|
|
def _get_instance(
|
|
self, instance: InstanceId, token: Optional[str] = None
|
|
) -> Instance:
|
|
"""
|
|
Get an instance by name or ID.
|
|
|
|
:param instance: The label, ID or host UUID of the instance.
|
|
:param token: Override the default token.
|
|
"""
|
|
client = self._get_client(token)
|
|
if isinstance(instance, str):
|
|
filters = [Instance.label == instance]
|
|
elif isinstance(instance, int):
|
|
filters = [Instance.id == instance]
|
|
else:
|
|
raise AssertionError(f'Invalid instance type: {type(instance)}')
|
|
|
|
instances = client.linode.instances(*filters)
|
|
assert instances, f'No such Linode instance: {instance}'
|
|
return instances[0]
|
|
|
|
def _linode_instance_to_dict(self, instance: Instance) -> dict:
|
|
"""
|
|
Convert an internal :class:`linode_api4.Instance` to a
|
|
dictionary representation that can be used to create a
|
|
:class:`platypush.entities.cloud.CloudInstance` object.
|
|
"""
|
|
return {
|
|
key: (value.dict if isinstance(value, objects.MappedObject) else value)
|
|
for key, value in instance.__dict__.items()
|
|
if not key.startswith('_')
|
|
}
|
|
|
|
def main(self):
|
|
instances = []
|
|
|
|
while not self.should_stop():
|
|
status = {instance.id: instance for instance in instances}
|
|
|
|
new_status = {
|
|
instance.id: instance
|
|
for instance in self.status(publish_entities=False).output
|
|
}
|
|
|
|
changed_instances = (
|
|
[
|
|
instance
|
|
for instance in new_status.values()
|
|
if not (
|
|
status.get(instance.id)
|
|
and status[instance.id].status == instance.status
|
|
)
|
|
]
|
|
if new_status
|
|
else []
|
|
)
|
|
|
|
if changed_instances:
|
|
for instance in changed_instances:
|
|
get_bus().post(
|
|
LinodeInstanceStatusChanged(
|
|
instance_id=instance.id,
|
|
instance_name=instance.name,
|
|
status=instance.status,
|
|
old_status=(
|
|
status[instance.id].status
|
|
if status.get(instance.id)
|
|
else None
|
|
),
|
|
)
|
|
)
|
|
|
|
self.publish_entities(changed_instances)
|
|
|
|
instances = new_status.values()
|
|
self.wait_stop(self.poll_interval)
|
|
|
|
def transform_entities(
|
|
self, entities: Collection[LinodeInstance]
|
|
) -> Collection[CloudInstance]:
|
|
schema = LinodeInstanceSchema()
|
|
return super().transform_entities(
|
|
[
|
|
CloudInstance(
|
|
reachable=instance.status == LinodeInstanceStatus.RUNNING,
|
|
children=[
|
|
EnumSwitch(
|
|
id=f'{instance.id}:__action__',
|
|
name='Actions',
|
|
values=['boot', 'reboot', 'shutdown'],
|
|
is_write_only=True,
|
|
)
|
|
],
|
|
**schema.dump(instance),
|
|
)
|
|
for instance in entities
|
|
]
|
|
)
|
|
|
|
@action
|
|
def status(
|
|
self,
|
|
*_,
|
|
instance: Optional[InstanceId] = None,
|
|
token: Optional[str] = None,
|
|
publish_entities: bool = True,
|
|
**__,
|
|
) -> List[LinodeInstance]:
|
|
"""
|
|
Get the full status and info of the instances associated to a selected account.
|
|
|
|
:param token: Override the default access token if you want to query another account.
|
|
:param instance: Select only one instance, either by name, ID or host UUID.
|
|
:param publish_entities: Whether
|
|
:class:`platypush.message.event.entities.EntityUpdateEvent` should
|
|
be published for all the instances, whether or not their status has
|
|
changed (default: ``True``).
|
|
:return: .. schema:: linode.LinodeInstanceSchema(many=True)
|
|
"""
|
|
instances = (
|
|
[self._get_instance(instance=instance)]
|
|
if instance
|
|
else [
|
|
instance
|
|
for page in self._get_client(token).linode.instances().lists
|
|
for instance in page
|
|
]
|
|
)
|
|
|
|
mapped_instances = LinodeInstanceSchema(many=True).load(
|
|
map(self._linode_instance_to_dict, instances)
|
|
)
|
|
|
|
if publish_entities:
|
|
self.publish_entities(mapped_instances)
|
|
|
|
return mapped_instances
|
|
|
|
@action
|
|
def reboot(self, instance: InstanceId, token: Optional[str] = None, **_):
|
|
"""
|
|
Reboot an instance.
|
|
|
|
:param instance: Instance ID, label or host UUID.
|
|
:param token: Default access token override.
|
|
"""
|
|
node = self._get_instance(instance=instance, token=token)
|
|
assert node.reboot(), 'Reboot failed'
|
|
|
|
@action
|
|
def boot(self, instance: InstanceId, token: Optional[str] = None, **_):
|
|
"""
|
|
Boot an instance.
|
|
|
|
:param instance: Instance ID, label or host UUID.
|
|
:param token: Default access token override.
|
|
"""
|
|
node = self._get_instance(instance=instance, token=token)
|
|
assert node.boot(), 'Boot failed'
|
|
|
|
@action
|
|
def shutdown(self, instance: InstanceId, token: Optional[str] = None, **_):
|
|
"""
|
|
Shutdown an instance.
|
|
|
|
:param instance: Instance ID, label or host UUID.
|
|
:param token: Default access token override.
|
|
"""
|
|
node = self._get_instance(instance=instance, token=token)
|
|
assert node.shutdown(), 'Shutdown failed'
|
|
|
|
@action
|
|
def set(self, entity: str, value: str, **kwargs):
|
|
"""
|
|
Entity framework compatible method to run an action on the instance
|
|
through an ``EnumSwitch``.
|
|
|
|
:param entity: Entity ID, as ``<instance_id>`` or
|
|
``<instance_id>:__action__``.
|
|
:param value: Action to run, one among ``boot``, ``reboot`` and
|
|
``shutdown``.
|
|
"""
|
|
try:
|
|
instance_id = int(entity.removesuffix(':__action__'))
|
|
except (TypeError, ValueError) as e:
|
|
raise AssertionError(f'Invalid entity: {entity}') from e
|
|
|
|
assert value in {'boot', 'reboot', 'shutdown'}, f'Invalid action: {value}'
|
|
method = getattr(self, value)
|
|
return method(instance_id, **kwargs)
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|