launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24573
[Merge] ~cjwatson/launchpad:oci-recipe-processors into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:oci-recipe-processors into launchpad:master.
Commit message:
Add processor selection for OCI recipes
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/381907
This is fairly directly lifted from similar code for snaps.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-recipe-processors into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 10e9d4b..1c5d696 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -30,6 +30,7 @@ from lp.app.browser.launchpadform import (
)
from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
from lp.app.browser.tales import format_link
+from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.browser.widgets.gitref import GitRefWidget
from lp.oci.interfaces.ocirecipe import (
IOCIRecipe,
@@ -53,6 +54,7 @@ from lp.services.webapp import (
)
from lp.services.webapp.breadcrumb import NameBreadcrumb
from lp.services.webhooks.browser import WebhookTargetNavigationMixin
+from lp.soyuz.browser.archive import EnableProcessorsMixin
from lp.soyuz.browser.build import get_build_by_id_str
@@ -160,7 +162,7 @@ class IOCIRecipeEditSchema(Interface):
])
-class OCIRecipeAddView(LaunchpadFormView):
+class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin):
"""View for creating OCI recipes."""
page_title = label = "Create a new OCI recipe"
@@ -181,6 +183,20 @@ class OCIRecipeAddView(LaunchpadFormView):
if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
raise OCIRecipeFeatureDisabled()
+ def setUpFields(self):
+ """See `LaunchpadFormView`."""
+ super(OCIRecipeAddView, self).setUpFields()
+ self.form_fields += self.createEnabledProcessors(
+ getUtility(IProcessorSet).getAll(),
+ "The architectures that this OCI recipe builds for. Some "
+ "architectures are restricted and may only be enabled or "
+ "disabled by administrators.")
+
+ def setUpWidgets(self):
+ """See `LaunchpadFormView`."""
+ super(OCIRecipeAddView, self).setUpWidgets()
+ self.widgets["processors"].widget_class = "processors"
+
@property
def cancel_url(self):
"""See `LaunchpadFormView`."""
@@ -192,6 +208,9 @@ class OCIRecipeAddView(LaunchpadFormView):
return {
"owner": self.user,
"build_file": "Dockerfile",
+ "processors": [
+ p for p in getUtility(IProcessorSet).getAll()
+ if p.build_by_default],
}
def validate(self, data):
@@ -213,7 +232,7 @@ class OCIRecipeAddView(LaunchpadFormView):
name=data["name"], registrant=self.user, owner=data["owner"],
oci_project=self.context, git_ref=data["git_ref"],
build_file=data["build_file"], description=data["description"],
- build_daily=data["build_daily"])
+ build_daily=data["build_daily"], processors=data["processors"])
self.next_url = canonical_url(recipe)
@@ -228,6 +247,12 @@ class BaseOCIRecipeEditView(LaunchpadEditFormView):
@action("Update OCI recipe", name="update")
def request_action(self, action, data):
+ new_processors = data.get("processors")
+ if new_processors is not None:
+ if set(self.context.processors) != set(new_processors):
+ self.context.setProcessors(
+ new_processors, check_permissions=True, user=self.user)
+ del data["processors"]
self.updateContextFromData(data)
self.next_url = canonical_url(self.context)
@@ -249,7 +274,7 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView):
field_names = ("require_virtualized",)
-class OCIRecipeEditView(BaseOCIRecipeEditView):
+class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
"""View for editing OCI recipes."""
@property
@@ -268,6 +293,15 @@ class OCIRecipeEditView(BaseOCIRecipeEditView):
)
custom_widget_git_ref = GitRefWidget
+ def setUpFields(self):
+ """See `LaunchpadFormView`."""
+ super(OCIRecipeEditView, self).setUpFields()
+ self.form_fields += self.createEnabledProcessors(
+ self.context.available_processors,
+ "The architectures that this OCI recipe builds for. Some "
+ "architectures are restricted and may only be enabled or "
+ "disabled by administrators.")
+
def validate(self, data):
"""See `LaunchpadFormView`."""
super(OCIRecipeEditView, self).validate(data)
@@ -288,6 +322,19 @@ class OCIRecipeEditView(BaseOCIRecipeEditView):
self.context.oci_project.display_name))
except NoSuchOCIRecipe:
pass
+ if "processors" in data:
+ available_processors = set(self.context.available_processors)
+ widget = self.widgets["processors"]
+ for processor in self.context.processors:
+ if processor not in data["processors"]:
+ if processor not in available_processors:
+ # This processor is not currently available for
+ # selection, but is enabled. Leave it untouched.
+ data["processors"].append(processor)
+ elif processor.name in widget.disabled_items:
+ # This processor is restricted and currently
+ # enabled. Leave it untouched.
+ data["processors"].append(processor)
class OCIRecipeDeleteView(BaseOCIRecipeEditView):
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index bd87975..b840d8c 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -16,6 +16,10 @@ import re
from fixtures import FakeLogger
import pytz
import soupmatchers
+from testtools.matchers import (
+ MatchesSetwise,
+ MatchesStructure,
+ )
from zope.component import getUtility
from zope.publisher.interfaces import NotFound
from zope.security.interfaces import Unauthorized
@@ -29,7 +33,11 @@ from lp.oci.browser.ocirecipe import (
OCIRecipeEditView,
OCIRecipeView,
)
-from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
+from lp.oci.interfaces.ocirecipe import (
+ CannotModifyOCIRecipeProcessor,
+ IOCIRecipeSet,
+ OCI_RECIPE_ALLOW_CREATE,
+ )
from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
from lp.services.propertycache import get_property_cache
@@ -104,7 +112,31 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
def setUp(self):
super(TestOCIRecipeAddView, self).setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.distroseries = self.factory.makeDistroSeries()
+ self.distribution = self.distroseries.distribution
+ self.useFixture(FeatureFixture({
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s" % self.distribution.name:
+ self.distroseries.name,
+ }))
+
+ def setUpDistroSeries(self):
+ """Set up self.distroseries with some available processors."""
+ processor_names = ["386", "amd64", "hppa"]
+ for name in processor_names:
+ processor = getUtility(IProcessorSet).getByName(name)
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag=name,
+ processor=processor)
+
+ def assertProcessorControls(self, processors_control, enabled, disabled):
+ matchers = [
+ MatchesStructure.byEquality(optionValue=name, disabled=False)
+ for name in enabled]
+ matchers.extend([
+ MatchesStructure.byEquality(optionValue=name, disabled=True)
+ for name in disabled])
+ self.assertThat(processors_control.controls, MatchesSetwise(*matchers))
def test_create_new_recipe_not_logged_in(self):
oci_project = self.factory.makeOCIProject()
@@ -158,6 +190,52 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
["Test Person (test-person)", "Test Team (test-team)"],
sorted(str(option) for option in options))
+ def test_create_new_recipe_display_processors(self):
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ browser = self.getViewBrowser(
+ oci_project, view_name="+new-recipe", user=self.person)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(
+ ["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
+ [extract_text(option) for option in processors.displayOptions])
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
+
+ def test_create_new_recipe_display_restricted_processors(self):
+ # A restricted processor is shown disabled in the UI.
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ proc_armhf = self.factory.makeProcessor(
+ name="armhf", restricted=True, build_by_default=False)
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag="armhf",
+ processor=proc_armhf)
+ browser = self.getViewBrowser(
+ oci_project, view_name="+new-recipe", user=self.person)
+ processors = browser.getControl(name="field.processors")
+ self.assertProcessorControls(
+ processors, ["386", "amd64", "hppa"], ["armhf"])
+
+ def test_create_new_recipe_processors(self):
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ [git_ref] = self.factory.makeGitRefs()
+ browser = self.getViewBrowser(
+ oci_project, view_name="+new-recipe", user=self.person)
+ processors = browser.getControl(name="field.processors")
+ processors.value = ["386", "amd64"]
+ browser.getControl(name="field.name").value = "recipe-name"
+ browser.getControl("Git repository").value = (
+ git_ref.repository.identity)
+ browser.getControl("Git branch").value = git_ref.path
+ browser.getControl("Create OCI recipe").click()
+ login_person(self.person)
+ recipe = getUtility(IOCIRecipeSet).getByName(
+ self.person, oci_project, "recipe-name")
+ self.assertContentEqual(
+ ["386", "amd64"], [proc.name for proc in recipe.processors])
+
class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
@@ -215,7 +293,35 @@ class TestOCIRecipeEditView(BaseTestOCIRecipeView):
def setUp(self):
super(TestOCIRecipeEditView, self).setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.distroseries = self.factory.makeDistroSeries()
+ self.distribution = self.distroseries.distribution
+ self.useFixture(FeatureFixture({
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s" % self.distribution.name:
+ self.distroseries.name,
+ }))
+
+ def setUpDistroSeries(self):
+ """Set up self.distroseries with some available processors."""
+ processor_names = ["386", "amd64", "hppa"]
+ for name in processor_names:
+ processor = getUtility(IProcessorSet).getByName(name)
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag=name,
+ processor=processor)
+
+ def assertRecipeProcessors(self, recipe, names):
+ self.assertContentEqual(
+ names, [processor.name for processor in recipe.processors])
+
+ def assertProcessorControls(self, processors_control, enabled, disabled):
+ matchers = [
+ MatchesStructure.byEquality(optionValue=name, disabled=False)
+ for name in enabled]
+ matchers.extend([
+ MatchesStructure.byEquality(optionValue=name, disabled=True)
+ for name in disabled])
+ self.assertThat(processors_control.controls, MatchesSetwise(*matchers))
def test_edit_recipe(self):
oci_project = self.factory.makeOCIProject()
@@ -290,6 +396,118 @@ class TestOCIRecipeEditView(BaseTestOCIRecipeView):
"this name." % oci_project_display,
extract_text(find_tags_by_class(browser.contents, "message")[1]))
+ def test_display_processors(self):
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person, oci_project=oci_project)
+ browser = self.getViewBrowser(
+ recipe, view_name="+edit", user=recipe.owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(
+ ["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
+ [extract_text(option) for option in processors.displayOptions])
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
+
+ def test_edit_processors(self):
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person, oci_project=oci_project)
+ self.assertRecipeProcessors(recipe, ["386", "amd64", "hppa"])
+ browser = self.getViewBrowser(
+ recipe, view_name="+edit", user=recipe.owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
+ processors.value = ["386", "amd64"]
+ browser.getControl("Update OCI recipe").click()
+ login_person(self.person)
+ self.assertRecipeProcessors(recipe, ["386", "amd64"])
+
+ def test_edit_with_invisible_processor(self):
+ # It's possible for existing recipes to have an enabled processor
+ # that's no longer usable with the current distroseries, which will
+ # mean it's hidden from the UI, but the non-admin
+ # OCIRecipe.setProcessors isn't allowed to disable it. Editing the
+ # processor list of such a recipe leaves the invisible processor
+ # intact.
+ proc_386 = getUtility(IProcessorSet).getByName("386")
+ proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
+ proc_armel = self.factory.makeProcessor(
+ name="armel", restricted=True, build_by_default=False)
+ self.setUpDistroSeries()
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person, oci_project=oci_project)
+ recipe.setProcessors([proc_386, proc_amd64, proc_armel])
+ browser = self.getViewBrowser(
+ recipe, view_name="+edit", user=recipe.owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64"], processors.value)
+ processors.value = ["amd64"]
+ browser.getControl("Update OCI recipe").click()
+ login_person(self.person)
+ self.assertRecipeProcessors(recipe, ["amd64", "armel"])
+
+ def test_edit_processors_restricted(self):
+ # A restricted processor is shown disabled in the UI and cannot be
+ # enabled.
+ self.setUpDistroSeries()
+ proc_armhf = self.factory.makeProcessor(
+ name="armhf", restricted=True, build_by_default=False)
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag="armhf",
+ processor=proc_armhf)
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person, oci_project=oci_project)
+ self.assertRecipeProcessors(recipe, ["386", "amd64", "hppa"])
+ browser = self.getViewBrowser(
+ recipe, view_name="+edit", user=recipe.owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
+ self.assertProcessorControls(
+ processors, ["386", "amd64", "hppa"], ["armhf"])
+ # Even if the user works around the disabled checkbox and forcibly
+ # enables it, they can't enable the restricted processor.
+ for control in processors.controls:
+ if control.optionValue == "armhf":
+ del control._control.attrs["disabled"]
+ processors.value = ["386", "amd64", "armhf"]
+ self.assertRaises(
+ CannotModifyOCIRecipeProcessor,
+ browser.getControl("Update OCI recipe").click)
+
+ def test_edit_processors_restricted_already_enabled(self):
+ # A restricted processor that is already enabled is shown disabled
+ # in the UI. This causes form submission to omit it, but the
+ # validation code fixes that up behind the scenes so that we don't
+ # get CannotModifyOCIRecipeProcessor.
+ proc_386 = getUtility(IProcessorSet).getByName("386")
+ proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
+ proc_armhf = self.factory.makeProcessor(
+ name="armhf", restricted=True, build_by_default=False)
+ self.setUpDistroSeries()
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag="armhf",
+ processor=proc_armhf)
+ oci_project = self.factory.makeOCIProject(pillar=self.distribution)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person, oci_project=oci_project)
+ recipe.setProcessors([proc_386, proc_amd64, proc_armhf])
+ self.assertRecipeProcessors(recipe, ["386", "amd64", "armhf"])
+ browser = self.getUserBrowser(
+ canonical_url(recipe) + "/+edit", user=recipe.owner)
+ processors = browser.getControl(name="field.processors")
+ # armhf is checked but disabled.
+ self.assertContentEqual(["386", "amd64", "armhf"], processors.value)
+ self.assertProcessorControls(
+ processors, ["386", "amd64", "hppa"], ["armhf"])
+ processors.value = ["386"]
+ browser.getControl("Update OCI recipe").click()
+ login_person(self.person)
+ self.assertRecipeProcessors(recipe, ["386", "armhf"])
+
class TestOCIRecipeDeleteView(BaseTestOCIRecipeView):
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index ca70e38..ed3f9b2 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'CannotModifyOCIRecipeProcessor',
'DuplicateOCIRecipeName',
'IOCIRecipe',
'IOCIRecipeEdit',
@@ -23,9 +24,14 @@ __all__ = [
]
from lazr.restful.declarations import (
+ call_with,
error_status,
export_as_webservice_entry,
+ export_write_operation,
exported,
+ operation_for_version,
+ operation_parameters,
+ REQUEST_USER,
)
from lazr.restful.fields import (
CollectionField,
@@ -33,11 +39,15 @@ from lazr.restful.fields import (
ReferenceChoice,
)
from six.moves import http_client
-from zope.interface import Interface
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
from zope.schema import (
Bool,
Datetime,
Int,
+ List,
Text,
TextLine,
)
@@ -47,8 +57,11 @@ from lp import _
from lp.app.errors import NameLookupFailed
from lp.app.validators.name import name_validator
from lp.app.validators.path import path_does_not_escape
+from lp.buildmaster.interfaces.processor import IProcessor
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.role import IHasOwner
from lp.services.database.constants import DEFAULT
@@ -105,6 +118,19 @@ class NoSourceForOCIRecipe(Exception):
"New OCI recipes must have a git branch and build file.")
+@error_status(http_client.FORBIDDEN)
+class CannotModifyOCIRecipeProcessor(Exception):
+ """Tried to enable or disable a restricted processor on an OCI recipe."""
+
+ _fmt = (
+ '%(processor)s is restricted, and may only be enabled or disabled '
+ 'by administrators.')
+
+ def __init__(self, processor):
+ super(CannotModifyOCIRecipeProcessor, self).__init__(
+ self._fmt % {'processor': processor.name})
+
+
class IOCIRecipeView(Interface):
"""`IOCIRecipe` attributes that require launchpad.View permission."""
@@ -119,6 +145,35 @@ class IOCIRecipeView(Interface):
description=_("The user who registered this recipe."),
vocabulary='ValidPersonOrTeam', required=True, readonly=True))
+ distribution = Reference(
+ IDistribution, title=_("Distribution"),
+ required=True, readonly=True,
+ description=_("The distribution that this recipe is associated with."))
+
+ distro_series = Reference(
+ IDistroSeries, title=_("Distro series"),
+ required=True, readonly=True,
+ description=_("The series for which the recipe should be built."))
+
+ available_processors = Attribute(
+ "The architectures that are available to be enabled or disabled for "
+ "this OCI recipe.")
+
+ @call_with(check_permissions=True, user=REQUEST_USER)
+ @operation_parameters(
+ processors=List(
+ value_type=Reference(schema=IProcessor), required=True))
+ @export_write_operation()
+ @operation_for_version("devel")
+ def setProcessors(processors, check_permissions=False, user=None):
+ """Set the architectures for which the recipe should be built."""
+
+ def getAllowedArchitectures():
+ """Return all distroarchseries that this recipe can build for.
+
+ :return: Sequence of `IDistroArchSeries` instances.
+ """
+
builds = CollectionField(
title=_("Completed builds of this OCI recipe."),
description=_(
@@ -253,6 +308,13 @@ class IOCIRecipeAdminAttributes(Interface):
title=_("Require virtualized builders"), required=True, readonly=False,
description=_("Only build this OCI recipe on virtual builders."))
+ processors = exported(CollectionField(
+ title=_("Processors"),
+ description=_(
+ "The architectures for which the OCI recipe should be built."),
+ value_type=Reference(schema=IProcessor),
+ readonly=False))
+
class IOCIRecipe(IOCIRecipeView, IOCIRecipeEdit, IOCIRecipeEditableAttributes,
IOCIRecipeAdminAttributes):
@@ -267,7 +329,7 @@ class IOCIRecipeSet(Interface):
def new(name, registrant, owner, oci_project, git_ref, build_file,
description=None, official=False, require_virtualized=True,
- build_daily=False, date_created=DEFAULT):
+ build_daily=False, processors=None, date_created=DEFAULT):
"""Create an IOCIRecipe."""
def exists(owner, oci_project, name):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 21a936b..4e3273a 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -27,16 +27,23 @@ from storm.locals import (
Storm,
Unicode,
)
-from zope.component import getUtility
+from zope.component import (
+ getAdapter,
+ getUtility,
+ )
from zope.event import notify
from zope.interface import implementer
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
+from lp.app.interfaces.security import IAuthorization
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.buildmaster.model.processor import Processor
from lp.oci.interfaces.ocirecipe import (
+ CannotModifyOCIRecipeProcessor,
DuplicateOCIRecipeName,
IOCIRecipe,
IOCIRecipeSet,
@@ -51,6 +58,9 @@ from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
from lp.oci.model.ocipushrule import OCIPushRule
from lp.oci.model.ocirecipebuild import OCIRecipeBuild
from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.role import IPersonRoles
+from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.series import ACTIVE_STATUSES
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
@@ -67,6 +77,7 @@ from lp.services.database.stormexpr import (
from lp.services.features import getFeatureFlag
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
+from lp.soyuz.model.distroarchseries import DistroArchSeries
def oci_recipe_modified(recipe, event):
@@ -141,6 +152,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
# XXX twom 2019-11-26 This needs to expand as more build artifacts
# are added
store = IStore(OCIRecipe)
+ store.find(OCIRecipeArch, OCIRecipeArch.recipe == self).remove()
buildqueue_records = store.find(
BuildQueue,
BuildQueue._build_farm_job_id == OCIRecipeBuild.build_farm_job_id,
@@ -172,6 +184,112 @@ class OCIRecipe(Storm, WebhookTargetMixin):
self.git_repository = None
self.git_path = None
+ @property
+ def distribution(self):
+ # XXX twom 2019-12-05 This may need to change when an OCIProject
+ # pillar isn't just a distribution
+ return self.oci_project.distribution
+
+ @property
+ def distro_series(self):
+ # For OCI builds we default to the series set by the feature flag.
+ # If the feature flag is not set we default to the current series of
+ # the recipe's distribution.
+ oci_series = getFeatureFlag('oci.build_series.%s'
+ % self.distribution.name)
+ if oci_series:
+ return self.distribution.getSeries(oci_series)
+ else:
+ return self.distribution.currentseries
+
+ @property
+ def available_processors(self):
+ """See `IOCIRecipe`."""
+ clauses = [Processor.id == DistroArchSeries.processor_id]
+ if self.distro_series is not None:
+ clauses.append(DistroArchSeries.id.is_in(
+ self.distro_series.enabled_architectures.get_select_expr(
+ DistroArchSeries.id)))
+ else:
+ # We might not know the series if the OCI project's distribution
+ # has no series at all, which can happen in tests. Fall back to
+ # just returning enabled architectures for any active series,
+ # which is a bit of a hack but works.
+ clauses.extend([
+ DistroArchSeries.enabled,
+ DistroArchSeries.distroseriesID == DistroSeries.id,
+ DistroSeries.status.is_in(ACTIVE_STATUSES),
+ ])
+ return Store.of(self).find(Processor, *clauses).config(distinct=True)
+
+ def _getProcessors(self):
+ return list(Store.of(self).find(
+ Processor,
+ Processor.id == OCIRecipeArch.processor_id,
+ OCIRecipeArch.recipe == self))
+
+ def setProcessors(self, processors, check_permissions=False, user=None):
+ """See `IOCIRecipe`."""
+ if check_permissions:
+ can_modify = None
+ if user is not None:
+ roles = IPersonRoles(user)
+ authz = lambda perm: getAdapter(self, IAuthorization, perm)
+ if authz('launchpad.Admin').checkAuthenticated(roles):
+ can_modify = lambda proc: True
+ elif authz('launchpad.Edit').checkAuthenticated(roles):
+ can_modify = lambda proc: not proc.restricted
+ if can_modify is None:
+ raise Unauthorized(
+ 'Permission launchpad.Admin or launchpad.Edit required '
+ 'on %s.' % self)
+ else:
+ can_modify = lambda proc: True
+
+ enablements = dict(Store.of(self).find(
+ (Processor, OCIRecipeArch),
+ Processor.id == OCIRecipeArch.processor_id,
+ OCIRecipeArch.recipe == self))
+ for proc in enablements:
+ if proc not in processors:
+ if not can_modify(proc):
+ raise CannotModifyOCIRecipeProcessor(proc)
+ Store.of(self).remove(enablements[proc])
+ for proc in processors:
+ if proc not in self.processors:
+ if not can_modify(proc):
+ raise CannotModifyOCIRecipeProcessor(proc)
+ Store.of(self).add(OCIRecipeArch(self, proc))
+
+ processors = property(_getProcessors, setProcessors)
+
+ def _isBuildableArchitectureAllowed(self, das):
+ """Check whether we may build for a buildable `DistroArchSeries`.
+
+ The caller is assumed to have already checked that a suitable chroot
+ is available (either directly or via
+ `DistroSeries.buildable_architectures`).
+ """
+ return (
+ das.enabled
+ and das.processor in self.processors
+ and (
+ das.processor.supports_virtualized
+ or not self.require_virtualized))
+
+ def _isArchitectureAllowed(self, das, pocket):
+ return (
+ das.getChroot(pocket=pocket) is not None
+ and self._isBuildableArchitectureAllowed(das))
+
+ def getAllowedArchitectures(self, distro_series=None):
+ """See `IOCIRecipe`."""
+ if distro_series is None:
+ distro_series = self.distro_series
+ return [
+ das for das in distro_series.buildable_architectures
+ if self._isBuildableArchitectureAllowed(das)]
+
def _checkRequestBuild(self, requester):
if not requester.inTeam(self.owner):
raise OCIRecipeNotOwner(
@@ -282,7 +400,7 @@ class OCIRecipeSet:
def new(self, name, registrant, owner, oci_project, git_ref, build_file,
description=None, official=False, require_virtualized=True,
- build_daily=False, date_created=DEFAULT):
+ build_daily=False, processors=None, date_created=DEFAULT):
"""See `IOCIRecipeSet`."""
if not registrant.inTeam(owner):
if owner.is_team:
@@ -307,6 +425,12 @@ class OCIRecipeSet:
date_created)
store.add(oci_recipe)
+ if processors is None:
+ processors = [
+ p for p in oci_recipe.available_processors
+ if p.build_by_default]
+ oci_recipe.setProcessors(processors)
+
return oci_recipe
def _getByName(self, owner, oci_project, name):
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index 12046d6..d8bd496 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -55,7 +55,6 @@ from lp.services.database.interfaces import (
IMasterStore,
IStore,
)
-from lp.services.features import getFeatureFlag
from lp.services.librarian.model import (
LibraryFileAlias,
LibraryFileContent,
@@ -139,13 +138,6 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
# it is not a target, nor referenced in the final build.
pocket = PackagePublishingPocket.UPDATES
- @property
- def distro_series(self):
- # XXX twom 2020-02-14 - This really needs to be set elsewhere,
- # as this may not be an LTS release and ties the OCI target to
- # a completely unrelated process.
- return self.distribution.currentseries
-
def __init__(self, build_farm_job, requester, recipe,
processor, virtualized, date_created):
@@ -245,27 +237,20 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
def archive(self):
# XXX twom 2019-12-05 This may need to change when an OCIProject
# pillar isn't just a distribution
- return self.recipe.oci_project.distribution.main_archive
+ return self.recipe.distribution.main_archive
@property
def distribution(self):
- # XXX twom 2019-12-05 This may need to change when an OCIProject
- # pillar isn't just a distribution
- return self.recipe.oci_project.distribution
+ return self.recipe.distribution
+
+ @property
+ def distro_series(self):
+ return self.recipe.distro_series
@property
def distro_arch_series(self):
- # For OCI builds we default to the series set by the feature flag.
- # If the feature flag is not set we default to current series under
- # the OCIRecipeBuild distribution.
-
- oci_series = getFeatureFlag('oci.build_series.%s'
- % self.distribution.name)
- if oci_series:
- oci_series = self.distribution.getSeries(oci_series)
- else:
- oci_series = self.distribution.currentseries
- return oci_series.getDistroArchSeriesByProcessor(self.processor)
+ return self.recipe.distro_series.getDistroArchSeriesByProcessor(
+ self.processor)
def updateStatus(self, status, builder=None, slave_status=None,
date_started=None, date_finished=None,
diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
index a2d3809..e5f2686 100644
--- a/lib/lp/oci/templates/ocirecipe-new.pt
+++ b/lib/lp/oci/templates/ocirecipe-new.pt
@@ -32,6 +32,9 @@
<tal:widget define="widget nocall:view/widgets/build_daily">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
+ <tal:widget define="widget nocall:view/widgets/processors">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
</table>
</metal:formbody>
</div>
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index e15dd8e..e158e2e 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -23,7 +23,9 @@ from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.oci.interfaces.ocirecipe import (
+ CannotModifyOCIRecipeProcessor,
DuplicateOCIRecipeName,
IOCIRecipe,
IOCIRecipeSet,
@@ -217,6 +219,127 @@ class TestOCIRecipe(TestCaseWithFactory):
self.assertEqual(rule.recipe, oci_recipe)
+class TestOCIRecipeProcessors(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestOCIRecipeProcessors, self).setUp(
+ user="foo.bar@xxxxxxxxxxxxx")
+ self.default_procs = [
+ getUtility(IProcessorSet).getByName("386"),
+ getUtility(IProcessorSet).getByName("amd64")]
+ self.unrestricted_procs = (
+ self.default_procs + [getUtility(IProcessorSet).getByName("hppa")])
+ self.arm = self.factory.makeProcessor(
+ name="arm", restricted=True, build_by_default=False)
+ self.distroseries = self.factory.makeDistroSeries()
+ self.useFixture(FeatureFixture({
+ OCI_RECIPE_ALLOW_CREATE: "on",
+ "oci.build_series.%s" % self.distroseries.distribution.name:
+ self.distroseries.name
+ }))
+
+ def test_available_processors(self):
+ # Only those processors that are enabled for the recipe's
+ # distroseries are available.
+ for processor in self.default_procs:
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag=processor.name,
+ processor=processor)
+ self.factory.makeDistroArchSeries(
+ architecturetag=self.arm.name, processor=self.arm)
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
+ recipe = self.factory.makeOCIRecipe(oci_project=oci_project)
+ self.assertContentEqual(
+ self.default_procs, recipe.available_processors)
+
+ def test_new_default_processors(self):
+ # OCIRecipeSet.new creates an OCIRecipeArch for each available
+ # Processor with build_by_default set.
+ new_procs = [
+ self.factory.makeProcessor(name="default", build_by_default=True),
+ self.factory.makeProcessor(
+ name="nondefault", build_by_default=False),
+ ]
+ owner = self.factory.makePerson()
+ for processor in self.unrestricted_procs + [self.arm] + new_procs:
+ self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag=processor.name,
+ processor=processor)
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
+ recipe = getUtility(IOCIRecipeSet).new(
+ name=self.factory.getUniqueUnicode(), registrant=owner,
+ owner=owner, oci_project=oci_project,
+ git_ref=self.factory.makeGitRefs()[0],
+ build_file=self.factory.getUniqueUnicode())
+ self.assertContentEqual(
+ ["386", "amd64", "hppa", "default"],
+ [processor.name for processor in recipe.processors])
+
+ def test_new_override_processors(self):
+ # OCIRecipeSet.new can be given a custom set of processors.
+ owner = self.factory.makePerson()
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
+ recipe = getUtility(IOCIRecipeSet).new(
+ name=self.factory.getUniqueUnicode(), registrant=owner,
+ owner=owner, oci_project=oci_project,
+ git_ref=self.factory.makeGitRefs()[0],
+ build_file=self.factory.getUniqueUnicode(), processors=[self.arm])
+ self.assertContentEqual(
+ ["arm"], [processor.name for processor in recipe.processors])
+
+ def test_set(self):
+ # The property remembers its value correctly.
+ recipe = self.factory.makeOCIRecipe()
+ recipe.setProcessors([self.arm])
+ self.assertContentEqual([self.arm], recipe.processors)
+ recipe.setProcessors(self.unrestricted_procs + [self.arm])
+ self.assertContentEqual(
+ self.unrestricted_procs + [self.arm], recipe.processors)
+ recipe.setProcessors([])
+ self.assertContentEqual([], recipe.processors)
+
+ def test_set_non_admin(self):
+ """Non-admins can only enable or disable unrestricted processors."""
+ recipe = self.factory.makeOCIRecipe()
+ recipe.setProcessors(self.default_procs)
+ self.assertContentEqual(self.default_procs, recipe.processors)
+ with person_logged_in(recipe.owner) as owner:
+ # Adding arm is forbidden ...
+ self.assertRaises(
+ CannotModifyOCIRecipeProcessor, recipe.setProcessors,
+ [self.default_procs[0], self.arm],
+ check_permissions=True, user=owner)
+ # ... but removing amd64 is OK.
+ recipe.setProcessors(
+ [self.default_procs[0]], check_permissions=True, user=owner)
+ self.assertContentEqual([self.default_procs[0]], recipe.processors)
+ with admin_logged_in() as admin:
+ recipe.setProcessors(
+ [self.default_procs[0], self.arm],
+ check_permissions=True, user=admin)
+ self.assertContentEqual(
+ [self.default_procs[0], self.arm], recipe.processors)
+ with person_logged_in(recipe.owner) as owner:
+ hppa = getUtility(IProcessorSet).getByName("hppa")
+ self.assertFalse(hppa.restricted)
+ # Adding hppa while removing arm is forbidden ...
+ self.assertRaises(
+ CannotModifyOCIRecipeProcessor, recipe.setProcessors,
+ [self.default_procs[0], hppa],
+ check_permissions=True, user=owner)
+ # ... but adding hppa while retaining arm is OK.
+ recipe.setProcessors(
+ [self.default_procs[0], self.arm, hppa],
+ check_permissions=True, user=owner)
+ self.assertContentEqual(
+ [self.default_procs[0], self.arm, hppa], recipe.processors)
+
+
class TestOCIRecipeSet(TestCaseWithFactory):
layer = DatabaseFunctionalLayer