Andrew Bogott has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/332646 )

Change subject: Designate:  Rename the nova_ldap sink handler to wmf_sink
......................................................................

Designate:  Rename the nova_ldap sink handler to wmf_sink

Soon this won't handle ldap at all, so let's get an accurate
name in place.

Change-Id: I3b179652f8659f2536e6d665113f8ac49f1f5cb7
---
D modules/openstack/files/liberty/designate/nova_ldap.egg-info/entry_points.txt
A modules/openstack/files/liberty/designate/wmf_sink.egg-info/entry_points.txt
R modules/openstack/files/liberty/designate/wmf_sink/__init__.py
R modules/openstack/files/liberty/designate/wmf_sink/base.py
R modules/openstack/files/liberty/designate/wmf_sink/wmfsink.py
D modules/openstack/files/mitaka/designate/nova_ldap.egg-info/entry_points.txt
D modules/openstack/files/mitaka/designate/nova_ldap/base.py
A modules/openstack/files/mitaka/designate/wmf_sink.egg-info/entry_points.txt
R modules/openstack/files/mitaka/designate/wmf_sink/__init__.py
C modules/openstack/files/mitaka/designate/wmf_sink/base.py
R modules/openstack/files/mitaka/designate/wmf_sink/novaldap.py
C modules/openstack/files/mitaka/designate/wmf_sink/wmfsink.py
M modules/openstack/manifests/designate/service.pp
M modules/openstack/templates/liberty/designate/designate.conf.erb
14 files changed, 27 insertions(+), 303 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/puppet 
refs/changes/46/332646/1

diff --git 
a/modules/openstack/files/liberty/designate/nova_ldap.egg-info/entry_points.txt 
b/modules/openstack/files/liberty/designate/nova_ldap.egg-info/entry_points.txt
deleted file mode 100644
index a65d78d..0000000
--- 
a/modules/openstack/files/liberty/designate/nova_ldap.egg-info/entry_points.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[designate.notification.handler]
-nova_ldap = nova_ldap.novaldap:NovaFixedLdapHandler
diff --git 
a/modules/openstack/files/liberty/designate/wmf_sink.egg-info/entry_points.txt 
b/modules/openstack/files/liberty/designate/wmf_sink.egg-info/entry_points.txt
new file mode 100644
index 0000000..f3c3cf8
--- /dev/null
+++ 
b/modules/openstack/files/liberty/designate/wmf_sink.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[designate.notification.handler]
+wmf_sink = wmf_sink.wmfsink:NovaFixedWMFHandler
diff --git a/modules/openstack/files/liberty/designate/nova_ldap/__init__.py 
b/modules/openstack/files/liberty/designate/wmf_sink/__init__.py
similarity index 100%
rename from modules/openstack/files/liberty/designate/nova_ldap/__init__.py
rename to modules/openstack/files/liberty/designate/wmf_sink/__init__.py
diff --git a/modules/openstack/files/liberty/designate/nova_ldap/base.py 
b/modules/openstack/files/liberty/designate/wmf_sink/base.py
similarity index 99%
rename from modules/openstack/files/liberty/designate/nova_ldap/base.py
rename to modules/openstack/files/liberty/designate/wmf_sink/base.py
index e0db247..12cc9ba 100644
--- a/modules/openstack/files/liberty/designate/nova_ldap/base.py
+++ b/modules/openstack/files/liberty/designate/wmf_sink/base.py
@@ -32,7 +32,7 @@
 central_api = central_rpcapi.CentralAPI()
 
 
-class BaseAddressLdapHandler(BaseAddressHandler):
+class BaseAddressWMFHandler(BaseAddressHandler):
     @staticmethod
     def _get_ip_data(addr_dict):
         ip = addr_dict['address']
diff --git a/modules/openstack/files/liberty/designate/nova_ldap/novaldap.py 
b/modules/openstack/files/liberty/designate/wmf_sink/wmfsink.py
similarity index 88%
rename from modules/openstack/files/liberty/designate/nova_ldap/novaldap.py
rename to modules/openstack/files/liberty/designate/wmf_sink/wmfsink.py
index d4aea83..9b476e6 100644
--- a/modules/openstack/files/liberty/designate/nova_ldap/novaldap.py
+++ b/modules/openstack/files/liberty/designate/wmf_sink/wmfsink.py
@@ -18,14 +18,14 @@
 #  in the designate source at designate/notification_handler/nova.py
 
 from oslo_config import cfg
