forked from platypush/platypush
Added Linode integration [closes #116]
This commit is contained in:
parent
096f84c865
commit
c26d456109
19 changed files with 365 additions and 7 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@ platypush/backend/http/static/css/*/.sass-cache/
|
||||||
.vscode
|
.vscode
|
||||||
platypush/backend/http/static/js/lib/vue.js
|
platypush/backend/http/static/js/lib/vue.js
|
||||||
platypush/notebooks
|
platypush/notebooks
|
||||||
|
platypush/requests
|
||||||
|
|
|
@ -29,6 +29,7 @@ Backends
|
||||||
platypush/backend/joystick.rst
|
platypush/backend/joystick.rst
|
||||||
platypush/backend/kafka.rst
|
platypush/backend/kafka.rst
|
||||||
platypush/backend/light.hue.rst
|
platypush/backend/light.hue.rst
|
||||||
|
platypush/backend/linode.rst
|
||||||
platypush/backend/local.rst
|
platypush/backend/local.rst
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
platypush/backend/mqtt.rst
|
platypush/backend/mqtt.rst
|
||||||
|
|
|
@ -245,6 +245,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
||||||
'pvporcupine ',
|
'pvporcupine ',
|
||||||
'pvcheetah',
|
'pvcheetah',
|
||||||
'pyotp',
|
'pyotp',
|
||||||
|
'linode_api4',
|
||||||
]
|
]
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -29,6 +29,7 @@ Events
|
||||||
platypush/events/joystick.rst
|
platypush/events/joystick.rst
|
||||||
platypush/events/kafka.rst
|
platypush/events/kafka.rst
|
||||||
platypush/events/light.rst
|
platypush/events/light.rst
|
||||||
|
platypush/events/linode.rst
|
||||||
platypush/events/media.rst
|
platypush/events/media.rst
|
||||||
platypush/events/midi.rst
|
platypush/events/midi.rst
|
||||||
platypush/events/mqtt.rst
|
platypush/events/mqtt.rst
|
||||||
|
|
5
docs/source/platypush/backend/linode.rst
Normal file
5
docs/source/platypush/backend/linode.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.backend.linode``
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. automodule:: platypush.backend.linode
|
||||||
|
:members:
|
5
docs/source/platypush/events/linode.rst
Normal file
5
docs/source/platypush/events/linode.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.event.linode``
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.event.linode
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/linode.rst
Normal file
5
docs/source/platypush/plugins/linode.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.plugins.linode``
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.linode
|
||||||
|
:members:
|
5
docs/source/platypush/responses/linode.rst
Normal file
5
docs/source/platypush/responses/linode.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``platypush.message.response.linode``
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.message.response.linode
|
||||||
|
:members:
|
|
@ -61,6 +61,7 @@ Plugins
|
||||||
platypush/plugins/lastfm.rst
|
platypush/plugins/lastfm.rst
|
||||||
platypush/plugins/light.rst
|
platypush/plugins/light.rst
|
||||||
platypush/plugins/light.hue.rst
|
platypush/plugins/light.hue.rst
|
||||||
|
platypush/plugins/linode.rst
|
||||||
platypush/plugins/logger.rst
|
platypush/plugins/logger.rst
|
||||||
platypush/plugins/media.rst
|
platypush/plugins/media.rst
|
||||||
platypush/plugins/media.chromecast.rst
|
platypush/plugins/media.chromecast.rst
|
||||||
|
|
|
@ -11,6 +11,7 @@ Responses
|
||||||
platypush/responses/camera.android.rst
|
platypush/responses/camera.android.rst
|
||||||
platypush/responses/chat.telegram.rst
|
platypush/responses/chat.telegram.rst
|
||||||
platypush/responses/google.drive.rst
|
platypush/responses/google.drive.rst
|
||||||
|
platypush/responses/linode.rst
|
||||||
platypush/responses/pihole.rst
|
platypush/responses/pihole.rst
|
||||||
platypush/responses/ping.rst
|
platypush/responses/ping.rst
|
||||||
platypush/responses/printer.cups.rst
|
platypush/responses/printer.cups.rst
|
||||||
|
|
|
@ -20,14 +20,14 @@ class BluetoothScannerBackend(SensorBackend):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[int] = None, scan_interval: int = 10, **kwargs):
|
def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
:param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None).
|
||||||
:param scan_interval: How long the scan should run (default: 10 seconds).
|
:param scan_duration: How long the scan should run (default: 10 seconds).
|
||||||
"""
|
"""
|
||||||
super().__init__(plugin='bluetooth', plugin_args={
|
super().__init__(plugin='bluetooth', plugin_args={
|
||||||
'device_id': device_id,
|
'device_id': device_id,
|
||||||
'duration': scan_interval,
|
'duration': scan_duration,
|
||||||
}, **kwargs)
|
}, **kwargs)
|
||||||
|
|
||||||
self._last_seen_devices = {}
|
self._last_seen_devices = {}
|
||||||
|
|
|
@ -19,14 +19,14 @@ class BluetoothBleScannerBackend(BluetoothScannerBackend):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, interface: Optional[int] = None, scan_interval: int = 10, **kwargs):
|
def __init__(self, interface: Optional[int] = None, scan_duration: int = 10, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param interface: Bluetooth adapter name to use (default configured on the ``bluetooth.ble`` plugin if None).
|
:param interface: Bluetooth adapter name to use (default configured on the ``bluetooth.ble`` plugin if None).
|
||||||
:param scan_interval: How long the scan should run (default: 10 seconds).
|
:param scan_duration: How long the scan should run (default: 10 seconds).
|
||||||
"""
|
"""
|
||||||
super().__init__(plugin='bluetooth.ble', plugin_args={
|
super().__init__(plugin='bluetooth.ble', plugin_args={
|
||||||
'interface': interface,
|
'interface': interface,
|
||||||
'duration': scan_interval,
|
'duration': scan_duration,
|
||||||
}, **kwargs)
|
}, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
46
platypush/backend/linode.py
Normal file
46
platypush/backend/linode.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
|
from platypush.backend.sensor import SensorBackend
|
||||||
|
from platypush.message.event.linode import LinodeInstanceStatusChanged
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeBackend(SensorBackend):
|
||||||
|
"""
|
||||||
|
This backend monitors the state of one or more Linode instances.
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
|
||||||
|
* :class:`platypush.message.event.linode.LinodeInstanceStatusChanged` when the status of an instance changes.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* The :class:`platypush.plugins.linode.LinodePlugin` plugin configured.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, instances: Optional[List[str]] = None, poll_seconds: float = 30.0, **kwargs):
|
||||||
|
"""
|
||||||
|
:param instances: List of instances to monitor, by label (default: monitor all the instances).
|
||||||
|
"""
|
||||||
|
super().__init__(plugin='linode', poll_seconds=poll_seconds, **kwargs)
|
||||||
|
self.instances = set(instances or [])
|
||||||
|
|
||||||
|
def process_data(self, data: Dict[str, dict], *args, **kwargs):
|
||||||
|
instances = data['instances']
|
||||||
|
old_instances = (self.data or {}).get('instances', {})
|
||||||
|
|
||||||
|
if self.instances:
|
||||||
|
instances = {label: instances[label] for label in self.instances if label in instances}
|
||||||
|
|
||||||
|
if not instances:
|
||||||
|
return
|
||||||
|
|
||||||
|
for label, instance in instances.items():
|
||||||
|
old_instance = old_instances.get(label, {})
|
||||||
|
if 'status' in old_instance and old_instance['status'] != instance['status']:
|
||||||
|
self.bus.post(LinodeInstanceStatusChanged(instance=label,
|
||||||
|
status=instance['status'],
|
||||||
|
old_status=old_instance['status']))
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -175,7 +175,7 @@ class SensorBackend(Backend):
|
||||||
|
|
||||||
def process_data(self, data, new_data):
|
def process_data(self, data, new_data):
|
||||||
if new_data:
|
if new_data:
|
||||||
self.bus.post(SensorDataChangeEvent(data=data, source=self.plugin or self.__class__.__name__))
|
self.bus.post(SensorDataChangeEvent(data=new_data, source=self.plugin or self.__class__.__name__))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
18
platypush/message/event/linode.py
Normal file
18
platypush/message/event/linode.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeEvent(Event):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeInstanceStatusChanged(LinodeEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when the status of a Linode instance changes.
|
||||||
|
"""
|
||||||
|
def __init__(self, instance: str, status: str, old_status: Optional[str] = None, *args, **kwargs):
|
||||||
|
super().__init__(*args, instance=instance, status=status, old_status=old_status, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
164
platypush/message/response/linode.py
Normal file
164
platypush/message/response/linode.py
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from linode_api4.objects.linode import Instance, Config, Disk, Backup, Image, Kernel, Type
|
||||||
|
|
||||||
|
from platypush.message import Mapping
|
||||||
|
from platypush.message.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeResponse(Response):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeConfigModel(Mapping):
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
super().__init__()
|
||||||
|
self.comments = config.comments
|
||||||
|
self.created = config.created
|
||||||
|
self.helpers = config.helpers.dict
|
||||||
|
self.id = config.id
|
||||||
|
self.initrd = config.initrd
|
||||||
|
self.kernel = dict(LinodeKernelModel(config.kernel))
|
||||||
|
self.label = config.label
|
||||||
|
self.linode_id = config.linode_id
|
||||||
|
self.memory_limit = config.memory_limit
|
||||||
|
self.parent_id_name = config.parent_id_name
|
||||||
|
self.root_device = config.root_device
|
||||||
|
self.run_level = config.run_level
|
||||||
|
self.updated = datetime.datetime.fromisoformat(config.updated)
|
||||||
|
self.virt_mode = config.virt_mode
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeKernelModel(Mapping):
|
||||||
|
def __init__(self, kernel: Kernel):
|
||||||
|
super().__init__()
|
||||||
|
self.architecture = kernel.architecture
|
||||||
|
self.created = kernel.created
|
||||||
|
self.deprecated = kernel.deprecated
|
||||||
|
self.description = kernel.description
|
||||||
|
self.id = kernel.id
|
||||||
|
self.kvm = kernel.kvm
|
||||||
|
self.label = kernel.label
|
||||||
|
self.version = kernel.version
|
||||||
|
self.xen = kernel.xen
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeBackupModel(Mapping):
|
||||||
|
def __init__(self, backup: Backup):
|
||||||
|
super().__init__()
|
||||||
|
self.created = backup.created
|
||||||
|
self.disks = {
|
||||||
|
disk.label: {
|
||||||
|
'label': disk.label,
|
||||||
|
'size': disk.size,
|
||||||
|
'filesystem': disk.filesystem,
|
||||||
|
}
|
||||||
|
for disk in backup.disks
|
||||||
|
}
|
||||||
|
|
||||||
|
self.duration = backup.duration
|
||||||
|
self.finished = backup.finished
|
||||||
|
self.id = backup.id
|
||||||
|
self.label = backup.label
|
||||||
|
self.linode_id = backup.linode_id
|
||||||
|
self.message = backup.message
|
||||||
|
self.parent_id_name = backup.parent_id_name
|
||||||
|
self.country = backup.region.country
|
||||||
|
self.status = backup.status
|
||||||
|
self.type = backup.type
|
||||||
|
self.updated = backup.updated
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeDiskModel(Mapping):
|
||||||
|
def __init__(self, disk: Disk):
|
||||||
|
super().__init__()
|
||||||
|
self.created = disk.created
|
||||||
|
self.filesystem = disk.filesystem
|
||||||
|
self.id = disk.id
|
||||||
|
self.label = disk.label
|
||||||
|
self.linode_id = disk.linode_id
|
||||||
|
self.parent_id_name = disk.parent_id_name
|
||||||
|
self.size = disk.size
|
||||||
|
self.status = disk.status
|
||||||
|
self.updated = disk.updated
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeImageModel(Mapping):
|
||||||
|
def __init__(self, image: Image):
|
||||||
|
super().__init__()
|
||||||
|
self.created = image.created
|
||||||
|
self.created_by = image.created_by
|
||||||
|
self.deprecated = image.deprecated
|
||||||
|
self.description = image.description
|
||||||
|
self.is_public = image.is_public
|
||||||
|
self.label = image.label
|
||||||
|
self.size = image.size
|
||||||
|
self.status = image.status
|
||||||
|
self.type = image.type
|
||||||
|
self.vendor = image.vendor
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeTypeModel(Mapping):
|
||||||
|
# noinspection PyShadowingBuiltins
|
||||||
|
def __init__(self, type: Type):
|
||||||
|
super().__init__()
|
||||||
|
self.disk = type.disk
|
||||||
|
self.id = type.id
|
||||||
|
self.label = type.label
|
||||||
|
self.memory = type.memory
|
||||||
|
self.network_out = type.network_out
|
||||||
|
self.price = type.price.dict
|
||||||
|
self.transfer = type.transfer
|
||||||
|
self.type_class = type.type_class
|
||||||
|
self.vcpus = type.vcpus
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeInstanceModel(Mapping):
|
||||||
|
def __init__(self, node: Instance):
|
||||||
|
super().__init__()
|
||||||
|
self.label = node.label
|
||||||
|
self.status = node.status
|
||||||
|
self.alerts = node.alerts.dict
|
||||||
|
self.available_backups = [
|
||||||
|
dict(LinodeBackupModel(backup))
|
||||||
|
for backup in node.available_backups.automatic
|
||||||
|
],
|
||||||
|
|
||||||
|
self.backups = {
|
||||||
|
'enabled': node.backups.enabled,
|
||||||
|
'schedule': node.backups.schedule.dict,
|
||||||
|
'last_successful': datetime.datetime.fromisoformat(node.backups.last_successful),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.configs = {config.label: dict(LinodeConfigModel(config)) for config in node.configs}
|
||||||
|
self.disks = {disk.label: dict(LinodeDiskModel(disk)) for disk in node.disks}
|
||||||
|
self.group = node.group
|
||||||
|
self.hypervisor = node.hypervisor
|
||||||
|
self.id = node.id
|
||||||
|
self.image = LinodeImageModel(node.image)
|
||||||
|
self.country = node.region.country
|
||||||
|
self.specs = node.specs.dict
|
||||||
|
self.tags = node.tags
|
||||||
|
self.transfer = node.transfer.dict
|
||||||
|
self.type = dict(LinodeTypeModel(node.type))
|
||||||
|
self.updated = node.updated
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeInstanceResponse(LinodeResponse):
|
||||||
|
def __init__(self, instance: Instance, *args, **kwargs):
|
||||||
|
super().__init__(*args, output={
|
||||||
|
'instance': dict(LinodeInstanceModel(instance))
|
||||||
|
}, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LinodeInstancesResponse(LinodeResponse):
|
||||||
|
def __init__(self,
|
||||||
|
instances: List[Instance],
|
||||||
|
*args, **kwargs):
|
||||||
|
super().__init__(*args, output={
|
||||||
|
'instances': {instance.label: dict(LinodeInstanceModel(instance)) for instance in instances},
|
||||||
|
}, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
99
platypush/plugins/linode.py
Normal file
99
platypush/plugins/linode.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from linode_api4 import LinodeClient, Instance
|
||||||
|
from platypush.plugins.sensor import SensorPlugin
|
||||||
|
|
||||||
|
from platypush.message.response.linode import LinodeInstancesResponse, LinodeInstanceResponse
|
||||||
|
from platypush.plugins import action
|
||||||
|
|
||||||
|
|
||||||
|
class LinodePlugin(SensorPlugin):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **linode_api4** (``pip install linode_api4``)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, token: str, **kwargs):
|
||||||
|
"""
|
||||||
|
:param token: Your Linode token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._token = token
|
||||||
|
|
||||||
|
def _get_client(self, token: Optional[str] = None) -> LinodeClient:
|
||||||
|
return LinodeClient(token or self._token)
|
||||||
|
|
||||||
|
def _get_instance(self, label: str, token: Optional[str] = None) -> Instance:
|
||||||
|
client = self._get_client(token)
|
||||||
|
instances = client.linode.instances(Instance.label == label)
|
||||||
|
assert instances, 'No such Linode instance: ' + label
|
||||||
|
return instances[0]
|
||||||
|
|
||||||
|
@action
|
||||||
|
def status(self, token: Optional[str] = None, instance: Optional[str] = None) \
|
||||||
|
-> Union[LinodeInstanceResponse, LinodeInstancesResponse]:
|
||||||
|
"""
|
||||||
|
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 node by label.
|
||||||
|
:return: :class:`platypush.message.response.linode.LinodeInstanceResponse` if ``label`` is specified,
|
||||||
|
:class:`platypush.message.response.linode.LinodeInstancesResponse` otherwise.
|
||||||
|
"""
|
||||||
|
if instance:
|
||||||
|
instance = self._get_instance(label=instance)
|
||||||
|
return LinodeInstanceResponse(instance=instance)
|
||||||
|
|
||||||
|
client = self._get_client(token)
|
||||||
|
return LinodeInstancesResponse(instances=client.linode.instances())
|
||||||
|
|
||||||
|
@action
|
||||||
|
def reboot(self, instance: str, token: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Reboot an instance.
|
||||||
|
|
||||||
|
:param instance: Label of the instance to be rebooted.
|
||||||
|
:param token: Default access token override.
|
||||||
|
"""
|
||||||
|
instance = self._get_instance(label=instance, token=token)
|
||||||
|
assert instance.reboot(), 'Reboot failed'
|
||||||
|
|
||||||
|
@action
|
||||||
|
def boot(self, instance: str, token: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Boot an instance.
|
||||||
|
|
||||||
|
:param instance: Label of the instance to be booted.
|
||||||
|
:param token: Default access token override.
|
||||||
|
"""
|
||||||
|
instance = self._get_instance(label=instance, token=token)
|
||||||
|
assert instance.boot(), 'Boot failed'
|
||||||
|
|
||||||
|
@action
|
||||||
|
def shutdown(self, instance: str, token: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Shutdown an instance.
|
||||||
|
|
||||||
|
:param instance: Label of the instance to be shut down.
|
||||||
|
:param token: Default access token override.
|
||||||
|
"""
|
||||||
|
instance = self._get_instance(label=instance, token=token)
|
||||||
|
assert instance.shutdown(), 'Shutdown failed'
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_measurement(self, *args, **kwargs):
|
||||||
|
return self.status(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -243,3 +243,6 @@ croniter
|
||||||
|
|
||||||
# Support for OTP (One-Time Password) generation
|
# Support for OTP (One-Time Password) generation
|
||||||
# pyotp
|
# pyotp
|
||||||
|
|
||||||
|
# Support for Linode integration
|
||||||
|
# linode_api4
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -291,5 +291,7 @@ setup(
|
||||||
'picovoice-speech': ['pvcheetah @ git+https://github.com/BlackLight/cheetah'],
|
'picovoice-speech': ['pvcheetah @ git+https://github.com/BlackLight/cheetah'],
|
||||||
# Support for OTP (One-Time Password) generation
|
# Support for OTP (One-Time Password) generation
|
||||||
'otp': ['pyotp'],
|
'otp': ['pyotp'],
|
||||||
|
# Support for Linode integration
|
||||||
|
'linode': ['linode_api4'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue