← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~flacoste/launchpad/bug-801233 into lp:launchpad

 

Francis J. Lacoste has proposed merging lp:~flacoste/launchpad/bug-801233 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~flacoste/launchpad/bug-801233/+merge/67353

= Summary =

This is a massive Yak shaving branch! The goal is to write an API script which
will allow copy rebuilds to be manually allocated a number of builders. To do
this, we need to find dynamically the number of available builders for a
particular processor. That information isn't currently available over the
API.

== Proposed fix ==

It exports IBuilder.processor, IBuilderSet.getBuidQueueSizes() and
IBuilderSet.getBuilderrsForQueue(). Along with numerous other yak that needed
shaving along the way. An important one worth mentioning here: I changed the
URL of IProcessor. They were living under their family and their DB id was
part of th URL. Since IProcessor name are globally unique, I added a
/+processors collection and their URL point their using their name. That will
make it easier to get processor by crafting the URL.

== Pre-implementation notes ==

No pre-implementation done.

== Implementation details ==

Here are the numerous yak shaved along the way:

 * Removed getBuildersByArch() which wasn't used anywhere.
 * getBuildQueueSize() returns a dict containing datetime.timedelta object.
 simplejson didn't know how to serialize those, so I hooked up a very
 simple one which simply str() it. That was added into
 lp.services.webservice.json and hooked through zcml.
 * Collections check that launchpad.View is available on the entry. So I had
 to define adapters for IBuilder, IProcessor and IProcessorFamily.
 * Since there is no web page for IProcessor nor IProcessorFamily, I changed
 pulish_web_link to False.

Everything else should be straightforward, let me know if you have any
questions.

== Tests ==

 ./bin/test -vvt
'lp.soyuz.tests.test_processor|lp.soyuz.browser.tests.test_processor|lp.buildmaster.tests.test_webservice'

== Demo and Q/A ==

This will be QA-ed by checking the exported attribute and methods on
qastaging.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/buildmaster/model/builder.py
  lib/lp/soyuz/tests/test_processor.py
  lib/lp/services/webservice/tests/__init__.py
  lib/lp/buildmaster/security.py
  lib/lp/soyuz/model/processor.py
  lib/lp/soyuz/interfaces/processor.py
  lib/lp/soyuz/security.py
  lib/lp/app/browser/launchpad.py
  lib/lp/testing/publication.py
  lib/lp/soyuz/configure.zcml
  lib/lp/testing/factory.py
  lib/lp/testing/__init__.py
  lib/lp/soyuz/interfaces/webservice.py
  lib/lp/buildmaster/configure.zcml
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/services/webservice/tests/test_json.py
  lib/lp/testing/tests/test_publication.py
  lib/canonical/launchpad/configure.zcml
  lib/lp/services/webservice/json.py
  lib/lp/soyuz/browser/processor.py
  lib/lp/services/webservice/configure.zcml
  lib/lp/buildmaster/tests/test_webservice.py
  lib/lp/buildmaster/interfaces/builder.py
  lib/lp/soyuz/browser/tests/test_processor.py

./lib/lp/buildmaster/model/builder.py
     174: Line exceeds 78 characters.
     261: Line exceeds 78 characters.
     219: W291 trailing whitespace
     284: E301 expected 1 blank line, found 0
     302: E301 expected 1 blank line, found 0
     373: E301 expected 1 blank line, found 0
     517: E301 expected 1 blank line, found 0
     519: E301 expected 1 blank line, found 0
     608: E301 expected 1 blank line, found 0
     683: E301 expected 1 blank line, found 0
     686: E301 expected 1 blank line, found 0
./lib/lp/soyuz/tests/test_processor.py
      48: Line exceeds 78 characters.
      55: Line exceeds 78 characters.
      57: Line exceeds 78 characters.
     123: Line exceeds 78 characters.
      88: E202 whitespace before ')'
     123: E501 line too long (123 characters)
     123: E202 whitespace before ')'
./lib/lp/soyuz/model/processor.py
      87: E301 expected 1 blank line, found 0
./lib/lp/soyuz/security.py
      18: E302 expected 2 blank lines, found 1
      22: E302 expected 2 blank lines, found 1
./lib/lp/testing/publication.py
      36: E302 expected 2 blank lines, found 1
./lib/lp/services/webservice/tests/test_json.py
      27: E303 too many blank lines (2)
      30: W291 trailing whitespace
      33: W391 blank line at end of file
./lib/lp/testing/tests/test_publication.py
      49: E301 expected 1 blank line, found 0
      58: W291 trailing whitespace
      79: E301 expected 1 blank line, found 0
     101: E301 expected 1 blank line, found 0
./lib/lp/buildmaster/interfaces/builder.py
     139: Line exceeds 78 characters.
     210: Line exceeds 78 characters.
     249: W293 blank line contains whitespace
     256: W293 blank line contains whitespace
./lib/lp/soyuz/browser/tests/test_processor.py
      13: E302 expected 2 blank lines, found 1
      41: W391 blank line at end of file

I'll fix the white-space lint post-review.
-- 
https://code.launchpad.net/~flacoste/launchpad/bug-801233/+merge/67353
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~flacoste/launchpad/bug-801233 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml	2011-02-23 21:15:23 +0000
+++ lib/canonical/launchpad/configure.zcml	2011-07-08 19:23:25 +0000
@@ -27,6 +27,7 @@
   <include package="lp.coop.answersbugs" />
   <include package="lp.code" />
   <include package="lp.soyuz" />
+  <include package="lp.services.webservice" />
   <include package="lp.translations" />
   <include package="lp.testopenid" />
   <include package="lp.blueprints" />

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2011-06-30 11:28:59 +0000
+++ lib/lp/app/browser/launchpad.py	2011-07-08 19:23:25 +0000
@@ -146,7 +146,10 @@
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
 from lp.soyuz.interfaces.packageset import IPackagesetSet
-from lp.soyuz.interfaces.processor import IProcessorFamilySet
+from lp.soyuz.interfaces.processor import (
+    IProcessorFamilySet,
+    IProcessorSet,
+    )
 from lp.testopenid.interfaces.server import ITestOpenIDApplication
 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
 from lp.translations.interfaces.translationimportqueue import (
@@ -617,6 +620,7 @@
         'people': IPersonSet,
         'pillars': IPillarNameSet,
         '+processor-families': IProcessorFamilySet,
+        '+processors': IProcessorSet,
         'projects': IProductSet,
         'projectgroups': IProjectGroupSet,
         'sourcepackagenames': ISourcePackageNameSet,

=== modified file 'lib/lp/buildmaster/configure.zcml'
--- lib/lp/buildmaster/configure.zcml	2010-11-19 13:25:25 +0000
+++ lib/lp/buildmaster/configure.zcml	2011-07-08 19:23:25 +0000
@@ -10,6 +10,7 @@
     xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc";
     i18n_domain="launchpad">
     <include package=".browser"/>
+    <authorizations module=".security" />
 
     <!-- Builder -->
     <class

=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py	2011-02-24 15:30:54 +0000
+++ lib/lp/buildmaster/interfaces/builder.py	2011-07-08 19:23:25 +0000
@@ -27,6 +27,12 @@
     exported,
     operation_parameters,
     operation_returns_entry,
+    operation_returns_collection_of,
+    operation_for_version,
+    )
+from lazr.restful.fields import (
+    Reference,
+    ReferenceChoice,
     )
 from zope.interface import (
     Attribute,
@@ -34,7 +40,6 @@
     )
 from zope.schema import (
     Bool,
-    Choice,
     Field,
     Int,
     Text,
@@ -45,6 +50,7 @@
 from lp.app.validators.name import name_validator
 from lp.app.validators.url import builder_url_validator
 from lp.registry.interfaces.role import IHasOwner
+from lp.soyuz.interfaces.processor import IProcessor
 from lp.services.fields import (
     Description,
     PersonChoice,
@@ -106,10 +112,12 @@
 
     id = Attribute("Builder identifier")
 
-    processor = Choice(
+    processor = exported(ReferenceChoice(
         title=_('Processor'), required=True, vocabulary='Processor',
+        schema=IProcessor,
         description=_('Build Slave Processor, used to identify '
-                      'which jobs can be built by this device.'))
+                      'which jobs can be built by this device.')),
+        as_of='devel', readonly=True)
 
     owner = exported(PersonChoice(
         title=_('Owner'), required=True, vocabulary='ValidOwner',
@@ -385,9 +393,8 @@
     def getBuilders():
         """Return all active configured builders."""
 
-    def getBuildersByArch(arch):
-        """Return all configured builders for a given DistroArchSeries."""
-
+    @export_read_operation()
+    @operation_for_version('devel')
     def getBuildQueueSizes():
         """Return the number of pending builds for each processor.
 
@@ -406,5 +413,13 @@
             as a timedelta or None for empty queues.
         """
 
+    @operation_parameters(
+        processor=Reference(
+            title=_("Processor"), required=True, schema=IProcessor),
+        virtualized=Bool(
+            title=_("Virtualized"), required=False, default=True))
+    @operation_returns_collection_of(IBuilder)
+    @export_read_operation()
+    @operation_for_version('devel')
     def getBuildersForQueue(processor, virtualized):
         """Return all builders for given processor/virtualization setting."""

=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py	2011-02-01 18:14:57 +0000
+++ lib/lp/buildmaster/model/builder.py	2011-07-08 19:23:25 +0000
@@ -896,13 +896,6 @@
         return Builder.selectBy(
             active=True, orderBy=['virtualized', 'processor', 'name'])
 
-    def getBuildersByArch(self, arch):
-        """See IBuilderSet."""
-        return Builder.select('builder.processor = processor.id '
-                              'AND processor.family = %d'
-                              % arch.processorfamily.id,
-                              clauseTables=("Processor",))
-
     def getBuildQueueSizes(self):
         """See `IBuilderSet`."""
         store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)

=== added file 'lib/lp/buildmaster/security.py'
--- lib/lp/buildmaster/security.py	1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/security.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,19 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the buildmaster package."""
+
+__metaclass__ = type
+__all__ = [
+    'ViewBuilder',
+    ]
+
+from lp.app.security import AnonymousAuthorization
+from lp.buildmaster.interfaces.builder import (
+    IBuilder,
+    )
+
+
+class ViewBuilder(AnonymousAuthorization):
+    """Anyone can view a `IBuilder`."""
+    usedfor = IBuilder

=== added file 'lib/lp/buildmaster/tests/test_webservice.py'
--- lib/lp/buildmaster/tests/test_webservice.py	1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/tests/test_webservice.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,68 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the builders webservice ."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
+from lp.testing import (
+    api_url,
+    logout,
+    TestCaseWithFactory,
+    )
+
+
+class TestBuildersCollection(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBuildersCollection, self).setUp()
+        self.webservice = LaunchpadWebServiceCaller()
+
+    def test_getBuildQueueSizes(self):
+        logout()
+        results = self.webservice.named_get(
+            '/builders', 'getBuildQueueSizes', api_version='devel')
+        self.assertEquals(
+            ['nonvirt', 'virt'], sorted(results.jsonBody().keys()))
+
+    def test_getBuildersForQueue(self):
+        g1 = self.factory.makeProcessorFamily('g1').processors[0]
+        quantum = self.factory.makeProcessorFamily('quantum').processors[0]
+        self.factory.makeBuilder(
+            processor=quantum, name='quantum_builder1')
+        self.factory.makeBuilder(
+            processor=quantum, name='quantum_builder2')
+        self.factory.makeBuilder(
+            processor=quantum, name='quantum_builder3', virtualized=False)
+        self.factory.makeBuilder(
+            processor=g1, name='g1_builder', virtualized=False)
+
+        logout()
+        results = self.webservice.named_get(
+            '/builders', 'getBuildersForQueue',
+            processor=api_url(quantum), virtualized=True,
+            api_version='devel').jsonBody()
+        self.assertEquals(
+            ['quantum_builder1', 'quantum_builder2'],
+            sorted(builder['name'] for builder in results['entries']))
+
+
+class TestBuilderEntry(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBuilderEntry, self).setUp()
+        self.webservice = LaunchpadWebServiceCaller()
+
+    def test_exports_processor(self):
+        processor_family = self.factory.makeProcessorFamily('s1')
+        builder = self.factory.makeBuilder(
+            processor=processor_family.processors[0])
+
+        logout()
+        entry = self.webservice.get(
+            api_url(builder), api_version='devel').jsonBody()
+        self.assertEndsWith(entry['processor_link'], '/+processors/s1')

=== added file 'lib/lp/services/webservice/configure.zcml'
--- lib/lp/services/webservice/configure.zcml	1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/configure.zcml	2011-07-08 19:23:25 +0000
@@ -0,0 +1,15 @@
+<!-- Copyright 2011 Canonical Ltd.  This software is licensed under the
+     GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<configure
+    xmlns="http://namespaces.zope.org/zope";
+    xmlns:i18n="http://namespaces.zope.org/i18n";
+    i18n_domain="launchpad">
+
+    <adapter
+            provides="lazr.restful.interfaces.IJSONPublishable"
+            for="zope.interface.common.idatetime.ITimeDelta"
+            factory="lp.services.webservice.json.StrJSONSerializer"
+            permission="zope.Public"/>
+</configure>

=== added file 'lib/lp/services/webservice/json.py'
--- lib/lp/services/webservice/json.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/json.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,22 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Additional JSON serializer for the web service."""
+
+__metaclass__ = type
+__all__ = []
+
+
+from lazr.restful.interfaces import IJSONPublishable
+from zope.interface import implements
+
+
+class StrJSONSerializer:
+    """Simple JSON serializer that simply str() it's context. """
+    implements(IJSONPublishable)
+
+    def __init__(self, context):
+        self.context = context
+
+    def toDataForJSON(self, media_type):
+        return str(self.context)

=== added directory 'lib/lp/services/webservice/tests'
=== added file 'lib/lp/services/webservice/tests/__init__.py'
=== added file 'lib/lp/services/webservice/tests/test_json.py'
--- lib/lp/services/webservice/tests/test_json.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/tests/test_json.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,33 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the JSON serializer."""
+
+__metaclass__ = type
+
+from datetime import timedelta
+
+from canonical.testing.layers import FunctionalLayer
+from lazr.restful.interfaces import IJSONPublishable
+from lp.services.webservice.json import StrJSONSerializer
+from lp.testing import TestCase
+
+
+class TestStrJSONSerializer(TestCase):
+    layer = FunctionalLayer
+
+    def test_toDataForJSON(self):
+        serializer = StrJSONSerializer(
+            timedelta(days=2, hours=2, seconds=5))
+        self.assertEquals(
+            '2 days, 2:00:05',
+            serializer.toDataForJSON('application/json'))
+
+
+    def test_timedelta_users_StrJSONSerializer(self):
+        delta = timedelta(seconds=5)
+        serializer = IJSONPublishable(delta)
+        self.assertEquals('0:00:05', 
+            serializer.toDataForJSON('application/json'))
+
+

=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml	2011-06-23 13:42:38 +0000
+++ lib/lp/soyuz/browser/configure.zcml	2011-07-08 19:23:25 +0000
@@ -32,18 +32,20 @@
             path_expression="string:+binarypub"
             attribute_to_parent="archive"
             urldata="lp.soyuz.browser.publishing.BinaryPublicationURL"/>
-        <browser:url
-	    for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
+        <browser:url for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
             path_expression="string:+processor-families"
             parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
         <browser:url
 	    for="lp.soyuz.interfaces.processor.IProcessorFamily"
             path_expression="string:${name}"
             parent_utility="lp.soyuz.interfaces.processor.IProcessorFamilySet" />
+        <browser:url for="lp.soyuz.interfaces.processor.IProcessorSet"
+            path_expression="string:+processors"
+            parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
         <browser:url
 	    for="lp.soyuz.interfaces.processor.IProcessor"
-            path_expression="string:${id}"
-            attribute_to_parent="family" />
+            path_expression="string:${name}"
+            parent_utility="lp.soyuz.interfaces.processor.IProcessorSet" />
     </facet>
     <browser:navigation
         module="lp.soyuz.browser.binarypackagerelease"
@@ -233,7 +235,7 @@
     <browser:navigation
         module="lp.soyuz.browser.processor"
 	classes="
-	    ProcessorFamilySetNavigation ProcessorFamilyNavigation"/>
+	    ProcessorFamilySetNavigation ProcessorSetNavigation"/>
     <browser:url
         for="lp.soyuz.interfaces.archive.IPPA"
         path_expression="string:+archive"

=== modified file 'lib/lp/soyuz/browser/processor.py'
--- lib/lp/soyuz/browser/processor.py	2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/browser/processor.py	2011-07-08 19:23:25 +0000
@@ -8,15 +8,15 @@
 
 __all__ = [
     'ProcessorFamilySetNavigation',
-    'ProcessorFamilyNavigation',
+    'ProcessorSetNavigation',
     ]
 
 
 from canonical.launchpad.webapp import Navigation
 from lp.app.errors import NotFoundError
 from lp.soyuz.interfaces.processor import (
-    IProcessorFamily,
     IProcessorFamilySet,
+    IProcessorSet,
     )
 
 
@@ -32,15 +32,9 @@
         return family
 
 
-class ProcessorFamilyNavigation(Navigation):
-    """IProcessorFamily navigation."""
-
-    usedfor = IProcessorFamily
-
-    def traverse(self, id_):
-        id_ = int(id_)
-        processors = self.processors
-        for p in processors:
-            if p.id == id_:
-                return p
-        raise NotFoundError(id_)
+class ProcessorSetNavigation(Navigation):
+    """IProcessorFamilySet navigation."""
+    usedfor = IProcessorSet
+
+    def traverse(self, name):
+        return self.context.getByName(name)

=== added file 'lib/lp/soyuz/browser/tests/test_processor.py'
--- lib/lp/soyuz/browser/tests/test_processor.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/tests/test_processor.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,41 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for process navigation."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.webapp.publisher import canonical_url
+from lp.testing import TestCaseWithFactory
+from lp.testing.publication import test_traverse
+
+class TestProcessorNavigation(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_processor_family_url(self):
+        family = self.factory.makeProcessorFamily('quantum')
+        self.assertEquals(
+            '/+processor-families/quantum',
+            canonical_url(family, force_local_path=True))
+
+    def test_processor_url(self):
+        family = self.factory.makeProcessorFamily('quantum')
+        quantum = family.processors[0]
+        self.assertEquals(
+            '/+processors/quantum',
+            canonical_url(quantum, force_local_path=True))
+
+    def test_processor_family_navigation(self):
+        family = self.factory.makeProcessorFamily('quantum')
+        obj, view, request = test_traverse(
+            'http://api.launchpad.dev/devel/+processor-families/quantum')
+        self.assertEquals(family, obj)
+
+    def test_processor_navigation(self):
+        family = self.factory.makeProcessorFamily('quantum')
+        obj, view, request = test_traverse(
+            'http://api.launchpad.dev/'
+            'devel/+processors/quantum')
+        self.assertEquals(family.processors[0], obj)
+

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2011-06-25 08:55:37 +0000
+++ lib/lp/soyuz/configure.zcml	2011-07-08 19:23:25 +0000
@@ -11,6 +11,7 @@
     i18n_domain="launchpad">
     <include
         package=".browser"/>
+    <authorizations module=".security" />
 
     <!-- PackageCloner -->
 
@@ -375,6 +376,12 @@
         <allow
             interface="lp.soyuz.interfaces.processor.IProcessorFamilySet"/>
     </securedutility>
+    <securedutility
+        class="lp.soyuz.model.processor.ProcessorSet"
+        provides="lp.soyuz.interfaces.processor.IProcessorSet">
+        <allow
+            interface="lp.soyuz.interfaces.processor.IProcessorSet"/>
+    </securedutility>
     <adapter
         for="lp.soyuz.interfaces.distroarchseriesbinarypackagerelease.IDistroArchSeriesBinaryPackageRelease"
         provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"

=== modified file 'lib/lp/soyuz/interfaces/processor.py'
--- lib/lp/soyuz/interfaces/processor.py	2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/interfaces/processor.py	2011-07-08 19:23:25 +0000
@@ -11,6 +11,8 @@
     'IProcessor',
     'IProcessorFamily',
     'IProcessorFamilySet',
+    'IProcessorSet',
+    'ProcessorNotFound',
     ]
 
 from zope.interface import (
@@ -38,6 +40,12 @@
     CollectionField,
     Reference,
     )
+from lp.app.errors import NameLookupFailed
+
+
+class ProcessorNotFound(NameLookupFailed):
+    """Exception raised when a processor name isn't found."""
+    _message_prefix = 'No such processor'
 
 
 class IProcessor(Interface):
@@ -49,7 +57,7 @@
     # the WADL generation work it must be back-dated to the earliest version.
     # Note that individual attributes and methods can and must truthfully set
     # 'devel' as their version.
-    export_as_webservice_entry(publish_web_link=True, as_of='beta')
+    export_as_webservice_entry(publish_web_link=False, as_of='beta')
     id = Attribute("The Processor ID")
     family = exported(
         Reference(
@@ -84,7 +92,7 @@
     # 'devel' as their version.
     export_as_webservice_entry(
         plural_name='processor_families',
-        publish_web_link=True,
+        publish_web_link=False,
         as_of='beta')
 
     id = Attribute("The ProcessorFamily ID")
@@ -123,14 +131,36 @@
         """
 
 
+class IProcessorSet(Interface):
+    """Operations related to Processor instances."""
+    export_as_webservice_collection(IProcessor)
+
+    @operation_parameters(
+        name=TextLine(required=True))
+    @operation_returns_entry(IProcessor)
+    @export_read_operation()
+    @operation_for_version('devel')
+    def getByName(name):
+        """Return the IProcessor instance with the matching name.
+
+        :param name: The name to look for.
+        :raise ProcessorNotFound: if there is no processor with that name.
+        :return: A `IProcessor` instance if found
+        """
+
+    @collection_default_content()
+    def getAll():
+        """Return all the `IProcessor` known to Launchpad."""
+
+
 class IProcessorFamilySet(Interface):
     """Operations related to ProcessorFamily instances."""
 
-    export_as_webservice_collection(Interface)
+    export_as_webservice_collection(IProcessorFamily)
 
     @operation_parameters(
         name=TextLine(required=True))
-    @operation_returns_entry(Interface)
+    @operation_returns_entry(IProcessorFamily)
     @export_read_operation()
     @operation_for_version('devel')
     def getByName(name):

=== modified file 'lib/lp/soyuz/interfaces/webservice.py'
--- lib/lp/soyuz/interfaces/webservice.py	2011-06-23 18:26:26 +0000
+++ lib/lp/soyuz/interfaces/webservice.py	2011-07-08 19:23:25 +0000
@@ -35,6 +35,7 @@
     'IProcessor',
     'IProcessorFamily',
     'IProcessorFamilySet',
+    'IProcessorSet',
     'ISourcePackagePublishingHistory',
     'IncompatibleArguments',
     'InsufficientUploadRights',
@@ -96,6 +97,7 @@
     IProcessor,
     IProcessorFamily,
     IProcessorFamilySet,
+    IProcessorSet,
     )
 from lp.soyuz.interfaces.publishing import (
     IBinaryPackagePublishingHistory,
@@ -105,7 +107,6 @@
 
 from canonical.launchpad.components.apihelpers import (
     patch_collection_property,
-    patch_entry_return_type,
     patch_plain_parameter_type,
     patch_reference_property,
     )
@@ -115,15 +116,10 @@
 from canonical.launchpad.interfaces import _schema_circular_imports
 _schema_circular_imports
 
-from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
-IProcessorFamilySet.queryTaggedValue(
-    LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IProcessorFamily
-
 # IProcessor
 patch_reference_property(
     IProcessor, 'family', IProcessorFamily)
 
-patch_entry_return_type(IProcessorFamilySet, 'getByName', IProcessorFamily)
 patch_collection_property(
     IArchive, 'enabled_restricted_families', IProcessorFamily)
 patch_plain_parameter_type(

=== modified file 'lib/lp/soyuz/model/processor.py'
--- lib/lp/soyuz/model/processor.py	2010-11-12 02:15:28 +0000
+++ lib/lp/soyuz/model/processor.py	2011-07-08 19:23:25 +0000
@@ -25,6 +25,8 @@
     IProcessor,
     IProcessorFamily,
     IProcessorFamilySet,
+    IProcessorSet,
+    ProcessorNotFound,
     )
 
 
@@ -42,6 +44,24 @@
         return "<Processor %r>" % self.title
 
 
+class ProcessorSet:
+    """See `IProcessorSet`."""
+    implements(IProcessorSet)
+
+    def getByName(self, name):
+        """See `IProcessorSet`."""
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        processor = store.find(Processor, Processor.name == name).one()
+        if processor is None:
+            raise ProcessorNotFound(name)
+        return processor
+
+    def getAll(self):
+        """See `IProcessorSet`."""
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        return store.find(Processor)
+
+
 class ProcessorFamily(SQLBase):
     implements(IProcessorFamily)
     _table = 'ProcessorFamily'

=== added file 'lib/lp/soyuz/security.py'
--- lib/lp/soyuz/security.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/security.py	2011-07-08 19:23:25 +0000
@@ -0,0 +1,24 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the soyuz module."""
+
+__metaclass__ = type
+__all__ = [
+    'ViewProcessor',
+    'ViewProcessorFamily',
+    ]
+
+from lp.app.security import AnonymousAuthorization
+from lp.soyuz.interfaces.processor import (
+    IProcessor,
+    IProcessorFamily,
+    )
+
+class ViewProcessor(AnonymousAuthorization):
+    """Anyone can view an `IProcessor`."""
+    usedfor = IProcessor
+
+class ViewProcessorFamily(AnonymousAuthorization):
+    """Anyone can view an `IProcessorFamily`."""
+    usedfor = IProcessorFamily

=== modified file 'lib/lp/soyuz/tests/test_processor.py'
--- lib/lp/soyuz/tests/test_processor.py	2010-10-04 19:50:45 +0000
+++ lib/lp/soyuz/tests/test_processor.py	2011-07-08 19:23:25 +0000
@@ -5,13 +5,29 @@
 
 from zope.component import getUtility
 
-from canonical.testing.layers import LaunchpadZopelessLayer
+from canonical.launchpad.webapp.interfaces import (
+    DEFAULT_FLAVOR,
+    IStoreSelector,
+    MAIN_STORE,
+    )
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadZopelessLayer,
+    )
+
 from lp.soyuz.interfaces.processor import (
     IProcessor,
     IProcessorFamily,
     IProcessorFamilySet,
-    )
-from lp.testing import TestCaseWithFactory
+    IProcessorSet,
+    ProcessorNotFound,
+    )
+from lp.testing import (
+    ExpectedException,
+    logout,
+    TestCaseWithFactory,
+    )
 
 
 class ProcessorFamilyTests(TestCaseWithFactory):
@@ -42,3 +58,66 @@
             "Another small processor family", restricted=True)
         self.assertFalse(normal_family in family_set.getRestricted())
         self.assertTrue(restricted_family in family_set.getRestricted())
+
+
+class ProcessorSetTests(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_getByName(self):
+        processor_set = getUtility(IProcessorSet)
+        q1 = self.factory.makeProcessorFamily(name='q1')
+        self.assertEquals(q1.processors[0], processor_set.getByName('q1'))
+
+    def test_getByName_not_found(self):
+        processor_set = getUtility(IProcessorSet)
+        with ExpectedException(ProcessorNotFound, 'No such processor.*'):
+            processor_set.getByName('q1')
+
+    def test_getAll(self):
+        processor_set = getUtility(IProcessorSet)
+        # Make it easy to filter out sample data
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        store.execute("UPDATE Processor SET name = 'sample_data_' || name")
+        self.factory.makeProcessorFamily(name='q1')
+        self.factory.makeProcessorFamily(name='i686')
+        self.factory.makeProcessorFamily(name='g4')
+        self.assertEquals(
+            ['g4', 'i686', 'q1'],
+            sorted(
+            processor.name for processor in processor_set.getAll()
+            if not processor.name.startswith('sample_data_') ))
+
+
+class ProcessorSetWebServiceTests(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(ProcessorSetWebServiceTests, self).setUp()
+        self.webservice = LaunchpadWebServiceCaller()
+
+    def test_getByName(self):
+        self.factory.makeProcessorFamily(name='transmeta')
+        logout()
+
+        processor = self.webservice.named_get(
+            '/+processors', 'getByName', name='transmeta',
+            api_version='devel',
+            ).jsonBody()
+        self.assertEquals('transmeta', processor['name'])
+
+    def test_default_collection(self):
+        # Make it easy to filter out sample data
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        store.execute("UPDATE Processor SET name = 'sample_data_' || name")
+        self.factory.makeProcessorFamily(name='q1')
+        self.factory.makeProcessorFamily(name='i686')
+        self.factory.makeProcessorFamily(name='g4')
+
+        logout()
+
+        collection = self.webservice.get(
+            '/+processors?ws.size=10', api_version='devel').jsonBody()
+        self.assertEquals(
+            ['g4', 'i686', 'q1'],
+            sorted(
+            processor['name'] for processor in collection['entries'] if not processor['name'].startswith('sample_data_') ))

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-07-07 12:54:38 +0000
+++ lib/lp/testing/__init__.py	2011-07-08 19:23:25 +0000
@@ -619,6 +619,17 @@
             self._unfoldEmailHeader(expected),
             self._unfoldEmailHeader(observed))
 
+    def assertStartsWith(self, s, prefix):
+        if not s.startswith(prefix):
+            raise AssertionError(
+                'string %r does not start with %r' % (s, prefix))
+
+    def assertEndsWith(self, s, suffix):
+        """Asserts that s ends with suffix."""
+        if not s.endswith(suffix):
+            raise AssertionError(
+                'string %r does not end with %r' % (s, suffix))
+
 
 class TestCaseWithFactory(TestCase):
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-07-07 15:47:36 +0000
+++ lib/lp/testing/factory.py	2011-07-08 19:23:25 +0000
@@ -894,6 +894,9 @@
                             restricted=False):
         """Create a new processor family.
 
+        A default processor for the family will be created with the
+        same name than the family.
+
         :param name: Name of the family (e.g. x86)
         :param title: Optional title of the family
         :param description: Optional extended description
@@ -906,11 +909,11 @@
             description = "Description of the %s processor family" % name
         if title is None:
             title = "%s and compatible processors." % name
-        family = getUtility(IProcessorFamilySet).new(name, title, description,
-            restricted=restricted)
+        family = getUtility(IProcessorFamilySet).new(
+            name, title, description, restricted=restricted)
         # Make sure there's at least one processor in the family, so that
         # other things can have a default processor.
-        self.makeProcessor(family=family)
+        self.makeProcessor(name=name, family=family)
         return family
 
     def makeProductRelease(self, milestone=None, product=None,

=== modified file 'lib/lp/testing/publication.py'
--- lib/lp/testing/publication.py	2010-10-03 15:30:06 +0000
+++ lib/lp/testing/publication.py	2011-07-08 19:23:25 +0000
@@ -20,6 +20,7 @@
 from zope.interface import providedBy
 from zope.publisher.interfaces.browser import IDefaultSkin
 from zope.security.management import restoreInteraction
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag
 import canonical.launchpad.layers as layers
@@ -119,9 +120,10 @@
     getUtility(IOpenLaunchBag).clear()
     app = publication.getApplication(request)
     view = request.traverse(app)
-    # Since the last traversed object is the view, the second last should be
-    # the object that the view is on.
-    obj = request.traversed_objects[-2]
+    # Find the object from the view instead on relying that it stays
+    # in the traversed_objects stack. That doesn't apply to the web
+    # service for example.
+    obj = removeSecurityProxy(view).context
 
     restoreInteraction()
 

=== modified file 'lib/lp/testing/tests/test_publication.py'
--- lib/lp/testing/tests/test_publication.py	2010-10-04 19:50:45 +0000
+++ lib/lp/testing/tests/test_publication.py	2011-07-08 19:23:25 +0000
@@ -23,6 +23,7 @@
 from canonical.launchpad.webapp.publisher import get_current_browser_request
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing.layers import DatabaseFunctionalLayer
+from lazr.restful import EntryResource
 from lp.testing import (
     ANONYMOUS,
     login,
@@ -47,6 +48,7 @@
         name = '+' + self.factory.getUniqueString()
         class new_class(simple):
             def __init__(self, context, request):
+                self.context = context
                 view_callable()
         required = {}
         for n in ('browserDefault', '__call__', 'publishTraverse'):
@@ -102,3 +104,11 @@
             self.registerViewCallable(record_user))
         self.assertEqual(1, len(users))
         self.assertEqual(person, users[0])
+
+    def test_webservice_traverse(self):
+        login(ANONYMOUS)
+        product = self.factory.makeProduct()
+        context, view, request = test_traverse(
+            'http://api.launchpad.dev/devel/' + product.name)
+        self.assertEqual(product, context)
+        self.assertIsInstance(view, EntryResource)


Follow ups