Sends notifications at lease events

Climate now uses oslo.notify to send notifications when
CRUD lease operations are executed and when relevant lease
events happen (start_lease, before_end_lease, end_lease).
This implementation also adds one new event type called
before_end_lease to be sent when a lease is close to be ended.
Moreover, a new oslo.notify wrapper class was also added.

Change-Id: I861540e5ec5d1309ded2de56b2135bae5b59dad9
Implements: blueprint notifications
This commit is contained in:
Cristian A Sanchez 2014-02-20 18:31:23 -03:00
parent 07ae1900af
commit 1d1ad49da0
9 changed files with 489 additions and 8 deletions

View File

@ -24,6 +24,7 @@ gettext.install('climate', unicode=1)
from climate.db import api as db_api
from climate.manager import service as manager_service
from climate.notification import notifier
from climate.openstack.common import service
from climate.utils import service as service_utils
@ -32,6 +33,7 @@ def main():
cfg.CONF(project='climate', prog='climate-manager')
service_utils.prepare_service(sys.argv)
db_api.setup_db()
notifier.init()
service.launch(
manager_service.ManagerService()
).wait()

View File

@ -26,6 +26,7 @@ from climate.db import exceptions as db_ex
from climate import exceptions as common_ex
from climate import manager
from climate.manager import exceptions
from climate.notification import api as notification_api
from climate.openstack.common.gettextutils import _
from climate.openstack.common import log as logging
from climate.utils import service as service_utils
@ -36,6 +37,12 @@ manager_opts = [
default=['dummy.vm.plugin'],
help='All plugins to use (one for every resource type to '
'support.)'),
cfg.IntOpt('notify_hours_before_lease_end',
default=48,
help='Number of hours prior to lease end in which a '
'notification of lease close to expire will be sent. If '
'this is set to 0, then this notification will '
'not be sent.')
]
CONF = cfg.CONF
@ -137,6 +144,11 @@ class ManagerService(service_utils.RPCServer):
try:
eventlet.spawn_n(service_utils.with_empty_context(event_fn),
event['lease_id'], event['id'])
lease = db_api.lease_get(event['lease_id'])
with trusts.create_ctx_from_trust(lease['trust_id']) as ctx:
self._send_notification(lease,
ctx,
events=['event.%s' % event_type])
except Exception:
db_api.event_update(event['id'], {'status': 'ERROR'})
LOG.exception(_('Error occurred while event handling.'))
@ -200,6 +212,15 @@ class ManagerService(service_utils.RPCServer):
'time': end_date,
'status': 'UNDONE'})
if CONF.manager.notify_hours_before_lease_end > 0:
delta = datetime.timedelta(
hours=CONF.manager.notify_hours_before_lease_end)
event = {'event_type': 'before_end_lease',
'status': 'UNDONE'}
lease_values['events'].append(event)
self._update_before_end_event_date(event, delta, lease_values)
try:
lease = db_api.lease_create(lease_values)
lease_id = lease['id']
@ -229,7 +250,10 @@ class ManagerService(service_utils.RPCServer):
raise
else:
return db_api.lease_get(lease['id'])
lease = db_api.lease_get(lease['id'])
with trusts.create_ctx_from_trust(lease['trust_id']) as ctx:
self._send_notification(lease, ctx, events=['create'])
return lease
def update_lease(self, lease_id, values):
if not values:
@ -316,14 +340,22 @@ class ManagerService(service_utils.RPCServer):
'End lease event not found')
db_api.event_update(event['id'], {'time': values['end_date']})
notifications = ['update']
self._update_before_end_event(lease, values, notifications)
db_api.lease_update(lease_id, values)
return db_api.lease_get(lease_id)
lease = db_api.lease_get(lease_id)
with trusts.create_ctx_from_trust(lease['trust_id']) as ctx:
self._send_notification(lease, ctx, events=notifications)
return lease
def delete_lease(self, lease_id):
lease = self.get_lease(lease_id)
if (datetime.datetime.utcnow() < lease['start_date'] or
datetime.datetime.utcnow() > lease['end_date']):
with trusts.create_ctx_from_trust(lease['trust_id']):
with trusts.create_ctx_from_trust(lease['trust_id']) as ctx:
for reservation in lease['reservations']:
try:
self.plugins[reservation['resource_type']]\
@ -333,6 +365,7 @@ class ManagerService(service_utils.RPCServer):
"for a lease.")
raise
db_api.lease_destroy(lease_id)
self._send_notification(lease, ctx, events=['delete'])
else:
raise common_ex.NotAuthorized(
'Already started lease cannot be deleted')
@ -347,6 +380,9 @@ class ManagerService(service_utils.RPCServer):
with trusts.create_ctx_from_trust(lease['trust_id']):
self._basic_action(lease_id, event_id, 'on_end', 'deleted')
def before_end_lease(self, lease_id, event_id):
pass
def _basic_action(self, lease_id, event_id, action_time,
reservation_status=None):
"""Commits basic lease actions such as starting and ending."""
@ -372,6 +408,39 @@ class ManagerService(service_utils.RPCServer):
db_api.event_update(event_id, {'status': 'DONE'})
def _send_notification(self, lease, ctx, events=[]):
payload = notification_api.format_lease_payload(lease)
for event in events:
notification_api.send_lease_notification(ctx, payload,
'lease.%s' % event)
def _update_before_end_event_date(self, event, delta, lease):
event['time'] = lease['end_date'] - delta
if event['time'] < lease['start_date']:
event['time'] = lease['start_date']
def _update_before_end_event(self, old_lease, new_lease, notifications):
event = db_api.event_get_first_sorted_by_filters(
'lease_id',
'asc',
{
'lease_id': old_lease['id'],
'event_type': 'before_end_lease'
}
)
if event:
# NOTE(casanch1) do nothing if the event does not exist.
# This is for backward compatibility
delta = old_lease['end_date'] - event['time']
update_values = {}
self._update_before_end_event_date(update_values, delta, new_lease)
if event['status'] == 'DONE':
update_values['status'] = 'UNDONE'
notifications.append('event.before_end_lease.stop')
db_api.event_update(event['id'], update_values)
def __getattr__(self, name):
"""RPC Dispatcher for plugins methods."""

