Support versioned notifications

Support nova versioned notifications. Unversioned notifications
are still supported and the default. The CI is configured to test
versioned notifications, and both implementations use the same methods.
Because of this, testing versioned notifications also covers
unversioned notifications, since the execution path flows through both.

Change-Id: If028afa9e9fbcb344786cd287605e0d9af5d3c01
This commit is contained in:
Grzegorz Grasza 2018-11-23 17:14:47 +01:00
parent 4d997dddc6
commit 609f6e2b2b
12 changed files with 577 additions and 34 deletions

View File

@ -117,6 +117,8 @@ novajoin REST service and enable notifications in
.. note::
Notifications have to be also enabled and configured on nova computes!
See also information about enabling versioned and neutron notifications
in the `Notification listener Configuration`_ section below.
Novajoin enables keystone authentication by default, as seen in
**/etc/novajoin/join-api-paste.ini**. So credentials need to be set for
@ -217,14 +219,27 @@ send notifications to the novajoin topic in /etc/nova/nova.conf::
[oslo_messaging_notifications]
...
topics=notifications,novajoin_notifications
topics = notifications,novajoin_notifications
In case of versioned notifications the configuration will look differently::
[notifications]
notify_on_state_change = vm_state
notification_format = versioned
versioned_notifications_topics = versioned_notifications,novajoin_notifications
.. note::
Notifications have to be also enabled and configured on nova computes!
If you simply use notifications and ceilometer is running then the
notifications will be roughly split between the two services in a
round-robin format.
To enable neutron notifications, in /etc/neutron/neutron.conf::
[oslo_messaging_notifications]
driver = messagingv2
topics = notifications,novajoin_notifications
If you use notifications without changing the topic and ceilometer is
running, then the notifications will be roughly split between the two
services in a round-robin fashion.
Usage
=====

View File

@ -51,6 +51,14 @@ service_opts = [
help='Number retries when downloading an image from glance'),
cfg.StrOpt('auth_strategy', default='keystone',
help='Strategy to use for authentication.'),
cfg.StrOpt('notification_format', default='unversioned',
choices=[
('versioned',
'Only the new versioned notifications are read'),
('unversioned',
'Only the legacy unversioned notifications are read'),
],
help='The format of notifications to read.'),
cfg.StrOpt('notifications_topic', default='novajoin_notifications',
help='Topic on which to listen to notifications.'),
]

View File

@ -151,3 +151,9 @@ class ImageNotFound(NotFound):
class PolicyNotAuthorized(NotAuthorized):
message = "Policy doesn't allow %(action)s to be performed."
class NotificationVersionMismatch(JoinException):
message = ("Provided notification version "
"%(provided_maj)s.%(provided_min)s did not match expected "
"%(expected_maj)s.%(expected_min)s for %(type)s")

View File

