[PATCH 3/3] [PW3] Remove XML-RPC API

Stephen Finucane stephen at that.guru
Fri Oct 18 16:56:53 AEDT 2019


On Fri, 2019-10-18 at 13:41 +1100, Daniel Axtens wrote:
> It's been deprecated since 2.0 with the new REST API. That API is
> now pretty solid, and git-pw is good. Drop the old API.
> 
> Provide a page letting people know that the API is gone if they
> access any of the old pages.
> 
> This breaks pwclient, which only supports the old API. So we delete
> a few things that referred to it or used it, including some old tools.

I'm still not convinced that dropping this is something we _have_ to do
right now. I know we said we'd remove it in 3.0 but it's not hurting
anyone, as both the code and the docs are pretty well isolated, and
doesn't seem to impact our plans to drop Python 2.7 (which will start
hurting us as it ties us to Django 1.11). It should go at some point,
but, only once we're sure there's nothing significant talking to it.
With that said, if you really, really want to do this, I won't block
but we *have* to get pwclient migrated first. We simply can't drop this
until that happens because we're breaking workflows without replacement
(see comments below on removal of scripts from 'tools' for a taster).
I've said it below, but I'm confident I'll be able to get this done
well before we release 3.0 so this isn't a serious concern.

Lots of comments, mostly repeating what I've said here, below.

Stephen

> Signed-off-by: Daniel Axtens <dja at axtens.net>
> ---
>  docs/TODO                                     |   6 -
>  docs/api/index.rst                            |   5 +-
>  docs/api/rest/index.rst                       |   4 +-
>  docs/api/xmlrpc.rst                           |  64 --
>  docs/deployment/configuration.rst             |   4 -
>  docs/deployment/installation.rst              |  35 +-
>  docs/deployment/management.rst                |   6 +-
>  docs/development/api.rst                      |   5 +-
>  docs/index.rst                                |   1 -
>  docs/usage/clients.rst                        |  27 +-
>  patchwork/settings/base.py                    |   3 -
>  patchwork/settings/dev.py                     |   2 -
>  patchwork/templates/patchwork/about.html      |  10 -
>  patchwork/templates/patchwork/project.html    |  16 +-
>  patchwork/templates/patchwork/pwclientrc      |  15 -
>  .../templates/patchwork/xmlrpc-removed.html   |  16 +
>  patchwork/tests/test_about.py                 |  16 -
>  patchwork/tests/test_xmlrpc.py                | 224 -----
>  patchwork/urls.py                             |  20 +-
>  patchwork/views/about.py                      |   6 +-
>  patchwork/views/project.py                    |   2 -
>  patchwork/views/pwclient.py                   |  28 -
>  patchwork/views/removed.py                    |  11 +
>  patchwork/views/xmlrpc.py                     | 951 ------------------
>  .../notes/remove-xmlrpc-b6d26084338efcb4.yaml |  11 +
>  tools/patchwork-update-commits                |  20 -
>  tools/post-receive.hook                       |  86 --
>  27 files changed, 61 insertions(+), 1533 deletions(-)
>  delete mode 100644 docs/api/xmlrpc.rst
>  delete mode 100644 patchwork/templates/patchwork/pwclientrc
>  create mode 100644 patchwork/templates/patchwork/xmlrpc-removed.html
>  delete mode 100644 patchwork/tests/test_xmlrpc.py
>  delete mode 100644 patchwork/views/pwclient.py
>  create mode 100644 patchwork/views/removed.py
>  delete mode 100644 patchwork/views/xmlrpc.py
>  create mode 100644 releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml
>  delete mode 100755 tools/patchwork-update-commits
>  delete mode 100755 tools/post-receive.hook
> 
> diff --git docs/TODO docs/TODO
> index 37c30fd951dd..693e4bb743be 100644
> --- docs/TODO
> +++ docs/TODO
> @@ -7,10 +7,4 @@
>  * store rejected mails
>  * In-message From: header
>  * Per-user default filter settings
> -* pwclient: -b <bundle> to specify multiple patch ids
> -* pwclient: add bundle manipulation and retrieval ops
> -* pwclient: specify multiple patches by ID range
> -* pwclient: integrate hash parser
> -* pwclient: add example scripts (eg, git post-commit hook) to online help
> -* pwclient: case-insensitive searches for author and project name
>  * changing primary email addresses for accounts

It's busy work, but any chance you could file these as RFEs against
[1]?

[1] https://github.com/getpatchwork/pwclient

> diff --git docs/api/index.rst docs/api/index.rst
> index c679dae133bf..6e89b7197e39 100644
> --- docs/api/index.rst
> +++ docs/api/index.rst
> @@ -3,12 +3,9 @@
>  API Documentation
>  =================
>  
> -Patchwork provides two APIs: a REST API and a legacy XML-RPC API. The REST API
> -is recommended as the XML-RPC API is deprecated and will be removed in a future
> -release.
> +Patchwork provides a REST API.

Can we add a note here that Patchwork previously provided an XML-RPC
API? Documentation should remain available for the older versions but
it'd be nice for this stuff to not just disappear into a documentation
black hole.

>  
>  .. toctree::
>     :maxdepth: 2
>  
>     /api/rest/index
> -   /api/xmlrpc