View File

View File

@ -0,0 +1,32 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from climate.notification import notifier
IMPL = notifier.Notifier()
def send_lease_notification(context, lease, notification):
IMPL.send_lease_notification(context, lease, notification)
def format_lease_payload(lease):
return {
'lease_id': lease['id'],
'user_id': lease['user_id'],
'tenant_id': lease['tenant_id'],
'start_date': lease['start_date'],
'end_date': lease['end_date']
}

View File

@ -0,0 +1,68 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from climate.openstack.common import log
from oslo.config import cfg
from oslo import messaging
notification_opts = [
cfg.StrOpt('publisher_id',
default="climate.lease",
help='Publisher ID for notifications')
]
LOG = log.getLogger(__name__)
SERVICE = 'lease'
CONF = cfg.CONF
CONF.register_opts(notification_opts, 'notifications')
TRANSPORT = None
NOTIFIER = None
def init():
global TRANSPORT, NOTIFIER
TRANSPORT = messaging.get_transport(CONF)
NOTIFIER = messaging.Notifier(TRANSPORT,
publisher_id=CONF.notifications.publisher_id)
def cleanup():
global TRANSPORT, NOTIFIER
assert TRANSPORT is not None
assert NOTIFIER is not None
TRANSPORT.cleanup()
TRANSPORT = NOTIFIER = None
def get_notifier(publisher_id):
assert NOTIFIER is not None
return NOTIFIER
class Notifier(object):
"""Notification class for climate
Responsible for sending lease events notifications using oslo.nofity
"""
def send_lease_notification(self, context, lease, notification):
"""Sends lease notification
"""
self._notify(context, 'info', notification, lease)
def _notify(self, context, level, event_type, payload):
notifier = get_notifier(CONF.notifications.publisher_id)
method = getattr(notifier, level, notifier.info)
method(context, event_type, payload)

