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.
"""
class CloudPolicy(BasePolicy):
PACKAGE_SET_FILE = "cloud_package_set"
PACKAGE_SET_FILE = "cloud_package_set.json"
STATE_FILE = "cloud_state"
DEFAULT_EMAILS = ["cpc@canonical.com"]
TEST_LOG_FILE = "CTF.log"
@ -89,16 +89,21 @@ class CloudPolicy(BasePolicy):
self.failures = {}
self.errors = {}
self.email_needed = False
self.package_set = {}
def initialise(self, 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()
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))
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
excuse.add_verdict_info(
verdict,
@ -119,7 +124,7 @@ class CloudPolicy(BasePolicy):
if self.reporting_enabled == "yes":
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)
if len(self.failures) > 0 or len(self.errors) > 0:
@ -223,34 +228,55 @@ class CloudPolicy(BasePolicy):
json.dump(self.state, data)
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
tests should be run.
Temporarily a static list retrieved from file. Will be updated to
retrieve from a database at a later date.
:param file_path The path to the package set file.
:param series The Ubuntu codename for the series (e.g. jammy)
"""
package_set = set()
package_set = {}
with open(self.PACKAGE_SET_FILE) as file:
for line in file:
package_set.add(line.strip())
with open(file_path) as file:
full_package_set = json.load(file)
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
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.
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 version Version of the package
: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 source_type Either 'archive' or 'ppa'
"""
self._run_azure_tests(package, version, series, sources, source_type)
for cloud in clouds:
if cloud == "azure":
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):
"""Sends email(s) if there are test failures and/or errors
@ -290,13 +316,14 @@ class CloudPolicy(BasePolicy):
return
urn = self._retrieve_urn(series)
binaries = self._retrieve_binaries("azure", package)
self.logger.info("Cloud Policy: Running Azure tests for: {} in {}".format(package, series))
params = [
"/snap/bin/cloud-test-framework",
"--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(
[
"azure_gen2",
@ -341,6 +368,14 @@ class CloudPolicy(BasePolicy):
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):
"""Find any test results files that match the given regex pattern.
@ -503,25 +538,28 @@ class CloudPolicy(BasePolicy):
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
: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 source_type Either 'archive' or 'ppa'
"""
install_flags = []
for source in sources:
flag = ""
if source_type == "archive":
install_flags.append("--install-archive-package")
install_flags.append("{}/{}".format(package, source))
flag = "--install-archive-package"
elif source_type == "ppa":
install_flags.append("--install-ppa-package")
install_flags.append("{}/{}".format(package, source))
flag = "--install-ppa-package"
else:
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
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
import unittest
from unittest.mock import MagicMock, patch
import tempfile
import xml.etree.ElementTree as ET
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):
"""Cloud tests should save state and not re-run tests for packages
already tested."""
self.policy.package_set = {"azure": {"chromium-browser": ["binary1"], "hello": ["binary2"]}}
expected_state = {
"azure": {
"archive": {
@ -71,7 +73,7 @@ class T(unittest.TestCase):
# Package already tested, no tests should run
self.policy.failures = {}
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)
mock_run.assert_not_called()
self.assertEqual(len(self.policy.failures), 1)
@ -85,7 +87,7 @@ class T(unittest.TestCase):
}
self.policy.failures = {}
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)
mock_run.assert_called()
self.assertEqual(len(self.policy.failures), 0)
@ -99,7 +101,7 @@ class T(unittest.TestCase):
}
self.policy.failures = {}
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.assertEqual(mock_run.call_count, 2)
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):
"""Cloud tests should save state and not re-run tests for packages
already tested."""
self.policy.package_set = {"azure": {"chromium-browser": ["binary1"]}}
start_state = {
"azure": {
"archive": {
@ -133,14 +136,16 @@ class T(unittest.TestCase):
self.policy._load_state()
# 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()
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
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.
"""
self.policy.package_set = set(["chromium-browser"])
self.policy.package_set = {
"acloud": {"chromium-browser": []}
}
self.policy.options.series = "jammy"
self.policy.apply_src_policy_impl(
@ -148,15 +153,16 @@ class T(unittest.TestCase):
)
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")
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"""
self.policy.package_set = set(["vim"])
self.policy.options.series = "jammy"
self.policy.package_set = {
"acloud": {"vim": []}
}
verdict = self.policy.apply_src_policy_impl(
None, FakeItem, None, FakeSourceData, MagicMock()
@ -169,7 +175,9 @@ class T(unittest.TestCase):
@patch("britney2.policies.cloud.CloudPolicy._run_cloud_tests")
def test_no_tests_run_during_dry_run(self, mock_run, smtp):
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.source = "jammy-proposed"
@ -187,7 +195,9 @@ class T(unittest.TestCase):
self.fake_options.cloud_enable_reporting = "yes"
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.apply_src_policy_impl(
@ -204,7 +214,9 @@ class T(unittest.TestCase):
self.fake_options.cloud_reporting_enabled = "no"
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.apply_src_policy_impl(
@ -302,10 +314,12 @@ class T(unittest.TestCase):
"""Ensure the correct flags are returned with PPA sources"""
expected_flags = [
"--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(
"tmux", ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa"
["tmux", "sed"], ["ppa_url=fingerprint", "ppa_url2=fingerprint"], "ppa"
)
self.assertListEqual(install_flags, expected_flags)
@ -313,14 +327,14 @@ class T(unittest.TestCase):
def test_format_install_flags_with_archive(self):
"""Ensure the correct flags are returned with archive sources"""
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)
def test_format_install_flags_with_incorrect_type(self):
"""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):
"""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"
)
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):
"""Helper function to generate an xunit test result file.