-from nova_ldap.base import BaseAddressLdapHandler
+from wmf_sink.base import BaseAddressWMFHandler
 from oslo_log import log as logging
 
 LOG = logging.getLogger(__name__)
 
 cfg.CONF.register_group(cfg.OptGroup(
-    name='handler:nova_ldap',
-    title="Configuration for Nova Ldap Handler (WMF-specific transitional)"
+    name='handler:wmf_sink',
+    title="Configuration for WMF-specific event handling)"
 ))
 
 cfg.CONF.register_opts([
@@ -51,12 +51,12 @@
     cfg.StrOpt('keystone_auth_pass', default=None),
     cfg.StrOpt('keystone_auth_project', default=None),
     cfg.StrOpt('keystone_auth_url', default=None),
-], group='handler:nova_ldap')
+], group='handler:wmf_sink')
 
 
-class NovaFixedLdapHandler(BaseAddressLdapHandler):
+class NovaFixedWMFHandler(BaseAddressWMFHandler):
     """ Handler for Nova's notifications """
-    __plugin_name__ = 'nova_ldap'
+    __plugin_name__ = 'wmf_sink'
 
     def get_exchange_topics(self):
         exchange = cfg.CONF[self.name].control_exchange
@@ -72,7 +72,7 @@
         ]
 
     def process_notification(self, context, event_type, payload):
