Alex Monk has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/312278

Change subject: openstack: Import nova_fixed_multi designate plugin
......................................................................

openstack: Import nova_fixed_multi designate plugin

Instead of deploying it with debs sourced from a repository outside the normal
naming conventions, import it into puppet.

Files come from sink_nova_fixed_multi.git, revision
8c58d90d36558f991f0cfbcfefda7a1a4931e290

Bug: T144317
Change-Id: I695dab2212550da053e26fa9dc6e38729c01a8c3
---
A 
modules/openstack/files/kilo/designate/nova_fixed_multi.egg-info/entry_points.txt
A modules/openstack/files/kilo/designate/nova_fixed_multi/__init__.py
A modules/openstack/files/kilo/designate/nova_fixed_multi/base.py
A modules/openstack/files/kilo/designate/nova_fixed_multi/novamulti.py
A 
modules/openstack/files/liberty/designate/nova_fixed_multi.egg-info/entry_points.txt
A modules/openstack/files/liberty/designate/nova_fixed_multi/__init__.py
A modules/openstack/files/liberty/designate/nova_fixed_multi/base.py
A modules/openstack/files/liberty/designate/nova_fixed_multi/novamulti.py
A 
modules/openstack/files/mitaka/designate/nova_fixed_multi.egg-info/entry_points.txt
A modules/openstack/files/mitaka/designate/nova_fixed_multi/__init__.py
A modules/openstack/files/mitaka/designate/nova_fixed_multi/base.py
A modules/openstack/files/mitaka/designate/nova_fixed_multi/novamulti.py
M modules/openstack/manifests/designate/service.pp
M tox.ini
14 files changed, 1,025 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/puppet 
refs/changes/78/312278/1

diff --git 
a/modules/openstack/files/kilo/designate/nova_fixed_multi.egg-info/entry_points.txt
 
