From bca0838f2f173b3f7905e7a6f1e09f34aac808f9 Mon Sep 17 00:00:00 2001 From: Laurent Bonnans Date: Fri, 26 Apr 2019 14:50:23 +0200 Subject: [PATCH] Add complete cycle ostree install test With some parts mocked (emulate booted deployments) Signed-off-by: Laurent Bonnans --- src/aktualizr_repo/image_repo.cc | 2 +- src/aktualizr_repo/image_repo.h | 2 +- src/libaktualizr/primary/CMakeLists.txt | 9 + src/libaktualizr/primary/aktualizr.h | 1 + .../primary/aktualizr_fullostree_test.cc | 167 ++++++++++++++++++ tests/fake_http_server/fake_http_server.py | 17 +- tests/fake_http_server/fake_test_server.py | 85 +++++---- tests/ostree_mock.c | 4 +- tests/test_utils.cc | 13 +- 9 files changed, 260 insertions(+), 40 deletions(-) create mode 100644 src/libaktualizr/primary/aktualizr_fullostree_test.cc diff --git a/src/aktualizr_repo/image_repo.cc b/src/aktualizr_repo/image_repo.cc index 466b79f943..3c3f548fcb 100644 --- a/src/aktualizr_repo/image_repo.cc +++ b/src/aktualizr_repo/image_repo.cc @@ -16,7 +16,7 @@ void ImageRepo::addImage(const std::string &name, const Json::Value &target, con } void ImageRepo::addBinaryImage(const boost::filesystem::path &image_path, const boost::filesystem::path &targetname, - const Delegation &delegation) { + const Delegation &delegation) { boost::filesystem::path repo_dir(path_ / "repo/image"); boost::filesystem::path targets_path = repo_dir / "targets"; diff --git a/src/aktualizr_repo/image_repo.h b/src/aktualizr_repo/image_repo.h index 0b6b6050fe..5c0d59dc60 100644 --- a/src/aktualizr_repo/image_repo.h +++ b/src/aktualizr_repo/image_repo.h @@ -8,7 +8,7 @@ class ImageRepo : public Repo { ImageRepo(boost::filesystem::path path, const std::string &expires, std::string correlation_id) : Repo(Uptane::RepositoryType::Image(), std::move(path), expires, std::move(correlation_id)) {} void addBinaryImage(const boost::filesystem::path &image_path, const boost::filesystem::path &targetname, - const Delegation &delegation = {}); + const Delegation &delegation = {}); void addCustomImage(const std::string &name, const Uptane::Hash &hash, uint64_t length, const Delegation &delegation, const Json::Value &custom = {}); void addDelegation(const Uptane::Role &name, const Uptane::Role &parent_role, const std::string &path, diff --git a/src/libaktualizr/primary/CMakeLists.txt b/src/libaktualizr/primary/CMakeLists.txt index 9dddf570e9..a3876385ba 100644 --- a/src/libaktualizr/primary/CMakeLists.txt +++ b/src/libaktualizr/primary/CMakeLists.txt @@ -16,6 +16,15 @@ add_library(primary OBJECT ${SOURCES}) add_aktualizr_test(NAME aktualizr SOURCES aktualizr_test.cc PROJECT_WORKING_DIRECTORY ARGS ${PROJECT_BINARY_DIR}/uptane_repos) add_dependencies(t_aktualizr uptane_repo_full_no_correlation_id) +if (BUILD_OSTREE) + add_aktualizr_test(NAME aktualizr_fullostree SOURCES aktualizr_fullostree_test.cc PROJECT_WORKING_DIRECTORY ARGS $ ${PROJECT_BINARY_DIR}/ostree_repo) + set_target_properties(t_aktualizr_fullostree PROPERTIES LINK_FLAGS -Wl,--export-dynamic) + add_dependencies(t_aktualizr_fullostree ostree_mock aktualizr-repo make_ostree_sysroot) + set_tests_properties(test_aktualizr_fullostree PROPERTIES ENVIRONMENT LD_PRELOAD=$) +else (BUILD_OSTREE) + aktualizr_source_file_checks(aktualizr_fullostree_test.cc) +endif (BUILD_OSTREE) + add_aktualizr_test(NAME reportqueue SOURCES reportqueue_test.cc PROJECT_WORKING_DIRECTORY) add_aktualizr_test(NAME emptytargets SOURCES empty_targets_test.cc PROJECT_WORKING_DIRECTORY ARGS "$") diff --git a/src/libaktualizr/primary/aktualizr.h b/src/libaktualizr/primary/aktualizr.h index a4711a8644..9c876e5d75 100644 --- a/src/libaktualizr/primary/aktualizr.h +++ b/src/libaktualizr/primary/aktualizr.h @@ -178,6 +178,7 @@ class Aktualizr { FRIEND_TEST(Aktualizr, AddSecondary); FRIEND_TEST(Aktualizr, EmptyTargets); FRIEND_TEST(Aktualizr, EmptyTargetsAfterInstall); + FRIEND_TEST(Aktualizr, FullOstreeUpdate); FRIEND_TEST(Delegation, Basic); FRIEND_TEST(Delegation, RevokeAfterCheckUpdates); FRIEND_TEST(Delegation, RevokeAfterInstall); diff --git a/src/libaktualizr/primary/aktualizr_fullostree_test.cc b/src/libaktualizr/primary/aktualizr_fullostree_test.cc new file mode 100644 index 0000000000..5758fed070 --- /dev/null +++ b/src/libaktualizr/primary/aktualizr_fullostree_test.cc @@ -0,0 +1,167 @@ +#include + +#include +#include +#include +#include +#include + +#include + +#include "uptane_test_common.h" + +#include "config/config.h" +#include "http/httpclient.h" +#include "httpfake.h" +#include "logging/logging.h" +#include "package_manager/ostreemanager.h" +#include "package_manager/packagemanagerfactory.h" +#include "primary/aktualizr.h" +#include "primary/events.h" +#include "primary/sotauptaneclient.h" +#include "storage/sqlstorage.h" +#include "test_utils.h" +#include "uptane/tuf.h" +#include "utilities/apiqueue.h" + +boost::filesystem::path aktualizr_repo_path; +static std::string server = "http://127.0.0.1:"; +static std::string treehub_server = "http://127.0.0.1:"; +static boost::filesystem::path sysroot; + +static struct { + int serial; + std::string rev; +} ostree_deployment; +static std::string new_rev; + +#include +extern "C" OstreeDeployment *ostree_sysroot_get_booted_deployment_mock(OstreeSysroot *self) { + (void)self; + static GObjectUniquePtr dep; + + dep.reset(ostree_deployment_new(0, "dummy-os", ostree_deployment.rev.c_str(), ostree_deployment.serial, + ostree_deployment.rev.c_str(), ostree_deployment.serial)); + return dep.get(); +} + +extern "C" const char *ostree_deployment_get_csum(OstreeDeployment *self) { + (void)self; + return ostree_deployment.rev.c_str(); +} + +TEST(Aktualizr, FullOstreeUpdate) { + TemporaryDirectory temp_dir; + Config conf = UptaneTestCommon::makeTestConfig(temp_dir, server); + conf.pacman.type = PackageManager::kOstree; + conf.pacman.sysroot = sysroot.string(); + conf.pacman.ostree_server = treehub_server; + conf.pacman.os = "dummy-os"; + conf.provision.device_id = "device_id"; + conf.provision.ecu_registration_endpoint = server + "/director/ecus"; + conf.provision.primary_ecu_serial = "CA:FE:A6:D2:84:9D"; + conf.provision.primary_ecu_hardware_id = "primary_hw"; + conf.provision.provision_path = "tests/test_data/cred.zip"; + conf.tls.server = server; + + LOG_INFO << "conf: " << conf; + + { + Aktualizr aktualizr(conf); + + aktualizr.Initialize(); + + result::UpdateCheck update_result = aktualizr.CheckUpdates().get(); + ASSERT_EQ(update_result.status, result::UpdateStatus::kUpdatesAvailable); + + result::Download download_result = aktualizr.Download(update_result.updates).get(); + EXPECT_EQ(download_result.status, result::DownloadStatus::kSuccess); + + result::Install install_result = aktualizr.Install(update_result.updates).get(); + EXPECT_EQ(install_result.ecu_reports.size(), 1); + EXPECT_EQ(install_result.ecu_reports[0].install_res.result_code.num_code, + data::ResultCode::Numeric::kNeedCompletion); + } + + // do "reboot" and finalize + ostree_deployment.serial = 1; + ostree_deployment.rev = new_rev; + boost::filesystem::remove(conf.bootloader.reboot_sentinel_dir / conf.bootloader.reboot_sentinel_name); + + { + Aktualizr aktualizr(conf); + + aktualizr.Initialize(); + + result::UpdateCheck update_result = aktualizr.CheckUpdates().get(); + ASSERT_EQ(update_result.status, result::UpdateStatus::kNoUpdatesAvailable); + } +} + +#ifndef __NO_MAIN__ +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + + logger_init(); + + if (argc != 3) { + std::cerr << "Error: " << argv[0] << " requires the path to the aktualizr-repo utility " + << "and an OStree sysroot\n"; + return EXIT_FAILURE; + } + aktualizr_repo_path = argv[1]; + + Process ostree("ostree"); + + TemporaryDirectory meta_dir; + TemporaryDirectory temp_sysroot; + sysroot = temp_sysroot / "sysroot"; + // uses cp, as boost doesn't like to copy bad symlinks + int res = system((std::string("cp -r ") + argv[2] + std::string(" ") + sysroot.string()).c_str()); + if (res != 0) { + return -1; + } + auto r = ostree.run( + {"rev-parse", std::string("--repo"), (sysroot / "/ostree/repo").string(), "generate-remote/generated"}); + if (std::get<0>(r) != 0) { + return -1; + } + ostree_deployment.serial = 0; + ostree_deployment.rev = ostree.lastStdOut(); + boost::trim_if(ostree_deployment.rev, boost::is_any_of(" \t\r\n")); + LOG_INFO << "ORIG: " << ostree_deployment.rev; + + std::string port = TestUtils::getFreePort(); + server += port; + boost::process::child http_server_process("tests/fake_http_server/fake_test_server.py", port, "-m", meta_dir.Path()); + TestUtils::waitForServer(server + "/"); + + std::string treehub_port = TestUtils::getFreePort(); + treehub_server += treehub_port; + TemporaryDirectory treehub_dir; + boost::process::child ostree_server_process("tests/sota_tools/treehub_server.py", std::string("-p"), treehub_port, + std::string("-d"), treehub_dir.PathString(), std::string("-s0.5"), + std::string("--create")); + TestUtils::waitForServer(treehub_server + "/"); + r = ostree.run({"rev-parse", std::string("--repo"), treehub_dir.PathString(), "master"}); + if (std::get<0>(r) != 0) { + return -1; + } + new_rev = ostree.lastStdOut(); + boost::trim_if(new_rev, boost::is_any_of(" \t\r\n")); + LOG_INFO << "DEST: " << new_rev; + + Process akt_repo(aktualizr_repo_path.string()); + akt_repo.run({"generate", "--path", meta_dir.PathString(), "--correlationid", "abc123"}); + akt_repo.run({"image", "--path", meta_dir.PathString(), "--targetname", "update_1.0", "--targetsha256", new_rev, + "--targetlength", "0", "--targetformat", "OSTREE"}); + akt_repo.run({"addtarget", "--path", meta_dir.PathString(), "--targetname", "update_1.0", "--hwid", "primary_hw", + "--serial", "CA:FE:A6:D2:84:9D"}); + akt_repo.run({"signtargets", "--path", meta_dir.PathString(), "--correlationid", "abc123"}); + LOG_INFO << akt_repo.lastStdOut(); + // Work around inconsistent directory naming. + Utils::copyDir(meta_dir.Path() / "repo/image", meta_dir.Path() / "repo/repo"); + + return RUN_ALL_TESTS(); +} +#endif // __NO_MAIN__ diff --git a/tests/fake_http_server/fake_http_server.py b/tests/fake_http_server/fake_http_server.py index be82793ab8..23bd8a21a9 100755 --- a/tests/fake_http_server/fake_http_server.py +++ b/tests/fake_http_server/fake_http_server.py @@ -79,14 +79,27 @@ def do_POST(self): return else: last_fails = False - if self.path == '/token': + if self.path == '/devices': + self.send_response(200) + self.end_headers() + with open('tests/test_data/cred.p12', 'rb') as source: + while True: + data = source.read(1024) + if not data: + break + self.wfile.write(data) + elif self.path == '/token': self.send_response(200) self.end_headers() if 'Authorization' in self.headers: self.wfile.write(b"{\"access_token\": \"token\"}") else: self.wfile.write(b'') - + elif self.path == "/director/ecus": + self.send_response(200) + self.end_headers() + self.wfile.write(b"{}") + return else: self.send_response(200) self.end_headers() diff --git a/tests/fake_http_server/fake_test_server.py b/tests/fake_http_server/fake_test_server.py index 795c455dc4..861705989d 100755 --- a/tests/fake_http_server/fake_test_server.py +++ b/tests/fake_http_server/fake_test_server.py @@ -1,48 +1,55 @@ #!/usr/bin/python3 +import argparse import sys import socket + from http.server import SimpleHTTPRequestHandler, HTTPServer from time import sleep +file_path = None +meta_path = None + + class Handler(SimpleHTTPRequestHandler): + def _serve_simple(self, uri): + with open(uri, 'rb') as source: + while True: + data = source.read(1024) + if not data: + break + self.wfile.write(data) + + def serve_meta(self, uri): + if meta_path is None: + raise RuntimeError("Please supply a path for metadata") + self._serve_simple(meta_path + uri) + + def serve_target(self, filename): + if file_path is None: + raise RuntimeError("Please supply a path for targets") + self._serve_simple(file_path + filename) + def do_GET(self): + print("path: " + self.path) if self.path.startswith("/director/") and self.path.endswith(".json"): self.send_response(200) self.end_headers() role = self.path[len("/director/"):] - with open(meta_path + '/repo/director/' + role, 'rb') as source: - while True: - data = source.read(1024) - if not data: - break - self.wfile.write(data) - return + self.serve_meta("/repo/director/" + role) elif self.path.startswith("/repo/") and self.path.endswith(".json"): self.send_response(200) self.end_headers() role = self.path[len("/repo/"):] - with open(meta_path + '/repo/image/' + role, 'rb') as source: - while True: - data = source.read(1024) - if not data: - break - self.wfile.write(data) - return + self.serve_meta('/repo/image/' + role) elif self.path.startswith("/repo/targets"): self.send_response(200) self.end_headers() filename = self.path[len("/repo/targets"):] - with open(file_path + filename, 'rb') as source: - while True: - data = source.read(1024) - if not data: - break - self.wfile.write(data) - return + self.serve_target(filename) - if self.path == '/download': + elif self.path == '/download': self.send_response(301) self.send_header('Location', '/download/file') self.end_headers() @@ -85,12 +92,12 @@ def do_GET(self): self.send_response(200) self.end_headers() for i in range(5): - self.wfile.write(b'aa') - sleep(1) + self.wfile.write(b'aa') + sleep(1) else: self.send_response(200) self.end_headers() - self.wfile.write(b'{"path": "%b"}'%bytes(self.path, "utf8")) + self.wfile.write(b'{"path": "%b"}' % bytes(self.path, "utf8")) def do_POST(self): if self.path == '/devices': @@ -137,13 +144,25 @@ def server_bind(self): HTTPServer.server_bind(self) -server_address = ('', int(sys.argv[1])) -file_path = sys.argv[2] -meta_path = sys.argv[3] +def main(): + global file_path, meta_path + + parser = argparse.ArgumentParser(description='Run a fake OTA backend') + parser.add_argument('port', type=int, help='server port') + parser.add_argument('-t', '--targets', help='targets directory', default=None) + parser.add_argument('-m', '--meta', help='meta directory', default=None) + args = parser.parse_args() + + server_address = ('', args.port) + file_path = args.targets + meta_path = args.meta + + httpd = ReUseHTTPServer(server_address, Handler) + try: + httpd.serve_forever() + except KeyboardInterrupt: + httpd.server_close() -httpd = ReUseHTTPServer(server_address, Handler) -try: - httpd.serve_forever() -except KeyboardInterrupt: - httpd.server_close() +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/ostree_mock.c b/tests/ostree_mock.c index 46b7f8e60c..fe3c985396 100644 --- a/tests/ostree_mock.c +++ b/tests/ostree_mock.c @@ -3,7 +3,7 @@ #include #include -OstreeDeployment *ostree_sysroot_get_booted_deployment (OstreeSysroot *self) { +OstreeDeployment* ostree_sysroot_get_booted_deployment(OstreeSysroot* self) { OstreeDeployment* (*orig)(OstreeSysroot*) = dlsym(RTLD_NEXT, __func__); char mocked_name[100]; snprintf(mocked_name, sizeof(mocked_name), "%s_mock", __func__); @@ -15,7 +15,7 @@ OstreeDeployment *ostree_sysroot_get_booted_deployment (OstreeSysroot *self) { return orig(self); } -const char *ostree_deployment_get_bootcsum (OstreeDeployment *self) { +const char* ostree_deployment_get_bootcsum(OstreeDeployment* self) { char* (*orig)(OstreeDeployment*) = dlsym(RTLD_NEXT, __func__); char mocked_name[100]; snprintf(mocked_name, sizeof(mocked_name), "%s_mock", __func__); diff --git a/tests/test_utils.cc b/tests/test_utils.cc index e9bd23d30d..abb59f97b4 100644 --- a/tests/test_utils.cc +++ b/tests/test_utils.cc @@ -16,6 +16,8 @@ #include #include +#include "logging/logging.h" + std::string TestUtils::getFreePort() { int s = socket(AF_INET, SOCK_STREAM, 0); if (s == -1) { @@ -90,7 +92,13 @@ Process::Result Process::spawn(const std::string &executable_to_run, const std:: boost::asio::io_service io_service; try { - boost::process::child child_process(boost::process::exe = executable_to_run, boost::process::args = executable_args, + std::string executable_path; + if (boost::filesystem::exists(executable_to_run)) { + executable_path = executable_to_run; + } else { + executable_path = boost::process::search_path(executable_to_run).string(); + } + boost::process::child child_process(boost::process::exe = executable_path, boost::process::args = executable_args, boost::process::std_out > output, boost::process::std_err > err_output, boost::process::on_exit = child_process_exit_code, io_service); @@ -119,6 +127,9 @@ Process::Result Process::run(const std::vector &args) { auto cred_gen_result = Process::spawn(exe_path_, args); std::tie(last_exit_code_, last_stdout_, last_stderr_) = cred_gen_result; + if (last_exit_code_ != 0) { + LOG_WARNING << last_stderr_; + } return cred_gen_result; }