Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openQA for openSUSE:Factory checked in at 2025-09-08 09:56:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Mon Sep 8 09:56:58 2025 rev:742 rq:1302962 version:5.1757084700.fad3731d Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2025-09-05 21:43:32.825239751 +0200 +++ /work/SRC/openSUSE:Factory/.openQA.new.1977/openQA.changes 2025-09-08 09:57:41.686818537 +0200 @@ -1,0 +2,12 @@ +Fri Sep 05 21:13:12 UTC 2025 - [email protected] + +- Update to version 5.1757084700.fad3731d: + * Ensure no untracked files in unit test run part 2 + * Dependency cron 2025-09-05 + * Add MCP support as an optional feature + * Allow specifying a full domain via `file_security_policy` + * Allow using a different subdomain via `file_security_policy` + * t: Ensure no leftover files in git directory + * ci: Ensure clean git status with Test::CheckGitStatus + +------------------------------------------------------------------- Old: ---- openQA-5.1757005118.aac56dbc.obscpio New: ---- openQA-5.1757084700.fad3731d.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:57:42.514852953 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:57:42.518853118 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1757005118.aac56dbc +Version: 5.1757084700.fad3731d Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:57:42.546854282 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:57:42.546854282 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1757005118.aac56dbc +Version: 5.1757084700.fad3731d Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:57:42.574855446 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:57:42.574855446 +0200 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1757005118.aac56dbc +Version: 5.1757084700.fad3731d Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:57:42.602856610 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:57:42.602856610 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1757005118.aac56dbc +Version: 5.1757084700.fad3731d Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:57:42.630857774 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:57:42.630857774 +0200 @@ -94,12 +94,12 @@ # The following line is generated from dependencies.yaml %define cover_requires perl(Devel::Cover) perl(Devel::Cover::Report::Codecovbash) # The following line is generated from dependencies.yaml -%define devel_no_selenium_requires %build_requires %cover_requires %qemu %style_check_requires %test_requires curl perl(Perl::Tidy) postgresql-devel rsync sudo tar xorg-x11-fonts +%define devel_no_selenium_requires %build_requires %cover_requires %qemu %style_check_requires %test_requires curl perl(Perl::Tidy) perl(Test::CheckGitStatus) postgresql-devel rsync sudo tar xorg-x11-fonts # The following line is generated from dependencies.yaml %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1757005118.aac56dbc +Version: 5.1757084700.fad3731d Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later @@ -221,7 +221,7 @@ Requires: %{mcp_requires} %description mcp -This package contains additional resources for AI support in openQA. +This package contains a plugin for AI support in openQA. %package client Summary: Client tools for remote openQA management @@ -642,6 +642,7 @@ %{_datadir}/openqa/lib/OpenQA/Scheduler/ %{_datadir}/openqa/lib/OpenQA/Schema/ %{_datadir}/openqa/lib/OpenQA/WebAPI/ +%exclude %{_datadir}/openqa/lib/OpenQA/WebAPI/Plugin/MCP.pm %{_datadir}/openqa/lib/OpenQA/WebSockets/ %{_datadir}/openqa/templates %{_datadir}/openqa/public @@ -861,4 +862,5 @@ %endif %files mcp +%{_datadir}/openqa/lib/OpenQA/WebAPI/Plugin/MCP.pm ++++++ openQA-5.1757005118.aac56dbc.obscpio -> openQA-5.1757084700.fad3731d.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/.gitignore new/openQA-5.1757084700.fad3731d/.gitignore --- old/openQA-5.1757005118.aac56dbc/.gitignore 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/.gitignore 2025-09-05 17:05:00.000000000 +0200 @@ -1,6 +1,11 @@ logs/*.tar.* etc/mine/ -cover_db/ +# Devel::Cover +/cover_db* +# TAP::Harness::JUnit +/test-results/ +# CircleCI cache +/logs/ temp /testresults /t/data/openqa/webui/cache diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/Makefile new/openQA-5.1757084700.fad3731d/Makefile --- old/openQA-5.1757005118.aac56dbc/Makefile 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/Makefile 2025-09-05 17:05:00.000000000 +0200 @@ -269,7 +269,7 @@ test-unit-and-integration: node_modules export GLOBIGNORE="$(GLOBIGNORE)";\ export DEVEL_COVER_DB_FORMAT=JSON;\ - export PERL5OPT="$(COVEROPT)$(PERL5OPT) -It/lib -I$(PWD)/t/lib -I$(PWD)/external/os-autoinst-common/lib -MOpenQA::Test::PatchDeparse";\ + export PERL5OPT="$(COVEROPT)$(PERL5OPT) -It/lib -I$(PWD)/t/lib -I$(PWD)/external/os-autoinst-common/lib $(CHECK_GIT_STATUS_OPT) -MOpenQA::Test::PatchDeparse";\ RETRY=${RETRY} HOOK=./tools/delete-coverdb-folder timeout -s SIGINT -k 5 -v ${TIMEOUT_RETRIES} tools/retry "${PROVE}" ${PROVE_LIB_ARGS} ${PROVE_ARGS} .PHONY: setup-database @@ -299,6 +299,10 @@ COVEROPT ?= -mJSON::PP -It/lib -MCoverageWorkaround -MDevel::Cover=-select_re,'^/lib',+ignore_re,lib/perlcritic/Perl/Critic/Policy|t/lib/CoverageWorkaround,-coverage,statement,-db,cover_db$(COVERDB_SUFFIX), endif +ifeq ($(CHECK_GIT_STATUS),1) +CHECK_GIT_STATUS_OPT ?= -MTest::CheckGitStatus +endif + .PHONY: coverage coverage: export DEVEL_COVER_DB_FORMAT=JSON;\ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/cpanfile new/openQA-5.1757084700.fad3731d/cpanfile --- old/openQA-5.1757005118.aac56dbc/cpanfile 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/cpanfile 2025-09-05 17:05:00.000000000 +0200 @@ -119,6 +119,7 @@ requires 'Perl::Critic'; requires 'Perl::Critic::Community'; requires 'Perl::Tidy', '== 20250711.0.0'; + requires 'Test::CheckGitStatus'; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/dependencies.yaml new/openQA-5.1757084700.fad3731d/dependencies.yaml --- old/openQA-5.1757005118.aac56dbc/dependencies.yaml 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/dependencies.yaml 2025-09-05 17:05:00.000000000 +0200 @@ -85,6 +85,7 @@ tar: xorg-x11-fonts: perl(Perl::Tidy): '== 20250711.0.0' + perl(Test::CheckGitStatus): devel_requires: '%devel_no_selenium_requires': diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/dist/rpm/openQA.spec new/openQA-5.1757084700.fad3731d/dist/rpm/openQA.spec --- old/openQA-5.1757005118.aac56dbc/dist/rpm/openQA.spec 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/dist/rpm/openQA.spec 2025-09-05 17:05:00.000000000 +0200 @@ -94,7 +94,7 @@ # The following line is generated from dependencies.yaml %define cover_requires perl(Devel::Cover) perl(Devel::Cover::Report::Codecovbash) # The following line is generated from dependencies.yaml -%define devel_no_selenium_requires %build_requires %cover_requires %qemu %style_check_requires %test_requires curl perl(Perl::Tidy) postgresql-devel rsync sudo tar xorg-x11-fonts +%define devel_no_selenium_requires %build_requires %cover_requires %qemu %style_check_requires %test_requires curl perl(Perl::Tidy) perl(Test::CheckGitStatus) postgresql-devel rsync sudo tar xorg-x11-fonts # The following line is generated from dependencies.yaml %define devel_requires %devel_no_selenium_requires chromedriver @@ -222,7 +222,7 @@ Requires: %{mcp_requires} %description mcp -This package contains additional resources for AI support in openQA. +This package contains a plugin for AI support in openQA. %package client Summary: Client tools for remote openQA management @@ -642,6 +642,7 @@ %{_datadir}/openqa/lib/OpenQA/Scheduler/ %{_datadir}/openqa/lib/OpenQA/Schema/ %{_datadir}/openqa/lib/OpenQA/WebAPI/ +%exclude %{_datadir}/openqa/lib/OpenQA/WebAPI/Plugin/MCP.pm %{_datadir}/openqa/lib/OpenQA/WebSockets/ %{_datadir}/openqa/templates %{_datadir}/openqa/public @@ -861,5 +862,6 @@ %endif %files mcp +%{_datadir}/openqa/lib/OpenQA/WebAPI/Plugin/MCP.pm %changelog diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/docs/WritingTests.asciidoc new/openQA-5.1757084700.fad3731d/docs/WritingTests.asciidoc --- old/openQA-5.1757005118.aac56dbc/docs/WritingTests.asciidoc 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/docs/WritingTests.asciidoc 2025-09-05 17:05:00.000000000 +0200 @@ -1919,6 +1919,81 @@ ignore this, but piping there output into `tee` is usually enough to stop them outputting non-printable characters. +=== MCP Support + +The https://modelcontextprotocol.io/[Model Context Protocol] (MCP) is a standard +that allows Large Language Models (LLMs) to interact with web services. MCP is +supported natively by openQA, but still considered experimental, and therefore +needs to be enabled manually in `openqa.ini`. + +[source,ini] +---- +[global] +mcp_enabled = read-only +---- + +Once enabled the experimental MCP endpoint becomes available under the path +`/experimental/mcp`. At the moment all implemented MCP tools are read-only, +that means LLMs can review infomation provided by openQA, but are unable to +make changes or trigger actions. Once write operations become available +with future updates, they will have to be enabled with a different setting +in `openqa.ini` for security reasons. + +Most MCP clients today support Bearer token authentication, so that is what +openQA relies on as well. More authentication mechanisms will be added as +the technology evolves. + +This example configuration in the `mcp.json` format, which is commonly used +by MCP clients, shows how to include an openQA personal access token by +setting the `Authorization` HTTP header: + +[source,json] +---- +{ + "mcpServers": { + "openqa": { + "url": "http://127.0.0.1:9526/experimental/mcp", + "headers": { + "Authorization": "Bearer USER:KEY:SECRET" + } + } + } +} +---- + +==== 3rd Party MCP Clients +===== gemini-cli + +Once you have installed and set up +https://github.com/google-gemini/gemini-cli[gemini-cli], you can use the +`gemini mcp` command to add openQA: + +[source,shell] +---- +gemini mcp add openqa http://127.0.0.1:9526/experimental/mcp -H 'Authorization: Bearer USER:KEY:SECRET' -t http +---- + +After restarting gemini-cli, it will automatically discover available openQA +tools and make use of them on its own: + +[source] +---- +╭────────────────────────────────────────────────────╮ +│ > give me a brief summary for openQA job 5265754 │ +╰────────────────────────────────────────────────────╯ + + ╭───────────────────────────────────────────────────────────────────╮ + │ ✔ openqa_get_job_info (openqa MCP Server) openqa_get_job_info... │ + │ │ + │ ... │ + ╰───────────────────────────────────────────────────────────────────╯ +✦ Job 5265754 was a passed test named opensuse-Tumbleweed-DVD-x86_64- + Build20250825-ltp_net_features@64bit. It ran on August 26, 2025, + and took about 21 minutes to complete. The job tested the + ltp_net_features test suite on the x86_64 architecture. Most of the + tests passed, with a few being skipped. +---- + == Test Development tricks === Trigger new tests by modifying settings from existing test runs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa-assets.inc new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa-assets.inc --- old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa-assets.inc 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa-assets.inc 2025-09-05 17:05:00.000000000 +0200 @@ -8,5 +8,5 @@ # Enforce download of assets so HTML assets cannot highjack session # note: Can be disabled when using the alternative of making a redirect to a -# different subdomain mentioned in "openqa-locations.inc". +# different domain mentioned in "openqa-locations.inc". add_header Content-Disposition 'attachment; filename="$1"'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa-locations.inc new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa-locations.inc --- old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa-locations.inc 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa-locations.inc 2025-09-05 17:05:00.000000000 +0200 @@ -2,9 +2,9 @@ #location /assets { # include vhosts.d/openqa-assets.inc; # -# ## Alternatively, make a redirect to a different subdomain in accordance -# ## with the file_subdomain in openQA config -# #return 301 http://file.$host$request_uri; +# ## Alternatively, make a redirect to a different domain in accordance with +# ## the file_security_policy in openQA config +# #return 301 http://$host-files$request_uri; #} include vhosts.d/openqa-endpoints.inc; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa.conf.template new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa.conf.template --- old/openQA-5.1757005118.aac56dbc/etc/nginx/vhosts.d/openqa.conf.template 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/etc/nginx/vhosts.d/openqa.conf.template 2025-09-05 17:05:00.000000000 +0200 @@ -18,14 +18,14 @@ # include vhosts.d/openqa-locations.inc; #} -# Provide different servers for serving assets under a different subdomain +# Provide different servers for serving assets under a different domain # (enable these in accordance to "Optional faster asset downloads …" in # "openqa-locations.inc") #server { # listen 80; # listen [::]:80; -# # Set server_name in accordance with the file_subdomain in openQA config -# server_name file.openqa.example.com; +# # Set server_name in accordance with the file_security_policy in openQA config +# server_name openqa-files.example.com; # location /assets { # include vhosts.d/openqa-assets.inc; # } @@ -34,8 +34,8 @@ #server { # listen 443; # listen [::]:443; -# # Set server_name in accordance with the file_subdomain in openQA config -# server_name file.openqa.example.com; +# # Set server_name in accordance with the file_security_policy in openQA config +# server_name openqa-files.example.com; # location /assets { # include vhosts.d/openqa-assets.inc; # } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/etc/openqa/openqa.ini new/openQA-5.1757084700.fad3731d/etc/openqa/openqa.ini --- old/openQA-5.1757005118.aac56dbc/etc/openqa/openqa.ini 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/etc/openqa/openqa.ini 2025-09-05 17:05:00.000000000 +0200 @@ -41,6 +41,12 @@ ## So don't enable this plugin in production. #monitoring_enabled = 0 +## Set to read-only to enable MCP support +## * A Model Context Protocol endpoint will be available to all users under +## /experimental/mcp route. +## * Do not enable this feature for openQA instances with special security requirements! +#mcp_enabled = no + ## space-separated list of extra plugins to load; plugin must be under ## OpenQA::WebAPI::Plugin and correctly-cased module name given here, ## this example loads OpenQA::WebAPI::Plugin::AMQP @@ -56,17 +62,17 @@ ## avoid potentially malicious JavaScript code from hijacking the user session. ## * Set to "insecure-browsing" so these files can be browsed directly. This is ## insecure as potentially malicious JavaScript can hijack the user session. -## * Set to "subdomain:…" to let openQA redirect these files to another -## subdomain, e.g. "subdomain:file" will redirect file downloads from e.g. -## "example.openqa.org" to "file.example.openqa.org". The files will no longer -## be served as attachments to HTML files can be browsed conveniently and -## securely from that subdomain. +## * Set to "domain:…" to let openQA redirect these files to another domain, +## e.g. "domain:openqa-files.org" can be used to redirect file downloads from +## e.g. "openqa.org" to "openqa-files.org". The files will no longer be served +## as attachments so HTML files can be browsed conveniently and securely from +## that second domain. ## note: Does *not* affect files served via a reverse proxy. The default NGINX ## config contained by the openQA repo shows how to enforce a download ## prompt for assets served via NGINX. It also shows how to setup -## redirections to a different subdomain which is a little bit more config -## effort and you also need to make sure your certificate is valid for -## this subdomain. +## redirections to a different domain which is a little bit more config +## effort and you also need to make sure you have a valid certificate for +## the other domain. #file_security_policy = download-prompt ## space-separated list of domains recognized by job labeling @@ -317,6 +323,8 @@ #worker_limit_retry_delay = 900 ## Maximum number of retries with exponential back-off for GruJobs to wait for a GruTask to appear in the database #wait_for_grutask_retries = 6 +## Maximum size of MCP results in bytes (to prevent performance issues) +# mcp_max_result_size = 500000 [archiving] ## Moves logs of jobs which are preserved during the cleanup because they are considered important diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Schema/ResultSet/Workers.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/Schema/ResultSet/Workers.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Schema/ResultSet/Workers.pm 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/Schema/ResultSet/Workers.pm 2025-09-05 17:05:00.000000000 +0200 @@ -0,0 +1,23 @@ +# Copyright SUSE LLC +# SPDX-License-Identifier: GPL-2.0-or-later + +package OpenQA::Schema::ResultSet::Workers; +use Mojo::Base 'DBIx::Class::ResultSet', -signatures; + +sub stats ($self) { + my $total = $self->count; + my $total_online = grep { !$_->dead } $self->all(); + my $free_active_workers = grep { !$_->dead } $self->search({job_id => undef, error => undef})->all(); + my $free_broken_workers = grep { !$_->dead } $self->search({job_id => undef, error => {'!=' => undef}})->all(); + my $busy_workers = grep { !$_->dead } $self->search({job_id => {'!=' => undef}})->all(); + + return { + total => $total, + total_online => $total_online, + free_active_workers => $free_active_workers, + free_broken_workers => $free_broken_workers, + busy_workers => $busy_workers, + }; +} + +1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Setup.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/Setup.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Setup.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/Setup.pm 2025-09-05 17:05:00.000000000 +0200 @@ -77,6 +77,7 @@ max_rss_limit => 0, profiling_enabled => 0, monitoring_enabled => 0, + mcp_enabled => 'no', plugins => undef, hide_asset_types => 'repo', file_security_policy => 'download-prompt', @@ -236,6 +237,7 @@ max_online_workers => 1000, wait_for_grutask_retries => 6, # exponential, ~4 minutes worker_limit_retry_delay => ONE_HOUR / 4, + mcp_max_result_size => 500000, }, archiving => { archive_preserved_important_jobs => 0, @@ -308,8 +310,8 @@ } sub _validate_security_policy ($app, $config) { - if ($config->{file_security_policy} =~ m/^(download-prompt|insecure-browsing|subdomain:(.+))$/) { - if (defined(my $subdomain = $2)) { $config->{file_subdomain} = "$subdomain." } + if ($config->{file_security_policy} =~ m/^(download-prompt|insecure-browsing|domain:(.+))$/) { + if (defined(my $domain = $2)) { $config->{file_domain} = $domain } } else { $config->{file_security_policy} = 'download-prompt'; @@ -409,6 +411,7 @@ push @{$server->plugins->namespaces}, 'OpenQA::WebAPI::Plugin'; $server->plugin($_) for qw(Helpers MIMETypes CSRF REST Gru YAML); $server->plugin('AuditLog') if $server->config->{global}{audit_enabled}; + $server->plugin('MCP') if $server->config->{global}{mcp_enabled} eq 'read-only'; # Load arbitrary plugins defined in config: 'plugins' in section # '[global]' can be a space-separated list of plugins to load, by # module name under OpenQA::WebAPI::Plugin:: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Controller/Auth.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Controller/Auth.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Controller/Auth.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Controller/Auth.pm 2025-09-05 17:05:00.000000000 +0200 @@ -12,7 +12,7 @@ sub check ($self) { my $config = $self->app->config; return 1 if $config->{no_localhost_auth} && $self->is_local_request; - return 0 if $self->via_subdomain($config->{global}->{file_subdomain}); + return 0 if $self->via_domain($config->{global}->{file_domain}); my $req = $self->req; my $headers = $req->headers; @@ -46,9 +46,9 @@ sub auth ($self) { my $log = $self->app->log; - # Prevent authentication via file subdomain (where potentially untrusted HTML is served) - if ($self->via_subdomain($self->config->{global}->{file_subdomain})) { - $self->render(json => {error => 'Forbidden via file subdomain'}, status => 403); + # Prevent authentication via file domain (where potentially untrusted HTML is served) + if ($self->via_domain($self->config->{global}->{file_domain})) { + $self->render(json => {error => 'Forbidden via file domain'}, status => 403); return 0; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Controller/Session.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Controller/Session.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Controller/Session.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Controller/Session.pm 2025-09-05 17:05:00.000000000 +0200 @@ -56,8 +56,8 @@ my $config = $self->app->config; my $auth_method = $config->{auth}->{method}; my $auth_module = "OpenQA::WebAPI::Auth::$auth_method"; - return $self->render(text => 'Forbidden via file subdomain', status => 403) - if $self->via_subdomain($config->{global}->{file_subdomain}); + return $self->render(text => 'Forbidden via file domain', status => 403) + if $self->via_domain($config->{global}->{file_domain}); # prevent redirecting loop when referrer is login page $ref = 'index' if !$ref or $ref eq $self->url_for('login'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Plugin/SharedHelpers.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Plugin/SharedHelpers.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/Shared/Plugin/SharedHelpers.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/Shared/Plugin/SharedHelpers.pm 2025-09-05 17:05:00.000000000 +0200 @@ -21,7 +21,7 @@ $app->helper(is_admin => \&_is_admin); $app->helper(is_local_request => \&_is_local_request); $app->helper(render_specific_not_found => \&_render_specific_not_found); - $app->helper(via_subdomain => \&_via_subdomain); + $app->helper(via_domain => \&_via_domain); } # returns the isotovideo command server web socket URL and the VNC argument for the given job or undef if not available @@ -80,9 +80,6 @@ return $c->render(status => 404, text => "$title - $error_message"); } -sub _via_subdomain ($c, $subdomain) { - return 0 unless defined $subdomain; - return index($c->req->url->to_abs->host, $subdomain) == 0; -} +sub _via_domain ($c, $domain) { defined $domain && $c->req->url->to_abs->host eq $domain } 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Controller/Admin/Workers.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Controller/Admin/Workers.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Controller/Admin/Workers.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Controller/Admin/Workers.pm 2025-09-05 17:05:00.000000000 +0200 @@ -29,12 +29,7 @@ sub index ($self) { my $workers_db = $self->schema->resultset('Workers'); - my $total_online = grep { !$_->dead } $workers_db->all(); - my $total = $workers_db->count; - my $free_active_workers = grep { !$_->dead } $workers_db->search({job_id => undef, error => undef})->all(); - my $free_broken_workers - = grep { !$_->dead } $workers_db->search({job_id => undef, error => {'!=' => undef}})->all(); - my $busy_workers = grep { !$_->dead } $workers_db->search({job_id => {'!=' => undef}})->all(); + my $worker_stats = $workers_db->stats; my %workers; while (my $w = $workers_db->next) { @@ -42,11 +37,11 @@ $workers{$w->name} = _extend_info($w); } $self->stash( - workers_online => $total_online, - total => $total, - workers_active_free => $free_active_workers, - workers_broken_free => $free_broken_workers, - workers_busy => $busy_workers, + workers_online => $worker_stats->{total_online}, + total => $worker_stats->{total}, + workers_active_free => $worker_stats->{free_active_workers}, + workers_broken_free => $worker_stats->{free_broken_workers}, + workers_busy => $worker_stats->{busy_workers}, is_admin => !!$self->is_admin, workers => \%workers ); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Controller/File.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Controller/File.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Controller/File.pm 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Controller/File.pm 2025-09-05 17:05:00.000000000 +0200 @@ -173,13 +173,11 @@ sub _redirect_if_configured ($self, $is_text) { # skip harmless text files as the viewer doesn't follow redirects and those files are not problematic anyway - return 0 if $is_text || !defined(my $subdomain = $self->app->config->{global}->{file_subdomain}); - # redirect to configured subdomain so potentially dangerious HTML files cannot use the current session + return 0 if $is_text || !defined(my $domain = $self->app->config->{global}->{file_domain}); + # redirect to configured domain so potentially dangerious HTML files cannot use the current session my $url = $self->req->url->to_abs; - my $host = $url->host; - # skip if already redirected - return 0 unless index($host, $subdomain) == -1; - $url->host($subdomain . $host); + return 0 if $url->host eq $domain; # skip if already redirected + $url->host($domain); $self->redirect_to($url); return 1; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Plugin/MCP.pm new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Plugin/MCP.pm --- old/openQA-5.1757005118.aac56dbc/lib/OpenQA/WebAPI/Plugin/MCP.pm 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1757084700.fad3731d/lib/OpenQA/WebAPI/Plugin/MCP.pm 2025-09-05 17:05:00.000000000 +0200 @@ -0,0 +1,175 @@ +# Copyright SUSE LLC +# SPDX-License-Identifier: GPL-2.0-or-later + +package OpenQA::WebAPI::Plugin::MCP; +use Mojo::Base 'Mojolicious::Plugin', -signatures; + +use MCP::Server; +use Mojo::Template; +use Mojo::Loader qw(data_section); +use Mojo::File qw(path); + +sub register ($self, $app, $config) { + my $mcp = MCP::Server->new; + $mcp->name('openQA'); + $mcp->version('1.0.0'); + + $mcp->tool( + name => 'openqa_get_info', + description => 'Get information about the openQA server, connected workers and the current user', + code => \&tool_openqa_get_info + ); + + $mcp->tool( + name => 'openqa_get_job_info', + description => 'Get information about a specific openQA job', + input_schema => { + type => 'object', + properties => {job_id => {type => 'integer', minimum => 1}}, + required => ['job_id'], + }, + code => \&tool_openqa_get_job_info + ); + + $mcp->tool( + name => 'openqa_get_log_file', + description => 'Get the content of a specific log file for an openQA job', + input_schema => { + type => 'object', + properties => { + job_id => {type => 'integer', minimum => 1}, + file_name => {type => 'string', minLength => 1} + }, + required => ['job_id', 'file_name'], + }, + code => \&tool_openqa_get_log_file + ); + + my $mcp_auth = $app->routes->under('/')->to('Auth#auth')->name('mcp_ensure_user'); + $mcp_auth->any('/experimental/mcp' => $mcp->to_action)->name('mcp'); +} + +sub tool_openqa_get_info ($tool, $args) { + my $c = _get_controller($tool); + my $user = _get_user($tool); + my $schema = _get_schema($tool); + + my $worker_stats = $schema->resultset('Workers')->stats; + my $vars = { + app_name => $c->stash->{appname}, + app_version => $c->stash->{current_version} // 'n/a', + user_name => $user->name, + user_id => $user->id, + is_admin => $user->is_admin ? 'yes' : 'no', + is_operator => $user->is_operator ? 'yes' : 'no', + workers_total => $worker_stats->{total}, + workers_total_online => $worker_stats->{total_online}, + workers_offline => $worker_stats->{total} - $worker_stats->{total_online}, + active_workers => $worker_stats->{free_active_workers}, + broken_workers => $worker_stats->{free_broken_workers}, + busy_workers => $worker_stats->{busy_workers}, + }; + return _render_from_data_section('openqa_get_info.txt.ep', $vars); +} + +sub tool_openqa_get_job_info ($tool, $args) { + my $job_id = $args->{job_id}; + + my $schema = _get_schema($tool); + return $tool->text_result('Job does not exist', 1) + unless my $job = $schema->resultset('Jobs')->find(int($job_id)); + my @comments = map { $_->extended_hash } $job->search_related(comments => {})->all; + + my $info = $job->to_hash(assets => 1, check_assets => 1, deps => 1, details => 1, parent_group => 1); + return _render_from_data_section('openqa_get_job_info.txt.ep', {job => $info, comments => \@comments}); +} + +sub tool_openqa_get_log_file ($tool, $args) { + my $job_id = $args->{job_id}; + my $file_name = $args->{file_name}; + + # Prevent directory traversal attacks + return $tool->text_result('Invalid file name', 1) unless $file_name =~ /^[\w.\-]+$/; + + # Only text logs for now + return $tool->text_result('File type not yet supported via MCP', 1) if $file_name !~ /\.txt$/; + + my $schema = _get_schema($tool); + return $tool->text_result('Job does not exist', 1) + unless my $job = $schema->resultset('Jobs')->find(int($job_id)); + + my $dir = $job->result_dir; + my $file = path($dir, $file_name); + return $tool->text_result('Log file does not exist', 1) unless -r $file; + + my $c = _get_controller($tool); + my $max = $c->app->config->{misc_limits}{mcp_max_result_size}; + return $tool->text_result('File too large to be transmitted via MCP', 1) if -s $file > $max; + return $tool->text_result($file->slurp); +} + +sub _get_controller ($tool) { $tool->context->{controller} } +sub _get_schema ($tool) { _get_controller($tool)->schema } +sub _get_user ($tool) { _get_controller($tool)->stash->{current_user}{user} } + +sub _render_from_data_section ($template_name, $vars) { + my $template = data_section(__PACKAGE__, $template_name); + return Mojo::Template->new->vars(1)->render($template, $vars); +} + +1; +__DATA__ +@@ openqa_get_info.txt.ep +Server: <%= $app_name %> (<%= $app_version %>) +Current User: <%= $user_name %> (id: <%= $user_id %>, admin: <%= $is_admin %>, operator: <%= $is_operator %>) +Workers: <%= $workers_total %> + - online: <%= $workers_total_online %> + - offline: <%= $workers_offline %> + - idle: <%= $active_workers %> + - busy: <%= $busy_workers %> + - broken: <%= $broken_workers %> + +@@ openqa_get_job_info.txt.ep +Job ID: <%= $job->{id} // 'Unknown' %> +Name: <%= $job->{name} // 'Unknown' %> +Group: <%= $job->{group} // 'Unknown' %> +Priority: <%= $job->{priority} // 'Unknown' %> +State: <%= $job->{state} // 'Unknown' %> +Result: <%= $job->{result} // 'Unknown' %> +Started: <%= $job->{t_started} // 'Never' %> +Finished: <%= $job->{t_finished} // 'Never' %> + +Test Results: +% if (@{$job->{testresults}}) { + % for my $result (@{$job->{testresults}}) { + - <%= $result->{name} %>: <%= $result->{result} %> + % } +% } +% else { + No test results available yet +% } + +Test Settings: +% for my $setting (sort keys %{$job->{settings}}) { + - <%= $setting %>: <%= $job->{settings}{$setting} %> +% } + +Available Logs: +% if (@{$job->{logs}}) { + % for my $log (@{$job->{logs}}) { + - <%= $log %> + % } +% } +% else { + No logs available +% } + +Comments: +% if (@$comments) { + % for my $comment (@$comments) { + - <%= $comment->{userName} %> (<%= $comment->{created} %>): <%= $comment->{renderedMarkdown} %> + % } +% } +% else { + No comments yet +% } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/32-openqa_client.t new/openQA-5.1757084700.fad3731d/t/32-openqa_client.t --- old/openQA-5.1757005118.aac56dbc/t/32-openqa_client.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/32-openqa_client.t 2025-09-05 17:05:00.000000000 +0200 @@ -8,11 +8,12 @@ use FindBin; use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib"; use Test::Mojo; -use Mojo::File qw(tempfile path); +use Mojo::File qw(tempfile path tempdir); use OpenQA::Events; use OpenQA::Test::Case; use OpenQA::Test::Client 'client'; use OpenQA::Test::TimeLimit '80'; +use Mojo::Util qw(scope_guard); plan skip_all => 'set HEAVY=1 to execute (takes longer)' unless $ENV{HEAVY}; @@ -22,6 +23,10 @@ # allow up to 200MB - videos mostly $ENV{MOJO_MAX_MESSAGE_SIZE} = 207741824; +my $tempdir = tempdir("$FindBin::Script-XXXX", TMPDIR => 1); +chdir $tempdir; +my $guard = scope_guard sub { chdir $FindBin::Bin }; + my @client_args = (apikey => 'PERCIVALKEY02', apisecret => 'PERCIVALSECRET02'); my $t = client(Test::Mojo->new('OpenQA::WebAPI'), @client_args); my $client = $t->ua; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/37-limit_assets.t new/openQA-5.1757084700.fad3731d/t/37-limit_assets.t --- old/openQA-5.1757005118.aac56dbc/t/37-limit_assets.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/37-limit_assets.t 2025-09-05 17:05:00.000000000 +0200 @@ -6,7 +6,7 @@ use FindBin; use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib"; -use Mojo::File 'path'; +use Mojo::File qw(path tempdir); use Test::Mojo; use Test::Warnings ':report_warnings'; use Test::MockModule; @@ -41,6 +41,8 @@ # assume some assets already have a last_use_job_id $assets->find($_)->update({last_use_job_id => 99963}) for (2, 3); +my $tempdir = tempdir("$FindBin::Script-XXXX", TMPDIR => 1); + note('Asset directory: ' . assetdir()); subtest 'configurable concurrency' => sub { @@ -55,6 +57,7 @@ }; subtest 'filesystem removal' => sub { + local $ENV{OPENQA_SHAREDIR} = "$tempdir/openqa"; my $asset_sub_dir = path(assetdir(), 'foo'); $asset_sub_dir->remove_tree->make_path; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/43-scheduling-and-worker-scalability.t new/openQA-5.1757084700.fad3731d/t/43-scheduling-and-worker-scalability.t --- old/openQA-5.1757005118.aac56dbc/t/43-scheduling-and-worker-scalability.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/43-scheduling-and-worker-scalability.t 2025-09-05 17:05:00.000000000 +0200 @@ -12,7 +12,7 @@ use Scalar::Util 'looks_like_number'; use List::Util qw(min max); use Mojo::File qw(path tempfile); -use Mojo::Util 'dumper'; +use Mojo::Util qw(dumper scope_guard); use IPC::Run qw(start); use FindBin; use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib"; @@ -56,6 +56,8 @@ # setup basedir, config dir and database my $tempdir = setup_fullstack_temp_dir('scalability'); +chdir $tempdir; +my $guard = scope_guard sub { chdir $FindBin::Bin }; my $schema = OpenQA::Test::Database->new->create; my $workers = $schema->resultset('Workers'); my $jobs = $schema->resultset('Jobs'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/api/03-auth.t new/openQA-5.1757084700.fad3731d/t/api/03-auth.t --- old/openQA-5.1757005118.aac56dbc/t/api/03-auth.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/api/03-auth.t 2025-09-05 17:05:00.000000000 +0200 @@ -273,17 +273,17 @@ ->json_is({error => 'invalid personal access token'}); }; -subtest 'auth forbidden via subdomain' => sub { +subtest 'auth forbidden via domain' => sub { my $rendered; my $req = Mojo::Message::Request->new; - $req->url->parse('http://foobar.openqa.de/test/42'); + $req->url->parse('http://foobar-openqa.de/test/42'); my $controller_mock = Test::MockModule->new('Mojolicious::Controller'); $controller_mock->redefine(req => $req); $controller_mock->redefine(render => sub ($c, @args) { $rendered = [@args] }); my $c = OpenQA::Shared::Controller::Auth->new(app => $t->app, req => $req); - $c->config->{global}->{file_subdomain} = 'foobar.'; - is $c->auth, 0, 'auth denied via subdomain'; - my %expected_json = (error => 'Forbidden via file subdomain'); + $c->config->{global}->{file_domain} = 'foobar-openqa.de'; + is $c->auth, 0, 'auth denied via domain'; + my %expected_json = (error => 'Forbidden via file domain'); my @expected = (json => \%expected_json, status => 403); is_deeply $rendered, \@expected, 'expected error and status'; $req->url->parse('http://openqa.de/test/42'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/api/04-jobs.t new/openQA-5.1757084700.fad3731d/t/api/04-jobs.t --- old/openQA-5.1757005118.aac56dbc/t/api/04-jobs.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/api/04-jobs.t 2025-09-05 17:05:00.000000000 +0200 @@ -9,6 +9,7 @@ use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../external/os-autoinst-common/lib"; use Mojo::Base -signatures; use File::Temp; +use File::Copy::Recursive qw(dircopy); use Test::Mojo; use Test::Output; use Test::Warnings ':report_warnings'; @@ -39,7 +40,7 @@ note("OPENQA_BASEDIR: $tempdir"); path($tempdir, '/openqa/testresults')->make_path; my $share_dir = path($tempdir, 'openqa/share')->make_path; -symlink "$FindBin::Bin/../data/openqa/share/factory", "$share_dir/factory"; +dircopy("$FindBin::Bin/../data/openqa/share/factory", "$share_dir/factory"); # ensure job events are logged $ENV{OPENQA_CONFIG} = $tempdir; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/api/17-mcp.t new/openQA-5.1757084700.fad3731d/t/api/17-mcp.t --- old/openQA-5.1757005118.aac56dbc/t/api/17-mcp.t 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1757084700.fad3731d/t/api/17-mcp.t 2025-09-05 17:05:00.000000000 +0200 @@ -0,0 +1,161 @@ +#!/usr/bin/env perl + +# Copyright SUSE LLC +# SPDX-License-Identifier: GPL-2.0-or-later + +use Test::Most; +use utf8; +use FindBin; +use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../external/os-autoinst-common/lib"; +use Mojo::Base -signatures; +use Test::Mojo; +use Test::Warnings ':report_warnings'; +use Test::MockModule; +use OpenQA::Test::TimeLimit '30'; +use OpenQA::Test::Case; +use OpenQA::Test::Client 'client'; +use MCP::Client; + +OpenQA::Test::Case->new(config_directory => "$FindBin::Bin/../data/17-mcp") + ->init_data(fixtures_glob => '01-jobs.pl 02-workers.pl 03-users.pl'); + +my $PERSONAL_ACCESS_TOKEN = 'lance:LANCELOTKEY01:MANYPEOPLEKNOW'; + +my $t = Test::Mojo->new('OpenQA::WebAPI'); +my $client = MCP::Client->new(ua => $t->ua, url => $t->ua->server->url->path('/experimental/mcp')); + +subtest 'Authentication' => sub { + $t->get_ok('/experimental/mcp')->status_is(403)->json_is({error => 'no api key'}); + + $t->ua->on(start => sub ($ua, $tx) { $tx->req->headers->authorization("Bearer $PERSONAL_ACCESS_TOKEN") }); + $t->get_ok('/experimental/mcp')->status_is(405); +}; + +subtest 'Start session' => sub { + is $client->session_id, undef, 'no session id'; + my $result = $client->initialize_session; + is $result->{serverInfo}{name}, 'openQA', 'server name'; + is $result->{serverInfo}{version}, '1.0.0', 'server version'; + ok $result->{capabilities}, 'has capabilities'; + ok $client->session_id, 'session id set'; +}; + +subtest 'List tools' => sub { + my $result = $client->list_tools; + is scalar @{$result->{tools}}, 3, 'three tools available'; + is $result->{tools}[0]{name}, 'openqa_get_info', 'right tool name'; + is $result->{tools}[1]{name}, 'openqa_get_job_info', 'right tool name'; + is $result->{tools}[2]{name}, 'openqa_get_log_file', 'right tool name'; +}; + +subtest 'openqa_get_info tool' => sub { + my $result = $client->call_tool('openqa_get_info'); + ok !$result->{isError}, 'not an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/Server: openQA \(.+\)/, 'openQA server'; + like $text, qr/Current User: lance \(id: 99902, admin: no, operator: no\)/, 'current user'; + like $text, qr/Workers: 2/, 'total workers'; + like $text, qr/- online: 0/, 'online workers'; + like $text, qr/- offline: 2/, 'offline workers'; + like $text, qr/- idle: 0/, 'idle workers'; + like $text, qr/- busy: 0/, 'busy workers'; + like $text, qr/- broken: 0/, 'broken workers'; +}; + +subtest 'openqa_get_job_info tool' => sub { + subtest 'Failed job' => sub { + my $result = $client->call_tool('openqa_get_job_info', {job_id => 99938}); + ok !$result->{isError}, 'not an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/Job ID: +99938/, 'has job id'; + like $text, qr/Name: +opensuse-Factory-DVD-x86_64-Build0048-doc\@64bit/, 'has name'; + like $text, qr/Group: +opensuse/, 'has group'; + like $text, qr/Priority: +36/, 'has priority'; + like $text, qr/State: +done/, 'has state'; + like $text, qr/Result: +failed/, 'has result'; + like $text, qr/Started: +\d+-\d+-\d+T\d+:\d+:\d+/, 'has started time'; + like $text, qr/Finished: +\d+-\d+-\d+T\d+:\d+:\d+/, 'has finished time'; + like $text, qr/- autoinst-log\.txt/s, 'has log files listed'; + like $text, qr/No test results available yet/s, 'no test results yet'; + like $text, qr/DISTRI: opensuse/, 'settings are present'; + like $text, qr/VERSION: Factory/, 'settings are present'; + like $text, qr/No comments yet/, 'no comments yet'; + }; + + subtest 'Passed job' => sub { + subtest 'Add comment to test job' => sub { + $t->post_ok("/api/v1/jobs/99764/comments" => form => {text => 'Just a test comment'})->status_is(200); + }; + + my $result = $client->call_tool('openqa_get_job_info', {job_id => 99764}); + ok !$result->{isError}, 'not an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/Job ID: +99764/, 'has job id'; + like $text, qr/Name: +opensuse-13.1-DVD-x86_64-Build0091-console_tests\@64bit/, 'has name'; + like $text, qr/Group: +Unknown/, 'unknown group'; + like $text, qr/Priority: +35/, 'has priority'; + like $text, qr/State: +done/, 'has state'; + like $text, qr/Result: +passed/, 'has result'; + like $text, qr/Started: +\d+-\d+-\d+T\d+:\d+:\d+/, 'has started time'; + like $text, qr/Finished: +\d+-\d+-\d+T\d+:\d+:\d+/, 'has finished time'; + like $text, qr/No logs available/s, 'no log files listed'; + like $text, qr/No test results available yet/s, 'no test results yet'; + like $text, qr/DISTRI: opensuse/, 'settings are present'; + like $text, qr/VERSION: 13\.1/, 'settings are present'; + like $text, qr/lance.+Just a test comment/, 'has comments'; + }; + + subtest 'Input validation failed' => sub { + eval { $client->call_tool('openqa_get_job_info', {job_id => 'abc'}) }; + like $@, qr/Error -32602: Invalid arguments/, 'right error message'; + }; +}; + +subtest 'openqa_get_log_file tool' => sub { + subtest 'Failed job' => sub { + my $result = $client->call_tool('openqa_get_log_file', {job_id => 99938, file_name => 'autoinst-log.txt'}); + ok !$result->{isError}, 'not an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/starting: \/usr\/bin\/qemu-kvm/, 'has log content from start'; + like $text, qr/test logpackages died/, 'has log content from end'; + }; + + subtest 'Input validation failed' => sub { + eval { $client->call_tool('openqa_get_log_file', {job_id => 'abc', file_name => 'autoinst-log.txt'}) }; + like $@, qr/Error -32602: Invalid arguments/, 'right error message'; + eval { $client->call_tool('openqa_get_log_file', {job_id => 99938, file_name => ''}) }; + like $@, qr/Error -32602: Invalid arguments/, 'right error message'; + }; + + subtest 'Path traversal attack' => sub { + my $result = $client->call_tool('openqa_get_log_file', {job_id => 99938, file_name => '../../etc/passwd'}); + ok $result->{isError}, 'is an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/Invalid file name/, 'error message'; + }; + + subtest 'Job does not exist' => sub { + local $t->app->config->{misc_limits}{mcp_max_result_size} = 100; + my $result = $client->call_tool('openqa_get_log_file', {job_id => 999999999, file_name => 'autoinst-log.txt'}); + ok $result->{isError}, 'is an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/Job does not exist/, 'error message'; + }; + + subtest 'Unsupported file type' => sub { + my $result = $client->call_tool('openqa_get_log_file', {job_id => 99938, file_name => 'video.ogv'}); + ok $result->{isError}, 'is an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/File type not yet supported via MCP/, 'error message'; + }; + + subtest 'File too large' => sub { + local $t->app->config->{misc_limits}{mcp_max_result_size} = 100; + my $result = $client->call_tool('openqa_get_log_file', {job_id => 99938, file_name => 'autoinst-log.txt'}); + ok $result->{isError}, 'is an error'; + my $text = $result->{content}[0]{text}; + like $text, qr/File too large to be transmitted via MCP/, 'error message'; + }; +}; + +done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/config.t new/openQA-5.1757084700.fad3731d/t/config.t --- old/openQA-5.1757005118.aac56dbc/t/config.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/config.t 2025-09-05 17:05:00.000000000 +0200 @@ -52,6 +52,7 @@ max_rss_limit => 0, profiling_enabled => 0, monitoring_enabled => 0, + mcp_enabled => 'no', hide_asset_types => 'repo', file_security_policy => 'download-prompt', recognized_referers => [], @@ -188,6 +189,7 @@ max_online_workers => 1000, wait_for_grutask_retries => 6, worker_limit_retry_delay => ONE_HOUR / 4, + mcp_max_result_size => 500000, }, archiving => { archive_preserved_important_jobs => 0, @@ -333,10 +335,10 @@ $config{file_security_policy} = 'wrong'; combined_like { OpenQA::Setup::_validate_security_policy($app, \%config) } qr/Invalid.*security/, 'warning logged'; is $config{file_security_policy}, 'download-prompt', 'default to "download-prompt" on invalid value'; - is $config{file_subdomain}, undef, 'file_subdomain not populated yet'; - $config{file_security_policy} = 'subdomain:foo'; + is $config{file_domain}, undef, 'file_domain not populated yet'; + $config{file_security_policy} = 'domain:openqa-foo'; OpenQA::Setup::_validate_security_policy($app, \%config); - is $config{file_subdomain}, 'foo.', 'file_subdomain populated via "subdomain:"'; + is $config{file_domain}, 'openqa-foo', 'file_domain populated via "domain:"'; }; subtest 'Multiple config files' => sub { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/data/17-mcp/openqa.ini new/openQA-5.1757084700.fad3731d/t/data/17-mcp/openqa.ini --- old/openQA-5.1757005118.aac56dbc/t/data/17-mcp/openqa.ini 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1757084700.fad3731d/t/data/17-mcp/openqa.ini 2025-09-05 17:05:00.000000000 +0200 @@ -0,0 +1,2 @@ +[global] +mcp_enabled = read-only diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/t/ui/07-file.t new/openQA-5.1757084700.fad3731d/t/ui/07-file.t --- old/openQA-5.1757005118.aac56dbc/t/ui/07-file.t 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/t/ui/07-file.t 2025-09-05 17:05:00.000000000 +0200 @@ -141,12 +141,12 @@ $t->get_ok('/assets/hdd/foo.qcow2')->status_is(200)->content_type_is('application/octet-stream'); $t->get_ok('/assets/repo/testrepo/doesnotexist')->status_is(404); -subtest 'redirection to different subdomain' => sub { +subtest 'redirection to different domain' => sub { my $config = $t->app->config->{global}; - $config->{file_security_policy} = 'subdomain:file'; - $config->{file_subdomain} = 'file.'; + $config->{file_security_policy} = 'domain:openqa-files'; + $config->{file_domain} = 'openqa-files'; $t->get_ok('/assets/repo/testrepo/README.html')->status_is(302); - $t->header_like(Location => qr|^http://file\.[^/]*/assets/repo/testrepo/README.html$|); + $t->header_like(Location => qr|^http://openqa-files(:\d+)?/assets/repo/testrepo/README.html$|); }; done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/tools/ci/ci-packages.txt new/openQA-5.1757084700.fad3731d/tools/ci/ci-packages.txt --- old/openQA-5.1757005118.aac56dbc/tools/ci/ci-packages.txt 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/tools/ci/ci-packages.txt 2025-09-05 17:05:00.000000000 +0200 @@ -59,6 +59,7 @@ perl-Crypt-DES-2.07 perl-Crypt-DH-GMP-0.00012 perl-Crypt-Rijndael-1.13 +perl-CryptX-0.87.0 perl-CSS-Minifier-XS-0.13 perl-Data-Dump-1.23 perl-Data-Dumper-Concise-2.023 @@ -135,7 +136,7 @@ perl-Log-Contextual-0.008001 perl-LWP-MediaTypes-6.02 perl-LWP-Protocol-https-6.06 -perl-MCP +perl-MCP-0.40.0 perl-Meta-Builder-0.004 perl-Minion-10.310.0 perl-Minion-Backend-SQLite-5.0.7 @@ -221,6 +222,7 @@ perl-Syntax-Keyword-Try-0.28 perl-TAP-Harness-JUnit-0.42 perl-Task-Weaken-1.06 +perl-Test-CheckGitStatus perl-Test-Deep-1.127 perl-Test-Differences-0.64 perl-Test-Exception-0.430000 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1757005118.aac56dbc/tools/ci/run_unit_tests.sh new/openQA-5.1757084700.fad3731d/tools/ci/run_unit_tests.sh --- old/openQA-5.1757005118.aac56dbc/tools/ci/run_unit_tests.sh 2025-09-04 18:58:38.000000000 +0200 +++ new/openQA-5.1757084700.fad3731d/tools/ci/run_unit_tests.sh 2025-09-05 17:05:00.000000000 +0200 @@ -16,6 +16,8 @@ # that context in the prove calling context. export PERL_TEST_HARNESS_DUMP_TAP=test-results export HARNESS='--harness TAP::Harness::JUnit --timer' +# Activate Test::CheckGitStatus +export CHECK_GIT_STATUS=1 mkdir -p test-results/junit sudo chown -R $USER test-results # circleCI can be particularly slow sometimes since some time around 2021-06 ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.F9UW4N/_old 2025-09-08 09:58:03.827738776 +0200 +++ /var/tmp/diff_new_pack.F9UW4N/_new 2025-09-08 09:58:03.835739109 +0200 @@ -1,5 +1,5 @@ name: openQA -version: 5.1757005118.aac56dbc -mtime: 1757005118 -commit: aac56dbcc9ccbfc204e7afc14817cddf0b2a01a0 +version: 5.1757084700.fad3731d +mtime: 1757084700 +commit: fad3731d7a90a6ca62baa984464d258845f20ca7
