feat(cloud): Add new cloud package set

The new cloud package set is a JSON file which has proper mapping of
source packages that Britney has to the binary packages that
cloud-test-framework expects.
This commit is contained in:
Aleksa Svitlica 2023-04-05 09:25:09 -04:00
parent b0a569d303
commit 04dc74a8cf
3 changed files with 5818 additions and 36 deletions

View File

@ -48,7 +48,7 @@ If you have any questions about this email, please ask them in #ubuntu-release c
Regards, Ubuntu Release Team. Regards, Ubuntu Release Team.
""" """
class CloudPolicy(BasePolicy): class CloudPolicy(BasePolicy):
PACKAGE_SET_FILE = "cloud_package_set" PACKAGE_SET_FILE = "cloud_package_set.json"
STATE_FILE = "cloud_state" STATE_FILE = "cloud_state"
DEFAULT_EMAILS = ["cpc@canonical.com"] DEFAULT_EMAILS = ["cpc@canonical.com"]
TEST_LOG_FILE = "CTF.log" TEST_LOG_FILE = "CTF.log"
@ -89,16 +89,21 @@ class CloudPolicy(BasePolicy):
self.failures = {} self.failures = {}
self.errors = {} self.errors = {}
self.email_needed = False self.email_needed = False
self.package_set = {}
def initialise(self, britney): def initialise(self, britney):
super().initialise(britney) super().initialise(britney)
self.package_set = self._retrieve_cloud_package_set_for_series(self.options.series) self.package_set = self._retrieve_cloud_package_set_for_series(
self.PACKAGE_SET_FILE,
self.options.series
)
self._load_state() self._load_state()
def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse): def apply_src_policy_impl(self, policy_info, item, source_data_tdist, source_data_srcdist, excuse):
self.logger.info("Cloud Policy: Looking at {}".format(item.package)) self.logger.info("Cloud Policy: Looking at {}".format(item.package))
if item.package not in self.package_set: clouds_to_test = self._check_if_tests_required(self.package_set, item.package)
if len(clouds_to_test) == 0:
verdict = PolicyVerdict.PASS verdict = PolicyVerdict.PASS
excuse.add_verdict_info( excuse.add_verdict_info(
verdict, verdict,
@ -119,7 +124,7 @@ class CloudPolicy(BasePolicy):
if self.reporting_enabled == "yes": if self.reporting_enabled == "yes":
self._report_test_start(item.package, source_data_srcdist.version, self.options.series) self._report_test_start(item.package, source_data_srcdist.version, self.options.series)
self._run_cloud_tests(item.package, source_data_srcdist.version, self.options.series, self._run_cloud_tests(clouds_to_test, item.package, source_data_srcdist.version, self.options.series,
self.sources, self.source_type) self.sources, self.source_type)
if len(self.failures) > 0 or len(self.errors) > 0: if len(self.failures) > 0 or len(self.errors) > 0:
@ -223,34 +228,55 @@ class CloudPolicy(BasePolicy):
json.dump(self.state, data) json.dump(self.state, data)
self.logger.info("Saved cloud policy state file %s" % self.state_filename) self.logger.info("Saved cloud policy state file %s" % self.state_filename)
def _retrieve_cloud_package_set_for_series(self, series): def _retrieve_cloud_package_set_for_series(self, file_path, series):
"""Retrieves a set of packages for the given series in which cloud """Retrieves a set of packages for the given series in which cloud
tests should be run. tests should be run.
Temporarily a static list retrieved from file. Will be updated to :param file_path The path to the package set file.
retrieve from a database at a later date.
:param series The Ubuntu codename for the series (e.g. jammy) :param series The Ubuntu codename for the series (e.g. jammy)
""" """
package_set = set() package_set = {}
with open(self.PACKAGE_SET_FILE) as file: with open(file_path) as file:
for line in file: full_package_set = json.load(file)
package_set.add(line.strip()) for cloud, cloud_set in full_package_set.items():
for cloud_series, series_set in cloud_set.items():
if cloud_series == series:
package_set[cloud] = series_set
return package_set return package_set
def _run_cloud_tests(self, package, version, series, sources, source_type): def _check_if_tests_required(self, package_set, package):
"""Checks the package set to see if the package is present and therefore tests are required.
Returns a list of the clouds which contain the package.
:param package_set The cloud package set as a dictionary.
:param package The name of the source package.
:returns A list of clouds which require tests for this package.
"""
clouds_to_test = []
for cloud, packages in package_set.items():
if package in packages:
clouds_to_test.append(cloud)
return clouds_to_test
def _run_cloud_tests(self, clouds, package, version, series, sources, source_type):
"""Runs any cloud tests for the given package. """Runs any cloud tests for the given package.
Nothing is returned but test failures and errors are stored in instance variables. Nothing is returned but test failures and errors are stored in instance variables.
:param clouds A list of clouds which need tests run
:param package The name of the package to test :param package The name of the package to test
:param version Version of the package :param version Version of the package
:param series The Ubuntu codename for the series (e.g. jammy) :param series The Ubuntu codename for the series (e.g. jammy)
:param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs) :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
:param source_type Either 'archive' or 'ppa' :param source_type Either 'archive' or 'ppa'
""" """
for cloud in clouds:
if cloud == "azure":
self._run_azure_tests(package, version, series, sources, source_type) self._run_azure_tests(package, version, series, sources, source_type)
else:
raise RuntimeError("Cloud Policy: Unexpected cloud to test: {}".format(cloud))
def _send_emails_if_needed(self, package, version, series): def _send_emails_if_needed(self, package, version, series):
"""Sends email(s) if there are test failures and/or errors """Sends email(s) if there are test failures and/or errors
@ -290,13 +316,14 @@ class CloudPolicy(BasePolicy):
return return
urn = self._retrieve_urn(series) urn = self._retrieve_urn(series)
binaries = self._retrieve_binaries("azure", package)
self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series)) self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series))
params = [ params = [
"/snap/bin/cloud-test-framework", "/snap/bin/cloud-test-framework",
"--instance-prefix", "britney-{}-{}".format(package, series) "--instance-prefix", "britney-{}-{}".format(package, series)
] ]
params.extend(self._format_install_flags(package, sources, source_type)) params.extend(self._format_install_flags(binaries, sources, source_type))
params.extend( params.extend(
[ [
"azure_gen2", "azure_gen2",
@ -341,6 +368,14 @@ class CloudPolicy(BasePolicy):
return urn return urn
def _retrieve_binaries(self, cloud, source_package):
"""Given a source package name and cloud, retrieves the associated binary package names from the package set.
:param source_package The name of a source package.
:returns The list of binary package names.
"""
return self.package_set[cloud][source_package]
def _find_results_files(self, file_regex): def _find_results_files(self, file_regex):
"""Find any test results files that match the given regex pattern. """Find any test results files that match the given regex pattern.
@ -503,25 +538,28 @@ class CloudPolicy(BasePolicy):
return info return info
def _format_install_flags(self, package, sources, source_type): def _format_install_flags(self, binaries, sources, source_type):
"""Determine the flags required to install the package from the given sources """Determine the flags required to install the package from the given sources
:param package The name of the package to test :param binaries A list of binary package names
:param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs) :param sources List of sources where the package should be installed from (e.g. [proposed] or PPAs)
:param source_type Either 'archive' or 'ppa' :param source_type Either 'archive' or 'ppa'
""" """
install_flags = [] install_flags = []
for source in sources: for source in sources:
flag = ""
if source_type == "archive": if source_type == "archive":
install_flags.append("--install-archive-package") flag = "--install-archive-package"
install_flags.append("{}/{}".format(package, source))
elif source_type == "ppa": elif source_type == "ppa":
install_flags.append("--install-ppa-package") flag = "--install-ppa-package"
install_flags.append("{}/{}".format(package, source))
else: else:
raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type)) raise RuntimeError("Cloud Policy: Unexpected source type, {}".format(source_type))
for binary in binaries:
install_flags.append(flag)
install_flags.append("{}/{}".format(binary, source))
return install_flags return install_flags
def _parse_ppas(self, ppas): def _parse_ppas(self, ppas):

5686
cloud_package_set.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import sys
from types import SimpleNamespace from types import SimpleNamespace
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import tempfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -51,6 +52,7 @@ class T(unittest.TestCase):
def test_run_cloud_tests_state_handling(self, mock_run, mock_xunit, mock_extra): def test_run_cloud_tests_state_handling(self, mock_run, mock_xunit, mock_extra):
"""Cloud tests should save state and not re-run tests for packages """Cloud tests should save state and not re-run tests for packages
already tested.""" already tested."""
self.policy.package_set = {"azure": {"chromium-browser": ["binary1"], "hello": ["binary2"]}}
expected_state = { expected_state = {
"azure": { "azure": {
"archive": { "archive": {
@ -71,7 +73,7 @@ class T(unittest.TestCase):
# Package already tested, no tests should run # Package already tested, no tests should run
self.policy.failures = {} self.policy.failures = {}
self.policy.errors = {} self.policy.errors = {}
self.policy._run_cloud_tests("chromium-browser", "55.0", "zazzy", ["proposed"], "archive") self.policy._run_cloud_tests(["azure"], "chromium-browser", "55.0", "zazzy", ["proposed"], "archive")
self.assertDictEqual(expected_state, self.policy.state) self.assertDictEqual(expected_state, self.policy.state)
mock_run.assert_not_called() mock_run.assert_not_called()
self.assertEqual(len(self.policy.failures), 1) self.assertEqual(len(self.policy.failures), 1)
@ -85,7 +87,7 @@ class T(unittest.TestCase):
} }
self.policy.failures = {} self.policy.failures = {}
self.policy.errors = {} self.policy.errors = {}
self.policy._run_cloud_tests("hello", "2.10", "zazzy", ["proposed"], "archive") self.policy._run_cloud_tests(["azure"], "hello", "2.10", "zazzy", ["proposed"], "archive")
self.assertDictEqual(expected_state, self.policy.state) self.assertDictEqual(expected_state, self.policy.state)
mock_run.assert_called() mock_run.assert_called()
self.assertEqual(len(self.policy.failures), 0) self.assertEqual(len(self.policy.failures), 0)
@ -99,7 +101,7 @@ class T(unittest.TestCase):
} }
self.policy.failures = {} self.policy.failures = {}
self.policy.errors = {} self.policy.errors = {}
self.policy._run_cloud_tests("chromium-browser", "55.1", "zazzy", ["proposed"], "archive") self.policy._run_cloud_tests(["azure"], "chromium-browser", "55.1", "zazzy", ["proposed"], "archive")
self.assertDictEqual(expected_state, self.policy.state) self.assertDictEqual(expected_state, self.policy.state)
self.assertEqual(mock_run.call_count, 2) self.assertEqual(mock_run.call_count, 2)
self.assertEqual(len(self.policy.failures), 0) self.assertEqual(len(self.policy.failures), 0)
@ -115,6 +117,7 @@ class T(unittest.TestCase):
def test_run_cloud_tests_state_handling_only_errors(self, mock_run, mock_xunit, mock_extra): def test_run_cloud_tests_state_handling_only_errors(self, mock_run, mock_xunit, mock_extra):
"""Cloud tests should save state and not re-run tests for packages """Cloud tests should save state and not re-run tests for packages
already tested.""" already tested."""
self.policy.package_set = {"azure": {"chromium-browser": ["binary1"]}}
start_state = { start_state = {
"azure": { "azure": {
"archive": { "archive": {
@ -133,14 +136,16 @@ class T(unittest.TestCase):
self.policy._load_state() self.policy._load_state()
# Package already tested, but only had errors - rerun # Package already tested, but only had errors - rerun
self.policy._run_cloud_tests("chromium-browser", "55.0", "zazzy", ["proposed"], "archive") self.policy._run_cloud_tests(["azure"], "chromium-browser", "55.0", "zazzy", ["proposed"], "archive")
mock_run.assert_called() mock_run.assert_called()
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run): def test_run_cloud_tests_called_for_package_in_manifest(self, mock_run):
"""Cloud tests should run for a package in the cloud package set. """Cloud tests should run for a package in the cloud package set.
""" """
self.policy.package_set = set(["chromium-browser"]) self.policy.package_set = {
"acloud": {"chromium-browser": []}
}
self.policy.options.series = "jammy" self.policy.options.series = "jammy"
self.policy.apply_src_policy_impl( self.policy.apply_src_policy_impl(
@ -148,15 +153,16 @@ class T(unittest.TestCase):
) )
mock_run.assert_called_once_with( mock_run.assert_called_once_with(
"chromium-browser", "55.0", "jammy", ["proposed"], "archive" ["acloud"], "chromium-browser", "55.0", "jammy", ["proposed"], "archive"
) )
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
def test_run_cloud_tests_not_called_for_package_not_in_manifest(self, mock_run): def test_run_cloud_tests_not_called_for_package_not_in_manifest(self, mock_run):
"""Cloud tests should not run for packages not in the cloud package set""" """Cloud tests should not run for packages not in the cloud package set"""
self.policy.package_set = set(["vim"]) self.policy.package_set = {
self.policy.options.series = "jammy" "acloud": {"vim": []}
}
verdict = self.policy.apply_src_policy_impl( verdict = self.policy.apply_src_policy_impl(
None, FakeItem, None, FakeSourceData, MagicMock() None, FakeItem, None, FakeSourceData, MagicMock()
@ -169,7 +175,9 @@ class T(unittest.TestCase):
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests") @patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
def test_no_tests_run_during_dry_run(self, mock_run, smtp): def test_no_tests_run_during_dry_run(self, mock_run, smtp):
self.policy = CloudPolicy(self.fake_options, {}, dry_run=True) self.policy = CloudPolicy(self.fake_options, {}, dry_run=True)
self.policy.package_set = set(["chromium-browser"]) self.policy.package_set = {
"acloud": {"chromium-browser": []}
}
self.policy.options.series = "jammy" self.policy.options.series = "jammy"
self.policy.source = "jammy-proposed" self.policy.source = "jammy-proposed"
@ -187,7 +195,9 @@ class T(unittest.TestCase):
self.fake_options.cloud_enable_reporting = "yes" self.fake_options.cloud_enable_reporting = "yes"
policy = CloudPolicy(self.fake_options, {}, dry_run=False) policy = CloudPolicy(self.fake_options, {}, dry_run=False)
policy.package_set = set(["chromium-browser"]) policy.package_set = {
"acloud": {"chromium-browser": []}
}
policy.options.series = "jammy" policy.options.series = "jammy"
policy.apply_src_policy_impl( policy.apply_src_policy_impl(
@ -204,7 +214,9 @@ class T(unittest.TestCase):
self.fake_options.cloud_reporting_enabled = "no" self.fake_options.cloud_reporting_enabled = "no"
policy = CloudPolicy(self.fake_options, {}, dry_run=False) policy = CloudPolicy(self.fake_options, {}, dry_run=False)
policy.package_set = set(["chromium-browser"]) policy.package_set = {
"acloud": {"chromium-browser": []}
}
policy.options.series = "jammy" policy.options.series = "jammy"
policy.apply_src_policy_impl( policy.apply_src_policy_impl(
@ -302,10 +314,12 @@ class T(unittest.TestCase):
"""Ensure the correct flags are returned with PPA sources""" """Ensure the correct flags are returned with PPA sources"""
expected_flags = [ expected_flags = [
"--install-ppa-package", "tmux/ppa_url=fingerprint", "--install-ppa-package", "tmux/ppa_url=fingerprint",
"--install-ppa-package", "tmux/ppa_url2=fingerprint" "--install-ppa-package", "sed/ppa_url=fingerprint",
"--install-ppa-package", "tmux/ppa_url2=fingerprint",
"--install-ppa-package", "sed/ppa_url2=fingerprint",
] ]
install_flags = self.policy._format_install_flags( install_flags = self.policy._format_install_flags(
"tmux", ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa" ["tmux", "sed"], ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa"
) )
self.assertListEqual(install_flags, expected_flags) self.assertListEqual(install_flags, expected_flags)
@ -313,14 +327,14 @@ class T(unittest.TestCase):
def test_format_install_flags_with_archive(self): def test_format_install_flags_with_archive(self):
"""Ensure the correct flags are returned with archive sources""" """Ensure the correct flags are returned with archive sources"""
expected_flags = ["--install-archive-package", "tmux/proposed"] expected_flags = ["--install-archive-package", "tmux/proposed"]
install_flags = self.policy._format_install_flags("tmux", ["proposed"], "archive") install_flags = self.policy._format_install_flags(["tmux"], ["proposed"], "archive")
self.assertListEqual(install_flags, expected_flags) self.assertListEqual(install_flags, expected_flags)
def test_format_install_flags_with_incorrect_type(self): def test_format_install_flags_with_incorrect_type(self):
"""Ensure errors are raised for unknown source types""" """Ensure errors are raised for unknown source types"""
self.assertRaises(RuntimeError, self.policy._format_install_flags, "tmux", ["a_source"], "something") self.assertRaises(RuntimeError, self.policy._format_install_flags, ["tmux"], ["a_source"], "something")
def test_parse_ppas(self): def test_parse_ppas(self):
"""Ensure correct conversion from Britney format to cloud test format """Ensure correct conversion from Britney format to cloud test format
@ -392,6 +406,50 @@ class T(unittest.TestCase):
self.policy.failures["FakeCloud"]["extra_info"]["install_source"], "source information" self.policy.failures["FakeCloud"]["extra_info"]["install_source"], "source information"
) )
def test_retrieve_cloud_package_set_for_series(self):
"""Tests that the package set is retrieved and only the given series is returned.
"""
with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
raw_package_set = {
"acloud": {
"focal": {"grep": [], "tmux": []},
"jammy": {"grep": [],},
},
"bcloud": {
"focal": {"grep": [], "tmux": [],},
"jammy": {"grep": [], "tmux": [],},
"kinetic": {},
}
}
json.dump(raw_package_set, f)
f.seek(0)
package_set = self.policy._retrieve_cloud_package_set_for_series(f.name, "focal")
expected_set = {
"acloud": {"grep": [], "tmux": []},
"bcloud": {"grep": [], "tmux": []},
}
self.assertDictEqual(package_set, expected_set)
def test_check_if_tests_required(self):
"""Test that the package set is correctly parsed.
Ensure that a package is found and the cloud is returned correctly.
Ensure that only the correct series is checked.
"""
package_set = {
"acloud": {"grep": []},
"bcloud": {"grep": [], "tmux": []},
}
clouds = self.policy._check_if_tests_required(package_set, "grep")
self.assertListEqual(clouds, ["acloud", "bcloud"])
clouds = self.policy._check_if_tests_required(package_set, "tmux")
self.assertListEqual(clouds, ["bcloud"])
clouds = self.policy._check_if_tests_required(package_set, "sed")
self.assertListEqual(clouds, [])
def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0): def _create_fake_test_result_file(self, num_pass=1, num_err=0, num_fail=0):
"""Helper function to generate an xunit test result file. """Helper function to generate an xunit test result file.