From e1e5013ad5bfef9e3ee9b523e0241eafa860c93f Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Wed, 4 Aug 2021 14:28:34 -0600 Subject: [PATCH 1/3] mirror: s/is_default/mirror_is_default Clarify that the mirror is the default, as there are other things under mirror that are not relevant to this determination. --- subiquity/models/mirror.py | 4 ++-- subiquity/server/controllers/mirror.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/subiquity/models/mirror.py b/subiquity/models/mirror.py index 96461ed6..31931ea8 100644 --- a/subiquity/models/mirror.py +++ b/subiquity/models/mirror.py @@ -52,11 +52,11 @@ class MirrorModel(object): self.architecture = get_architecture() self.default_mirror = self.get_mirror() - def is_default(self): + def mirror_is_default(self): return self.get_mirror() == self.default_mirror def set_country(self, cc): - if not self.is_default(): + if not self.mirror_is_default(): return uri = self.get_mirror() parsed = parse.urlparse(uri) diff --git a/subiquity/server/controllers/mirror.py b/subiquity/server/controllers/mirror.py index 6c23785a..67afdfe6 100644 --- a/subiquity/server/controllers/mirror.py +++ b/subiquity/server/controllers/mirror.py @@ -55,7 +55,7 @@ class MirrorController(SubiquityController): return geoip = data.pop('geoip', True) merge_config(self.model.config, data) - self.geoip_enabled = geoip and self.model.is_default() + self.geoip_enabled = geoip and self.model.mirror_is_default() @with_context() async def apply_autoinstall_config(self, context): From e32b7f28ac3d4abc1f4cd77c913ad77a38599980 Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Wed, 4 Aug 2021 14:27:32 -0600 Subject: [PATCH 2/3] mirror: add disable_components API --- autoinstall-schema.json | 13 +++++++ subiquity/common/apidef.py | 9 ++++- subiquity/models/mirror.py | 6 +++ subiquity/models/tests/test_mirror.py | 10 +++++ subiquity/server/apt.py | 2 +- subiquity/server/controllers/mirror.py | 17 ++++++++- .../server/controllers/tests/test_mirror.py | 38 +++++++++++++++++++ 7 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 subiquity/server/controllers/tests/test_mirror.py diff --git a/autoinstall-schema.json b/autoinstall-schema.json index 31f22696..5e27b33b 100644 --- a/autoinstall-schema.json +++ b/autoinstall-schema.json @@ -281,6 +281,19 @@ }, "sources": { "type": "object" + }, + "disable_components": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "universe", + "multiverse", + "restricted", + "contrib", + "non-free" + ] + } } } }, diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index de9c932c..aa538876 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -60,7 +60,6 @@ class API: """The API offered by the subiquity installer process.""" identity = simple_endpoint(IdentityData) locale = simple_endpoint(str) - mirror = simple_endpoint(str) proxy = simple_endpoint(str) ssh = simple_endpoint(SSHData) updates = simple_endpoint(str) @@ -297,6 +296,14 @@ class API: class shutdown: def POST(mode: ShutdownMode, immediate: bool = False): ... + class mirror: + def GET() -> str: ... + def POST(data: Payload[str]): ... + + class disable_components: + def GET() -> List[str]: ... + def POST(data: Payload[List[str]]): ... + class LinkAction(enum.Enum): NEW = enum.auto() diff --git a/subiquity/models/mirror.py b/subiquity/models/mirror.py index 31931ea8..af57cb5c 100644 --- a/subiquity/models/mirror.py +++ b/subiquity/models/mirror.py @@ -51,6 +51,12 @@ class MirrorModel(object): self.config = copy.deepcopy(DEFAULT) self.architecture = get_architecture() self.default_mirror = self.get_mirror() + self.disable_components = set() + + def get_config(self): + config = copy.deepcopy(self.config) + config['disable_components'] = list(self.disable_components) + return config def mirror_is_default(self): return self.get_mirror() == self.default_mirror diff --git a/subiquity/models/tests/test_mirror.py b/subiquity/models/tests/test_mirror.py index 8a5bf424..1a23087f 100644 --- a/subiquity/models/tests/test_mirror.py +++ b/subiquity/models/tests/test_mirror.py @@ -42,3 +42,13 @@ class TestMirrorModel(unittest.TestCase): model.set_mirror("http://mymirror.invalid/") model.set_country("CC") self.assertEqual(model.get_mirror(), "http://mymirror.invalid/") + + def test_default_disable_components(self): + config = MirrorModel().get_config() + self.assertEqual([], config['disable_components']) + + def test_set_disable_components(self): + model = MirrorModel() + model.disable_components = set(['universe']) + config = model.get_config() + self.assertEqual(['universe'], config['disable_components']) diff --git a/subiquity/server/apt.py b/subiquity/server/apt.py index 2f9536aa..bf520c10 100644 --- a/subiquity/server/apt.py +++ b/subiquity/server/apt.py @@ -104,7 +104,7 @@ class AptConfigurer: config_upper = await self.setup_overlay(self.source, self.configured) config = { - 'apt': self.app.base_model.mirror.config, + 'apt': self.app.base_model.mirror.get_config(), } config_location = os.path.join( self.app.root, 'var/log/installer/subiquity-curtin-apt.conf') diff --git a/subiquity/server/controllers/mirror.py b/subiquity/server/controllers/mirror.py index 67afdfe6..e623ba4c 100644 --- a/subiquity/server/controllers/mirror.py +++ b/subiquity/server/controllers/mirror.py @@ -16,6 +16,7 @@ import asyncio import copy import logging +from typing import List from curtin.config import merge_config @@ -40,8 +41,16 @@ class MirrorController(SubiquityController): 'primary': {'type': 'array'}, 'geoip': {'type': 'boolean'}, 'sources': {'type': 'object'}, - }, + 'disable_components': { + 'type': 'array', + 'items': { + 'type': 'string', + 'enum': ['universe', 'multiverse', 'restricted', + 'contrib', 'non-free'] + } + } } + } model_name = "mirror" def __init__(self, app): @@ -89,3 +98,9 @@ class MirrorController(SubiquityController): async def POST(self, data: str): self.model.set_mirror(data) await self.configured() + + async def disable_components_GET(self) -> List[str]: + return list(self.model.disable_components) + + async def disable_components_POST(self, data: List[str]): + self.model.disable_components = set(data) diff --git a/subiquity/server/controllers/tests/test_mirror.py b/subiquity/server/controllers/tests/test_mirror.py new file mode 100644 index 00000000..475315a5 --- /dev/null +++ b/subiquity/server/controllers/tests/test_mirror.py @@ -0,0 +1,38 @@ +# Copyright 2021 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import jsonschema +import unittest + +from subiquity.server.controllers.mirror import MirrorController + + +class TestMirrorSchema(unittest.TestCase): + def validate(self, data): + jsonschema.validate(data, MirrorController.autoinstall_schema) + + def test_empty(self): + self.validate({}) + + def test_disable_components(self): + self.validate({'disable_components': ['universe']}) + + def test_no_disable_main(self): + with self.assertRaises(jsonschema.ValidationError): + self.validate({'disable_components': ['main']}) + + def test_no_disable_random_junk(self): + with self.assertRaises(jsonschema.ValidationError): + self.validate({'disable_components': ['not-a-component']}) From 1b74779aef24e4a781225196a87049ce59e9d98f Mon Sep 17 00:00:00 2001 From: Dan Bungert Date: Mon, 22 Nov 2021 14:45:59 -0700 Subject: [PATCH 3/3] api: add free_only --- subiquity/common/apidef.py | 8 ++++++++ subiquity/server/server.py | 13 +++++++++++++ subiquity/tests/api/test_api.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py index aa538876..845248dd 100644 --- a/subiquity/common/apidef.py +++ b/subiquity/common/apidef.py @@ -95,6 +95,14 @@ class API: class ssh_info: def GET() -> Optional[LiveSessionSSHInfo]: ... + class free_only: + def GET() -> bool: ... + + def POST(enable: bool) -> None: + """Enable or disable free-only mode. Currently only controlls + the list of components. free-only choice must be made prior to + confirmation of filesystem changes""" + class errors: class wait: def GET(error_ref: ErrorReportRef) -> ErrorReportRef: diff --git a/subiquity/server/server.py b/subiquity/server/server.py index 5e377c98..aaf59eaf 100644 --- a/subiquity/server/server.py +++ b/subiquity/server/server.py @@ -86,6 +86,7 @@ class MetaController: def __init__(self, app): self.app = app self.context = app.context.child("Meta") + self.free_only = False async def status_GET(self, cur: Optional[ApplicationState] = None) \ -> ApplicationStatus: @@ -158,6 +159,18 @@ class MetaController: ips=ips, host_key_fingerprints=host_fingerprints) + async def free_only_GET(self) -> bool: + return self.free_only + + async def free_only_POST(self, enable: bool) -> None: + self.free_only = enable + to_disable = {'restricted', 'multiverse'} + if enable: + # enabling free only mode means disabling components + self.app.base_model.mirror.disable_components |= to_disable + else: + self.app.base_model.mirror.disable_components -= to_disable + def get_installer_password_from_cloudinit_log(): try: diff --git a/subiquity/tests/api/test_api.py b/subiquity/tests/api/test_api.py index 09092233..6ea0117f 100755 --- a/subiquity/tests/api/test_api.py +++ b/subiquity/tests/api/test_api.py @@ -667,6 +667,25 @@ class TestInfo(TestAPI): self.assertEqual('/dev/sda', sda['path']) +class TestFree(TestAPI): + @timeout(5) + async def test_free_only(self): + async with start_server('examples/simple.json') as inst: + await inst.post('/meta/free_only', enable=True) + components = await inst.get('/mirror/disable_components') + components.sort() + self.assertEqual(['multiverse', 'restricted'], components) + + @timeout(5) + async def test_not_free_only(self): + async with start_server('examples/simple.json') as inst: + comps = ['universe', 'multiverse'] + await inst.post('/mirror/disable_components', comps) + await inst.post('/meta/free_only', enable=False) + components = await inst.get('/mirror/disable_components') + self.assertEqual(['universe'], components) + + class TestRegression(TestAPI): @timeout(5) async def test_edit_not_trigger_boot_device(self):