View File

@ -26,6 +26,7 @@ from climate.db import exceptions as db_ex
from climate import exceptions
from climate.manager import exceptions as manager_ex
from climate.manager import service
from climate.notification import api as notifier_api
from climate.plugins import base
from climate.plugins import dummy_vm_plugin
from climate.plugins.oshosts import host_plugin
@ -80,6 +81,7 @@ class ServiceTestCase(tests.TestCase):
self.db_api = db_api
self.dummy_plugin = dummy_vm_plugin
self.trusts = trusts
self.notifier_api = notifier_api
self.fake_plugin = self.patch(self.dummy_plugin, 'DummyVMPlugin')
@ -88,10 +90,17 @@ class ServiceTestCase(tests.TestCase):
'PhysicalHostPlugin')
self.ext_manager = self.patch(self.enabled, 'EnabledExtensionManager')
self.fake_notifier = self.patch(self.notifier_api,
'send_lease_notification')
self.manager = self.service.ManagerService()
self.lease_id = '11-22-33'
self.user_id = '123'
self.tenant_id = '555'
self.lease = {'id': self.lease_id,
'user_id': self.user_id,
'tenant_id': self.tenant_id,
'reservations': [{'id': '111',
'resource_id': '111',
'resource_type': 'virtual:instance',
@ -104,7 +113,7 @@ class ServiceTestCase(tests.TestCase):
self.good_date = datetime.datetime.strptime('2012-12-13 13:13',
'%Y-%m-%d %H:%M')
self.patch(self.context, 'ClimateContext')
self.ctx = self.patch(self.context, 'ClimateContext')
self.trust_ctx = self.patch(self.trusts, 'create_ctx_from_trust')
self.lease_get = self.patch(self.db_api, 'lease_get')
self.lease_get.return_value = self.lease
@ -120,6 +129,13 @@ class ServiceTestCase(tests.TestCase):
{'on_start': self.fake_plugin.on_start,
'on_end': self.fake_plugin.on_end}}
self.addCleanup(self.cfg.CONF.clear_override,
'notify_hours_before_lease_end',
group='manager')
def tearDown(self):
super(ServiceTestCase, self).tearDown()
def test_start(self):
#NOTE(starodubcevna): it's useless to test start() now, but may be in
#future it become useful
@ -178,6 +194,11 @@ class ServiceTestCase(tests.TestCase):
event_update.assert_called_once_with('111-222-333',
{'status': 'IN_PROGRESS'})
expected_context = self.trust_ctx.return_value
self.fake_notifier.assert_called_once_with(
expected_context.__enter__.return_value,
notifier_api.format_lease_payload(self.lease),
'lease.event.end_lease')
def test_event_wrong_event_status(self):
events = self.patch(self.db_api, 'event_get_first_sorted_by_filters')
@ -231,6 +252,11 @@ class ServiceTestCase(tests.TestCase):
self.lease_create.assert_called_once_with(lease_values)
self.assertEqual(lease, self.lease)
expected_context = self.trust_ctx.return_value
self.fake_notifier.assert_called_once_with(
expected_context.__enter__.return_value,
notifier_api.format_lease_payload(lease),
'lease.create')
def test_create_lease_some_time(self):
lease_values = {
@ -248,6 +274,113 @@ class ServiceTestCase(tests.TestCase):
self.lease_create.assert_called_once_with(lease_values)
self.assertEqual(lease, self.lease)
def test_create_lease_validate_created_events(self):
lease_values = {
'id': self.lease_id,
'reservations': [{'id': '111',
'resource_id': '111',
'resource_type': 'virtual:instance',
'status': 'FAKE PROGRESS'}],
'start_date': '2026-11-13 13:13',
'end_date': '2026-12-13 13:13'}
self.lease['start_date'] = '2026-11-13 13:13'
lease = self.manager.create_lease(lease_values)
self.lease_create.assert_called_once_with(lease_values)
self.assertEqual(lease, self.lease)
self.assertEqual(3, len(lease_values['events']))
# start lease event
event = lease_values['events'][0]
self.assertEqual('start_lease', event['event_type'])
self.assertEqual(lease_values['start_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
# end lease event
event = lease_values['events'][1]
self.assertEqual('end_lease', event['event_type'])
self.assertEqual(lease_values['end_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
# end lease event
event = lease_values['events'][2]
self.assertEqual('before_end_lease', event['event_type'])
delta = datetime.timedelta(
hours=self.cfg.CONF.manager.notify_hours_before_lease_end)
self.assertEqual(lease_values['end_date'] - delta, event['time'])
self.assertEqual('UNDONE', event['status'])
def test_create_lease_before_end_event_is_before_lease_start(self):
lease_values = {
'id': self.lease_id,
'reservations': [{'id': '111',
'resource_id': '111',
'resource_type': 'virtual:instance',
'status': 'FAKE PROGRESS'}],
'start_date': '2026-11-13 13:13',
'end_date': '2026-11-14 13:13'}
self.lease['start_date'] = '2026-11-13 13:13'
self.cfg.CONF.set_override('notify_hours_before_lease_end', 36,
group='manager')
lease = self.manager.create_lease(lease_values)
self.lease_create.assert_called_once_with(lease_values)
self.assertEqual(lease, self.lease)
self.assertEqual(3, len(lease_values['events']))
# start lease event
event = lease_values['events'][0]
self.assertEqual('start_lease', event['event_type'])
self.assertEqual(lease_values['start_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
# end lease event
event = lease_values['events'][1]
self.assertEqual('end_lease', event['event_type'])
self.assertEqual(lease_values['end_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
# end lease event
event = lease_values['events'][2]
self.assertEqual('before_end_lease', event['event_type'])
self.assertEqual(lease_values['start_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
def test_create_lease_no_before_end_event(self):
lease_values = {
'id': self.lease_id,
'reservations': [{'id': '111',
'resource_id': '111',
'resource_type': 'virtual:instance',
'status': 'FAKE PROGRESS'}],
'start_date': '2026-11-13 13:13',
'end_date': '2026-11-14 13:13'}
self.lease['start_date'] = '2026-11-13 13:13'
self.cfg.CONF.set_override('notify_hours_before_lease_end', 0,
group='manager')
lease = self.manager.create_lease(lease_values)
self.lease_create.assert_called_once_with(lease_values)
self.assertEqual(lease, self.lease)
self.assertEqual(2, len(lease_values['events']))
# start lease event
event = lease_values['events'][0]
self.assertEqual('start_lease', event['event_type'])
self.assertEqual(lease_values['start_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
# end lease event
event = lease_values['events'][1]
self.assertEqual('end_lease', event['event_type'])
self.assertEqual(lease_values['end_date'], event['time'])
self.assertEqual('UNDONE', event['status'])
def test_create_lease_wrong_date(self):
lease_values = {'start_date': '2025-13-35 13:13',
'end_date': '2025-12-31 13:13'}
@ -305,8 +438,13 @@ class ServiceTestCase(tests.TestCase):
def fake_event_get(sort_key, sort_dir, filters):
if filters['event_type'] == 'start_lease':
return {'id': u'2eeb784a-2d84-4a89-a201-9d42d61eecb1'}
else:
elif filters['event_type'] == 'end_lease':
return {'id': u'7085381b-45e0-4e5d-b24a-f965f5e6e5d7'}
elif filters['event_type'] == 'before_end_lease':
delta = datetime.timedelta(hours=1)
return {'id': u'452bf850-e223-4035-9d13-eb0b0197228f',
'time': self.lease['end_date'] - delta,
'status': 'UNDONE'}
lease_values = {
'name': 'renamed',
@ -343,17 +481,21 @@ class ServiceTestCase(tests.TestCase):
calls = [mock.call('2eeb784a-2d84-4a89-a201-9d42d61eecb1',
{'time': datetime.datetime(2015, 12, 1, 20, 00)}),
mock.call('7085381b-45e0-4e5d-b24a-f965f5e6e5d7',
{'time': datetime.datetime(2015, 12, 1, 22, 00)})
{'time': datetime.datetime(2015, 12, 1, 22, 00)}),
mock.call('452bf850-e223-4035-9d13-eb0b0197228f',
{'time': datetime.datetime(2015, 12, 1, 21, 00)})
]
self.event_update.assert_has_calls(calls)
self.lease_update.assert_called_once_with(self.lease_id, lease_values)
def test_update_lease_started_modify_end_date(self):
def test_update_lease_started_modify_end_date_without_before_end(self):
def fake_event_get(sort_key, sort_dir, filters):
if filters['event_type'] == 'start_lease':
return {'id': u'2eeb784a-2d84-4a89-a201-9d42d61eecb1'}
else:
elif filters['event_type'] == 'end_lease':
return {'id': u'7085381b-45e0-4e5d-b24a-f965f5e6e5d7'}
else:
return None
lease_values = {
'name': 'renamed',
@ -394,6 +536,70 @@ class ServiceTestCase(tests.TestCase):
self.event_update.assert_has_calls(calls)
self.lease_update.assert_called_once_with(self.lease_id, lease_values)
def test_update_lease_started_modify_end_date_and_before_end(self):
def fake_event_get(sort_key, sort_dir, filters):
if filters['event_type'] == 'start_lease':
return {'id': u'2eeb784a-2d84-4a89-a201-9d42d61eecb1'}
elif filters['event_type'] == 'end_lease':
return {'id': u'7085381b-45e0-4e5d-b24a-f965f5e6e5d7'}
elif filters['event_type'] == 'before_end_lease':
delta = datetime.timedelta(hours=1)
return {'id': u'452bf850-e223-4035-9d13-eb0b0197228f',
'time': self.lease['end_date'] - delta,
'status': 'DONE'}
lease_values = {
'name': 'renamed',
'end_date': '2013-12-20 16:00'
}
reservation_get_all = \
self.patch(self.db_api, 'reservation_get_all_by_lease_id')
reservation_get_all.return_value = [
{
'id': u'593e7028-c0d1-4d76-8642-2ffd890b324c',
'resource_type': 'virtual:instance',
'start_date': datetime.datetime(2013, 12, 20, 13, 00),
'end_date': datetime.datetime(2013, 12, 20, 15, 00)
}
]
event_get = self.patch(db_api, 'event_get_first_sorted_by_filters')
event_get.side_effect = fake_event_get
target = datetime.datetime(2013, 12, 20, 14, 00)
with mock.patch.object(datetime,
'datetime',
mock.Mock(wraps=datetime.datetime)) as patched:
patched.utcnow.return_value = target
self.manager.update_lease(self.lease_id, lease_values)
self.fake_plugin.update_reservation.assert_called_with(
'593e7028-c0d1-4d76-8642-2ffd890b324c',
{
'id': '593e7028-c0d1-4d76-8642-2ffd890b324c',
'resource_type': 'virtual:instance',
'start_date': datetime.datetime(2013, 12, 20, 13, 00),
'end_date': datetime.datetime(2013, 12, 20, 16, 00)
}
)
expected_context = self.trust_ctx.return_value
calls = [mock.call(expected_context.__enter__.return_value,
notifier_api.format_lease_payload(self.lease),
'lease.update'),
mock.call(expected_context.__enter__.return_value,
notifier_api.format_lease_payload(self.lease),
'lease.event.before_end_lease.stop'),
]
self.fake_notifier.assert_has_calls(calls)
calls = [mock.call('2eeb784a-2d84-4a89-a201-9d42d61eecb1',
{'time': datetime.datetime(2013, 12, 20, 13, 00)}),
mock.call('7085381b-45e0-4e5d-b24a-f965f5e6e5d7',
{'time': datetime.datetime(2013, 12, 20, 16, 00)}),
mock.call('452bf850-e223-4035-9d13-eb0b0197228f',
{'time': datetime.datetime(2013, 12, 20, 15, 00),
'status': 'UNDONE'})
]
self.event_update.assert_has_calls(calls)
self.lease_update.assert_called_once_with(self.lease_id, lease_values)
def test_update_lease_is_not_values(self):
lease_values = None
lease = self.manager.update_lease(self.lease_id, lease_values)
@ -524,7 +730,12 @@ class ServiceTestCase(tests.TestCase):
patched.utcnow.return_value = target
self.manager.delete_lease(self.lease_id)
expected_context = self.trust_ctx.return_value
self.lease_destroy.assert_called_once_with(self.lease_id)
self.fake_notifier.assert_called_once_with(
expected_context.__enter__.return_value,
self.notifier_api.format_lease_payload(self.lease),
'lease.delete')
def test_delete_lease_after_starting_date(self):
self.patch(self.manager, 'get_lease').\
@ -559,6 +770,9 @@ class ServiceTestCase(tests.TestCase):
basic_action.assert_called_once_with(self.lease_id, '1', 'on_end',
'deleted')
def test_before_end_lease(self):
self.manager.before_end_lease(self.lease_id, '1')
def test_basic_action_no_res_status(self):
self.patch(self.manager, 'get_lease').return_value = self.lease

View File

View File

@ -0,0 +1,81 @@
# Copyright 2014 Intel Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo.config import cfg
from oslo import messaging
from climate.notification import notifier as notification
from climate import tests
CONF = cfg.CONF
class FakeNotifier(object):
def info(self):
pass
class NotifierTestCase(tests.TestCase):
def setUp(self):
super(NotifierTestCase, self).setUp()
self.group = 'notifications'
CONF.set_override('publisher_id', 'lease-service', self.group)
# Fake Oslo notifier
self.fake_notifier = self.patch(messaging, 'Notifier')
self.fake_notifier.return_value = FakeNotifier()
self.fake_transport = self.patch(messaging,
'get_transport').return_value
self.info_method = self.patch(FakeNotifier, 'info')
self.context = {'user_id': 1, 'token': 'aabbcc'}
self.payload = {'id': 1, 'name': 'Lease1', 'start-date': 'now'}
notification.init()
self.notifier = notification.Notifier()
def test_notify_with_wrong_level(self):
self.notifier._notify(self.context, 'wrong', 'event', self.payload)
self.info_method.assert_called_once_with(self.context,
'event', self.payload)
def test_send_lease_event(self):
self.notifier.send_lease_notification(self.context, self.payload,
'start')
self.info_method.assert_called_once_with(self.context,
'start',
self.payload)
def test_cleanup(self):
notification.cleanup()
self.fake_transport.cleanup.assert_called_once_with()
self.assertIsNone(notification.NOTIFIER)
self.assertIsNone(notification.TRANSPORT)
def test_init(self):
self.fake_transport.called_once
self.fake_notifier.called_once_with(self.fake_transport,
publisher_id='lease-service')
def test_init_called_twice_returns_same_instance(self):
prev_notifier = notification.NOTIFIER
prev_transport = notification.TRANSPORT
notification.init()
self.assertIs(prev_notifier, notification.NOTIFIER)
self.assertIs(prev_transport, notification.TRANSPORT)

View File

@ -704,6 +704,11 @@
# (list value)
#plugins=dummy.vm.plugin
# Number of hours prior to lease end in which a notification
# of lease close to expire will be sent. If this is set to 0,
# then this notification will not be sent. (integer value)
#notify_hours_before_lease_end=48
[matchmaker_ring]
@ -716,6 +721,16 @@
#ringfile=/etc/oslo/matchmaker_ring.json
[notifications]
#
# Options defined in climate.notification.notifier
#
# Publisher ID for notifications (string value)
#publisher_id=climate.lease
[physical:host]
#