-        LOG.debug('NovaLdapHandler received notification - %s' % event_type)
+        LOG.debug('received notification - %s' % event_type)
 
         if event_type == 'compute.instance.create.end':
             self._create(payload['fixed_ips'], payload,
diff --git 
a/modules/openstack/files/mitaka/designate/nova_ldap.egg-info/entry_points.txt 
b/modules/openstack/files/mitaka/designate/nova_ldap.egg-info/entry_points.txt
deleted file mode 100644
index a65d78d..0000000
--- 
a/modules/openstack/files/mitaka/designate/nova_ldap.egg-info/entry_points.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[designate.notification.handler]
-nova_ldap = nova_ldap.novaldap:NovaFixedLdapHandler
diff --git a/modules/openstack/files/mitaka/designate/nova_ldap/base.py 
b/modules/openstack/files/mitaka/designate/nova_ldap/base.py
deleted file mode 100644
index 7ae9a46..0000000
--- a/modules/openstack/files/mitaka/designate/nova_ldap/base.py
+++ /dev/null
@@ -1,276 +0,0 @@
-# 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.
-
-from oslo_config import cfg
-from designate.central import rpcapi as central_rpcapi
-from designate.notification_handler.base import BaseAddressHandler
-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
-from oslo_log import log as logging
-
-import ldap
-import ldap.modlist
-import pipes
-import subprocess
-
-LOG = logging.getLogger(__name__)
-central_api = central_rpcapi.CentralAPI()
-
-
-class BaseAddressLdapHandler(BaseAddressHandler):
-    @staticmethod
-    def _get_ip_data(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
-
-    @staticmethod
-    def _getLdapInfo(attr, conffile="/etc/ldap.conf"):
-        try:
-            f = open(conffile)
-        except IOError:
-            if conffile == "/etc/ldap.conf":
-                # fallback to /etc/ldap/ldap.conf, which will likely
-                # have less information
-                f = open("/etc/ldap/ldap.conf")
-        for line in f:
-            if line.strip() == "":
-                continue
-            if line.split()[0].lower() == attr.lower():
-                return line.split(None, 1)[1].strip()
-                break
-
-    def _openLdap(self):
-        ldapHost = self._getLdapInfo("uri")
-        sslType = self._getLdapInfo("ssl")
-
-        binddn = cfg.CONF[self.name].get('ldapusername')
-        bindpw = cfg.CONF[self.name].get('ldappassword')
-        ds = ldap.initialize(ldapHost)
-        ds.protocol_version = ldap.VERSION3
-        if sslType == "start_tls":
-            ds.start_tls_s()
-
-        try:
-            ds.simple_bind_s(binddn, bindpw)
-            return ds
-        except ldap.CONSTRAINT_VIOLATION:
-            LOG.debug("LDAP bind failure:  Too many failed attempts.\n")
-        except ldap.INVALID_DN_SYNTAX:
-            LOG.debug("LDAP bind failure:  The bind DN is incorrect... \n")
-        except ldap.NO_SUCH_OBJECT:
-            LOG.debug("LDAP bind failure:  "
-                      "Unable to locate the bind DN account.\n")
-        except ldap.UNWILLING_TO_PERFORM as msg:
-            LOG.debug("LDAP bind failure:  "
-                      "The LDAP server was unwilling to perform the action"
-                      " requested.\nError was: %s\n" % msg[0]["info"])
-        except ldap.INVALID_CREDENTIALS:
-            LOG.debug("LDAP bind failure:  Password incorrect.\n")
-
-        return None
-
-    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
-        """
-        ds = self._openLdap()
-        if not ds:
-            return
-
-        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']
-
-        project_name = self._resolve_project_name(data['tenant_id'])
-        data['project_name'] = project_name
-
-        # Just one ldap entry per host, please.
-        addr = addresses[0]
-
-        event_data = data.copy()
-        event_data.update(self._get_ip_data(addr))
-        dc = "%(hostname)s.%(project_name)s.%(domain)s" % event_data
-        # ldap doesn't like trailing .s
-        dc = dc.rstrip('.').encode('utf8')
-        dn = "dc=%s,ou=hosts,dc=wikimedia,dc=org" % dc
-
-        hostEntry = {}
-        hostEntry['objectClass'] = ['domainrelatedobject',
-                                    'dnsdomain',
-                                    'puppetclient',
-                                    'domain',
-                                    'dcobject',
-                                    'top']
-        hostEntry['l'] = 'eqiad'
-        hostEntry['dc'] = dc
-        hostEntry['aRecord'] = addr['address'].encode('utf8')
-        hostEntry['puppetClass'] = []
-        hostEntry['puppetVar'] = []
-        for cls in cfg.CONF[self.name].get('puppetdefaultclasses'):
-            hostEntry['puppetClass'].append(cls)
-        for var in cfg.CONF[self.name].get('puppetdefaultvars'):
-            hostEntry['puppetVar'].append(var)
-        hostEntry['associatedDomain'] = []
-        hostEntry['puppetVar'].append('instanceproject=%s' %
-                                      event_data['project_name'].encode(
-                                          'utf8'))
-        hostEntry['puppetVar'].append('instancename=%s' %
-                                      event_data['hostname'].encode(
-                                          'utf8'))
-
-        for fmt in cfg.CONF[self.name].get('format'):
-            hostEntry['associatedDomain'].append(
-                (fmt % event_data).rstrip('.').encode('utf8'))
-
-        if managed:
-            LOG.debug('Creating ldap record')
-
-            modlist = ldap.modlist.addModlist(hostEntry)
-            try:
-                ds.add_s(dn, modlist)
-            except ldap.LDAPError as e:
-                LOG.debug('Ldap exception %s' % e)
-
-        ds.unbind()
-
-    def _delete(self, extra, 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
-        """
-        ds = self._openLdap()
-        if not ds:
-            return
-
-        LOG.debug('Delete 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']
-
-        project_name = self._resolve_project_name(data['tenant_id'])
-        data['project_name'] = project_name
-
-        event_data = data.copy()
-
-        dc = "%(hostname)s.%(project_name)s.%(domain)s" % event_data
-        dc = dc.rstrip('.').encode('utf8')
-        dn = "dc=%s,ou=hosts,dc=wikimedia,dc=org" % dc
-
-        LOG.debug('Deleting ldap record: %s' % dn)
-        try:
-            ds.delete_s(dn)
-        except ldap.NO_SUCH_OBJECT:
-            LOG.debug('Warning:  %s not found in ldap.  Not deleted.' % dn)
-
-        ds.unbind()
-
-        # WMF-specific add-on:  Clean salt and puppet keys for deleted
-        #  instance
-        if (cfg.CONF[self.name].puppet_key_format and
-                cfg.CONF[self.name].puppet_master_host):
-            puppetkey = cfg.CONF[self.name].puppet_key_format % event_data
-            puppetkey = puppetkey.rstrip('.').encode('utf8')
-            LOG.debug('Cleaning puppet key %s' % puppetkey)
-            self._run_remote_command(cfg.CONF[self.name].puppet_master_host,
-                                     cfg.CONF[self.name].certmanager_user,
-                                     'sudo puppet cert clean %s' %
-                                     pipes.quote(puppetkey))
-
-        if (cfg.CONF[self.name].salt_key_format and
-                cfg.CONF[self.name].salt_master_host):
-            saltkey = cfg.CONF[self.name].salt_key_format % event_data
-            saltkey = saltkey.rstrip('.').encode('utf8')
-            LOG.debug('Cleaning salt key %s' % saltkey)
-            self._run_remote_command(cfg.CONF[self.name].salt_master_host,
-                                     cfg.CONF[self.name].certmanager_user,
-                                     'sudo salt-key -y -d  %s' %
-                                     pipes.quote(saltkey))
-
-    @staticmethod
-    def _run_remote_command(server, username, command):
-        ssh_command = ['/usr/bin/ssh', '-l%s' % username, server, command]
-
-        p = subprocess.Popen(ssh_command,
-                             stdout=subprocess.PIPE,
-                             stderr=subprocess.PIPE)
-        (out, error) = p.communicate()
-        rcode = p.wait()
-        return out, error, rcode
-
-        if rcode:
-            LOG.warning('Remote call %s to server %s failed: \n%s\n%s' %
-                        (command, server, out, error))
-            return False
-        return True
-
-    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/wmf_sink.egg-info/entry_points.txt 
b/modules/openstack/files/mitaka/designate/wmf_sink.egg-info/entry_points.txt
new file mode 100644
index 0000000..f3c3cf8
--- /dev/null
+++ 
b/modules/openstack/files/mitaka/designate/wmf_sink.egg-info/entry_points.txt
@@ -0,0 +1,2 @@
+[designate.notification.handler]
+wmf_sink = wmf_sink.wmfsink:NovaFixedWMFHandler
diff --git a/modules/openstack/files/mitaka/designate/nova_ldap/__init__.py 
b/modules/openstack/files/mitaka/designate/wmf_sink/__init__.py
similarity index 100%
rename from modules/openstack/files/mitaka/designate/nova_ldap/__init__.py
rename to modules/openstack/files/mitaka/designate/wmf_sink/__init__.py
diff --git a/modules/openstack/files/liberty/designate/nova_ldap/base.py 
b/modules/openstack/files/mitaka/designate/wmf_sink/base.py
similarity index 99%
copy from modules/openstack/files/liberty/designate/nova_ldap/base.py
copy to modules/openstack/files/mitaka/designate/wmf_sink/base.py
index e0db247..12cc9ba 100644
--- a/modules/openstack/files/liberty/designate/nova_ldap/base.py
+++ b/modules/openstack/files/mitaka/designate/wmf_sink/base.py
@@ -32,7 +32,7 @@
 central_api = central_rpcapi.CentralAPI()
 
 
-class BaseAddressLdapHandler(BaseAddressHandler):
+class BaseAddressWMFHandler(BaseAddressHandler):
     @staticmethod
     def _get_ip_data(addr_dict):
         ip = addr_dict['address']
diff --git a/modules/openstack/files/mitaka/designate/nova_ldap/novaldap.py 
b/modules/openstack/files/mitaka/designate/wmf_sink/novaldap.py
similarity index 100%
rename from modules/openstack/files/mitaka/designate/nova_ldap/novaldap.py
rename to modules/openstack/files/mitaka/designate/wmf_sink/novaldap.py
diff --git a/modules/openstack/files/liberty/designate/nova_ldap/novaldap.py 
b/modules/openstack/files/mitaka/designate/wmf_sink/wmfsink.py
similarity index 88%
copy from modules/openstack/files/liberty/designate/nova_ldap/novaldap.py
copy to modules/openstack/files/mitaka/designate/wmf_sink/wmfsink.py
index d4aea83..9b476e6 100644
--- a/modules/openstack/files/liberty/designate/nova_ldap/novaldap.py
+++ b/modules/openstack/files/mitaka/designate/wmf_sink/wmfsink.py
@@ -18,14 +18,14 @@
 #  in the designate source at designate/notification_handler/nova.py
 
 from oslo_config import cfg
-from nova_ldap.base import BaseAddressLdapHandler
+from wmf_sink.base import BaseAddressWMFHandler
 from oslo_log import log as logging
 
 LOG = logging.getLogger(__name__)
 
 cfg.CONF.register_group(cfg.OptGroup(
-    name='handler:nova_ldap',
-    title="Configuration for Nova Ldap Handler (WMF-specific transitional)"
+    name='handler:wmf_sink',
+    title="Configuration for WMF-specific event handling)"
 ))
 
 cfg.CONF.register_opts([
@@ -51,12 +51,12 @@
     cfg.StrOpt('keystone_auth_pass', default=None),
     cfg.StrOpt('keystone_auth_project', default=None),
     cfg.StrOpt('keystone_auth_url', default=None),
-], group='handler:nova_ldap')
+], group='handler:wmf_sink')
 
 
-class NovaFixedLdapHandler(BaseAddressLdapHandler):
+class NovaFixedWMFHandler(BaseAddressWMFHandler):
     """ Handler for Nova's notifications """