@ -21,9 +21,11 @@ import json
import sys
import time
import glanceclient as glance_client
from neutronclient.v2_0 import client as neutron_client
from novaclient import client as nova_client
from novajoin import config
from novajoin import exception
from novajoin.ipa import IPAClient
from novajoin import join
from novajoin.keystone_client import get_session
@ -57,12 +59,53 @@ def neutronclient():
return neutron_client.Client(session=session)
def glanceclient():
session = get_session()
return glance_client.Client('2', session=session)
class Registry(dict):
def __call__(self, name):
def decorator(fun):
def __call__(self, name, version=None, service='nova'):
def register_event(fun):
if version:
def check_event(sself, payload):
self.check_version(payload, version, service)
return fun(sself, payload[service + '_object.data'])
self[name] = check_event
return check_event
self[name] = fun
return fun
return decorator
return register_event
@staticmethod
def check_version(payload, expected_version, service):
"""Check nova notification version
If actual's major version is different from expected, a
NotificationVersionMismatch error is raised.
If the minor versions are different, a DEBUG level log
message is output
"""
notification_version = payload[service + '_object.version']
notification_name = payload[service + '_object.name']
maj_ver, min_ver = map(int, notification_version.split('.'))
expected_maj, expected_min = map(int, expected_version.split('.'))
if maj_ver != expected_maj:
raise exception.NotificationVersionMismatch(
provided_maj=maj_ver, provided_min=min_ver,
expected_maj=expected_maj, expected_min=expected_min,
type=notification_name)
if min_ver != expected_min:
LOG.debug(
"Notification %(type)s minor version mismatch, "
"provided: %(provided_maj)s.%(provided_min)s, "
"expected: %(expected_maj)s.%(expected_min)s.",
{"type": notification_name,
"provided_maj": maj_ver, "provided_min": min_ver,
"expected_maj": expected_maj, "expected_min": expected_min}
)
class NotificationEndpoint(object):
@ -90,19 +133,19 @@ class NotificationEndpoint(object):
event_handler(self, payload)
@event_handlers('compute.instance.create.end')
def instance_create(self, payload):
def compute_instance_create(self, payload):
hostname = self._generate_hostname(payload.get('hostname'))
instance_id = payload.get('instance_id')
instance_id = payload['instance_id']
LOG.info("Add new host %s (%s)", instance_id, hostname)
@event_handlers('compute.instance.update')
def instance_update(self, payload):
def compute_instance_update(self, payload):
ipa = ipaclient()
join_controller = join.JoinController(ipa)
hostname_short = payload.get('hostname')
instance_id = payload.get('instance_id')
payload_metadata = payload.get('metadata')
image_metadata = payload.get('image_meta')
hostname_short = payload['hostname']
instance_id = payload['instance_id']
payload_metadata = payload['metadata']
image_metadata = payload['image_meta']
hostname = self._generate_hostname(hostname_short)
@ -133,11 +176,11 @@ class NotificationEndpoint(object):
ipa.flush_batch_operation()
@event_handlers('compute.instance.delete.end')
def instance_delete(self, payload):
hostname_short = payload.get('hostname')
instance_id = payload.get('instance_id')
payload_metadata = payload.get('metadata')
image_metadata = payload.get('image_meta')
def compute_instance_delete(self, payload):
hostname_short = payload['hostname']
instance_id = payload['instance_id']
payload_metadata = payload['metadata']
image_metadata = payload['image_meta']
hostname = self._generate_hostname(hostname_short)
@ -156,20 +199,20 @@ class NotificationEndpoint(object):
@event_handlers('network.floating_ip.associate')
def floaitng_ip_associate(self, payload):
floating_ip = payload.get('floating_ip')
floating_ip = payload['floating_ip']
LOG.info("Associate floating IP %s" % floating_ip)
ipa = ipaclient()
nova = novaclient()
server = nova.servers.get(payload.get('instance_id'))
server = nova.servers.get(payload['instance_id'])
if server:
ipa.add_ip(server.get, floating_ip)
ipa.add_ip(server.name, floating_ip)
else:
LOG.error("Could not resolve %s into a hostname",
payload.get('instance_id'))
payload['instance_id'])
@event_handlers('network.floating_ip.disassociate')
def floating_ip_disassociate(self, payload):
floating_ip = payload.get('floating_ip')
floating_ip = payload['floating_ip']
LOG.info("Disassociate floating IP %s" % floating_ip)
ipa = ipaclient()
ipa.remove_ip(floating_ip)
@ -177,9 +220,9 @@ class NotificationEndpoint(object):
@event_handlers('floatingip.update.end')
def floating_ip_update(self, payload):
"""Neutron event"""
floatingip = payload.get('floatingip')
floating_ip = floatingip.get('floating_ip_address')
port_id = floatingip.get('port_id')
floatingip = payload['floatingip']
floating_ip = floatingip['floating_ip_address']
port_id = floatingip['port_id']
ipa = ipaclient()
if port_id:
LOG.info("Neutron floating IP associate: %s" % floating_ip)
@ -295,6 +338,48 @@ class NotificationEndpoint(object):
return host
class VersionedNotificationEndpoint(NotificationEndpoint):
filter_rule = oslo_messaging.notify.filter.NotificationFilter(
publisher_id='^nova-compute.*|^network.*',
event_type='^instance.create.end|'
'^instance.delete.end|'
'^instance.update|'
'^floatingip.update.end')
event_handlers = Registry(NotificationEndpoint.event_handlers)
@event_handlers('instance.create.end', '1.10')
def instance_create(self, payload):
newpayload = {
'hostname': payload['host_name'],
'instance_id': payload['uuid'],
}
self.compute_instance_create(newpayload)
@event_handlers('instance.update', '1.8')
def instance_update(self, payload):
glance = glanceclient()
newpayload = {
'hostname': payload['host_name'],
'instance_id': payload['uuid'],
'metadata': payload['metadata'],
'image_meta': glance.images.get(payload['image_uuid'])
}
self.compute_instance_update(newpayload)
@event_handlers('instance.delete.end', '1.7')
def instance_delete(self, payload):
glance = glanceclient()
newpayload = {
'hostname': payload['host_name'],
'instance_id': payload['uuid'],
'metadata': payload['metadata'],
'image_meta': glance.images.get(payload['image_uuid'])
}
self.compute_instance_delete(newpayload)
def main():
register_keystoneauth_opts(CONF)
CONF(sys.argv[1:], version='1.0.21',
@ -303,7 +388,10 @@ def main():
transport = oslo_messaging.get_notification_transport(CONF)
targets = [oslo_messaging.Target(topic=CONF.notifications_topic)]
endpoints = [NotificationEndpoint()]
if CONF.notification_format == 'unversioned':
endpoints = [NotificationEndpoint()]
elif CONF.notification_format == 'versioned':
endpoints = [VersionedNotificationEndpoint()]
server = oslo_messaging.get_notification_listener(transport,
targets,

View File

@ -0,0 +1,18 @@
{
"priority" : "INFO",
"message_id" : "281218d3-0764-4397-b844-936c93fb89e6",
"event_type" : "floatingip.update.end",
"timestamp" : "2012-11-18 01:29:29.497899",
"payload" : {
"floatingip" : {
"floating_network_id" : "d9edfcd5-f245-4f45-be26-4383942fd74c",
"tenant_id" : "c97027dd880d4c129ae7a4ba7edade05",
"fixed_ip_address" : "172.16.59.10",
"router_id" : "62c1fd2b-8149-4222-8d6b-e581c55e5264",
"port_id" : "289ed46b-274c-444d-9fd4-bddf8acc7d7c",
"floating_ip_address" : "192.168.5.201",
"id" : "f38ff2b6-cd4d-433e-8a9c-9e00dfc05b1e"
}
},
"publisher_id" : "network.svc02.os.lan"
}

View File

@ -0,0 +1,18 @@
{
"priority" : "INFO",
"message_id" : "e9667b80-d2dc-4687-b2c6-2e648520157c",
"event_type" : "floatingip.update.end",
"timestamp" : "2012-11-18 01:35:08.312766",
"payload" : {
"floatingip" : {
"floating_network_id" : "d9edfcd5-f245-4f45-be26-4383942fd74c",
"tenant_id" : "c97027dd880d4c129ae7a4ba7edade05",
"fixed_ip_address" : null,
"router_id" : null,
"port_id" : null,
"floating_ip_address" : "192.168.5.201",
"id" : "f38ff2b6-cd4d-433e-8a9c-9e00dfc05b1e"
}
},
"publisher_id" : "network.svc02.os.lan"
}

View File

@ -0,0 +1,105 @@
{
"event_type": "instance.create.end",
"payload": {
"nova_object.data": {
"action_initiator_project": "6f70656e737461636b20342065766572",
"action_initiator_user": "fake",
"architecture": "x86_64",
"auto_disk_config": "MANUAL",
"availability_zone": "nova",
"block_devices": [],
"created_at": "2012-10-29T13:42:11Z",
"deleted_at": null,
"display_description": "some-server",
"display_name": "some-server",
"fault": null,
"flavor": {
"nova_object.data": {
"description": null,
"disabled": false,
"ephemeral_gb": 0,
"extra_specs": {
"hw:watchdog_action": "disabled"
},
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"is_public": true,
"memory_mb": 512,
"name": "test_flavor",
"projects": null,
"root_gb": 1,
"rxtx_factor": 1.0,
"swap": 0,
"vcpu_weight": 0,
"vcpus": 1
},
"nova_object.name": "FlavorPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.4"
},
"host": "compute",
"host_name": "some-server",
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ip_addresses": [
{
"nova_object.data": {
"address": "192.168.1.3",
"device_name": "tapce531f90-19",
"label": "private-network",
"mac": "fa:16:3e:4c:2c:30",
"meta": {},
"port_uuid": "ce531f90-199f-48c0-816c-13e38010b442",
"version": 4
},
"nova_object.name": "IpPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
}
],
"kernel_id": "",
"key_name": "my-key",
"keypairs": [
{
"nova_object.data": {
"fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
"name": "my-key",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova",
"type": "ssh",
"user_id": "fake"
},
"nova_object.name": "KeypairPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
}
],
"launched_at": "2012-10-29T13:42:11Z",
"locked": false,
"metadata": {},
"node": "fake-mini",
"os_type": null,
"power_state": "running",
"progress": 0,
"ramdisk_id": "",
"request_id": "req-5b6c791d-5709-4f36-8fbe-c3e02869e35d",
"reservation_id": "r-npxv0e40",
"state": "active",
"tags": [
"tag"
],
"task_state": null,
"tenant_id": "6f70656e737461636b20342065766572",
"terminated_at": null,
"trusted_image_certificates": [
"cert-id-1",
"cert-id-2"
],
"updated_at": "2012-10-29T13:42:11Z",
"user_id": "fake",
"uuid": "178b0921-8f85-4257-88b6-2e743b5a975c"
},
"nova_object.name": "InstanceCreatePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.10"
},
"priority": "INFO",
"publisher_id": "nova-compute:compute"
}

View File

@ -0,0 +1,82 @@
{
"event_type": "instance.delete.end",
"payload": {
"nova_object.data": {
"action_initiator_project": "6f70656e737461636b20342065766572",
"action_initiator_user": "fake",
"architecture": "x86_64",
"auto_disk_config": "MANUAL",
"availability_zone": "nova",
"block_devices": [
{
"nova_object.data": {
"boot_index": null,
"delete_on_termination": false,
"device_name": "/dev/sdb",
"tag": null,
"volume_id": "a07f71dc-8151-4e7d-a0cc-cd24a3f11113"
},
"nova_object.name": "BlockDevicePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
}
],
"created_at": "2012-10-29T13:42:11Z",
"deleted_at": "2012-10-29T13:42:11Z",
"display_description": "some-server",
"display_name": "some-server",
"fault": null,
"flavor": {
"nova_object.data": {
"description": null,
"disabled": false,
"ephemeral_gb": 0,
"extra_specs": {
"hw:watchdog_action": "disabled"
},
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"is_public": true,
"memory_mb": 512,
"name": "test_flavor",
"projects": null,
"root_gb": 1,
"rxtx_factor": 1.0,
"swap": 0,
"vcpu_weight": 0,
"vcpus": 1
},
"nova_object.name": "FlavorPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.4"
},
"host": "compute",
"host_name": "some-server",
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ip_addresses": [],
"kernel_id": "",
"key_name": "my-key",
"launched_at": "2012-10-29T13:42:11Z",
"locked": false,
"metadata": {},
"node": "fake-mini",
"os_type": null,
"power_state": "pending",
"progress": 0,
"ramdisk_id": "",
"request_id": "req-5b6c791d-5709-4f36-8fbe-c3e02869e35d",
"reservation_id": "r-npxv0e40",
"state": "deleted",
"task_state": null,
"tenant_id": "6f70656e737461636b20342065766572",
"terminated_at": "2012-10-29T13:42:11Z",
"updated_at": "2012-10-29T13:42:11Z",
"user_id": "fake",
"uuid": "178b0921-8f85-4257-88b6-2e743b5a975c"
},
"nova_object.name": "InstanceActionPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.7"
},
"priority": "INFO",
"publisher_id": "nova-compute:compute"
}

View File

@ -0,0 +1,91 @@
{
"event_type": "instance.update",
"payload": {
"nova_object.data": {
"action_initiator_project": "6f70656e737461636b20342065766572",
"action_initiator_user": "fake",
"architecture": "x86_64",
"audit_period": {
"nova_object.data": {
"audit_period_beginning": "2012-10-01T00:00:00Z",
"audit_period_ending": "2012-10-29T13:42:11Z"
},
"nova_object.name": "AuditPeriodPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"auto_disk_config": "MANUAL",
"availability_zone": "nova",
"bandwidth": [],
"block_devices": [],
"created_at": "2012-10-29T13:42:11Z",
"deleted_at": null,
"display_description": "some-server",
"display_name": "some-server",
"flavor": {
"nova_object.data": {
"description": null,
"disabled": false,
"ephemeral_gb": 0,
"extra_specs": {
"hw:watchdog_action": "disabled"
},
"flavorid": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"is_public": true,
"memory_mb": 512,
"name": "test_flavor",
"projects": null,
"root_gb": 1,
"rxtx_factor": 1.0,
"swap": 0,
"vcpu_weight": 0,
"vcpus": 1
},
"nova_object.name": "FlavorPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.4"
},
"host": "compute",
"host_name": "some-server",
"image_uuid": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ip_addresses": [],
"kernel_id": "",
"key_name": "my-key",
"launched_at": null,
"locked": false,
"metadata": {},
"node": "fake-mini",
"old_display_name": null,
"os_type": null,
"power_state": "pending",
"progress": 0,
"ramdisk_id": "",
"request_id": "req-5b6c791d-5709-4f36-8fbe-c3e02869e35d",
"reservation_id": "r-npxv0e40",
"state": "active",
"state_update": {
"nova_object.data": {
"new_task_state": null,
"old_state": "building",
"old_task_state": null,
"state": "building"
},
"nova_object.name": "InstanceStateUpdatePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.0"
},
"tags": [],
"task_state": "scheduling",
"tenant_id": "6f70656e737461636b20342065766572",
"terminated_at": null,
"updated_at": null,
"user_id": "fake",
"uuid": "178b0921-8f85-4257-88b6-2e743b5a975c"
},
"nova_object.name": "InstanceUpdatePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.8"
},
"priority": "INFO",
"publisher_id": "nova-compute:fake-mini"
}

View File

@ -0,0 +1,94 @@
# Copyright 2018 Red Hat, Inc.
#
# 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.
import mock
import os
from oslo_messaging.notify import dispatcher as notify_dispatcher
from oslo_messaging.notify import NotificationResult
from oslo_serialization import jsonutils
from novajoin import notifications
from novajoin import test
SAMPLES_DIR = os.path.dirname(os.path.realpath(__file__))
class NotificationFormatsTest(test.TestCase):
def _get_event(self, filename):
json_sample = os.path.join(SAMPLES_DIR, filename)
with open(json_sample) as sample_file:
return jsonutils.loads(sample_file.read())
def _run_dispatcher(self, event):
dispatcher = notify_dispatcher.NotificationDispatcher(
[notifications.VersionedNotificationEndpoint()], None)
return dispatcher.dispatch(mock.Mock(ctxt={}, message=event))
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_instance_create(self, generate_hostname):
event = self._get_event('instance.create.end.json')
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.HANDLED)
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_instance_create_wrong_version(self, generate_hostname):
event = self._get_event('instance.create.end.json')
event['payload']['nova_object.version'] = '999.999'
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.REQUEUE)
@mock.patch('novajoin.notifications.glanceclient')
@mock.patch('novajoin.notifications.ipaclient')
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_instance_update(self, glanceclient, ipaclient, gen_hostname):
event = self._get_event('instance.update.json')
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.HANDLED)
@mock.patch('novajoin.notifications.glanceclient')
@mock.patch('novajoin.notifications.ipaclient')
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_instance_delete(self, glanceclient, ipaclient, gen_hostname):
event = self._get_event('instance.delete.end.json')
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.HANDLED)
@mock.patch('novajoin.notifications.neutronclient')
@mock.patch('novajoin.notifications.novaclient')
@mock.patch('novajoin.notifications.ipaclient')
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_floatingip_associate(self, neutronclient, novaclient,
ipaclient, generate_hostname):
event = self._get_event('floatingip.update.end_associate.json')
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.HANDLED)
@mock.patch('novajoin.notifications.neutronclient')
@mock.patch('novajoin.notifications.novaclient')
@mock.patch('novajoin.notifications.ipaclient')
@mock.patch('novajoin.notifications.NotificationEndpoint'
'._generate_hostname')
def test_floatingip_disassociate(self, neutronclient, novaclient,
ipaclient, generate_hostname):
event = self._get_event('floatingip.update.end_disassociate.json')
result = self._run_dispatcher(event)
self.assertEqual(result, NotificationResult.HANDLED)