Speaking of which, could we uplevel all this documentation now? Since
there's no longer two APIs, we could probably 'mv api/rest/* api/' and
just put redirects in place on readthedocs.

> diff --git docs/api/rest/index.rst docs/api/rest/index.rst
> index d1169e56e07a..293490b2874f 100644
> --- docs/api/rest/index.rst
> +++ docs/api/rest/index.rst
> @@ -25,8 +25,8 @@ If all you want is reference guides, skip straight to :ref:`rest-api-schemas`.
>  .. versionadded:: 2.0
>  
>     The REST API was introduced in Patchwork v2.0. Users of earlier Patchwork
> -   versions should instead refer to :doc:`XML-RPC API </api/xmlrpc>`
> -   documentation.
> +   versions should instead refer to XML-RPC API documentation provided with
> +   those versions.
>  
>  .. versionchanged:: 2.1
>  

If we do up-level stuff, you can obviously ignore my earlier comment
about calling out the removal of XML-RPC since this will become the
index page of '/api'

> diff --git docs/api/xmlrpc.rst docs/api/xmlrpc.rst
> deleted file mode 100644
> index 5412cce5d155..000000000000
> --- docs/api/xmlrpc.rst
> +++ /dev/null
> @@ -1,64 +0,0 @@
> -The XML-RPC API
> -===============
> -
> -Patchwork provides an XML-RPC API. This API can be used to be used to retrieve
> -and modify information about patches, projects and more.
> -
> -.. important::
> -
> -   The XML-RPC API can be enabled/disabled by the administrator: it may not be
> -   available in every instance. Refer to ``/about`` on your given instance for
> -   the status of the API, e.g.
> -
> -       https://patchwork.ozlabs.org/about
> -
> -   Alternatively, simply attempt to make a request to the API.
> -
> -.. deprecated:: 2.0
> -
> -    The XML-RPC API is a legacy API and has been deprecated in favour of the
> -    :doc:`REST API <rest/index>`. It will be removed in Patchwork 3.0.
> -
> -Getting Started
> ----------------
> -
> -The Patchwork XML-RPC API provides a number of "methods". Some methods require
> -authentication (via HTTP Basic Auth) while others do not. Authentication uses
> -your Patchwork account and the on-server documentation will indicate where it
> -is necessary. We will only cover the unauthenticated method here for brevity -
> -consult the `xmlrpclib`_ documentation for more detailed examples:
> -
> -To interact with the Patchwork XML-RPC API, a XML-RPC library should be used.
> -Python provides such a library - `xmlrpclib`_ - in its standard library. For
> -example, to get the version of the XML-RPC API for a Patchwork instance hosted
> -at `patchwork.example.com`, run:
> -
> -.. code-block:: pycon
> -
> -    $ python
> -    >>> import xmlrpclib  # or 'xmlrpc.client' for Python 3
> -    >>> rpc = xmlrpclib.ServerProxy('http://patchwork.example.com/xmlrpc/')
> -    >>> rpc.pw_rpc_version()
> -    1.1
> -
> -Once connected, the ``rpc`` object will be populated with a list of available
> -functions (or procedures, in RPC terminology). In the above example, we used
> -the ``pw_rpc_version`` method, however, it should be possible to use all the
> -methods listed in the server documentation.
> -
> -Further Information
> --------------------
> -
> -Patchwork provides automatically generated documentation for the XML-RPC API.
> -You can find this at the following URL:
> -
> -    https://patchwork.example.com/xmlrpc/
> -
> -where `patchwork.example.com` refers to the URL of your Patchwork instance.
> -
> -.. versionchanged:: 1.1
> -
> -   Automatic documentation generation for the Patchwork API was introduced in
> -   Patchwork v1.1. Prior versions of Patchwork do not offer this functionality.
> -
> -.. _xmlrpclib: https://docs.python.org/2/library/xmlrpclib.html
> diff --git docs/deployment/configuration.rst docs/deployment/configuration.rst
> index a71dd3f4bb57..482712f96773 100644
> --- docs/deployment/configuration.rst
> +++ docs/deployment/configuration.rst
> @@ -80,10 +80,6 @@ Enable the :doc:`REST API <../api/rest/index>`.
>  
>  .. versionadded:: 2.0
>  
> -``ENABLE_XMLRPC``
> -~~~~~~~~~~~~~~~~~
> -
> -Enable the :doc:`XML-RPC API <../api/xmlrpc>`.
>  
>  .. TODO(stephenfin) Deprecate this in favor of SECURE_SSL_REDIRECT
>  
> diff --git docs/deployment/installation.rst docs/deployment/installation.rst
> index f477a110f292..a505796e6381 100644
> --- docs/deployment/installation.rst
> +++ docs/deployment/installation.rst
> @@ -280,15 +280,8 @@ described in :doc:`configuration`.
>  * ``NOTIFICATION_FROM_EMAIL``
>  
>  These are not configurable using environment variables and must be configured
> -directly in the ``production.py`` settings file instead. For example, if you
> -wish to enable the XML-RPC API, you should add the following:
> -
> -.. code-block:: python
> -
> -   ENABLE_XMLRPC = True
> -
> -Similarly, should you wish to disable the REST API, you should add the
> -following:
> +directly in the ``production.py`` settings file instead. For example, should
> +you wish to disable the REST API, you should add the following:
>  
>  .. code-block:: python
>  
> @@ -506,8 +499,7 @@ doing the following:
>  
>  Once the administrative console is accessible, you would want to configure your
>  different sites and their corresponding domain names, which is required for the
> -different emails sent by Patchwork (registration, password recovery) as well as
> -the sample ``pwclientrc`` files provided by your project's page.
> +different emails sent by Patchwork (registration, password recovery).
>  
>  .. _deployment-parsemail:
>  
> @@ -646,27 +638,6 @@ You can also create such as service yourself using a PaaS provider that
>  supports incoming mail and writing a little web app.
>  
>  
> -.. _deployment-vcs:
> -
> -(Optional) Configure your VCS to Automatically Update Patches
> --------------------------------------------------------------
> -
> -The ``tools`` directory of the Patchwork distribution contains a file named
> -``post-receive.hook`` which is a sample Git hook that can be used to
> -automatically update patches to the *Accepted* state when corresponding commits
> -are pushed via Git.
> -
> -To install this hook, simply copy it to the ``.git/hooks`` directory on your
> -server, name it ``post-receive``, and make it executable.
> -
> -This sample hook has support to update patches to different states depending on
> -which branch is being pushed to. See the ``STATE_MAP`` setting in that file.
> -
> -If you are using a system other than Git, you can likely write a similar hook
> -using the :doc:`APIs </api/index>` or :doc:`API clients </usage/clients>` to to
> -update patch state. If you do write one, please contribute it.
> -
> -
>  .. _deployment-cron:
>  
>  (Optional) Configure the Patchwork Cron Job
> diff --git docs/deployment/management.rst docs/deployment/management.rst
> index 9c57f1962283..0881dc296cc9 100644
> --- docs/deployment/management.rst
> +++ docs/deployment/management.rst
> @@ -128,9 +128,9 @@ Update the hashes on existing patches.
>     ./manage.py rehash [<patch_id>, ...]
>  
>  Patchwork stores hashes for each patch it receives. These hashes can be used to
> -uniquely identify a patch for things like :ref:`automatically changing the
> -state of the patch in Patchwork when it merges <deployment-vcs>`. If you change
> -your hashing algorithm, you may wish to rehash the patches.
> +uniquely identify a patch for things like automatically changing the state of
> +the patch in Patchwork when it is merged. If you change your hashing algorithm,
> +you may wish to rehash the patches.
>  
>  .. option:: patch_id
>  
> diff --git docs/development/api.rst docs/development/api.rst
> index cea7bc78dd55..e5298e5e9d23 100644
> --- docs/development/api.rst
> +++ docs/development/api.rst
> @@ -3,9 +3,8 @@
>  Using the APIs
>  ==============
>  
> -Patchwork provides two APIs: the legacy :doc:`XML-RPC API </api/xmlrpc>` and
> -the :doc:`REST API </api/rest/index>`. You can use these APIs to interact with
> -Patchwork programmatically and to develop your own clients.
> +Patchwork provides a :doc:`REST API </api/rest/index>`. You can use this API
> +to interact with Patchwork programmatically and to develop your own clients.
>  
>  For quick usage examples of the APIs, refer to the :doc:`documentation
>  <../api/index>`. For examples of existing clients, refer to
> diff --git docs/index.rst docs/index.rst
> index b73c647c5222..1eff93d83080 100644
> --- docs/index.rst
> +++ docs/index.rst
> @@ -51,7 +51,6 @@ of community projects.
>     :caption: API Documentation
>  
>     api/rest/index
> -   api/xmlrpc
>  
>  .. toctree::
>     :maxdepth: 2
> diff --git docs/usage/clients.rst docs/usage/clients.rst
> index 01dd62a28e50..40b93f27b344 100644
> --- docs/usage/clients.rst
> +++ docs/usage/clients.rst
> @@ -1,31 +1,8 @@
>  Clients
>  =======
>  
> -A number of clients are available for interacting with Patchwork's various
> -APIs.
> -
> -
> -pwclient
> ---------
> -
> -.. versionchanged:: 2.2
> -
> -   :program:`pwclient` was previously provided with Patchwork. It has been
> -   packaged as a separate application since Patchwork v2.2.0.
> -
> -The :program:`pwclient` application can be used to interact with Patchwork from
> -the command line. Functionality provided by :program:`pwclient` includes:
> -
> -- Listing patches, projects, and checks
> -- Downloading and applying patches to a local code base
> -- Modifying the status of patches
> -- Creating new checks
> -
> -More information on :program:`pwclient`, including installation and usage
> -instructions, can be found in the `documentation`__ and the `GitHub repo`__.
> -
> -__ https://pwclient.readthedocs.io/
> -__ https://github.com/getpatchwork/pwclient/

As above, I'd *much* rather we fixed this than drop it entirely since
there are users of this in the wild that we're going to break with the
switchover, and if they haven't read the Patchwork documentation (why
would they, if they're just consuming stuff via pwclient) it's unlikely
they'll know that things have changed underfoot until we take this away
without an alternative. As a concrete example, I know DPDK are using
pwclient for some CI tooling they have since git-pw doesn't (by design)
exposes the checks framework.

Could we consider this a blocker to dropping the XML-RPC API? I have a
lot of rework done on pwclient lately (check out the git repo) so I
should be able to get the REST API side of this closed out well before
we'd be removing 3.0.

> +A REST client is available for interacting with Patchwork's API, and other
> +projects build on top of the API to provide other functionality.
>  
>  
>  git-pw
> diff --git patchwork/settings/base.py patchwork/settings/base.py
> index b86cdc276d5a..fbb0b0d8565b 100644
> --- patchwork/settings/base.py
> +++ patchwork/settings/base.py
> @@ -211,9 +211,6 @@ NOTIFICATION_DELAY_MINUTES = 10
>  
>  NOTIFICATION_FROM_EMAIL = DEFAULT_FROM_EMAIL
>  
> -# Set to True to enable the Patchwork XML-RPC interface
> -ENABLE_XMLRPC = False
> -
>  # Set to True to enable the Patchwork REST API
>  ENABLE_REST_API = True
>  
> diff --git patchwork/settings/dev.py patchwork/settings/dev.py
> index e110e74579ca..141915fdd001 100644
> --- patchwork/settings/dev.py
> +++ patchwork/settings/dev.py
> @@ -99,6 +99,4 @@ if dbbackup:
>  # Patchwork settings
>  #
>  
> -ENABLE_XMLRPC = True
> -
>  ENABLE_REST_API = True
> diff --git patchwork/templates/patchwork/about.html patchwork/templates/patchwork/about.html
> index 210e9513c4f4..9c83ba0e4173 100644
> --- patchwork/templates/patchwork/about.html
> +++ patchwork/templates/patchwork/about.html
> @@ -56,16 +56,6 @@
>          <span class="label label-warning pull-right">disabled</span>
>          {% endif %}
>        </li>
> -      <li class="list-group-item">
> -        XML-RPC
> -        <span class="glyphicon glyphicon-question-sign" title="The XML-RPC
> -          API"></span>
> -        {% if enabled_apis.xmlrpc %}
> -        <span class="label label-success pull-right">enabled</span>
> -        {% else %}
> -        <span class="label label-warning pull-right">disabled</span>
> -        {% endif %}
> -      </li>

I wonder if we want to keep this but mark it always disabled? Probably
not that useful, I guess.

>      </ul>
>    </div>
>  </div>
> diff --git patchwork/templates/patchwork/project.html patchwork/templates/patchwork/project.html
> index bd9d20e263d8..ce647b9df04b 100644
> --- patchwork/templates/patchwork/project.html
> +++ patchwork/templates/patchwork/project.html
> @@ -55,19 +55,5 @@
>  {% endif %}
>  </table>
>  
> -{% if enable_xmlrpc %}
> -<h2>pwclient</h2>
> -
> -<p><code>pwclient</code> is the command-line client for Patchwork. Currently,
> -it provides access to some read-only features of Patchwork, such as downloading
> -and applying patches.</p>
> -
> -<p>To use pwclient, you will need:</p>
> -<ul>
> - <li>The <a href="https://github.com/getpatchwork/pwclient">pwclient</a> program</li>
> - <li>(optional) A <code><a href="{% url 'pwclientrc' project.linkname %}"
> - >.pwclientrc</a></code> file for this project, which should be stored in your
> - home directory.</li>
> -</ul>
> -{% endif %}
> +<!-- TODO add git pw info -->

Just link to the documentation we have on clients? Also, consider
adding a comment in said document stating that if we move the document
we should update this link.

>  {% endblock %}
> diff --git patchwork/templates/patchwork/pwclientrc patchwork/templates/patchwork/pwclientrc
> deleted file mode 100644
> index 7d466d890da5..000000000000
> --- patchwork/templates/patchwork/pwclientrc
> +++ /dev/null
> @@ -1,15 +0,0 @@
> -# Sample .pwclientrc file for the {{ project.linkname }} project,
> -# running on {{ site.domain }}.
> -#
> -# Just append this file to your existing ~/.pwclientrc
> -# If you do not already have a ~/.pwclientrc, then copy this file to
> -# ~/.pwclientrc, and uncomment the following two lines:
> -# [options]
> -# default={{ project.linkname }}
> -
> -[{{ project.linkname }}]
> -url = {{ scheme }}://{{ site.domain }}{% url 'xmlrpc' %}
> -{% if user.is_authenticated %}
> -username = {{ user.username }}
> -password = <add your patchwork password here>
> -{% endif %}
> diff --git patchwork/templates/patchwork/xmlrpc-removed.html patchwork/templates/patchwork/xmlrpc-removed.html

Consider simply calling this 'xmlrpc'. The removed part feels
unnecessary.

> new file mode 100644
> index 000000000000..bc2e41ebf313
> --- /dev/null
> +++ patchwork/templates/patchwork/xmlrpc-removed.html
> @@ -0,0 +1,16 @@
> +{% extends "base.html" %}
> +
> +{% block title %}XML-RPC API removed{% endblock %}
> +{% block heading %}XML-RPC API removed{% endblock %}
> +
> +{% block body %}
> +<div class="container">
> +  <h1>XML-RPC API removed</h1>
> +
> +  <p>The XML-RPC API has been removed in Patchwork 3 in favour of the REST API.</p>
> +
> +  <p>If you were using pwclient, try
> +    <a href="https://github.com/getpatchwork/git-pw" target="_blank">git-pw</a>.
> +  </p>
> +</div>
> +{% endblock %}
> diff --git patchwork/tests/test_about.py patchwork/tests/test_about.py
> index d8c35b9f3e5f..2d9385d8ea8b 100644
> --- patchwork/tests/test_about.py
> +++ patchwork/tests/test_about.py
> @@ -15,7 +15,6 @@ class AboutViewTest(TestCase):
>      def _test_redirect(self, view):
>          requested_url = reverse(view)
>          redirect_url = reverse('about')
> -

unrelated change?

>          response = self.client.get(requested_url)
>          self.assertRedirects(response, redirect_url, 301)
>  
> @@ -23,21 +22,6 @@ class AboutViewTest(TestCase):
>          for view in ['help', 'help-about']:
>              self._test_redirect(view)
>  
> -    @unittest.skipUnless(settings.ENABLE_XMLRPC,
> -                         'requires xmlrpc interface (use the ENABLE_XMLRPC '
> -                         'setting)')
> -    def test_redirects_xmlrpc(self):
> -        self._test_redirect('help-pwclient')
> -
> -    def test_xmlrpc(self):
> -        with self.settings(ENABLE_XMLRPC=False):
> -            response = self.client.get(reverse('about'))
> -            self.assertFalse(response.context['enabled_apis']['xmlrpc'])
> -
> -        with self.settings(ENABLE_XMLRPC=True):
> -            response = self.client.get(reverse('about'))
> -            self.assertTrue(response.context['enabled_apis']['xmlrpc'])
> -
>      def test_rest(self):
>          with self.settings(ENABLE_REST_API=False):
>              response = self.client.get(reverse('about'))
> diff --git patchwork/tests/test_xmlrpc.py patchwork/tests/test_xmlrpc.py
> deleted file mode 100644
> index 79c6c848a0c2..000000000000
> --- patchwork/tests/test_xmlrpc.py
> +++ /dev/null

In case I don't see it below, shouldn't this test the 'xmlrpc-
removed.html' (or 'xmlrpc.html') template gets rendered wherever it's
supposed to be?

> @@ -1,224 +0,0 @@
> -# Patchwork - automated patch tracking system
> -# Copyright (C) 2014 Jeremy Kerr <jk at ozlabs.org>
> -#
> -# SPDX-License-Identifier: GPL-2.0-or-later
> -
> -import unittest
> -
> -from django.conf import settings
> -from django.test import LiveServerTestCase
> -from django.urls import reverse
> -from django.utils.six.moves import xmlrpc_client
> -
> -from patchwork.tests import utils
> -
> -
> - at unittest.skipUnless(settings.ENABLE_XMLRPC,
> -                     'requires xmlrpc interface (use the ENABLE_XMLRPC '
> -                     'setting)')
> -class XMLRPCTest(LiveServerTestCase):
> -
> -    def setUp(self):
> -        self.url = self.live_server_url + reverse('xmlrpc')
> -        self.rpc = xmlrpc_client.Server(self.url)
> -
> -
> -class XMLRPCGenericTest(XMLRPCTest):
> -
> -    def test_pw_rpc_version(self):
> -        # If you update the RPC version, update the tests!
> -        self.assertEqual(self.rpc.pw_rpc_version(), [1, 3, 0])
> -
> -    def test_get_redirect(self):
> -        response = self.client.patch(self.url)
> -        self.assertRedirects(response, reverse('project-list'))
> -
> -    def test_invalid_method(self):
> -        with self.assertRaises(xmlrpc_client.Fault):
> -            self.rpc.xyzzy()
> -that d
> -    def test_absent_auth(self):
> -        with self.assertRaises(xmlrpc_client.Fault):
> -            self.rpc.patch_set(0, {})
> -
> -
> - at unittest.skipUnless(settings.ENABLE_XMLRPC,
> -                     'requires xmlrpc interface (use the ENABLE_XMLRPC '
> -                     'setting)')
> -class XMLRPCAuthenticatedTest(LiveServerTestCase):
> -
> -    def setUp(self):
> -        self.url = self.live_server_url + reverse('xmlrpc')
> -        # url is of the form http://localhost:PORT/PATH
> -        # strip the http and replace it with the username/passwd of a user.
> -        self.project = utils.create_project()
> -        self.user = utils.create_maintainer(self.project)
> -        self.url = ('http://%s:%s@' + self.url[7:]) % (self.user.username,
> -                                                       self.user.username)
> -        self.rpc = xmlrpc_client.Server(self.url)
> -
> -    def test_patch_set(self):
> -        patch = utils.create_patch(project=self.project)
> -        result = self.rpc.patch_get(patch.id)
> -        self.assertFalse(result['archived'])
> -
> -        self.rpc.patch_set(patch.id, {'archived': True})
> -
> -        # reload the patch
> -        result = self.rpc.patch_get(patch.id)
> -        self.assertTrue(result['archived'])
> -
> -
> -class XMLRPCModelTestMixin(object):
> -
> -    def create_multiple(self, count):
> -        return [self.create_single() for i in range(count)]
> -
> -    def test_get_none(self):
> -        self.assertEqual(self.get_endpoint(0), {})
> -
> -    def test_list_none(self):
> -        self.assertEqual(self.list_endpoint(), [])
> -
> -    def test_list_single(self):
> -        obj = self.create_single()
> -        result = self.list_endpoint()
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], obj.id)
> -
> -    def test_list_named(self):
> -        obj = self.create_single(name='FOOBARBAZ')
> -        self.create_multiple(5)
> -        result = self.list_endpoint('oobarb')
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], obj.id)
> -
> -    def test_list_named_none(self):
> -        self.create_multiple(5)
> -        result = self.list_endpoint('invisible')
> -        self.assertEqual(len(result), 0)
> -
> -    def test_get_single(self):
> -        obj = self.create_single()
> -        result = self.get_endpoint(obj.id)
> -        self.assertEqual(result['id'], obj.id)
> -
> -    def test_get_invalid(self):
> -        obj = self.create_single()
> -        result = self.get_endpoint(obj.id + 1)
> -        self.assertEqual(result, {})
> -
> -    def test_list_multiple(self):
> -        self.create_multiple(5)
> -        result = self.list_endpoint()
> -        self.assertEqual(len(result), 5)
> -
> -    def test_list_max_count(self):
> -        objs = self.create_multiple(5)
> -        result = self.list_endpoint("", 2)
> -        self.assertEqual(len(result), 2)
> -        self.assertEqual(result[0]['id'], objs[0].id)
> -
> -    def test_list_negative_max_count(self):
> -        objs = self.create_multiple(5)
> -        result = self.list_endpoint("", -1)
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], objs[-1].id)
> -
> -
> -class XMLRPCFilterModelTestMixin(XMLRPCModelTestMixin):
> -
> -    # override these tests due to the way you pass in filters
> -    def test_list_max_count(self):
> -        objs = self.create_multiple(5)
> -        result = self.list_endpoint({'max_count': 2})
> -        self.assertEqual(len(result), 2)
> -        self.assertEqual(result[0]['id'], objs[0].id)
> -
> -    def test_list_negative_max_count(self):
> -        objs = self.create_multiple(5)
> -        result = self.list_endpoint({'max_count': -1})
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], objs[-1].id)
> -
> -    def test_list_named(self):
> -        obj = self.create_single(name='FOOBARBAZ')
> -        self.create_multiple(5)
> -        result = self.list_endpoint({'name__icontains': 'oobarb'})
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], obj.id)
> -
> -    def test_list_named_none(self):
> -        self.create_multiple(5)
> -        result = self.list_endpoint({'name__icontains': 'invisible'})
> -        self.assertEqual(len(result), 0)
> -
> -
> -class XMLRPCPatchTest(XMLRPCTest, XMLRPCFilterModelTestMixin):
> -    def setUp(self):
> -        super(XMLRPCPatchTest, self).setUp()
> -        self.get_endpoint = self.rpc.patch_get
> -        self.list_endpoint = self.rpc.patch_list
> -        self.create_multiple = utils.create_patches
> -
> -    def create_single(self, **kwargs):
> -        return utils.create_patches(**kwargs)[0]
> -
> -    def test_patch_check_get(self):
> -        patch = self.create_single()
> -        check = utils.create_check(patch=patch)
> -        result = self.rpc.patch_check_get(patch.id)
> -        self.assertEqual(result['total'], 1)
> -        self.assertEqual(result['checks'][0]['id'], check.id)
> -        self.assertEqual(result['checks'][0]['patch_id'], patch.id)
> -
> -    def test_patch_get_by_hash(self):
> -        patch = self.create_single()
> -        result = self.rpc.patch_get_by_hash(patch.hash)
> -        self.assertEqual(result['id'], patch.id)
> -
> -
> -class XMLRPCPersonTest(XMLRPCTest, XMLRPCModelTestMixin):
> -
> -    def setUp(self):
> -        super(XMLRPCPersonTest, self).setUp()
> -        self.get_endpoint = self.rpc.person_get
> -        self.list_endpoint = self.rpc.person_list
> -        self.create_single = utils.create_person
> -
> -
> -class XMLRPCProjectTest(XMLRPCTest, XMLRPCModelTestMixin):
> -
> -    def setUp(self):
> -        super(XMLRPCProjectTest, self).setUp()
> -        self.get_endpoint = self.rpc.project_get
> -        self.list_endpoint = self.rpc.project_list
> -        self.create_single = utils.create_project
> -
> -    def test_list_named(self):
> -        # project filters by linkname, not name!
> -        obj = self.create_single(linkname='FOOBARBAZ')
> -        result = self.list_endpoint('oobarb')
> -        self.assertEqual(len(result), 1)
> -        self.assertEqual(result[0]['id'], obj.id)
> -
> -
> -class XMLRPCStateTest(XMLRPCTest, XMLRPCModelTestMixin):
> -
> -    def setUp(self):
> -        super(XMLRPCStateTest, self).setUp()
> -        self.get_endpoint = self.rpc.state_get
> -        self.list_endpoint = self.rpc.state_list
> -        self.create_single = utils.create_state
> -
> -
> -class XMLRPCCheckTest(XMLRPCTest, XMLRPCFilterModelTestMixin):
> -
> -    def setUp(self):
> -        super(XMLRPCCheckTest, self).setUp()
> -        self.get_endpoint = self.rpc.check_get
> -        self.list_endpoint = self.rpc.check_list
> -        self.create_single = utils.create_check
> -
> -    def test_list_named(self):
> -        pass
> diff --git patchwork/urls.py patchwork/urls.py
> index dcdcfb49e67e..2f2b448da331 100644
> --- patchwork/urls.py
> +++ patchwork/urls.py
> @@ -18,10 +18,9 @@ from patchwork.views import mail as mail_views
>  from patchwork.views import notification as notification_views
>  from patchwork.views import patch as patch_views
>  from patchwork.views import project as project_views
> -from patchwork.views import pwclient as pwclient_views
>  from patchwork.views import series as series_views
>  from patchwork.views import user as user_views
> -from patchwork.views import xmlrpc as xmlrpc_views
> +from patchwork.views import removed as removed_views
>  
>  
>  admin.autodiscover()
> @@ -163,16 +162,6 @@ if 'debug_toolbar' in settings.INSTALLED_APPS:
>          url(r'^__debug__/', include(debug_toolbar.urls)),
>      ]
>  
> -if settings.ENABLE_XMLRPC:
> -    urlpatterns += [
> -        url(r'xmlrpc/$', xmlrpc_views.xmlrpc, name='xmlrpc'),
> -        url(r'^project/(?P<project_id>[^/]+)/pwclientrc/$',
> -            pwclient_views.pwclientrc,
> -            name='pwclientrc'),
> -        # legacy redirect
> -        url(r'^help/pwclient/$', about_views.redirect, name='help-pwclient'),
> -    ]
> -
>  if settings.ENABLE_REST_API:
>      if 'rest_framework' not in settings.INSTALLED_APPS:
>          raise RuntimeError(
> @@ -276,3 +265,10 @@ if settings.COMPAT_REDIR:
>              bundle_views.bundle_mbox_redir,
>              name='bundle-mbox-redir'),
>      ]
> +
> +    urlpatterns += [
> +        url(r'xmlrpc/$', removed_views.xmlrpc_removed),
> +        url(r'^project/(?P<project_id>[^/]+)/pwclientrc/$',
> +            removed_views.xmlrpc_removed),
> +        url(r'^help/pwclient/$', removed_views.xmlrpc_removed),
> +    ]
> diff --git patchwork/views/about.py patchwork/views/about.py
> index 91c3b74ebf8f..0a6f75d3b218 100644
> --- patchwork/views/about.py
> +++ patchwork/views/about.py
> @@ -14,7 +14,6 @@ def about(request):
>      context = {
>          'enabled_apis': {
>              'rest': settings.ENABLE_REST_API,
> -            'xmlrpc': settings.ENABLE_XMLRPC,
>          },
>          'admins': () if settings.ADMINS_HIDE else settings.ADMINS,
>      }
> @@ -23,8 +22,5 @@ def about(request):
>  
>  
>  def redirect(request):
> -    """Redirect for legacy URLs.
> -
> -    Remove this when Patchwork 3.0 is released.
> -    """
> +    """Redirect for legacy URLs."""
>      return HttpResponsePermanentRedirect(reverse('about'))
> diff --git patchwork/views/project.py patchwork/views/project.py
> index 8fa41794f5db..621457d5b732 100644
> --- patchwork/views/project.py
> +++ patchwork/views/project.py
> @@ -3,7 +3,6 @@
>  #
>  # SPDX-License-Identifier: GPL-2.0-or-later
>  
> -from django.conf import settings
>  from django.contrib.auth.models import User
>  from django.http import HttpResponseRedirect
>  from django.shortcuts import get_object_or_404
> @@ -59,6 +58,5 @@ def project_detail(request, project_id):
>              profile__maintainer_projects=project).select_related('profile'),
>          'n_patches': n_patches[False] if False in n_patches else 0,
>          'n_archived_patches': n_patches[True] if True in n_patches else 0,
> -        'enable_xmlrpc': settings.ENABLE_XMLRPC,
>      }
>      return render(request, 'patchwork/project.html', context)
> diff --git patchwork/views/pwclient.py patchwork/views/pwclient.py
> deleted file mode 100644
> index 72ebcbbb9e90..000000000000
> --- patchwork/views/pwclient.py
> +++ /dev/null
> @@ -1,28 +0,0 @@
> -# Patchwork - automated patch tracking system
> -# Copyright (C) 2008 Jeremy Kerr <jk at ozlabs.org>
> -#
> -# SPDX-License-Identifier: GPL-2.0-or-later
> -
> -from django.conf import settings
> -from django.shortcuts import get_object_or_404
> -from django.shortcuts import render
> -
> -from patchwork.models import Project
> -
> -
> -def pwclientrc(request, project_id):
> -    project = get_object_or_404(Project, linkname=project_id)
> -
> -    context = {
> -        'project': project,
> -    }
> -    if settings.FORCE_HTTPS_LINKS or request.is_secure():
> -        context['scheme'] = 'https'
> -    else:
> -        context['scheme'] = 'http'
> -
> -    response = render(request, 'patchwork/pwclientrc', context,
> -                      content_type='text/plain')
> -    response['Content-Disposition'] = 'attachment; filename=.pwclientrc'
> -
> -    return response
> diff --git patchwork/views/removed.py patchwork/views/removed.py
> new file mode 100644
> index 000000000000..7db3c0e3b8d0
> --- /dev/null
> +++ patchwork/views/removed.py
> @@ -0,0 +1,11 @@
> +# Patchwork - automated patch tracking system
> +# Copyright (C) 2019 IBM Corporation
> +# Author: Daniel Axtens <dja at axtens.net>
> +#
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +from django.shortcuts import render
> +
> +
> +def xmlrpc_removed(request, project_id=None):
> +    return render(request, 'patchwork/xmlrpc-removed.html', {}, status=410)
> diff --git patchwork/views/xmlrpc.py patchwork/views/xmlrpc.py
> deleted file mode 100644
> index f60725044ebe..000000000000
> --- patchwork/views/xmlrpc.py
> +++ /dev/null
> @@ -1,951 +0,0 @@
> -# Patchwork - automated patch tracking system
> -# Copyright (C) 2008 Jeremy Kerr <jk at ozlabs.org>
> -#
> -# SPDX-License-Identifier: GPL-2.0-or-later
> -
> -import base64
> -# NOTE(stephenfin) six does not seem to support this
> -try:
> -    from DocXMLRPCServer import XMLRPCDocGenerator
> -except ImportError:
> -    from xmlrpc.server import XMLRPCDocGenerator
> -import sys
> -
> -from django.contrib.auth import authenticate
> -from django.http import HttpResponse
> -from django.http import HttpResponseRedirect
> -from django.http import HttpResponseServerError
> -from django.views.decorators.csrf import csrf_exempt
> -from django.urls import reverse
> -from django.utils import six
> -from django.utils.six.moves import xmlrpc_client
> -from django.utils.six.moves.xmlrpc_server import SimpleXMLRPCDispatcher
> -
> -from patchwork.models import Check
> -from patchwork.models import Patch
> -from patchwork.models import Person
> -from patchwork.models import Project
> -from patchwork.models import State
> -from patchwork.views.utils import patch_to_mbox
> -
> -
> -class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher,
> -                                XMLRPCDocGenerator):
> -
> -    server_name = 'Patchwork XML-RPC API'
> -    server_title = 'Patchwork XML-RPC API v1 Documentation'
> -
> -    def __init__(self):
> -        SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
> -                                        encoding=None)
> -        XMLRPCDocGenerator.__init__(self)
> -
> -        def _dumps(obj, *args, **kwargs):
> -            kwargs['allow_none'] = self.allow_none
> -            kwargs['encoding'] = self.encoding
> -            return xmlrpc_client.dumps(obj, *args, **kwargs)
> -
> -        self.dumps = _dumps
> -
> -        # map of name => (auth, func)
> -        self.func_map = {}
> -
> -    def register_function(self, fn, auth_required):
> -        self.funcs[fn.__name__] = fn  # needed by superclass methods
> -        self.func_map[fn.__name__] = (auth_required, fn)
> -
> -    def _user_for_request(self, request):
> -        auth_header = None
> -
> -        if 'HTTP_AUTHORIZATION' in request.META:
> -            auth_header = request.META.get('HTTP_AUTHORIZATION')
> -        elif 'Authorization' in request.META:
> -            auth_header = request.META.get('Authorization')
> -
> -        if auth_header is None or auth_header == '':
> -            raise Exception('No authentication credentials given')
> -
> -        header = auth_header.strip()
> -
> -        if not header.startswith('Basic '):
> -            raise Exception('Authentication scheme not supported')
> -
> -        header = header[len('Basic '):].strip()
> -
> -        try:
> -            decoded = base64.b64decode(header.encode('ascii')).decode('ascii')
> -            username, password = decoded.split(':', 1)
> -        except ValueError:
> -            raise Exception('Invalid authentication credentials')
> -
> -        return authenticate(username=username, password=password)
> -
> -    def _dispatch(self, request, method, params):
> -        if method not in list(self.func_map.keys()):
> -            raise Exception('method "%s" is not supported' % method)
> -
> -        auth_required, fn = self.func_map[method]
> -
> -        if auth_required:
> -            user = self._user_for_request(request)
> -            if not user:
> -                raise Exception('Invalid username/password')
> -
> -            params = (user,) + params
> -
> -        return fn(*params)
> -
> -    def _marshaled_dispatch(self, request):
> -        try:
> -            params, method = six.moves.xmlrpc_client.loads(request.body)
> -
> -            response = self._dispatch(request, method, params)
> -            # wrap response in a singleton tuple
> -            response = (response,)
> -            response = self.dumps(response, methodresponse=1)
> -        except six.moves.xmlrpc_client.Fault as fault:
> -            response = self.dumps(fault)
> -        except Exception:  # noqa
> -            # report exception back to server
> -            response = self.dumps(
> -                six.moves.xmlrpc_client.Fault(
> -                    1, '%s:%s' % (sys.exc_info()[0], sys.exc_info()[1])),
> -            )
> -
> -        return response
> -
> -
> -dispatcher = PatchworkXMLRPCDispatcher()
> -
> -# XMLRPC view function
> -
> -
> - at csrf_exempt
> -def xmlrpc(request):
> -    if request.method not in ['POST', 'GET']:
> -        return HttpResponseRedirect(reverse('project-list'))
> -
> -    response = HttpResponse()
> -
> -    if request.method == 'POST':
> -        try:
> -            ret = dispatcher._marshaled_dispatch(request)
> -        except Exception:  # noqa
> -            return HttpResponseServerError()
> -    else:
> -        ret = dispatcher.generate_html_documentation()
> -
> -    response.write(ret)
> -
> -    return response
> -
> -# decorator for XMLRPC methods. Setting login_required to true will call
> -# the decorated function with a non-optional user as the first argument.
> -
> -
> -def xmlrpc_method(login_required=False):
> -    def wrap(f):
> -        dispatcher.register_function(f, login_required)
> -        return f
> -
> -    return wrap
> -
> -
> -# We allow most of the Django field lookup types for remote queries
> -LOOKUP_TYPES = ['iexact', 'contains', 'icontains', 'gt', 'gte', 'lt',
> -                'in', 'startswith', 'istartswith', 'endswith',
> -                'iendswith', 'range', 'year', 'month', 'day', 'isnull']
> -
> -
> -#######################################################################
> -# Helper functions
> -#######################################################################
> -
> -def project_to_dict(obj):
> -    """Serialize a project object.
> -
> -    Return a trimmed down dictionary representation of a Project
> -    object which is safe to send to the client. For example:
> -
> -    {
> -        'id': 1,
> -        'linkname': 'my-project',
> -        'name': 'My Project',
> -    }
> -
> -    Args:
> -        Project object to serialize.
> -
> -    Returns:
> -        Serialized Project object.
> -    """
> -    return {
> -        'id': obj.id,
> -        'linkname': obj.linkname,
> -        'name': obj.name,
> -    }
> -
> -
> -def person_to_dict(obj):
> -    """Serialize a person object.
> -
> -    Return a trimmed down dictionary representation of a Person
> -    object which is safe to send to the client. For example:
> -
> -    {
> -        'id': 1,
> -        'email': 'joe.bloggs at example.com',
> -        'name': 'Joe Bloggs',
> -        'user': None,
> -    }
> -
> -    Args:
> -        Person object to serialize.
> -
> -    Returns:
> -        Serialized Person object.
> -    """
> -
> -    # Make sure we don't return None even if the user submitted a patch
> -    # with no real name.  XMLRPC can't marshall None.
> -    if obj.name is not None:
> -        name = obj.name
> -    else:
> -        name = obj.email
> -
> -    return {
> -        'id': obj.id,
> -        'email': obj.email,
> -        'name': name,
> -        'user': six.text_type(obj.user).encode('utf-8'),
> -    }
> -
> -
> -def patch_to_dict(obj):
> -    """Serialize a patch object.
> -
> -    Return a trimmed down dictionary representation of a Patch
> -    object which is safe to send to the client. For example:
> -
> -    {
> -        'id': 1
> -        'date': '2000-12-31 00:11:22',
> -        'filename': 'Fix-all-the-bugs.patch',
> -        'msgid': '<BLU438-SMTP36690BBDD2CE71A7138B082511A at phx.gbl>',
> -        'name': "Fix all the bugs",
> -        'project': 'my-project',
> -        'project_id': 1,
> -        'state': 'New',
> -        'state_id': 1,
> -        'archived': False,
> -        'submitter': 'Joe Bloggs <joe.bloggs at example.com>',
> -        'submitter_id': 1,
> -        'delegate': 'admin',
> -        'delegate_id': 1,
> -        'commit_ref': '',
> -        'hash': '',
> -    }
> -
> -    Args:
> -        Patch object to serialize.
> -
> -    Returns:
> -        Serialized Patch object.
> -    """
> -    return {
> -        'id': obj.id,
> -        'date': six.text_type(obj.date).encode('utf-8'),
> -        'filename': obj.filename,
> -        'msgid': obj.msgid,
> -        'name': obj.name,
> -        'project': six.text_type(obj.project).encode('utf-8'),
> -        'project_id': obj.project_id,
> -        'state': six.text_type(obj.state).encode('utf-8'),
> -        'state_id': obj.state_id,
> -        'archived': obj.archived,
> -        'submitter': six.text_type(obj.submitter).encode('utf-8'),
> -        'submitter_id': obj.submitter_id,
> -        'delegate': six.text_type(obj.delegate).encode('utf-8'),
> -        'delegate_id': obj.delegate_id or 0,
> -        'commit_ref': obj.commit_ref or '',
> -        'hash': obj.hash or '',
> -    }
> -
> -
> -def state_to_dict(obj):
> -    """Serialize a state object.
> -
> -    Return a trimmed down dictionary representation of a State
> -    object which is safe to send to the client. For example:
> -
> -    {
> -        'id': 1,
> -        'name': 'New',
> -    }
> -
> -    Args:
> -        State object to serialize.
> -
> -    Returns:
> -        Serialized State object.
> -    """
> -    return {
> -        'id': obj.id,
> -        'name': obj.name,
> -    }
> -
> -
> -def check_to_dict(obj):
> -    """Return a trimmed down dictionary representation of a Check
> -    object which is OK to send to the client."""
> -    return {
> -        'id': obj.id,
> -        'date': six.text_type(obj.date).encode('utf-8'),
> -        'patch': six.text_type(obj.patch).encode('utf-8'),
> -        'patch_id': obj.patch_id,
> -        'user': six.text_type(obj.user).encode('utf-8'),
> -        'user_id': obj.user_id,
> -        'state': obj.get_state_display(),
> -        'target_url': obj.target_url,
> -        'description': obj.description,
> -        'context': obj.context,
> -    }
> -
> -
> -def patch_check_to_dict(obj):
> -    """Return a combined patch check."""
> -    return {
> -        'state': obj.combined_check_state,
> -        'total': len(obj.checks),
> -        'checks': [check_to_dict(check) for check in obj.checks]
> -    }
> -
> -
> -#######################################################################
> -# Public XML-RPC methods
> -#######################################################################
> -
> -def _get_objects(serializer, objects, max_count):
> -    if max_count > 0:
> -        return [serializer(x) for x in objects[:max_count]]
> -    elif max_count < 0:
> -        min_count = objects.count() + max_count
> -        return [serializer(x) for x in objects[min_count:]]
> -    else:
> -        return [serializer(x) for x in objects]
> -
> -
> - at xmlrpc_method()
> -def pw_rpc_version():
> -    """Return Patchwork XML-RPC interface version.
> -
> -    The API is versioned separately from patchwork itself. The API
> -    version only changes when the API itself changes. As these changes
> -    can include the removal or modification of methods, it is highly
> -    recommended that one first test the API version for compatibility
> -    before making method calls.
> -
> -    History:
> -
> -        1.0.0: Patchwork 1.0 release
> -        1.1.0: ???
> -        1.2.0: ???
> -        1.3.0: Add support for negative indexing of Checks
> -
> -    Returns:
> -        Version of the API.
> -    """
> -    return (1, 3, 0)
> -
> -
> - at xmlrpc_method()
> -def project_list(search_str=None, max_count=0):
> -    """List projects matching a given linkname filter.
> -
> -    Filter projects by linkname. Projects are compared to the search
> -    string via a case-insensitive containment test, a.k.a. a partial
> -    match.
> -
> -    Args:
> -        search_str: The string to compare project names against. If
> -            blank, all projects will be returned.
> -        max_count (int): The maximum number of projects to return.
> -
> -    Returns:
> -        A serialized list of projects matching filter, if any. A list
> -        of all projects if no filter given.
> -    """
> -    if search_str:
> -        projects = Project.objects.filter(linkname__icontains=search_str)
> -    else:
> -        projects = Project.objects.all()
> -
> -    return _get_objects(project_to_dict, projects, max_count)
> -
> -
> - at xmlrpc_method()
> -def project_get(project_id):
> -    """Get a project by its ID.
> -
> -    Retrieve a project matching a given project ID, if any exists.
> -
> -    Args:
> -        project_id (int): The ID of the project to retrieve.
> -
> -    Returns:
> -        The serialized project matching the ID, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        project = Project.objects.get(id=project_id)
> -        return project_to_dict(project)
> -    except Project.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def person_list(search_str=None, max_count=0):
> -    """List persons matching a given name or email filter.
> -
> -    Filter persons by name and email. Persons are compared to the
> -    search string via a case-insensitive containment test, a.k.a. a
> -    partial match.
> -
> -    Args:
> -        search_str: The string to compare person names or emails
> -            against. If blank, all persons will be returned.
> -        max_count (int): The maximum number of persons to return.
> -
> -    Returns:
> -        A serialized list of persons matching filter, if any. A list
> -        of all persons if no filter given.
> -    """
> -    if search_str:
> -        people = (Person.objects.filter(name__icontains=search_str) |
> -                  Person.objects.filter(email__icontains=search_str))
> -    else:
> -        people = Person.objects.all()
> -
> -    return _get_objects(person_to_dict, people, max_count)
> -
> -
> - at xmlrpc_method()
> -def person_get(person_id):
> -    """Get a person by its ID.
> -
> -    Retrieve a person matching a given person ID, if any exists.
> -
> -    Args:
> -        person_id (int): The ID of the person to retrieve.
> -
> -    Returns:
> -        The serialized person matching the ID, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        person = Person.objects.get(id=person_id)
> -        return person_to_dict(person)
> -    except Person.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def patch_list(filt=None):
> -    """List patches matching all of a given set of filters.
> -
> -    Filter patches by one or more of the below fields:
> -
> -     * id
> -     * name
> -     * project_id
> -     * submitter_id
> -     * delegate_id
> -     * archived
> -     * state_id
> -     * date
> -     * commit_ref
> -     * hash
> -     * msgid
> -
> -    It is also possible to specify the number of patches returned via
> -    a ``max_count`` filter.
> -
> -     * max_count
> -
> -    With the exception of ``max_count``, the specified field of the
> -    patches are compared to the search string using a provided
> -    field lookup type, which can be one of:
> -
> -     * iexact
> -     * contains
> -     * icontains
> -     * gt
> -     * gte
> -     * lt
> -     * in
> -     * startswith
> -     * istartswith
> -     * endswith
> -     * iendswith
> -     * range
> -     * year
> -     * month
> -     * day
> -     * isnull
> -
> -    Please refer to the Django documentation for more information on
> -    these field lookup types.
> -
> -    An example filter would look like so:
> -
> -    {
> -        'name__icontains': 'Joe Bloggs',
> -        'max_count': 1,
> -    }
> -
> -    Args:
> -        filt (dict): The filters specifying the field to compare, the
> -            lookup type and the value to compare against. Keys are of
> -            format ``[FIELD_NAME]`` or ``[FIELD_NAME]__[LOOKUP_TYPE]``.
> -            Example: ``name__icontains``. Values are plain strings to
> -            compare against.
> -
> -    Returns:
> -        A serialized list of patches matching filters, if any. A list
> -        of all patches if no filter given.
> -    """
> -    if filt is None:
> -        filt = {}
> -
> -    # We allow access to many of the fields.  But, some fields are
> -    # filtered by raw object so we must lookup by ID instead over
> -    # XML-RPC.
> -    ok_fields = [
> -        'id',
> -        'name',
> -        'project_id',
> -        'submitter_id',
> -        'delegate_id',
> -        'archived',
> -        'state_id',
> -        'date',
> -        'commit_ref',
> -        'hash',
> -        'msgid',
> -        'max_count',
> -    ]
> -
> -    dfilter = {}
> -    max_count = 0
> -
> -    for key in filt:
> -        parts = key.split('__')
> -        if parts[0] not in ok_fields:
> -            # Invalid field given
> -            return []
> -        if len(parts) > 1 and LOOKUP_TYPES.count(parts[1]) == 0:
> -            # Invalid lookup type given
> -            return []
> -
> -        try:
> -            if parts[0] == 'project_id':
> -                dfilter['project'] = Project.objects.get(id=filt[key])
> -            elif parts[0] == 'submitter_id':
> -                dfilter['submitter'] = Person.objects.get(id=filt[key])
> -            elif parts[0] == 'delegate_id':
> -                dfilter['delegate'] = Person.objects.get(id=filt[key])
> -            elif parts[0] == 'state_id':
> -                dfilter['state'] = State.objects.get(id=filt[key])
> -            elif parts[0] == 'max_count':
> -                max_count = filt[key]
> -            else:
> -                dfilter[key] = filt[key]
> -        except (Project.DoesNotExist, Person.DoesNotExist, State.DoesNotExist):
> -            # Invalid Project, Person or State given
> -            return []
> -
> -    patches = Patch.objects.filter(**dfilter)
> -
> -    # Only extract the relevant fields. This saves a big db load as we
> -    # no longer fetch content/headers/etc for potentially every patch
> -    # in a project.
> -    patches = patches.defer('content', 'headers', 'diff')
> -
> -    return _get_objects(patch_to_dict, patches, max_count)
> -
> -
> - at xmlrpc_method()
> -def patch_get(patch_id):
> -    """Get a patch by its ID.
> -
> -    Retrieve a patch matching a given patch ID, if any exists.
> -
> -    Args:
> -        patch_id (int): The ID of the patch to retrieve
> -
> -    Returns:
> -        The serialized patch matching the ID, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        patch = Patch.objects.get(id=patch_id)
> -        return patch_to_dict(patch)
> -    except Patch.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def patch_get_by_hash(hash):  # noqa
> -    """Get a patch by its hash.
> -
> -    Retrieve a patch matching a given patch hash, if any exists.
> -
> -    Args:
> -        hash: The hash of the patch to retrieve
> -
> -    Returns:
> -        The serialized patch matching the hash, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        patch = Patch.objects.get(hash=hash)
> -        return patch_to_dict(patch)
> -    except Patch.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def patch_get_by_project_hash(project, hash):
> -    """Get a patch by its project and hash.
> -
> -    Retrieve a patch matching a given project and patch hash, if any
> -    exists.
> -
> -    Args:
> -        project (str): The project of the patch to retrieve.
> -        hash: The hash of the patch to retrieve.
> -
> -    Returns:
> -        The serialized patch matching both the project and the hash,
> -        if any, else an empty dict.
> -    """
> -    try:
> -        patch = Patch.objects.get(project__linkname=project,
> -                                  hash=hash)
> -        return patch_to_dict(patch)
> -    except Patch.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def patch_get_mbox(patch_id):
> -    """Get a patch by its ID in mbox format.
> -
> -    Retrieve a patch matching a given patch ID, if any exists, and
> -    return in mbox format.
> -
> -    Args:
> -        patch_id (int): The ID of the patch to retrieve.
> -
> -    Returns:
> -        The serialized patch matching the ID, if any, in mbox format,
> -        else an empty string.
> -    """
> -    try:
> -        patch = Patch.objects.get(id=patch_id)
> -        return patch_to_mbox(patch)
> -    except Patch.DoesNotExist:
> -        return ''
> -
> -
> - at xmlrpc_method()
> -def patch_get_diff(patch_id):
> -    """Get a patch by its ID in diff format.
> -
> -    Retrieve a patch matching a given patch ID, if any exists, and
> -    return in diff format.
> -
> -    Args:
> -        patch_id (int): The ID of the patch to retrieve.
> -
> -    Returns:
> -        The serialized patch matching the ID, if any, in diff format,
> -        else an empty string.
> -    """
> -    try:
> -        patch = Patch.objects.get(id=patch_id)
> -        return patch.diff
> -    except Patch.DoesNotExist:
> -        return ''
> -
> -
> - at xmlrpc_method(login_required=True)
> -def patch_set(user, patch_id, params):
> -    """Set fields of a patch.
> -
> -    Modify a patch matching a given patch ID, if any exists, and using
> -    the provided ``key,value`` pairs. Only the following parameters may
> -    be set:
> -
> -     * state
> -     * commit_ref
> -     * archived
> -
> -    Any other field will be rejected.
> -
> -    **NOTE:** Authentication is required for this method.
> -
> -    Args:
> -        user (User): The user making the request. This will be
> -            populated from HTTP Basic Auth.
> -        patch_id (int): The ID of the patch to modify.
> -        params (dict): A dictionary of keys corresponding to patch
> -            object fields and the values that said fields should be
> -            set to.
> -
> -    Returns:
> -        True, if successful else raise exception.
> -
> -    Raises:
> -        Exception: User did not have necessary permissions to edit this
> -            patch
> -        Patch.DoesNotExist: The patch did not exist.
> -    """
> -    ok_params = ['state', 'commit_ref', 'archived']
> -
> -    patch = Patch.objects.get(id=patch_id)
> -
> -    if not patch.is_editable(user):
> -        raise Exception('No permissions to edit this patch')
> -
> -    for (k, v) in params.items():
> -        if k not in ok_params:
> -            continue
> -
> -        if k == 'state':
> -            patch.state = State.objects.get(id=v)
> -
> -        else:
> -            setattr(patch, k, v)
> -
> -    patch.save()
> -
> -    return True
> -
> -
> - at xmlrpc_method()
> -def state_list(search_str=None, max_count=0):
> -    """List states matching a given name filter.
> -
> -    Filter states by name. States are compared to the search string
> -    via a case-insensitive containment test, a.k.a. a partial match.
> -
> -    Args:
> -        search_str: The string to compare state names against. If
> -            blank, all states will be returned.
> -        max_count (int): The maximum number of states to return.
> -
> -    Returns:
> -        A serialized list of states matching filter, if any. A list
> -        of all states if no filter given.
> -    """
> -    if search_str:
> -        states = State.objects.filter(name__icontains=search_str)
> -    else:
> -        states = State.objects.all()
> -
> -    return _get_objects(state_to_dict, states, max_count)
> -
> -
> - at xmlrpc_method()
> -def state_get(state_id):
> -    """Get a state by its ID.
> -
> -    Retrieve a state matching a given state ID, if any exists.
> -
> -    Args:
> -        state_id: The ID of the state to retrieve.
> -
> -    Returns:
> -        The serialized state matching the ID, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        state = State.objects.get(id=state_id)
> -        return state_to_dict(state)
> -    except State.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method()
> -def check_list(filt=None):
> -    """List checks matching all of a given set of filters.
> -
> -    Filter checks by one or more of the below fields:
> -
> -     * id
> -     * user
> -     * project_id
> -     * patch_id
> -
> -    It is also possible to specify the number of patches returned via
> -    a ``max_count`` filter.
> -
> -     * max_count
> -
> -    With the exception of ``max_count``, the specified field of the
> -    patches are compared to the search string using a provided
> -    field lookup type, which can be one of:
> -
> -     * iexact
> -     * contains
> -     * icontains
> -     * gt
> -     * gte
> -     * lt
> -     * in
> -     * startswith
> -     * istartswith
> -     * endswith
> -     * iendswith
> -     * range
> -     * year
> -     * month
> -     * day
> -     * isnull
> -
> -    Please refer to the Django documentation for more information on
> -    these field lookup types.
> -
> -    An example filter would look like so:
> -
> -    {
> -        'user__icontains': 'Joe Bloggs',
> -        'max_count': 1,
> -    }
> -
> -    Args:
> -        filt (dict): The filters specifying the field to compare, the
> -            lookup type and the value to compare against. Keys are of
> -            format ``[FIELD_NAME]`` or ``[FIELD_NAME]__[LOOKUP_TYPE]``.
> -            Example: ``name__icontains``. Values are plain strings to
> -            compare against.
> -
> -    Returns:
> -        A serialized list of Checks matching filters, if any. A list
> -        of all Checks if no filter given.
> -    """
> -    if filt is None:
> -        filt = {}
> -
> -    # We allow access to many of the fields. But, some fields are
> -    # filtered by raw object so we must lookup by ID instead over
> -    # XML-RPC.
> -    ok_fields = [
> -        'id',
> -        'user',
> -        'project_id',
> -        'patch_id',
> -        'max_count',
> -    ]
> -
> -    dfilter = {}
> -    max_count = 0
> -
> -    for key in filt:
> -        parts = key.split('__')
> -        if parts[0] not in ok_fields:
> -            # Invalid field given
> -            return []
> -        if len(parts) > 1:
> -            if LOOKUP_TYPES.count(parts[1]) == 0:
> -                # Invalid lookup type given
> -                return []
> -
> -        if parts[0] == 'user_id':
> -            dfilter['user'] = Person.objects.filter(id=filt[key])[0]
> -        if parts[0] == 'project_id':
> -            dfilter['patch__project'] = Project.objects.filter(
> -                id=filt[key])[0]
> -        elif parts[0] == 'patch_id':
> -            dfilter['patch'] = Patch.objects.filter(id=filt[key])[0]
> -        elif parts[0] == 'max_count':
> -            max_count = filt[key]
> -        else:
> -            dfilter[key] = filt[key]
> -
> -    checks = Check.objects.filter(**dfilter)
> -
> -    return _get_objects(check_to_dict, checks, max_count)
> -
> -
> - at xmlrpc_method()
> -def check_get(check_id):
> -    """Get a check by its ID.
> -
> -    Retrieve a check matching a given check ID, if any exists.
> -
> -    Args:
> -        check_id (int): The ID of the check to retrieve
> -
> -    Returns:
> -        The serialized check matching the ID, if any, else an empty
> -        dict.
> -    """
> -    try:
> -        check = Check.objects.get(id=check_id)
> -        return check_to_dict(check)
> -    except Check.DoesNotExist:
> -        return {}
> -
> -
> - at xmlrpc_method(login_required=True)
> -def check_create(user, patch_id, context, state, target_url="",
> -                 description=""):
> -    """Add a Check to a patch.
> -
> -    **NOTE:** Authentication is required for this method.
> -
> -    Args:
> -        patch_id (id): The ID of the patch to create the check against.
> -        context: Type of test or system that generated this check.
> -        state: "pending", "success", "warning", or "fail"
> -        target_url: Link to artifact(s) relating to this check.
> -        description: A brief description of the check.
> -
> -    Returns:
> -        True, if successful else raise exception.
> -    """
> -    patch = Patch.objects.get(id=patch_id)
> -    if not patch.is_editable(user):
> -        raise Exception('No permissions to edit this patch')
> -    for state_val, state_str in Check.STATE_CHOICES:
> -        if state == state_str:
> -            state = state_val
> -            break
> -    else:
> -        raise Exception("Invalid check state: %s" % state)
> -    Check.objects.create(patch=patch, context=context, state=state, user=user,
> -                         target_url=target_url, description=description)
> -    return True
> -
> -
> - at xmlrpc_method()
> -def patch_check_get(patch_id):
> -    """Get a patch's combined checks by its ID.
> -
> -    Retrieve a patch's combined checks for the patch matching a given
> -    patch ID, if any exists.
> -
> -    Args:
> -        patch_id (int): The ID of the patch to retrieve checks for
> -
> -    Returns:
> -        The serialized combined patch checks matching the ID, if any,
> -        else an empty dict.
> -    """
> -    try:
> -        patch = Patch.objects.get(id=patch_id)
> -        return patch_check_to_dict(patch)
> -    except Patch.DoesNotExist:
> -        return {}
> diff --git releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml
> new file mode 100644
> index 000000000000..e86c156844d8
> --- /dev/null
> +++ releasenotes/notes/remove-xmlrpc-b6d26084338efcb4.yaml
> @@ -0,0 +1,11 @@
> +---
> +features:
> +  - |
> +    To simplify the code base, the XML-RPC API has been removed. This
> +    means that `pwclient` will no longer work.
> +
> +    As a result, `tools/patchwork-update-commits` and
> +    `tools/post-receive.hook` have also been removed.

Yeah, we shouldn't do this. There are definitely people that are using
this (DPDK jumps to mind again). We _need_ to port pwclient before we
can do this, just so these scripts can keep on working (albeit with
different configuration).

> +
> +    Users are encouraged to try
> +    `git-pw <https://github.com/getpatchwork/git-pw>`_.
> diff --git tools/patchwork-update-commits tools/patchwork-update-commits
> deleted file mode 100755
> index 269dac9eee4e..000000000000
> --- tools/patchwork-update-commits
> +++ /dev/null
> @@ -1,20 +0,0 @@
> -#!/bin/bash
> -#
> -# Patchwork - automated patch tracking system
> -# Copyright (C) 2010 Jeremy Kerr <jk at ozlabs.org>
> -#
> -# SPDX-License-Identifier: GPL-2.0-or-later
> -
> -TOOLS_DIR="$(dirname "$0")"
> -PW_DIR="${TOOLS_DIR}/../patchwork"
> -
> -if [ "$#" -lt 1 ]; then
> -    echo "usage: $0 <revspec>" >&2
> -    exit 1
> -fi
> -
> -git rev-list --reverse "$@" |
> -while read -r commit; do
> -    hash=$(git diff "$commit~..$commit" | python "$PW_DIR/hasher.py")
> -    pwclient update -s Accepted -c "$commit" -h "$hash"
> -done
> diff --git tools/post-receive.hook tools/post-receive.hook
> deleted file mode 100755
> index 9f2f0503d7ee..000000000000
> --- tools/post-receive.hook
> +++ /dev/null
> @@ -1,86 +0,0 @@
> -#!/bin/bash
> -
> -# Patchwork - automated patch tracking system
> -# Copyright (C) 2010 martin f. krafft <madduck at madduck.net>
> -#
> -# SPDX-License-Identifier: GPL-2.0-or-later
> -
> -# Git post-receive hook to update Patchwork patches after Git pushes
> -set -eu
> -
> -PW_DIR=/opt/patchwork/patchwork
> -
> -#TODO: the state map should really live in the repo's git-config
> -STATE_MAP="refs/heads/master:Accepted"
> -
> -# ignore all commits already present in these refs
> -# e.g.,
> -#   EXCLUDE="refs/heads/upstream refs/heads/other-project"
> -EXCLUDE=""
> -
> -do_exit=0
> -trap "do_exit=1" INT
> -
> -get_patchwork_hash() {
> -    local hash
> -    hash=$(git diff "$1~..$1" | python $PW_DIR/hasher.py)
> -    echo "$hash"
> -    test -n "$hash"
> -}
> -
> -get_patch_id() {
> -    local id
> -    id=$(pwclient info -h "$1" 2>/dev/null | \
> -         sed -rne 's,- id[[:space:]]*: ,,p')
> -    echo "$id"
> -    test -n "$id"
> -}
> -
> -set_patch_state() {
> -    pwclient update -s "$2" -c "$3" "$1" 2>&1
> -}
> -
> -update_patches() {
> -    local cnt; cnt=0
> -    for rev in $(git rev-parse --not ${EXCLUDE} |
> -                 git rev-list --stdin --no-merges --reverse "${1}".."${2}"); do
> -        if [ "$do_exit" = 1 ]; then
> -            echo "I: exiting..." >&2
> -            break
> -        fi
> -        hash=$(get_patchwork_hash "$rev")
> -        if [ -z "$hash" ]; then
> -            echo "E: failed to hash rev $rev." >&2
> -            continue
> -        fi
> -        id=$(get_patch_id "$hash" || true)
> -        if [ -z "$id" ]; then
> -            echo "E: failed to find patch for rev $rev." >&2
> -            continue
> -        fi
> -        reason="$(set_patch_state "$id" "$3" "$rev")"
> -        if [ -n "$reason" ]; then
> -            echo "E: failed to update patch #$id${reason:+: $reason}." >&2
> -            continue
> -        fi
> -        echo "I: patch #$id updated using rev $rev." >&2
> -        cnt=$((cnt + 1))
> -    done
> -
> -    echo "I: $cnt patch(es) updated to state $3." >&2
> -}
> -
> -while read -r oldrev newrev refname; do
> -    found=0
> -    for i in $STATE_MAP; do
> -        key="${i%:*}"
> -        if [ "$key" = "$refname" ]; then
> -            update_patches "$oldrev" "$newrev" ${i#*:}
> -            found=1
> -            break
> -        fi
> -    done
> -    if [ $found -eq 0 ]; then
> -        echo "E: STATE_MAP has no mapping for branch $refname" >&2
> -    fi
> -done



More information about the Patchwork mailing list