-    __plugin_name__ = 'nova_ldap'
+    __plugin_name__ = 'wmf_sink'
 
     def get_exchange_topics(self):
         exchange = cfg.CONF[self.name].control_exchange
@@ -72,7 +72,7 @@
         ]
 
     def process_notification(self, context, event_type, payload):
-        LOG.debug('NovaLdapHandler received notification - %s' % event_type)
+        LOG.debug('received notification - %s' % event_type)
 
         if event_type == 'compute.instance.create.end':
             self._create(payload['fixed_ips'], payload,
diff --git a/modules/openstack/manifests/designate/service.pp 
b/modules/openstack/manifests/designate/service.pp
index 3c80dce..e5fa5fe 100644
--- a/modules/openstack/manifests/designate/service.pp
+++ b/modules/openstack/manifests/designate/service.pp
@@ -30,15 +30,15 @@
         'python-novaclient'
     )
 
-    file { '/usr/lib/python2.7/dist-packages/nova_ldap':
-        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/nova_ldap",
+    file { '/usr/lib/python2.7/dist-packages/wmf_sink':
+        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/wmf_sink",
         owner   => 'root',
         group   => 'root',
         mode    => '0644',
         recurse => true,
     }
-    file { '/usr/lib/python2.7/dist-packages/nova_ldap.egg-info':
-        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/nova_ldap.egg-info",
+    file { '/usr/lib/python2.7/dist-packages/wmf_sink.egg-info':
+        source  => 
"puppet:///modules/openstack/${::openstack::version}/designate/wmf_sink.egg-info",
         owner   => 'root',
         group   => 'root',
         mode    => '0644',
diff --git a/modules/openstack/templates/liberty/designate/designate.conf.erb 
b/modules/openstack/templates/liberty/designate/designate.conf.erb
index 2cdd845..f9076f8 100644
--- a/modules/openstack/templates/liberty/designate/designate.conf.erb
+++ b/modules/openstack/templates/liberty/designate/designate.conf.erb
@@ -145,7 +145,7 @@
 # List of notification handlers to enable, configuration of these needs to
 # correspond to a [handler:my_driver] section below or else in the config
 # Can be one or more of : nova_fixed, neutron_floatingip
-enabled_notification_handlers = nova_fixed_multi, nova_ldap
+enabled_notification_handlers = nova_fixed_multi, wmf_sink
 
 #-----------------------
 # mDNS Service
@@ -297,9 +297,9 @@
 keystone_auth_url = "http://<%= @designateconfig['controller_hostname'] 
%>:35357/v3"
 
 #-----------------------
-# Nova Fixed Ldap Handler
+# WMF-specific handler for Fixed IPs
 #-----------------------
-[handler:nova_ldap]
+[handler:wmf_sink]
 # Domain ID of domain to create records in. For a pre-existing domain, in this 
case eqiad.wmflabs
 domain_id = '<%= @designateconfig["domain_id_internal_forward"] %>'
 notification_topics = monitor

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I3b179652f8659f2536e6d665113f8ac49f1f5cb7
Gerrit-PatchSet: 1
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Andrew Bogott <abog...@wikimedia.org>

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

Reply via email to