View File

@ -138,6 +138,12 @@ def install(opts):
config.set('keystone_authtoken', 'project_domain_name', 'default')
config.set('keystone_authtoken', 'user_domain_id', 'default')
if opts.notification_format == 'versioned':
config.set('DEFAULT', 'notification_format', 'versioned')
else:
config.set('DEFAULT', 'notification_format', 'unversioned')
config.set('DEFAULT', 'notifications_topic', 'novajoin_notifications')
with open(opts.novajoin_conf, 'w') as f:
config.write(f)
@ -193,13 +199,21 @@ def install(opts):
'notify_on_state_change',
'vm_state')
config.set('notifications',
'notification_format',
'unversioned')
if opts.notification_format == 'versioned':
config.set('notifications',
'notification_format',
'versioned')
config.set('notifications',
'versioned_notifications_topics',
'versioned_notifications,novajoin_notifications')
else:
config.set('notifications',
'notification_format',
'unversioned')
config.set('oslo_messaging_notifications',
'topics',
'notifications,novajoin_notifications')
config.set('oslo_messaging_notifications',
'topics',
'notifications,novajoin_notifications')
with open(conf, 'w') as f:
config.write(f)
@ -259,6 +273,10 @@ def parse_args():
parser.add_argument('--novajoin-conf', dest='novajoin_conf',
help='novajoin configuration file',
default=JOINCONF)
parser.add_argument('--notification-format', dest='notification_format',
help='The format of notifications to emit and read.',
choices=['versioned', 'unversioned'],
default='versioned')
parser = configure_ipa.ipa_options(parser)
opts = parser.parse_args()