b/modules/openstack/files/kilo/designate/nova_fixed_multi.egg-info/entry_points.txt
new file mode 100644
index 0000000..e890a22
--- /dev/null
+++ 
b/modules/openstack/files/kilo/designate/nova_fixed_multi.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[designate.notification.handler]
+nova_fixed_multi = nova_fixed_multi.novamulti:NovaFixedMultiHandler
diff --git 
a/modules/openstack/files/kilo/designate/nova_fixed_multi/__init__.py 
b/modules/openstack/files/kilo/designate/nova_fixed_multi/__init__.py
new file mode 100644
index 0000000..711e39e
--- /dev/null
+++ b/modules/openstack/files/kilo/designate/nova_fixed_multi/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+import gettext
+import logging
diff --git a/modules/openstack/files/kilo/designate/nova_fixed_multi/base.py 
b/modules/openstack/files/kilo/designate/nova_fixed_multi/base.py
new file mode 100644
index 0000000..a4f544e
--- /dev/null
+++ b/modules/openstack/files/kilo/designate/nova_fixed_multi/base.py
@@ -0,0 +1,229 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This is a slight form of the designate source file found at
+# designate/notification_handler/base.py
+
+import abc
+from oslo_config import cfg
+from designate import exceptions
+from oslo_log import log as logging
+from designate.central import rpcapi as central_rpcapi
+from designate.context import DesignateContext
+from designate.notification_handler.base import BaseAddressHandler
+from designate.objects import Record
+from designate.plugin import ExtensionPlugin
+from keystoneclient.auth.identity import v3
+from keystoneclient import client
+from keystoneclient import exceptions as keystoneexceptions
+from keystoneclient.v3 import projects
+from keystoneclient import session
+
+
+LOG = logging.getLogger(__name__)
+central_api = central_rpcapi.CentralAPI()
+
+
+class BaseAddressMultiHandler(BaseAddressHandler):
+    def _get_ip_data(self, addr_dict):
+        ip = addr_dict['address']
+        version = addr_dict['version']
+
+        data = {
+            'ip_version': version,
+        }
+
+        # TODO(endre): Add v6 support
+        if version == 4:
+            data['ip_address'] = ip.replace('.', '-')
+            ip_data = ip.split(".")
+            for i in [0, 1, 2, 3]:
+                data["octet%s" % i] = ip_data[i]
+        return data
+
+    def _create(self, addresses, extra, managed=True,
+                resource_type=None, resource_id=None):
+        """
+        Create a a record from addresses
+
+        :param addresses: Address objects like
+                          {'version': 4, 'ip': '10.0.0.1'}
+        :param extra: Extra data to use when formatting the record
+        :param managed: Is it a managed resource
+        :param resource_type: The managed resource type
+        :param resource_id: The managed resource ID
+        """
+        LOG.debug('Using DomainID: %s' % cfg.CONF[self.name].domain_id)
+        domain = self.get_domain(cfg.CONF[self.name].domain_id)
+        LOG.debug('Domain: %r' % domain)
+
+        data = extra.copy()
+        LOG.debug('Event data: %s' % data)
+        data['domain'] = domain['name']
+
+        context = DesignateContext.get_admin_context(all_tenants=True)
+
+        # Extra magic!  The event record contains a tenant id but not a tenant 
name.  So
+        #  if our formats include project_name then we need to ask keystone 
for the name.
+        need_project_name = False
+        for fmt in cfg.CONF[self.name].get('format'):
+            if 'project_name' in fmt:
+                need_project_name = True
+                break
+        if 'project_name' in cfg.CONF[self.name].get('reverse_format'):
+            need_project_name = True
+        if need_project_name:
+            project_name = self._resolve_project_name(data['tenant_id'])
+            data['project_name'] = project_name
+
+        for addr in addresses:
+            event_data = data.copy()
+            event_data.update(self._get_ip_data(addr))
+
+            if addr['version'] == 4:
+                reverse_format = cfg.CONF[self.name].get('reverse_format')
+                reverse_domain_id = 
cfg.CONF[self.name].get('reverse_domain_id')
+                if reverse_format and reverse_domain_id:
+                    reverse_domain = self.get_domain(reverse_domain_id)
+                    LOG.debug('Reverse domain: %r' % reverse_domain)
+
+                    ip_digits = addr['address'].split('.')
+                    ip_digits.reverse()
+                    name = "%s.in-addr.arpa." % '.'.join(ip_digits)
+
+                    recordset_values = {
+                        'domain_id': reverse_domain['id'],
+                        'name': name,
+                        'type': 'PTR',
+                    }
+                    recordset = self._find_or_create_recordset(
+                        context, **recordset_values)
+
+                    record_values = {'data': reverse_format % event_data}
+
+                    if managed:
+                        record_values.update({
+                            'managed': managed,
+                            'managed_plugin_name': self.get_plugin_name(),
+                            'managed_plugin_type': self.get_plugin_type(),
+                            'managed_resource_type': resource_type,
+                            'managed_resource_id': resource_id})
+
+                    LOG.debug('Creating record in %s / %s with values %r',
+                              reverse_domain['id'],
+                              recordset['id'], record_values)
+                    central_api.create_record(context,
+                                              reverse_domain['id'],
+                                              recordset['id'],
+                                              Record(**record_values))
+
+            for fmt in cfg.CONF[self.name].get('format'):
+                recordset_values = {
+                    'domain_id': domain['id'],
+                    'name': fmt % event_data,
+                    'type': 'A' if addr['version'] == 4 else 'AAAA'}
+
+                recordset = self._find_or_create_recordset(
+                    context, **recordset_values)
+
+                record_values = {
+                    'data': addr['address']}
+
+                if managed:
+                    record_values.update({
+                        'managed': managed,
+                        'managed_plugin_name': self.get_plugin_name(),
+                        'managed_plugin_type': self.get_plugin_type(),
+                        'managed_resource_type': resource_type,
+                        'managed_resource_id': resource_id})
+
+                LOG.debug('Creating record in %s / %s with values %r',
+                          domain['id'], recordset['id'], record_values)
+                central_api.create_record(context,
+                                          domain['id'],
+                                          recordset['id'],
+                                          Record(**record_values))
+
+    def _delete(self, managed=True, resource_id=None, resource_type='instance',
+                criterion={}):
+        """
+        Handle a generic delete of a fixed ip within a domain
+
+        :param criterion: Criterion to search and destroy records
+        """
+        context = DesignateContext().elevated()
+        context.all_tenants = True
+        context.edit_managed_records = True
+
+        criterion.update({'domain_id': cfg.CONF[self.name].domain_id})
+
+        if managed:
+            criterion.update({
+                'managed': managed,
+                'managed_plugin_name': self.get_plugin_name(),
+                'managed_plugin_type': self.get_plugin_type(),
+                'managed_resource_id': resource_id,
+                'managed_resource_type': resource_type
+            })
+
+        records = central_api.find_records(context, criterion)
+
+        for record in records:
+            LOG.debug('Deleting record %s' % record['id'])
+
+            central_api.delete_record(context, cfg.CONF[self.name].domain_id,
+                                      record['recordset_id'], record['id'])
+
+        reverse_domain_id = cfg.CONF[self.name].get('reverse_domain_id')
+        if reverse_domain_id:
+            criterion.update({'domain_id': reverse_domain_id})
+
+            records = central_api.find_records(context, criterion)
+
+            for record in records:
+                LOG.debug('Deleting record %s' % record['id'])
+
+                central_api.delete_record(context,
+                                          reverse_domain_id,
+                                          record['recordset_id'], record['id'])
+
+    def _resolve_project_name(self, tenant_id):
+        try:
+            username = cfg.CONF[self.name].keystone_auth_name
+            passwd = cfg.CONF[self.name].keystone_auth_pass
+            project = cfg.CONF[self.name].keystone_auth_project
+            url = cfg.CONF[self.name].keystone_auth_url
+        except keyerror:
+            LOG.debug('Missing a config setting for keystone auth.')
+            return
+
+        try:
+            auth = v3.Password(auth_url=url,
+                               user_id=username,
+                               password=passwd,
+                               project_id=project)
+            sess = session.Session(auth=auth)
+            keystone = client.Client(session=sess, auth_url=url)
+        except keystoneexceptions.AuthorizationFailure:
+            LOG.debug('Keystone client auth failed.')
+            return
+        projectmanager = projects.ProjectManager(keystone)
+        proj = projectmanager.get(tenant_id)
+        if proj:
+            LOG.debug('Resolved project id %s as %s' % (tenant_id, proj.name))
+            return proj.name
+        else:
+            return 'unknown'
diff --git 
a/modules/openstack/files/kilo/designate/nova_fixed_multi/novamulti.py 
b/modules/openstack/files/kilo/designate/nova_fixed_multi/novamulti.py
new file mode 100644
index 0000000..0a73b49
--- /dev/null
+++ b/modules/openstack/files/kilo/designate/nova_fixed_multi/novamulti.py
@@ -0,0 +1,87 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This file is a slight modification of the nova notification driver found
+#  in the designate source at designate/notification_handler/nova.py
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from nova_fixed_multi.base import BaseAddressMultiHandler
+
+import sys
+
+LOG = logging.getLogger(__name__)
+
+cfg.CONF.register_group(cfg.OptGroup(
+    name='handler:nova_fixed_multi',
+    title="Configuration for Nova Notification Handler"
+))
+
+cfg.CONF.register_opts([
+    cfg.ListOpt('notification-topics', default=['monitor']),
+    cfg.StrOpt('control-exchange', default='nova'),
+    cfg.StrOpt('domain-id', default=None),
+    cfg.MultiStrOpt('format', default=[]),
+    cfg.StrOpt('reverse-domain-id', default=None),
+    cfg.StrOpt('reverse-format', default=None),
+
+    cfg.StrOpt('keystone_auth_name', default=None),
+    cfg.StrOpt('keystone_auth_pass', default=None),
+    cfg.StrOpt('keystone_auth_project', default=None),
+    cfg.StrOpt('keystone_auth_url', default=None),
+], group='handler:nova_fixed_multi')
+
+
+class NovaFixedMultiHandler(BaseAddressMultiHandler):
+    """ Handler for Nova's notifications """
+    __plugin_name__ = 'nova_fixed_multi'
+
+    def get_exchange_topics(self):
+        exchange = cfg.CONF[self.name].control_exchange
+
+        topics = [topic for topic in cfg.CONF[self.name].notification_topics]
+
+        return (exchange, topics)
+
+    def get_event_types(self):
+        return [
+            'compute.instance.create.end',
+            'compute.instance.delete.start',
+        ]
+
+    def process_notification(self, context, event_type, payload):
+        LOG.debug('NovaFixedHandler received notification - %s' % event_type)
+
+        if event_type == 'compute.instance.create.end':
+            try:
+                self._create(payload['fixed_ips'], payload,
+                             resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+
+        elif event_type == 'compute.instance.delete.start':
+            try:
+                self._delete(resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+        else:
+            raise ValueError('NovaFixedHandler received an invalid event type')
diff --git 
a/modules/openstack/files/liberty/designate/nova_fixed_multi.egg-info/entry_points.txt
 
b/modules/openstack/files/liberty/designate/nova_fixed_multi.egg-info/entry_points.txt
new file mode 100644
index 0000000..e890a22
--- /dev/null
+++ 
b/modules/openstack/files/liberty/designate/nova_fixed_multi.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[designate.notification.handler]
+nova_fixed_multi = nova_fixed_multi.novamulti:NovaFixedMultiHandler
diff --git 
a/modules/openstack/files/liberty/designate/nova_fixed_multi/__init__.py 
b/modules/openstack/files/liberty/designate/nova_fixed_multi/__init__.py
new file mode 100644
index 0000000..711e39e
--- /dev/null
+++ b/modules/openstack/files/liberty/designate/nova_fixed_multi/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+import gettext
+import logging
diff --git a/modules/openstack/files/liberty/designate/nova_fixed_multi/base.py 
b/modules/openstack/files/liberty/designate/nova_fixed_multi/base.py
new file mode 100644
index 0000000..a4f544e
--- /dev/null
+++ b/modules/openstack/files/liberty/designate/nova_fixed_multi/base.py
@@ -0,0 +1,229 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This is a slight form of the designate source file found at
+# designate/notification_handler/base.py
+
+import abc
+from oslo_config import cfg
+from designate import exceptions
+from oslo_log import log as logging
+from designate.central import rpcapi as central_rpcapi
+from designate.context import DesignateContext
+from designate.notification_handler.base import BaseAddressHandler
+from designate.objects import Record
+from designate.plugin import ExtensionPlugin
+from keystoneclient.auth.identity import v3
+from keystoneclient import client
+from keystoneclient import exceptions as keystoneexceptions
+from keystoneclient.v3 import projects
+from keystoneclient import session
+
+
+LOG = logging.getLogger(__name__)
+central_api = central_rpcapi.CentralAPI()
+
+
+class BaseAddressMultiHandler(BaseAddressHandler):
+    def _get_ip_data(self, addr_dict):
+        ip = addr_dict['address']
+        version = addr_dict['version']
+
+        data = {
+            'ip_version': version,
+        }
+
+        # TODO(endre): Add v6 support
+        if version == 4:
+            data['ip_address'] = ip.replace('.', '-')
+            ip_data = ip.split(".")
+            for i in [0, 1, 2, 3]:
+                data["octet%s" % i] = ip_data[i]
+        return data
+
+    def _create(self, addresses, extra, managed=True,
+                resource_type=None, resource_id=None):
+        """
+        Create a a record from addresses
+
+        :param addresses: Address objects like
+                          {'version': 4, 'ip': '10.0.0.1'}
+        :param extra: Extra data to use when formatting the record
+        :param managed: Is it a managed resource
+        :param resource_type: The managed resource type
+        :param resource_id: The managed resource ID
+        """
+        LOG.debug('Using DomainID: %s' % cfg.CONF[self.name].domain_id)
+        domain = self.get_domain(cfg.CONF[self.name].domain_id)
+        LOG.debug('Domain: %r' % domain)
+
+        data = extra.copy()
+        LOG.debug('Event data: %s' % data)
+        data['domain'] = domain['name']
+
+        context = DesignateContext.get_admin_context(all_tenants=True)
+
+        # Extra magic!  The event record contains a tenant id but not a tenant 
name.  So
+        #  if our formats include project_name then we need to ask keystone 
for the name.
+        need_project_name = False
+        for fmt in cfg.CONF[self.name].get('format'):
+            if 'project_name' in fmt:
+                need_project_name = True
+                break
+        if 'project_name' in cfg.CONF[self.name].get('reverse_format'):
+            need_project_name = True
+        if need_project_name:
+            project_name = self._resolve_project_name(data['tenant_id'])
+            data['project_name'] = project_name
+
+        for addr in addresses:
+            event_data = data.copy()
+            event_data.update(self._get_ip_data(addr))
+
+            if addr['version'] == 4:
+                reverse_format = cfg.CONF[self.name].get('reverse_format')
+                reverse_domain_id = 
cfg.CONF[self.name].get('reverse_domain_id')
+                if reverse_format and reverse_domain_id:
+                    reverse_domain = self.get_domain(reverse_domain_id)
+                    LOG.debug('Reverse domain: %r' % reverse_domain)
+
+                    ip_digits = addr['address'].split('.')
+                    ip_digits.reverse()
+                    name = "%s.in-addr.arpa." % '.'.join(ip_digits)
+
+                    recordset_values = {
+                        'domain_id': reverse_domain['id'],
+                        'name': name,
+                        'type': 'PTR',
+                    }
+                    recordset = self._find_or_create_recordset(
+                        context, **recordset_values)
+
+                    record_values = {'data': reverse_format % event_data}
+
+                    if managed:
+                        record_values.update({
+                            'managed': managed,
+                            'managed_plugin_name': self.get_plugin_name(),
+                            'managed_plugin_type': self.get_plugin_type(),
+                            'managed_resource_type': resource_type,
+                            'managed_resource_id': resource_id})
+
+                    LOG.debug('Creating record in %s / %s with values %r',
+                              reverse_domain['id'],
+                              recordset['id'], record_values)
+                    central_api.create_record(context,
+                                              reverse_domain['id'],
+                                              recordset['id'],
+                                              Record(**record_values))
+
+            for fmt in cfg.CONF[self.name].get('format'):
+                recordset_values = {
+                    'domain_id': domain['id'],
+                    'name': fmt % event_data,
+                    'type': 'A' if addr['version'] == 4 else 'AAAA'}
+
+                recordset = self._find_or_create_recordset(
+                    context, **recordset_values)
+
+                record_values = {
+                    'data': addr['address']}
+
+                if managed:
+                    record_values.update({
+                        'managed': managed,
+                        'managed_plugin_name': self.get_plugin_name(),
+                        'managed_plugin_type': self.get_plugin_type(),
+                        'managed_resource_type': resource_type,
+                        'managed_resource_id': resource_id})
+
+                LOG.debug('Creating record in %s / %s with values %r',
+                          domain['id'], recordset['id'], record_values)
+                central_api.create_record(context,
+                                          domain['id'],
+                                          recordset['id'],
+                                          Record(**record_values))
+
+    def _delete(self, managed=True, resource_id=None, resource_type='instance',
+                criterion={}):
+        """
+        Handle a generic delete of a fixed ip within a domain
+
+        :param criterion: Criterion to search and destroy records
+        """
+        context = DesignateContext().elevated()
+        context.all_tenants = True
+        context.edit_managed_records = True
+
+        criterion.update({'domain_id': cfg.CONF[self.name].domain_id})
+
+        if managed:
+            criterion.update({
+                'managed': managed,
+                'managed_plugin_name': self.get_plugin_name(),
+                'managed_plugin_type': self.get_plugin_type(),
+                'managed_resource_id': resource_id,
+                'managed_resource_type': resource_type
+            })
+
+        records = central_api.find_records(context, criterion)
+
+        for record in records:
+            LOG.debug('Deleting record %s' % record['id'])
+
+            central_api.delete_record(context, cfg.CONF[self.name].domain_id,
+                                      record['recordset_id'], record['id'])
+
+        reverse_domain_id = cfg.CONF[self.name].get('reverse_domain_id')
+        if reverse_domain_id:
+            criterion.update({'domain_id': reverse_domain_id})
+
+            records = central_api.find_records(context, criterion)
+
+            for record in records:
+                LOG.debug('Deleting record %s' % record['id'])
+
+                central_api.delete_record(context,
+                                          reverse_domain_id,
+                                          record['recordset_id'], record['id'])
+
+    def _resolve_project_name(self, tenant_id):
+        try:
+            username = cfg.CONF[self.name].keystone_auth_name
+            passwd = cfg.CONF[self.name].keystone_auth_pass
+            project = cfg.CONF[self.name].keystone_auth_project
+            url = cfg.CONF[self.name].keystone_auth_url
+        except keyerror:
+            LOG.debug('Missing a config setting for keystone auth.')
+            return
+
+        try:
+            auth = v3.Password(auth_url=url,
+                               user_id=username,
+                               password=passwd,
+                               project_id=project)
+            sess = session.Session(auth=auth)
+            keystone = client.Client(session=sess, auth_url=url)
+        except keystoneexceptions.AuthorizationFailure:
+            LOG.debug('Keystone client auth failed.')
+            return
+        projectmanager = projects.ProjectManager(keystone)
+        proj = projectmanager.get(tenant_id)
+        if proj:
+            LOG.debug('Resolved project id %s as %s' % (tenant_id, proj.name))
+            return proj.name
+        else:
+            return 'unknown'
diff --git 
a/modules/openstack/files/liberty/designate/nova_fixed_multi/novamulti.py 
b/modules/openstack/files/liberty/designate/nova_fixed_multi/novamulti.py
new file mode 100644
index 0000000..0a73b49
--- /dev/null
+++ b/modules/openstack/files/liberty/designate/nova_fixed_multi/novamulti.py
@@ -0,0 +1,87 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This file is a slight modification of the nova notification driver found
+#  in the designate source at designate/notification_handler/nova.py
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from nova_fixed_multi.base import BaseAddressMultiHandler
+
+import sys
+
+LOG = logging.getLogger(__name__)
+
+cfg.CONF.register_group(cfg.OptGroup(
+    name='handler:nova_fixed_multi',
+    title="Configuration for Nova Notification Handler"
+))
+
+cfg.CONF.register_opts([
+    cfg.ListOpt('notification-topics', default=['monitor']),
+    cfg.StrOpt('control-exchange', default='nova'),
+    cfg.StrOpt('domain-id', default=None),
+    cfg.MultiStrOpt('format', default=[]),
+    cfg.StrOpt('reverse-domain-id', default=None),
+    cfg.StrOpt('reverse-format', default=None),
+
+    cfg.StrOpt('keystone_auth_name', default=None),
+    cfg.StrOpt('keystone_auth_pass', default=None),
+    cfg.StrOpt('keystone_auth_project', default=None),
+    cfg.StrOpt('keystone_auth_url', default=None),
+], group='handler:nova_fixed_multi')
+
+
+class NovaFixedMultiHandler(BaseAddressMultiHandler):
+    """ Handler for Nova's notifications """
+    __plugin_name__ = 'nova_fixed_multi'
+
+    def get_exchange_topics(self):
+        exchange = cfg.CONF[self.name].control_exchange
+
+        topics = [topic for topic in cfg.CONF[self.name].notification_topics]
+
+        return (exchange, topics)
+
+    def get_event_types(self):
+        return [
+            'compute.instance.create.end',
+            'compute.instance.delete.start',
+        ]
+
+    def process_notification(self, context, event_type, payload):
+        LOG.debug('NovaFixedHandler received notification - %s' % event_type)
+
+        if event_type == 'compute.instance.create.end':
+            try:
+                self._create(payload['fixed_ips'], payload,
+                             resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+
+        elif event_type == 'compute.instance.delete.start':
+            try:
+                self._delete(resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+        else:
+            raise ValueError('NovaFixedHandler received an invalid event type')
diff --git 
a/modules/openstack/files/mitaka/designate/nova_fixed_multi.egg-info/entry_points.txt
 
b/modules/openstack/files/mitaka/designate/nova_fixed_multi.egg-info/entry_points.txt
new file mode 100644
index 0000000..e890a22
--- /dev/null
+++ 
b/modules/openstack/files/mitaka/designate/nova_fixed_multi.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[designate.notification.handler]
+nova_fixed_multi = nova_fixed_multi.novamulti:NovaFixedMultiHandler
diff --git 
a/modules/openstack/files/mitaka/designate/nova_fixed_multi/__init__.py 
b/modules/openstack/files/mitaka/designate/nova_fixed_multi/__init__.py
new file mode 100644
index 0000000..711e39e
--- /dev/null
+++ b/modules/openstack/files/mitaka/designate/nova_fixed_multi/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# 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.
+
+import gettext
+import logging
diff --git a/modules/openstack/files/mitaka/designate/nova_fixed_multi/base.py 
b/modules/openstack/files/mitaka/designate/nova_fixed_multi/base.py
new file mode 100644
index 0000000..a4f544e
--- /dev/null
+++ b/modules/openstack/files/mitaka/designate/nova_fixed_multi/base.py
@@ -0,0 +1,229 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This is a slight form of the designate source file found at
+# designate/notification_handler/base.py
+
+import abc
+from oslo_config import cfg
+from designate import exceptions
+from oslo_log import log as logging
+from designate.central import rpcapi as central_rpcapi
+from designate.context import DesignateContext
+from designate.notification_handler.base import BaseAddressHandler
+from designate.objects import Record
+from designate.plugin import ExtensionPlugin
+from keystoneclient.auth.identity import v3
+from keystoneclient import client
+from keystoneclient import exceptions as keystoneexceptions
+from keystoneclient.v3 import projects
+from keystoneclient import session
+
+
+LOG = logging.getLogger(__name__)
+central_api = central_rpcapi.CentralAPI()
+
+
+class BaseAddressMultiHandler(BaseAddressHandler):
+    def _get_ip_data(self, addr_dict):
+        ip = addr_dict['address']
+        version = addr_dict['version']
+
+        data = {
+            'ip_version': version,
+        }
+
+        # TODO(endre): Add v6 support
+        if version == 4:
+            data['ip_address'] = ip.replace('.', '-')
+            ip_data = ip.split(".")
+            for i in [0, 1, 2, 3]:
+                data["octet%s" % i] = ip_data[i]
+        return data
+
+    def _create(self, addresses, extra, managed=True,
+                resource_type=None, resource_id=None):
+        """
+        Create a a record from addresses
+
+        :param addresses: Address objects like
+                          {'version': 4, 'ip': '10.0.0.1'}
+        :param extra: Extra data to use when formatting the record
+        :param managed: Is it a managed resource
+        :param resource_type: The managed resource type
+        :param resource_id: The managed resource ID
+        """
+        LOG.debug('Using DomainID: %s' % cfg.CONF[self.name].domain_id)
+        domain = self.get_domain(cfg.CONF[self.name].domain_id)
+        LOG.debug('Domain: %r' % domain)
+
+        data = extra.copy()
+        LOG.debug('Event data: %s' % data)
+        data['domain'] = domain['name']
+
+        context = DesignateContext.get_admin_context(all_tenants=True)
+
+        # Extra magic!  The event record contains a tenant id but not a tenant 
name.  So
+        #  if our formats include project_name then we need to ask keystone 
for the name.
+        need_project_name = False
+        for fmt in cfg.CONF[self.name].get('format'):
+            if 'project_name' in fmt:
+                need_project_name = True
+                break
+        if 'project_name' in cfg.CONF[self.name].get('reverse_format'):
+            need_project_name = True
+        if need_project_name:
+            project_name = self._resolve_project_name(data['tenant_id'])
+            data['project_name'] = project_name
+
+        for addr in addresses:
+            event_data = data.copy()
+            event_data.update(self._get_ip_data(addr))
+
+            if addr['version'] == 4:
+                reverse_format = cfg.CONF[self.name].get('reverse_format')
+                reverse_domain_id = 
cfg.CONF[self.name].get('reverse_domain_id')
+                if reverse_format and reverse_domain_id:
+                    reverse_domain = self.get_domain(reverse_domain_id)
+                    LOG.debug('Reverse domain: %r' % reverse_domain)
+
+                    ip_digits = addr['address'].split('.')
+                    ip_digits.reverse()
+                    name = "%s.in-addr.arpa." % '.'.join(ip_digits)
+
+                    recordset_values = {
+                        'domain_id': reverse_domain['id'],
+                        'name': name,
+                        'type': 'PTR',
+                    }
+                    recordset = self._find_or_create_recordset(
+                        context, **recordset_values)
+
+                    record_values = {'data': reverse_format % event_data}
+
+                    if managed:
+                        record_values.update({
+                            'managed': managed,
+                            'managed_plugin_name': self.get_plugin_name(),
+                            'managed_plugin_type': self.get_plugin_type(),
+                            'managed_resource_type': resource_type,
+                            'managed_resource_id': resource_id})
+
+                    LOG.debug('Creating record in %s / %s with values %r',
+                              reverse_domain['id'],
+                              recordset['id'], record_values)
+                    central_api.create_record(context,
+                                              reverse_domain['id'],
+                                              recordset['id'],
+                                              Record(**record_values))
+
+            for fmt in cfg.CONF[self.name].get('format'):
+                recordset_values = {
+                    'domain_id': domain['id'],
+                    'name': fmt % event_data,
+                    'type': 'A' if addr['version'] == 4 else 'AAAA'}
+
+                recordset = self._find_or_create_recordset(
+                    context, **recordset_values)
+
+                record_values = {
+                    'data': addr['address']}
+
+                if managed:
+                    record_values.update({
+                        'managed': managed,
+                        'managed_plugin_name': self.get_plugin_name(),
+                        'managed_plugin_type': self.get_plugin_type(),
+                        'managed_resource_type': resource_type,
+                        'managed_resource_id': resource_id})
+
+                LOG.debug('Creating record in %s / %s with values %r',
+                          domain['id'], recordset['id'], record_values)
+                central_api.create_record(context,
+                                          domain['id'],
+                                          recordset['id'],
+                                          Record(**record_values))
+
+    def _delete(self, managed=True, resource_id=None, resource_type='instance',
+                criterion={}):
+        """
+        Handle a generic delete of a fixed ip within a domain
+
+        :param criterion: Criterion to search and destroy records
+        """
+        context = DesignateContext().elevated()
+        context.all_tenants = True
+        context.edit_managed_records = True
+
+        criterion.update({'domain_id': cfg.CONF[self.name].domain_id})
+
+        if managed:
+            criterion.update({
+                'managed': managed,
+                'managed_plugin_name': self.get_plugin_name(),
+                'managed_plugin_type': self.get_plugin_type(),
+                'managed_resource_id': resource_id,
+                'managed_resource_type': resource_type
+            })
+
+        records = central_api.find_records(context, criterion)
+
+        for record in records:
+            LOG.debug('Deleting record %s' % record['id'])
+
+            central_api.delete_record(context, cfg.CONF[self.name].domain_id,
+                                      record['recordset_id'], record['id'])
+
+        reverse_domain_id = cfg.CONF[self.name].get('reverse_domain_id')
+        if reverse_domain_id:
+            criterion.update({'domain_id': reverse_domain_id})
+
+            records = central_api.find_records(context, criterion)
+
+            for record in records:
+                LOG.debug('Deleting record %s' % record['id'])
+
+                central_api.delete_record(context,
+                                          reverse_domain_id,
+                                          record['recordset_id'], record['id'])
+
+    def _resolve_project_name(self, tenant_id):
+        try:
+            username = cfg.CONF[self.name].keystone_auth_name
+            passwd = cfg.CONF[self.name].keystone_auth_pass
+            project = cfg.CONF[self.name].keystone_auth_project
+            url = cfg.CONF[self.name].keystone_auth_url
+        except keyerror:
+            LOG.debug('Missing a config setting for keystone auth.')
+            return
+
+        try:
+            auth = v3.Password(auth_url=url,
+                               user_id=username,
+                               password=passwd,
+                               project_id=project)
+            sess = session.Session(auth=auth)
+            keystone = client.Client(session=sess, auth_url=url)
+        except keystoneexceptions.AuthorizationFailure:
+            LOG.debug('Keystone client auth failed.')
+            return
+        projectmanager = projects.ProjectManager(keystone)
+        proj = projectmanager.get(tenant_id)
+        if proj:
+            LOG.debug('Resolved project id %s as %s' % (tenant_id, proj.name))
+            return proj.name
+        else:
+            return 'unknown'
diff --git 
a/modules/openstack/files/mitaka/designate/nova_fixed_multi/novamulti.py 
b/modules/openstack/files/mitaka/designate/nova_fixed_multi/novamulti.py
new file mode 100644
index 0000000..0a73b49
--- /dev/null
+++ b/modules/openstack/files/mitaka/designate/nova_fixed_multi/novamulti.py
@@ -0,0 +1,87 @@
+# Copyright 2015 Andrew Bogott for the Wikimedia Foundation
+# 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.
+
+
+# This file is a slight modification of the nova notification driver found
+#  in the designate source at designate/notification_handler/nova.py
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from nova_fixed_multi.base import BaseAddressMultiHandler
+
+import sys
+
+LOG = logging.getLogger(__name__)
+
+cfg.CONF.register_group(cfg.OptGroup(
+    name='handler:nova_fixed_multi',
+    title="Configuration for Nova Notification Handler"
+))
+
+cfg.CONF.register_opts([
+    cfg.ListOpt('notification-topics', default=['monitor']),
+    cfg.StrOpt('control-exchange', default='nova'),
+    cfg.StrOpt('domain-id', default=None),
+    cfg.MultiStrOpt('format', default=[]),
+    cfg.StrOpt('reverse-domain-id', default=None),
+    cfg.StrOpt('reverse-format', default=None),
+
+    cfg.StrOpt('keystone_auth_name', default=None),
+    cfg.StrOpt('keystone_auth_pass', default=None),
+    cfg.StrOpt('keystone_auth_project', default=None),
+    cfg.StrOpt('keystone_auth_url', default=None),
+], group='handler:nova_fixed_multi')
+
+
+class NovaFixedMultiHandler(BaseAddressMultiHandler):
+    """ Handler for Nova's notifications """
+    __plugin_name__ = 'nova_fixed_multi'
+
+    def get_exchange_topics(self):
+        exchange = cfg.CONF[self.name].control_exchange
+
+        topics = [topic for topic in cfg.CONF[self.name].notification_topics]
+
+        return (exchange, topics)
+
+    def get_event_types(self):
+        return [
+            'compute.instance.create.end',
+            'compute.instance.delete.start',
+        ]
+
+    def process_notification(self, context, event_type, payload):
+        LOG.debug('NovaFixedHandler received notification - %s' % event_type)
+
+        if event_type == 'compute.instance.create.end':
+            try:
+                self._create(payload['fixed_ips'], payload,
+                             resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+
+        elif event_type == 'compute.instance.delete.start':
+            try:
+                self._delete(resource_id=payload['instance_id'],
+                             resource_type='instance')
+            except:
+                LOG.debug("--------------------     Unexpected error: %s" %
+                          sys.exc_info()[0])
+                LOG.debug("--------------------     (swallowed)")
+        else:
+            raise ValueError('NovaFixedHandler received an invalid event type')
diff --git a/modules/openstack/manifests/designate/service.pp 
b/modules/openstack/manifests/designate/service.pp
index 0449472..9e8301b 100644
--- a/modules/openstack/manifests/designate/service.pp
+++ b/modules/openstack/manifests/designate/service.pp
@@ -27,8 +27,7 @@
         'designate-api',
         'designate-doc',
         'designate-central',
-        'python-novaclient',
-        'python-nova-fixed-multi'
+        'python-novaclient'
     )
 
     file { '/usr/lib/python2.7/dist-packages/nova_ldap':
@@ -46,6 +45,21 @@
         recurse => true,
     }
 
+    file { '/usr/lib/python2.7/dist-packages/nova_fixed_multi':
+        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/nova_fixed_multi",
+        owner   => 'root',
+        group   => 'root',
+        mode    => '0644',
+        recurse => true,
+    }
+    file { '/usr/lib/python2.7/dist-packages/nova_fixed_multi.egg-info':
+        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/nova_fixed_multi.egg-info",
+        owner   => 'root',
+        group   => 'root',
+        mode    => '0644',
+        recurse => true,
+    }
+
     file {
         '/etc/designate/designate.conf':
             content => 
template("openstack/${openstack_version}/designate/designate.conf.erb"),
diff --git a/tox.ini b/tox.ini
index 5cd81db..b9a3d3e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,7 @@
 # E402: module level import not at top of file
 ignore = E123,E133,E226,E241,E242,E402
 # Upstream files that don't pass flake8 but should not be locally modified
-exclude = 
modules/letsencrypt/files/acme_tiny.py,modules/varnish/files/varnishapi.py,modules/postgresql/files/check_postgres_replication_lag.py
+exclude = 
modules/letsencrypt/files/acme_tiny.py,modules/varnish/files/varnishapi.py,modules/postgresql/files/check_postgres_replication_lag.py,modules/openstack/files/kilo/designate/nova_fixed_multi,modules/openstack/files/liberty/designate/nova_fixed_multi,modules/openstack/files/mitaka/designate/nova_fixed_multi
 
 [testenv]
 deps =

-- 
To view, visit https://gerrit.wikimedia.org/r/312278
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I695dab2212550da053e26fa9dc6e38729c01a8c3
Gerrit-PatchSet: 1
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Alex Monk <a...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to