diff --git a/.github/workflows/build-and-test-charm.yaml b/.github/workflows/build-and-test-charm.yaml new file mode 100644 index 0000000..49938c7 --- /dev/null +++ b/.github/workflows/build-and-test-charm.yaml @@ -0,0 +1,103 @@ +name: Build and test charm + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + DEBIAN_FRONTEND: noninteractive + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + - name: Install dependencies + run: sudo snap install ruff --classic + - name: Run linters + run: ruff check --preview . + working-directory: charm + + lib-check: + name: Check libraries + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + # fetch-depth: 0 # TODO optimize that for speed + persist-credentials: false + - name: Check libs + uses: canonical/charming-actions/check-libraries@1753e0803f70445132e92acd45c905aba6473225 # 2.7.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + charm-path: . + + pack-charm: + name: Build charm + runs-on: ubuntu-latest + steps: + - name: Aggressive cleanup to regain disk space + run: | + # Remove Java (JDKs) + sudo rm -rf /usr/lib/jvm + # Remove .NET SDKs + sudo rm -rf /usr/share/dotnet + # Remove Swift toolchain + sudo rm -rf /usr/share/swift + # Remove Haskell (GHC) + sudo rm -rf /usr/local/.ghcup + # Remove Julia + sudo rm -rf /usr/local/julia* + # Remove Android SDKs + sudo rm -rf /usr/local/lib/android + # Remove Chromium (optional if not using for browser tests) + sudo rm -rf /usr/local/share/chromium + # Remove Microsoft/Edge and Google Chrome builds + sudo rm -rf /opt/microsoft /opt/google + + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Setup LXD + uses: canonical/setup-lxd@a3c85fc6fb7fff43fcfeae87659e41a8f635b7dd + + - name: Install ~~charmcraft~~spread + run: sudo snap install charmcraft --classic + + - name: Pack charm + run: charmcraft pack -v + + - name: Run charm tests + run: | + charmcraft.spread -v charm/tests/ + + - name: Upload charm artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: error-tracker.charm + path: "*.charm" + + - name: Upload charm to charmhub + uses: canonical/charming-actions/upload-charm@1753e0803f70445132e92acd45c905aba6473225 # 2.7.0 + with: + credentials: '${{ secrets.CHARMHUB_TOKEN }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' + tag-prefix: ${{ matrix.charm }} + built-charm-path: error-tracker_ubuntu@24.04-amd64.charm + if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/build-and-test-charms.yaml b/.github/workflows/build-and-test-charms.yaml deleted file mode 100644 index 473e699..0000000 --- a/.github/workflows/build-and-test-charms.yaml +++ /dev/null @@ -1,121 +0,0 @@ -name: Build and test charms - -on: - push: - pull_request: - -env: - DEBIAN_FRONTEND: noninteractive - -jobs: - define-charm-list: - name: Define charm list to build - runs-on: ubuntu-24.04 - outputs: - charms: ${{ steps.charms.outputs.charms }} - steps: - - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - - name: Generate list - id: charms - run: | - list="$(find . -mindepth 1 -maxdepth 1 -type d -printf '"%P", ')" - echo "charms=[$list]" - echo "charms=[$list]" >> $GITHUB_OUTPUT - working-directory: charms - - lint: - name: Lint - (${{ matrix.charm }}) - runs-on: ubuntu-24.04 - needs: - - define-charm-list - strategy: - fail-fast: false - matrix: - charm: ${{ fromJSON(needs.define-charm-list.outputs.charms) }} - steps: - - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - name: Install dependencies - run: sudo snap install ruff --classic - - name: Run linters - run: ruff check --preview . - working-directory: charms/${{ matrix.charm }} - - lib-check: - name: Check libraries - (${{ matrix.charm }}) - runs-on: ubuntu-24.04 - needs: - - define-charm-list - strategy: - matrix: - charm: ${{ fromJSON(needs.define-charm-list.outputs.charms) }} - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - # fetch-depth: 0 # TODO optimize that for speed - persist-credentials: false - - name: Check libs - uses: canonical/charming-actions/check-libraries@1753e0803f70445132e92acd45c905aba6473225 # 2.7.0 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - charm-path: charms/${{ matrix.charm }} - - pack-charm: - name: Build charm - (${{ matrix.charm }}) - runs-on: ubuntu-24.04 - needs: - - define-charm-list - strategy: - matrix: - charm: ${{ fromJSON(needs.define-charm-list.outputs.charms) }} - steps: - - name: Checkout - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - with: - persist-credentials: false - - - name: Setup LXD - uses: canonical/setup-lxd@a3c85fc6fb7fff43fcfeae87659e41a8f635b7dd - - - name: Install and bootstrap toolchains - run: | - sudo snap install astral-uv --classic - sudo snap install concierge --classic - sudo concierge prepare \ - --juju-channel=3/stable \ - --charmcraft-channel=3.x/stable \ - --preset machine - - - name: Pack charm - run: charmcraft pack -v - working-directory: charms/${{ matrix.charm }} - - - name: Test charm - run: uv run pytest tests/ -vv --log-level=INFO - working-directory: charms/${{ matrix.charm }} - - - name: Upload charm artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: ${{ matrix.charm }}.charm - path: charms/${{ matrix.charm }}/*.charm - - - name: Upload charm to charmhub - uses: canonical/charming-actions/upload-charm@1753e0803f70445132e92acd45c905aba6473225 # 2.7.0 - with: - credentials: '${{ secrets.CHARMHUB_TOKEN }}' - github-token: '${{ secrets.GITHUB_TOKEN }}' - charm-path: charms/${{ matrix.charm }} - tag-prefix: ${{ matrix.charm }} - built-charm-path: error-tracker-${{ matrix.charm }}_ubuntu@24.04-amd64.charm - if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 98c27ee..851014d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,9 +1,13 @@ --- name: CI -on: # yamllint disable-line rule:truthy - - push - - pull_request +on: + push: + branches: + - main + pull_request: + branches: + - main env: DEBIAN_FRONTEND: noninteractive @@ -27,36 +31,25 @@ jobs: # TODO: reenable that after cleaning everything. Those tests are nowhere # near ready to be run again, and will likely just get replaced once bigger # refactoring will have occured. - # tests: - # runs-on: ubuntu-latest - # strategy: - # fail-fast: false - # matrix: - # container: - # - ubuntu:latest - # - ubuntu:rolling - # container: - # image: ${{ matrix.container }} - # steps: - # - name: Sanitize container name (for artifact name) - # run: | - # container=$(echo "${{ matrix.container }}" | sed 's/:/-/') - # echo "JOB=${GITHUB_JOB}-${container}" >> "$GITHUB_ENV" - # - name: Install dependencies - # run: > - # apt-get update - # && apt-get install --no-install-recommends --yes - # apport-retrace git python3-amqp python3-cassandra python3-pygit2 - # python3-swiftclient ubuntu-dbgsym-keyring - # - name: Run unit and integration tests - # run: > - # python3 -m pytest -ra --cov=$(pwd) --cov-branch --cov-report=xml - # --durations=0 src/test - # - name: Upload coverage - # uses: actions/upload-artifact@v4 - # with: - # name: coverage-${{ env.JOB }} - # path: ./coverage*.xml + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Setup LXD + uses: canonical/setup-lxd@a3c85fc6fb7fff43fcfeae87659e41a8f635b7dd + - name: Install ~~charmcraft~~spread + run: sudo snap install charmcraft --classic + - name: Run Error Tracker tests + run: | + charmcraft.spread -v tests/errortracker/ + # spread artifacts need to work to re-enable this + # - name: Upload coverage + # uses: actions/upload-artifact@v4 + # with: + # name: coverage-${{ env.JOB }} + # path: ./coverage.xml woke: name: woke diff --git a/.gitignore b/.gitignore index 82f9275..4b9558c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Charm +*.charm + +# Spread +.spread* + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -57,7 +63,7 @@ cover/ # Django stuff: *.log -local_settings.py +local_config.py db.sqlite3 db.sqlite3-journal diff --git a/README.md b/README.md index abd6ef1..6aa5501 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,63 @@ # error-tracker + Code behind https://errors.ubuntu.com -## Dependencies +## Running the tests locally with spread + +This avoids having to install all the Python dependencies and runs everything +isolated in a LXD VM, but is a bit slower for development. +This is also how the CI runs the tests, so if it breaks, that's likely to be the +first step to reproduce locally. +``` +sudo snap install lxd --classic +sudo snap install charmcraft --classic +charmcraft.spread -v -reuse -resend +``` + +## Setting up local development + + +Start with the Python dependencies +``` +sudo apt install apport-retrace python3-amqp python3-bson python3-cassandra python3-flask python3-mock python3-pygit2 python3-pytest python3-pytest-cov python3-swiftclient ubuntu-dbgsym-keyring +``` + +Then start a local Cassandra, RabbitMQ and swift (`docker` should works fine too): +``` +podman run --name cassandra --network host --rm -d -e HEAP_NEWSIZE=10M -e MAX_HEAP_SIZE=200M docker.io/cassandra +podman run --name rabbitmq --network host --rm -d docker.io/rabbitmq +podman run --name swift --network host --rm -d docker.io/openstackswift/saio +``` + +You can then then run the tests with `pytest`: +``` +cd src +python3 -m pytest -o log_cli=1 -vv --log-level=INFO tests/ +``` + +Or start each individual process (from the `./src` folder): + +daisy: +``` +python3 ./daisy/app.py +``` + +retracer: +``` +python3 ./retracer.py -a amd64 --sandbox-dir /tmp/sandbox -v --config-dir ./retracer/config +``` + +From there, you can manually upload a crash with the following, from any folder +containing a `.crash` file with its corresponding `.upload` file: ``` -sudo apt install python3-amqp python3-cassandra apport-retrace ubuntu-dbgsym-keyring +CRASH_DB_URL=http://127.0.0.1:5000 APPORT_REPORT_DIR=$(pwd) CRASH_DB_IDENTIFIER=my_custom_machine_id whoopsie --no-polling -f ``` +This will create a corresponding `.uploaded` file containing the OOPS ID, that +you need to delete if you want to upload the crash again. -## Documentation +## More documentation ### Opening a new series diff --git a/charm/charm.py b/charm/charm.py new file mode 100755 index 0000000..a35b14e --- /dev/null +++ b/charm/charm.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright 2025 Skia +# See LICENSE file for licensing details. + +"""Charm the Error Tracker.""" + +import logging + +import ops +from charms.haproxy.v1.haproxy_route import HaproxyRouteRequirer + +from errortracker import ErrorTracker + +logger = logging.getLogger(__name__) + + +class ErrorTrackerCharm(ops.CharmBase): + """Charm the application.""" + + def __init__(self, *args): + super().__init__(*args) + self._error_tracker = ErrorTracker() + self.route_daisy = HaproxyRouteRequirer( + self, + service="daisy", + ports=[self._error_tracker.daisy_port], + relation_name="route_daisy", + ) + + self.framework.observe(self.on.start, self._on_start) + self.framework.observe(self.on.install, self._on_install) + self.framework.observe(self.on.config_changed, self._on_config_changed) + + def _on_start(self, event: ops.StartEvent): + """Handle start event.""" + self.unit.status = ops.ActiveStatus() + + def _on_install(self, event: ops.InstallEvent): + """Handle install event.""" + self.unit.status = ops.MaintenanceStatus("Installing the error tracker") + try: + self._error_tracker.install() + except Exception as e: + logger.error("Failed to install the Error Tracker: %s", str(e)) + self.unit.status = ops.BlockedStatus("Failed installing the Error Tracker") + return + + self.unit.status = ops.ActiveStatus("Ready") + + def _on_config_changed(self, event: ops.ConfigChangedEvent): + enable_daisy = self.config.get("enable_daisy") + enable_retracer = self.config.get("enable_retracer") + enable_timers = self.config.get("enable_timers") + enable_web = self.config.get("enable_web") + + config = self.config.get("configuration") + + self._error_tracker.configure(config) + + # TODO: the charms know how to enable components, but not disable them. + # This is a bit annoying, but also doesn't have a very big impact in + # practice. This charm has no configuration where it's supposed to store + # data, so it's always very easy to remove a unit and recreate. + if enable_daisy: + self._error_tracker.configure_daisy() + self.unit.set_ports(self._error_tracker.daisy_port) + if enable_retracer: + self._error_tracker.configure_retracer(self.config.get("retracer_failed_queue")) + if enable_timers: + self._error_tracker.configure_timers() + if enable_web: + self._error_tracker.configure_web() + + self.unit.set_workload_version(self._error_tracker.get_version()) + self.unit.status = ops.ActiveStatus("Ready") + + +if __name__ == "__main__": # pragma: nocover + ops.main(ErrorTrackerCharm) # type: ignore diff --git a/charm/errortracker.py b/charm/errortracker.py new file mode 100644 index 0000000..dbc00bc --- /dev/null +++ b/charm/errortracker.py @@ -0,0 +1,224 @@ +import logging +import shutil +from pathlib import Path +from subprocess import CalledProcessError, check_call, check_output + +logger = logging.getLogger(__name__) + +HOME = Path("~ubuntu").expanduser() +REPO_LOCATION = HOME / "error-tracker" + + +def setup_systemd_timer(unit_name, description, command, calendar): + systemd_unit_location = Path("/") / "etc" / "systemd" / "system" + systemd_unit_location.mkdir(parents=True, exist_ok=True) + + (systemd_unit_location / f"{unit_name}.service").write_text( + f""" +[Unit] +Description={description} + +[Service] +Type=oneshot +User=ubuntu +Environment=PYTHONPATH={HOME}/config +ExecStart={command} +""" + ) + (systemd_unit_location / f"{unit_name}.timer").write_text( + f""" +[Unit] +Description={description} + +[Timer] +OnCalendar={calendar} +Persistent=true + +[Install] +WantedBy=timers.target +""" + ) + + check_call(["systemctl", "daemon-reload"]) + check_call(["systemctl", "enable", "--now", f"{unit_name}.timer"]) + + +class ErrorTracker: + def __init__(self): + self.enable_retracer = True + self.enable_timers = True + self.enable_daisy = True + self.enable_web = True + self.daisy_port = 8000 + + def install(self): + self._install_deps() + self._install_et() + + def _install_et(self): + shutil.copytree(".", REPO_LOCATION) + check_call(["chown", "-R", "ubuntu:ubuntu", str(REPO_LOCATION)]) + + def get_version(self): + """Get the retracer version""" + try: + version = check_output( + [ + "sudo", + "-u", + "ubuntu", + "python3", + "-c", + "import errortracker; print(errortracker.__version__)", + ], + cwd=REPO_LOCATION / "src", + ) + return version.decode() + except CalledProcessError as e: + logger.error("Unable to get version (%d, %s)", e.returncode, e.stderr) + return "unknown" + + def _install_deps(self): + try: + check_call(["apt-get", "update", "-y"]) + check_call( + [ + "apt-get", + "install", + "-y", + "git", + "python3-amqp", + "python3-apport", + "python3-apt", + "python3-bson", + "python3-cassandra", + "python3-flask", + "python3-swiftclient", + ] + ) + except CalledProcessError as e: + logger.debug("Package install failed with return code %d", e.returncode) + return + + def configure(self, config: str): + config_location = REPO_LOCATION / "src" + (config_location / "local_config.py").write_text(config) + + def configure_daisy(self): + logger.info("Configuring daisy") + logger.info("Installing additional daisy dependencies") + check_call(["apt-get", "install", "-y", "gunicorn"]) + systemd_unit_location = Path("/") / "etc" / "systemd" / "system" + systemd_unit_location.mkdir(parents=True, exist_ok=True) + (systemd_unit_location / "daisy.service").write_text( + f""" +[Unit] +Description=Daisy +After=network.target + +[Service] +User=ubuntu +Group=ubuntu +WorkingDirectory={REPO_LOCATION}/src +ExecStart=gunicorn -c {REPO_LOCATION}/src/daisy/gunicorn_config.py 'daisy.app:app' +Restart=always + +[Install] +WantedBy=multi-user.target +""" + ) + + check_call(["systemctl", "daemon-reload"]) + + logger.info("enabling systemd units") + check_call(["systemctl", "enable", "daisy"]) + + logger.info("restarting systemd units") + check_call(["systemctl", "restart", "daisy"]) + + def configure_retracer(self, retracer_failed_queue: bool): + logger.info("Configuring retracer") + failed = "--failed" if retracer_failed_queue else "" + # Work around https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918 + # Apport will not be run as root, thus the included workaround here will hit ENOPERM + (Path("/") / "usr" / "lib" / "debug" / ".dwz").mkdir(parents=True, exist_ok=True) + logger.info("Installing additional retracer dependencies") + check_call( + [ + "apt-get", + "install", + "-y", + "apport-retrace", + "ubuntu-dbgsym-keyring", + ] + ) + + logger.info("Configuring retracer systemd units") + systemd_unit_location = Path("/") / "etc" / "systemd" / "system" + systemd_unit_location.mkdir(parents=True, exist_ok=True) + (systemd_unit_location / "retracer@.service").write_text( + f""" +[Unit] +Description=Retracer + +[Service] +User=ubuntu +Group=ubuntu +Environment=PYTHONPATH={REPO_LOCATION}/src +ExecStart=python3 {REPO_LOCATION}/src/retracer.py --config-dir {REPO_LOCATION}/src/retracer/config --sandbox-dir {HOME}/cache --cleanup-debs --cleanup-sandbox --architecture %i --core-storage {HOME}/var --verbose {failed} +Restart=on-failure + +[Install] +WantedBy=multi-user.target +""" + ) + + check_call(["systemctl", "daemon-reload"]) + + logger.info("enabling systemd units") + check_call(["systemctl", "enable", "retracer@amd64"]) + check_call(["systemctl", "enable", "retracer@arm64"]) + check_call(["systemctl", "enable", "retracer@armhf"]) + check_call(["systemctl", "enable", "retracer@i386"]) + + logger.info("restarting systemd units") + check_call(["systemctl", "restart", "retracer@amd64"]) + check_call(["systemctl", "restart", "retracer@arm64"]) + check_call(["systemctl", "restart", "retracer@armhf"]) + check_call(["systemctl", "restart", "retracer@i386"]) + + def configure_timers(self): + logger.info("Configuring timers") + setup_systemd_timer( + "et-unique-users-daily-update", + "Error Tracker - Unique users daily update", + f"{REPO_LOCATION}/src/tools/unique_users_daily_update.py", + "*-*-* 00:30:00", # every day at 00:30 + ) + setup_systemd_timer( + "et-import-bugs", + "Error Tracker - Import bugs", + f"{REPO_LOCATION}/src/tools/import_bugs.py", + "*-*-* 01,04,07,10,13,16,19,22:00:00", # every three hours + ) + setup_systemd_timer( + "et-import-team-packages", + "Error Tracker - Import team packages", + f"{REPO_LOCATION}/src/tools/import_team_packages.py", + "*-*-* 02:30:00", # every day at 02:30 + ) + setup_systemd_timer( + "et-swift-corrupt-core-check", + "Error Tracker - Swift - Check for corrupt cores", + f"{REPO_LOCATION}/src/tools/swift_corrupt_core_check.py", + "*-*-* 04:30:00", # every day at 04:30 + ) + setup_systemd_timer( + "et-swift-handle-old-cores", + "Error Tracker - Swift - Handle old cores", + f"{REPO_LOCATION}/src/tools/swift_handle_old_cores.py", + "*-*-* *:45:00", # every hour at minute 45 + ) + + def configure_web(self): + logger.info("Configuring web") diff --git a/charm/tests/conftest.py b/charm/tests/conftest.py new file mode 100644 index 0000000..34e2017 --- /dev/null +++ b/charm/tests/conftest.py @@ -0,0 +1,174 @@ +import configparser +import logging +import subprocess +from pathlib import Path +from typing import Generator + +import jubilant +import pytest +from _pytest.config.argparsing import Parser + +logger = logging.getLogger() + + +def pytest_addoption(parser: Parser): + parser.addoption( + "--charm-path", + help="Pre-built charm file to deploy, rather than building from source", + ) + + +@pytest.fixture(scope="module") +def charm_path(request): + charm_file = request.config.getoption("--charm-path") + if charm_file: + return charm_file + + charm_dir = Path(__file__).parent.parent.parent + subprocess.run( + ["/snap/bin/charmcraft", "pack", "--verbose", "--project-dir", charm_dir], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + return next(Path.glob(Path("."), "*.charm")).absolute() + + +@pytest.fixture(scope="module") +def juju() -> Generator[jubilant.Juju, None, None]: + with jubilant.temp_model() as juju: + yield juju + + +@pytest.fixture +def services(juju: jubilant.Juju) -> None: + """ + This fixture is just an optimization to start all services at once and + have them get installed in parallel. Don't use it directly and use each + individual service instead. + """ + juju.deploy( + "cassandra", + config={ + "install_keys": "7464AAD9068241C50BA6A26232F35CB2F546D93E", + "install_sources": "deb https://debian.cassandra.apache.org 311x main", + }, + ) + juju.deploy("rabbitmq-server") + juju.deploy(charm="ubuntu", app="swift") + + +@pytest.fixture +def cassandra(juju: jubilant.Juju, services) -> dict[str, str]: + juju.wait( + lambda status: jubilant.all_active(status, "cassandra"), + timeout=900, + ) + + # Get Cassandra credentials + task = juju.exec("cat", "/home/ubuntu/.cassandra/cqlshrc", unit="cassandra/0") + logger.info("Cassandra config: " + task.stdout) + cassandra_creds = configparser.ConfigParser() + cassandra_creds.read_string(task.stdout) + return { + "host": cassandra_creds["connection"]["hostname"], + "username": cassandra_creds["authentication"]["username"], + "password": cassandra_creds["authentication"]["password"], + } + + +@pytest.fixture +def amqp(juju: jubilant.Juju, services) -> dict[str, str]: + juju.wait( + lambda status: jubilant.all_active(status, "rabbitmq-server"), + timeout=600, + ) + amqp_host = juju.status().get_units("rabbitmq-server")["rabbitmq-server/0"].public_address + logger.info("RabbitMQ address: " + amqp_host) + + # Useful to push arbitrary messages in test cases. + juju.exec("sudo", "apt-get", "install", "-y", "amqp-tools", unit="rabbitmq-server/0") + + # Set up rabbitmq test user + juju.exec("sudo", "rabbitmqctl", "add_user", "test", "test", unit="rabbitmq-server/0") + juju.exec( + "sudo", + "rabbitmqctl", + "set_user_tags", + "test", + "administrator", + unit="rabbitmq-server/0", + ) + juju.exec( + "sudo", + "rabbitmqctl", + "set_permissions", + "-p", + "/", + "test", + "'.*'", + "'.*'", + "'.*'", + unit="rabbitmq-server/0", + ) + return {"host": amqp_host, "username": "test", "password": "test"} + + +@pytest.fixture +def swift(juju: jubilant.Juju, services) -> dict[str, str]: + juju.wait( + lambda status: jubilant.all_active(status, "swift"), + timeout=600, + ) + swift_host = juju.status().get_units("swift")["swift/0"].public_address + logger.info("swift address: " + swift_host) + + # juju.exec("sudo", "apt-get", "update", unit="swift/0") + juju.exec("sudo", "apt-get", "install", "-Uy", "docker.io", unit="swift/0") + juju.exec( + "docker", + "run", + "--name", + "swift", + "--network", + "host", + "--rm", + "-d", + "docker.io/openstackswift/saio", + unit="swift/0", + ) + return { + "os_auth_url": f"http://{swift_host}:8080/auth/v1.0", + "os_username": "test:tester", + "os_password": "testing", + "auth_version": "1.0", + } + + +@pytest.fixture +def error_tracker_config( + amqp: dict[str, str], cassandra: dict[str, str], swift: dict[str, str] +) -> str: + return f""" +amqp_creds = {{ + "host": "{amqp["host"]}", + "username": "{amqp["username"]}", + "password": "{amqp["password"]}", +}} + +cassandra_creds = {{ + "keyspace": "crashdb", + "hosts": [ "{cassandra["host"]}" ], + "username": "{cassandra["username"]}", + "password": "{cassandra["password"]}", +}} + +swift_creds = {{ + "os_auth_url": "{swift["os_auth_url"]}", + "os_username": "{swift["os_username"]}", + "os_password": "{swift["os_password"]}", + "auth_version": "{swift["auth_version"]}", +}} +""" diff --git a/charm/tests/integration/task.yaml b/charm/tests/integration/task.yaml new file mode 100644 index 0000000..b18805c --- /dev/null +++ b/charm/tests/integration/task.yaml @@ -0,0 +1,12 @@ +summary: Run charm integration tests +execute: | + # Workaround apparmor denying Juju access to the charm + sed '/^profile/a /root/ ixr,' /var/lib/snapd/apparmor/profiles/snap.juju.juju | apparmor_parser -r + sed '/^profile/a /root/** ixr,' /var/lib/snapd/apparmor/profiles/snap.juju.juju | apparmor_parser -r + + args="" + if [[ -f "$SPREAD_PATH/error-tracker_ubuntu@24.04-amd64.charm" ]]; then + args="--charm-path=$SPREAD_PATH/error-tracker_ubuntu@24.04-amd64.charm" + fi + + uv run --all-extras pytest . -vv --log-level=INFO -o log_cli=1 $args \ No newline at end of file diff --git a/charm/tests/integration/test_daisy.py b/charm/tests/integration/test_daisy.py new file mode 100644 index 0000000..0eeb52b --- /dev/null +++ b/charm/tests/integration/test_daisy.py @@ -0,0 +1,69 @@ +import logging + +import jubilant +from requests import Session +from tenacity import Retrying, stop_after_attempt, wait_exponential +from utils import DNSResolverHTTPSAdapter, check_config + +logger = logging.getLogger() + +HAPROXY = "haproxy" +SSC = "self-signed-certificates" + + +def test_deploy( + juju: jubilant.Juju, + amqp: dict[str, str], + cassandra: dict[str, str], + swift: dict[str, str], + error_tracker_config: str, + charm_path: str, +): + juju.deploy( + charm=charm_path, + app="daisy", + config={ + "configuration": error_tracker_config, + "enable_daisy": True, + "enable_retracer": False, + "enable_timers": False, + "enable_web": False, + }, + ) + + juju.wait(lambda status: jubilant.all_active(status, "daisy"), timeout=600) + + check_config(juju, amqp, cassandra, swift, "daisy/0") + + +def test_http(juju: jubilant.Juju): + juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": "daisy.internal"}) + juju.deploy(SSC, channel="1/edge") + + juju.integrate(HAPROXY + ":certificates", SSC + ":certificates") + juju.integrate("daisy:route_daisy", HAPROXY) + juju.wait(lambda status: jubilant.all_active(status, HAPROXY, SSC), timeout=1800) + + haproxy_ip = juju.status().apps[HAPROXY].units[f"{HAPROXY}/0"].public_address + external_hostname = "daisy.internal" + + session = Session() + session.mount("https://", DNSResolverHTTPSAdapter(external_hostname, haproxy_ip)) + + # Let give this test a few chances to succeed, as it can sometimes be a bit + # early and hit 503 + for attempt in Retrying( + stop=stop_after_attempt(10), + wait=wait_exponential(min=5, max=30), + reraise=True, + ): + with attempt: + response = session.post( + f"https://{haproxy_ip}/random_machine_id", + headers={"Host": external_hostname}, + data=b"Hello there", + verify=False, + timeout=30, + ) + assert response.status_code == 400 + assert "Invalid BSON" in response.text diff --git a/charm/tests/integration/test_retracer.py b/charm/tests/integration/test_retracer.py new file mode 100644 index 0000000..eb94fe9 --- /dev/null +++ b/charm/tests/integration/test_retracer.py @@ -0,0 +1,89 @@ +import logging +import time + +import jubilant +from utils import check_config + +logger = logging.getLogger() + + +def test_deploy( + juju: jubilant.Juju, + amqp: dict[str, str], + cassandra: dict[str, str], + swift: dict[str, str], + error_tracker_config: str, + charm_path: str, +): + juju.deploy( + charm=charm_path, + app="retracer", + config={ + "configuration": error_tracker_config, + "enable_daisy": False, + "enable_retracer": True, + "enable_timers": False, + "enable_web": False, + }, + ) + + juju.wait(lambda status: jubilant.all_active(status, "retracer"), timeout=600) + + check_config(juju, amqp, cassandra, swift, "retracer/0") + + # Check retracer processes + task = juju.exec("ps", "aux", unit="retracer/0") + processes = task.stdout.splitlines() + retracer_processes = [p for p in processes if "src/retracer.py" in p] + try: + assert len(retracer_processes) == 4, "wrong number of retracers processes" + except AssertionError as e: + # dump one retracer log. It's likely that all fail in the same way. + logger.warning(juju.exec("journalctl", "-u", "retracer@amd64", unit="retracer/0").stdout) + raise e + + # Send an empty crash, to verify it gets processed, even though not really retraced + juju.exec( + "swift", + "-A", + swift["os_auth_url"], + "-U", + swift["os_username"], + "-K", + swift["os_password"], + "upload", + "--object-name", + "00000000-1111-4222-3333-444444444444", + "cores", + "/etc/os-release", # Obviously not a valid core, but the file exists and is not empty + unit="retracer/0", + ) + juju.exec( + "amqp-publish", + "--server", + amqp["host"], + "--username", + amqp["username"], + "--password", + amqp["password"], + "-r", + "retrace_amd64", + "-b", + "00000000-1111-4222-3333-444444444444:swift", + unit="rabbitmq-server/0", + ) + # Give some arbitrary time to process. + # For now I'm taking that cursed path, and depending on how flaky this + # becomes, I'll see later to implement a wait loop. + time.sleep(4) + + # Verify that the retracer didn't Traceback and processed the sent crash + task = juju.exec("journalctl", "-u", "retracer@amd64.service", unit="retracer/0") + retracer_logs = task.stdout + assert "Waiting for messages in `retrace_amd64`" in retracer_logs, ( + "retracer didn't reach waiting on amqp" + ) + assert ( + "00000000-1111-4222-3333-444444444444:swift:Failed to decompress core: Error -3 while decompressing data: incorrect header check" + in retracer_logs + ), "retracer didn't try to decompress the core. Either `swift` or `amqp` is broken." diff --git a/charm/tests/integration/test_timers.py b/charm/tests/integration/test_timers.py new file mode 100644 index 0000000..bdaa58d --- /dev/null +++ b/charm/tests/integration/test_timers.py @@ -0,0 +1,39 @@ +import json +import logging + +import jubilant +from utils import check_config + +logger = logging.getLogger() + + +def test_deploy( + juju: jubilant.Juju, + amqp: dict[str, str], + cassandra: dict[str, str], + swift: dict[str, str], + error_tracker_config: str, + charm_path: str, +): + juju.deploy( + charm=charm_path, + app="timers", + config={ + "configuration": error_tracker_config, + "enable_daisy": False, + "enable_retracer": False, + "enable_timers": True, + "enable_web": False, + }, + ) + + juju.wait(lambda status: jubilant.all_active(status, "timers"), timeout=600) + + check_config(juju, amqp, cassandra, swift, "timers/0") + + # Check deployed systemd units + task = juju.exec("systemctl", "list-units", "-o", "json", unit="timers/0") + units = json.loads(task.stdout) + et_units = [u for u in units if u["unit"].startswith("et-")] + assert len(et_units) == 5, "wrong number of error tracker systemd units" + assert all([u["active"] == "active" for u in et_units]), "not all systemd units are active" diff --git a/charm/tests/integration/utils.py b/charm/tests/integration/utils.py new file mode 100644 index 0000000..9af5b5b --- /dev/null +++ b/charm/tests/integration/utils.py @@ -0,0 +1,77 @@ +from urllib.parse import urlparse + +from requests.adapters import DEFAULT_POOLBLOCK, DEFAULT_POOLSIZE, DEFAULT_RETRIES, HTTPAdapter + + +# Shamelessly stolen from ubuntu-manpages-operator (Hi Jon 👋 and thankies!) +class DNSResolverHTTPSAdapter(HTTPAdapter): + """A simple mounted DNS resolver for HTTP requests.""" + + def __init__( + self, + hostname, + ip, + ): + """Initialize the dns resolver. + + Args: + hostname: DNS entry to resolve. + ip: Target IP address. + """ + self.hostname = hostname + self.ip = ip + super().__init__( + pool_connections=DEFAULT_POOLSIZE, + pool_maxsize=DEFAULT_POOLSIZE, + max_retries=DEFAULT_RETRIES, + pool_block=DEFAULT_POOLBLOCK, + ) + + # Ignore pylint rule as this is the parent method signature + def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): # pylint: disable=too-many-arguments, too-many-positional-arguments + """Wrap HTTPAdapter send to modify the outbound request. + + Args: + request: Outbound HTTP request. + stream: argument used by parent method. + timeout: argument used by parent method. + verify: argument used by parent method. + cert: argument used by parent method. + proxies: argument used by parent method. + + Returns: + Response: HTTP response after modification. + """ + connection_pool_kwargs = self.poolmanager.connection_pool_kw + + result = urlparse(request.url) + if result.hostname == self.hostname: + ip = self.ip + if result.scheme == "https" and ip: + request.url = request.url.replace( + "https://" + result.hostname, + "https://" + ip, + ) + connection_pool_kwargs["server_hostname"] = result.hostname + connection_pool_kwargs["assert_hostname"] = result.hostname + request.headers["Host"] = result.hostname + else: + connection_pool_kwargs.pop("server_hostname", None) + connection_pool_kwargs.pop("assert_hostname", None) + + return super().send(request, stream, timeout, verify, cert, proxies) + + +def check_config(juju, amqp, cassandra, swift, unit): + # Check local config + task = juju.exec("cat", "/home/ubuntu/error-tracker/src/local_config.py", unit=unit) + config = task.stdout + assert f'amqp_creds = {{\n "host": "{amqp["host"]}"' in config, ( + "missing or wrong amqp entries in configuration" + ) + assert f'[ "{cassandra["host"]}" ]' in config, ( + "missing or wrong cassandra entry in configuration" + ) + assert f'"os_auth_url": "{swift["os_auth_url"]}"' in config, ( + "missing or wrong swift entry in configuration" + ) diff --git a/charm/tests/utils.py b/charm/tests/utils.py new file mode 100644 index 0000000..30d2f25 --- /dev/null +++ b/charm/tests/utils.py @@ -0,0 +1,13 @@ +def check_config(juju, amqp, cassandra, swift, unit): + # Check local config + task = juju.exec("cat", "/home/ubuntu/error-tracker/src/local_config.py", unit=unit) + config = task.stdout + assert f'amqp_creds = {{\n "host": "{amqp["host"]}"' in config, ( + "missing or wrong amqp entries in configuration" + ) + assert f'[ "{cassandra["host"]}" ]' in config, ( + "missing or wrong cassandra entry in configuration" + ) + assert f'"os_auth_url": {swift["os_auth_url"]}' in config, ( + "missing or wrong swift entry in configuration" + ) diff --git a/charm_requirements.txt b/charm_requirements.txt new file mode 100644 index 0000000..2df265e --- /dev/null +++ b/charm_requirements.txt @@ -0,0 +1,3 @@ +ops ~= 3.1 +pydantic ~= 2.10.6 + diff --git a/charmcraft.yaml b/charmcraft.yaml new file mode 100644 index 0000000..9e1539a --- /dev/null +++ b/charmcraft.yaml @@ -0,0 +1,72 @@ +name: error-tracker +type: charm +title: Error Tracker +summary: The whole infrastructure collecting and processing crashes for Ubuntu +description: | + The Error Tracker is made of the following components: + * daisy - receiving crashes + * web UI - displaying and visualizing data for developers + * timers - for processing and housekeeping tasks + * retracer - to get symbolic stacktraces out of coredumps + + This charm can deploy either of those components depending on the + configuration. Default to deploying everything. +links: + source: + - https://github.com/ubuntu/error-tracker/ + +platforms: + ubuntu@24.04:amd64: + +parts: + error-tracker: + source: . + plugin: dump + charm: + source: . + plugin: charm + charm-entrypoint: charm/charm.py + charm-requirements: [charm_requirements.txt] + +charm-libs: + - lib: haproxy.haproxy_route + version: "1" + +config: + options: + enable_daisy: + description: | + Whether to enable the 'daisy' component of the Error Tracker + default: true + type: boolean + enable_retracer: + description: | + Whether to enable the 'retracer' component of the Error Tracker + default: true + type: boolean + enable_timers: + description: | + Whether to enable the 'timers' component of the Error Tracker + default: true + type: boolean + enable_web: + description: | + Whether to enable the 'web' component of the Error Tracker + default: true + type: boolean + configuration: + description: | + Full configuration file. Must be valid Python, will be imported directly by the errortracker. + default: "" + type: string + retracer_failed_queue: + description: | + Whether the retracer will listen on the "failed_retrace_" queue + default: false + type: boolean + +requires: + route_daisy: + interface: haproxy-route + limit: 1 + optional: true diff --git a/charms/README.md b/charms/README.md deleted file mode 100644 index 89135a2..0000000 --- a/charms/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Ubuntu Error Tracker charms - -Each folder each represent one charm, representing one component of the Error Tracker. - -## Testing locally - -In each folder, one can get the integration tests running by using the following commands: -* `charmcraft pack` - This will pack the charm in its own folder. The tests have some magic fixtures - to find the right `.charm` files to use in the tests. -* `uv run pytest tests/ -vv --log-level=INFO -o log_cli=1` - This is for maximum live verbosity. You don't actually need all those flags to - just run the tests, but I find that convenient in development. diff --git a/charms/conftest.py b/charms/conftest.py deleted file mode 100644 index 1ee19b4..0000000 --- a/charms/conftest.py +++ /dev/null @@ -1,124 +0,0 @@ -import configparser -import logging -from pathlib import Path - -import jubilant -import pytest - -logger = logging.getLogger() - - -def charm_path(name: str) -> Path: - """Return full absolute path to given test charm.""" - charm_dir = Path(__file__).parent / name - charms = [p.absolute() for p in charm_dir.glob(f"error-tracker-{name}_*.charm")] - assert charms, f"error-tracker-{name}_*.charm not found" - assert len(charms) == 1, "more than one .charm file, unsure which to use" - return charms[0] - - -@pytest.fixture(scope="module") -def juju() -> jubilant.Juju: - with jubilant.temp_model() as juju: - yield juju - - -@pytest.fixture -def cassandra(juju: jubilant.Juju) -> dict[str, str]: - juju.deploy( - "cassandra", - config={ - "install_keys": "7464AAD9068241C50BA6A26232F35CB2F546D93E", - "install_sources": "deb https://debian.cassandra.apache.org 311x main", - }, - ) - juju.wait( - lambda status: jubilant.all_active(status, "cassandra"), - timeout=600, - ) - # Create basic schema - task = juju.exec( - "cqlsh", - "-e", - "CREATE KEYSPACE IF NOT EXISTS crashdb WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1 };", - unit="cassandra/0", - ) - - task = juju.exec( - "cqlsh", - "-e", - 'CREATE TABLE IF NOT EXISTS crashdb."OOPS" ( key blob, column1 text, value text, PRIMARY KEY (key, column1) );', - unit="cassandra/0", - ) - - # Get Cassandra credentials - task = juju.exec("cat", "/home/ubuntu/.cassandra/cqlshrc", unit="cassandra/0") - logger.info("Cassandra config: " + task.stdout) - cassandra_creds = configparser.ConfigParser() - cassandra_creds.read_string(task.stdout) - return { - "host": cassandra_creds["connection"]["hostname"], - "username": cassandra_creds["authentication"]["username"], - "password": cassandra_creds["authentication"]["password"], - } - - -@pytest.fixture -def amqp(juju: jubilant.Juju) -> dict[str, str]: - juju.deploy("rabbitmq-server") - juju.wait( - lambda status: jubilant.all_active(status, "rabbitmq-server"), - timeout=600, - ) - amqp_host = juju.status().get_units("rabbitmq-server")["rabbitmq-server/0"].public_address - logger.info("RabbitMQ address: " + amqp_host) - - # Useful to push arbitrary messages in test cases. - juju.exec("sudo", "apt-get", "install", "-y", "amqp-tools", unit="rabbitmq-server/0") - - # Set up rabbitmq test user - juju.exec("sudo", "rabbitmqctl", "add_user", "test", "test", unit="rabbitmq-server/0") - juju.exec( - "sudo", - "rabbitmqctl", - "set_user_tags", - "test", - "administrator", - unit="rabbitmq-server/0", - ) - juju.exec( - "sudo", - "rabbitmqctl", - "set_permissions", - "-p", - "/", - "test", - "'.*'", - "'.*'", - "'.*'", - unit="rabbitmq-server/0", - ) - return {"host": amqp_host, "username": "test", "password": "test"} - - -@pytest.fixture -def error_tracker_config(amqp: dict[str, str], cassandra: dict[str, str]) -> str: - return f""" -amqp_host = '{amqp["host"]}' -amqp_username = '{amqp["username"]}' -amqp_password = '{amqp["password"]}' -amqp_vhost = '/' - -cassandra_keyspace = "crashdb" -cassandra_hosts = [ '{cassandra["host"]}' ] -cassandra_username = '{cassandra["username"]}' -cassandra_password = '{cassandra["password"]}' - -os_auth_url = 'https://keystone.local:5000/v3' -os_username = 'admin' -os_password = '123456' -os_tenant_name = 'error-tracker_project' -os_region_name = 'default' - -swift_bucket = "daisy-production-cores" -""" diff --git a/charms/retracer/.gitignore b/charms/retracer/.gitignore deleted file mode 100644 index a26d707..0000000 --- a/charms/retracer/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ diff --git a/charms/retracer/LICENSE b/charms/retracer/LICENSE deleted file mode 100644 index a1f16c2..0000000 --- a/charms/retracer/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Skia - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/charms/retracer/charmcraft.yaml b/charms/retracer/charmcraft.yaml deleted file mode 100644 index e0caad7..0000000 --- a/charms/retracer/charmcraft.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -name: error-tracker-retracer -type: charm -title: Error Tracker - Retracer -summary: Retrace user crashes to get traceback with symbols -description: | - The retracer part of the Error Tracker. This is what connects the RabbitMQ - retracing queue to `apport-retrace`, and updates the results in Cassandra. -links: - source: - - https://github.com/ubuntu/error-tracker/tree/main/charms/retracer - -platforms: - ubuntu@24.04:amd64: - -parts: - retracer: - source: . - plugin: charm - -# More information on this section at https://juju.is/docs/sdk/charmcraft-yaml#heading--config -# General configuration documentation: https://juju.is/docs/sdk/config -config: - options: - repo-url: - description: | - Address of the git repository to clone - default: "https://github.com/ubuntu/error-tracker" - type: string - repo-branch: - description: | - Branch of the git repository to check out - default: "main" - type: string - log-level: - description: | - Configures the log level. - - Acceptable values are: "info", "debug", "warning", "error" and "critical" - default: "info" - type: string - failed_queue: - description: | - Whether the retracer will listen on the "failed_retrace_" queue - default: false - type: boolean - configuration: - description: | - Full configuration file. Must be valid Python, will be imported directly by daisy. - default: | - amqp_host = '127.0.0.1' - amqp_username = 'guest' - amqp_password = 'guest' - amqp_vhost = '/' - - cassandra_keyspace = "crashdb" - cassandra_hosts = [ '127.0.0.1' ] - cassandra_username = 'guest' - cassandra_password = 'guest' - - os_auth_url = 'https://keystone.local:5000/v3' - os_username = 'admin' - os_password = '123456' - os_tenant_name = 'error-tracker_project' - os_region_name = 'default' - - swift_bucket = "daisy-production-cores" - type: string diff --git a/charms/retracer/requirements.txt b/charms/retracer/requirements.txt deleted file mode 100644 index b765c42..0000000 --- a/charms/retracer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ops ~=3.1 diff --git a/charms/retracer/src/charm.py b/charms/retracer/src/charm.py deleted file mode 100755 index b4de4a6..0000000 --- a/charms/retracer/src/charm.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2024 Skia -# See LICENSE file for licensing details. - -"""Charm the retracer for Error Tracker.""" - -import logging -from pathlib import Path -from subprocess import CalledProcessError, check_call, check_output - -import ops - -logger = logging.getLogger(__name__) - -HOME = Path("~ubuntu").expanduser() -REPO_LOCATION = HOME / "error-tracker" - - -class RetracerCharm(ops.CharmBase): - """Charm the application.""" - - def __init__(self, *args): - super().__init__(*args) - self.framework.observe(self.on.start, self._on_start) - self.framework.observe(self.on.install, self._on_install) - self.framework.observe(self.on.config_changed, self._on_config_changed) - - def _on_start(self, event: ops.StartEvent): - """Handle start event.""" - self.unit.status = ops.ActiveStatus() - - def _on_install(self, event: ops.InstallEvent): - """Handle install event.""" - # Work around https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918 - # Apport will not be run as root, thus the included workaround here will hit ENOPERM - (Path("/") / "usr" / "lib" / "debug" / ".dwz").mkdir( - parents=True, exist_ok=True - ) - try: - self.unit.status = ops.MaintenanceStatus("Installing apt dependencies") - check_call(["apt-get", "update", "-y"]) - check_call( - [ - "apt-get", - "install", - "-y", - "apport-retrace", - "git", - "python3-amqp", - "python3-cassandra", - "python3-pygit2", - "python3-swiftclient", - "ubuntu-dbgsym-keyring", - "vim", - ] - ) - except CalledProcessError as e: - logger.debug("Package install failed with return code %d", e.returncode) - self.unit.status = ops.BlockedStatus("Failed installing apt packages.") - return - - try: - self.unit.status = ops.MaintenanceStatus("Installing retracer code") - repo_url = self.config.get("repo-url") - repo_branch = self.config.get("repo-branch") - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "clone", - "-b", - repo_branch, - repo_url, - REPO_LOCATION, - ] - ) - self.unit.status = ops.ActiveStatus("Ready") - except CalledProcessError as e: - logger.debug( - "Git clone of the code failed with return code %d", e.returncode - ) - self.unit.status = ops.BlockedStatus("Failed git cloning the code.") - return - - def _on_config_changed(self, event: ops.ConfigChangedEvent): - # Make sure the repo is up to date - repo_url = self.config.get("repo-url") - repo_branch = self.config.get("repo-branch") - self.unit.status = ops.MaintenanceStatus( - "Fetching latest updates of retracer code" - ) - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "fetch", - "--update-head-ok", - "--force", - repo_url, - f"refs/heads/{repo_branch}:refs/heads/{repo_branch}", - ] - ) - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "reset", - "--hard", - repo_branch, - ] - ) - - failed_queue = self.config.get("failed_queue") - failed = "--failed" if failed_queue else "" - config = self.config.get("configuration") - - config_location = HOME / "config" - config_location.mkdir(parents=True, exist_ok=True) - self.unit.status = ops.MaintenanceStatus("writing retracer configuration") - (config_location / "local_config.py").write_text(config) - - systemd_unit_location = Path("/") / "etc" / "systemd" / "system" - systemd_unit_location.mkdir(parents=True, exist_ok=True) - self.unit.status = ops.MaintenanceStatus("setting up systemd units") - (systemd_unit_location / "retracer@.service").write_text( - f""" -[Unit] -Description=Retracer - -[Service] -User=ubuntu -Group=ubuntu -Environment=PYTHONPATH={HOME}/config -ExecStart={HOME}/error-tracker/src/retracer.py --config-dir {HOME}/error-tracker/src/retracer/config --sandbox-dir {HOME}/cache --cleanup-debs --cleanup-sandbox --architecture %i --core-storage {HOME}/var --verbose {failed} -Restart=on-failure - -[Install] -WantedBy=multi-user.target -""" - ) - - check_call(["systemctl", "daemon-reload"]) - self.unit.status = ops.MaintenanceStatus("enabling systemd units") - check_call(["systemctl", "enable", "retracer@amd64"]) - check_call(["systemctl", "enable", "retracer@arm64"]) - check_call(["systemctl", "enable", "retracer@armhf"]) - check_call(["systemctl", "enable", "retracer@i386"]) - self.unit.status = ops.MaintenanceStatus("restarting systemd units") - check_call(["systemctl", "restart", "retracer@amd64"]) - check_call(["systemctl", "restart", "retracer@arm64"]) - check_call(["systemctl", "restart", "retracer@armhf"]) - check_call(["systemctl", "restart", "retracer@i386"]) - self.unit.set_workload_version(self._getWorkloadVersion()) - self.unit.status = ops.ActiveStatus("Ready") - - def _getWorkloadVersion(self): - """Get the retracer version from the git repository""" - try: - self.unit.status = ops.MaintenanceStatus("fetching code version") - version = check_output( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "describe", - "--tags", - "--always", - "--dirty", - ] - ) - return version.decode() - except CalledProcessError as e: - logger.debug( - "Unable to get workload version (%d, %s)", e.returncode, e.stderr - ) - self.unit.status = ops.BlockedStatus("Failed git describe.") - - -if __name__ == "__main__": # pragma: nocover - ops.main(RetracerCharm) # type: ignore diff --git a/charms/retracer/tests/test_retracer.py b/charms/retracer/tests/test_retracer.py deleted file mode 100644 index cc7b249..0000000 --- a/charms/retracer/tests/test_retracer.py +++ /dev/null @@ -1,70 +0,0 @@ -import logging -import time - -import jubilant -from conftest import charm_path - -logger = logging.getLogger() - - -def test_deploy( - juju: jubilant.Juju, amqp: dict[str, str], cassandra: dict[str, str], error_tracker_config: str -): - juju.deploy( - charm=charm_path("retracer"), - app="retracer", - config={"configuration": error_tracker_config}, - ) - - juju.wait(lambda status: jubilant.all_active(status, "retracer"), timeout=600) - - # Check local config - task = juju.exec("cat", "/home/ubuntu/config/local_config.py", unit="retracer/0") - config = task.stdout - assert f"amqp_host = '{amqp['host']}'" in config, ( - "missing or wrong amqp entries in configuration" - ) - assert f"cassandra_hosts = [ '{cassandra['host']}' ]" in config, ( - "missing or wrong cassandra entry in configuration" - ) - assert "swift_bucket = " in config, "missing swift entries in configuration" - - # Check retracer processes - task = juju.exec("ps", "aux", unit="retracer/0") - processes = task.stdout.splitlines() - retracer_processes = [p for p in processes if "src/retracer.py" in p] - assert len(retracer_processes) == 4, "wrong number of retracers processes" - - # Send an empty crash, to verify it gets processed, even though not really retraced - juju.exec( - "amqp-publish", - "--server", - amqp["host"], - "--username", - amqp["username"], - "--password", - amqp["password"], - "-r", - "retrace_amd64", - "-b", - "00000000-1111-4222-3333-444444444444:swift", - unit="rabbitmq-server/0", - ) - # Give some arbitrary time to process. - # For now I'm taking that cursed path, and depending on how flaky this - # becomes, I'll see later to implement a wait loop. - time.sleep(4) - - # Verify that the retracer didn't Traceback and processed the sent crash - task = juju.exec("journalctl", "-u", "retracer@amd64.service", unit="retracer/0") - retracer_logs = task.stdout - assert "Waiting for messages in `retrace_amd64`" in retracer_logs, ( - "retracer didn't reach waiting on amqp" - ) - assert "Ack'ing message about old missing core." in retracer_logs, ( - "retracer didn't ack the OOPS retracing request" - ) - assert ( - "Could not remove from the retracing row (00000000-1111-4222-3333-444444444444) (DoesNotExist())" - in retracer_logs - ), "retracer didn't try to remove the OOPS retracing request" diff --git a/charms/timers/.gitignore b/charms/timers/.gitignore deleted file mode 100644 index a26d707..0000000 --- a/charms/timers/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -venv/ -build/ -*.charm -.tox/ -.coverage -__pycache__/ -*.py[cod] -.idea -.vscode/ diff --git a/charms/timers/LICENSE b/charms/timers/LICENSE deleted file mode 100644 index a1f16c2..0000000 --- a/charms/timers/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2024 Skia - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/charms/timers/charmcraft.yaml b/charms/timers/charmcraft.yaml deleted file mode 100644 index 09b0e6a..0000000 --- a/charms/timers/charmcraft.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. -name: error-tracker-timers -type: charm -title: Error Tracker - Timers -summary: Regularly run some script, for various purposes -description: | - This is what regularly updates various indexes and counters, along with some - clean up and self-healing. -links: - source: - - https://github.com/ubuntu/error-tracker/tree/main/charms/timers - -platforms: - ubuntu@24.04:amd64: - -parts: - timers: - source: . - plugin: charm - -# More information on this section at https://juju.is/docs/sdk/charmcraft-yaml#heading--config -# General configuration documentation: https://juju.is/docs/sdk/config -config: - options: - repo-url: - description: | - Address of the git repository to clone - default: "https://github.com/ubuntu/error-tracker" - type: string - repo-branch: - description: | - Branch of the git repository to check out - default: "main" - type: string - log-level: - description: | - Configures the log level. - - Acceptable values are: "info", "debug", "warning", "error" and "critical" - default: "info" - type: string - configuration: - description: | - Full configuration file. Must be valid Python, will be imported directly by daisy. - default: | - amqp_host = '127.0.0.1' - amqp_username = 'guest' - amqp_password = 'guest' - amqp_vhost = '/' - - cassandra_keyspace = "crashdb" - cassandra_hosts = [ '127.0.0.1' ] - cassandra_username = 'guest' - cassandra_password = 'guest' - - os_auth_url = 'https://keystone.local:5000/v3' - os_username = 'admin' - os_password = '123456' - os_tenant_name = 'error-tracker_project' - os_region_name = 'default' - - swift_bucket = "daisy-production-cores" - type: string diff --git a/charms/timers/requirements.txt b/charms/timers/requirements.txt deleted file mode 100644 index b765c42..0000000 --- a/charms/timers/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ops ~=3.1 diff --git a/charms/timers/src/charm.py b/charms/timers/src/charm.py deleted file mode 100755 index 2410906..0000000 --- a/charms/timers/src/charm.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025 Skia -# See LICENSE file for licensing details. - -"""Charm the timers for Error Tracker.""" - -import logging -from pathlib import Path -from subprocess import CalledProcessError, check_call, check_output - -import ops - -logger = logging.getLogger(__name__) - -HOME = Path("~ubuntu").expanduser() -REPO_LOCATION = HOME / "error-tracker" - - -def setup_systemd_timer(unit_name, description, command, calendar): - systemd_unit_location = Path("/") / "etc" / "systemd" / "system" - systemd_unit_location.mkdir(parents=True, exist_ok=True) - - (systemd_unit_location / f"{unit_name}.service").write_text( - f""" -[Unit] -Description={description} - -[Service] -Type=oneshot -User=ubuntu -Environment=PYTHONPATH={HOME}/config -ExecStart={command} -""" - ) - (systemd_unit_location / f"{unit_name}.timer").write_text( - f""" -[Unit] -Description={description} - -[Timer] -OnCalendar={calendar} -Persistent=true - -[Install] -WantedBy=timers.target -""" - ) - - check_call(["systemctl", "daemon-reload"]) - check_call(["systemctl", "enable", "--now", f"{unit_name}.timer"]) - - -class TimersCharm(ops.CharmBase): - """Charm the application.""" - - def __init__(self, *args): - super().__init__(*args) - self.framework.observe(self.on.start, self._on_start) - self.framework.observe(self.on.install, self._on_install) - self.framework.observe(self.on.config_changed, self._on_config_changed) - - def _on_start(self, event: ops.StartEvent): - """Handle start event.""" - self.unit.status = ops.ActiveStatus() - - def _on_install(self, event: ops.InstallEvent): - """Handle install event.""" - try: - self.unit.status = ops.MaintenanceStatus("Installing apt dependencies") - check_call(["apt-get", "update", "-y"]) - check_call( - [ - "apt-get", - "install", - "-y", - "apport-retrace", - "git", - "python3-amqp", - "python3-cassandra", - "python3-pygit2", - "python3-requests", - "python3-swiftclient", - "vim", - ] - ) - except CalledProcessError as e: - logger.debug("Package install failed with return code %d", e.returncode) - self.unit.status = ops.BlockedStatus("Failed installing apt packages.") - return - - try: - self.unit.status = ops.MaintenanceStatus("Installing error-tracker code") - repo_url = self.config.get("repo-url") - repo_branch = self.config.get("repo-branch") - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "clone", - "-b", - repo_branch, - repo_url, - REPO_LOCATION, - ] - ) - self.unit.status = ops.ActiveStatus("Ready") - except CalledProcessError as e: - logger.debug( - "Git clone of the code failed with return code %d", e.returncode - ) - self.unit.status = ops.BlockedStatus("Failed git cloning the code.") - return - - def _on_config_changed(self, event: ops.ConfigChangedEvent): - # Make sure the repo is up to date - repo_url = self.config.get("repo-url") - repo_branch = self.config.get("repo-branch") - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "fetch", - "--update-head-ok", - "--force", - repo_url, - f"refs/heads/{repo_branch}:refs/heads/{repo_branch}", - ] - ) - check_call( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "reset", - "--hard", - repo_branch, - ] - ) - - config = self.config.get("configuration") - - config_location = HOME / "config" - config_location.mkdir(parents=True, exist_ok=True) - (config_location / "local_config.py").write_text(config) - - setup_systemd_timer( - "et-unique-users-daily-update", - "Error Tracker - Unique users daily update", - f"{REPO_LOCATION}/src/tools/unique_users_daily_update.py", - "*-*-* 00:30:00", # every day at 00:30 - ) - setup_systemd_timer( - "et-import-bugs", - "Error Tracker - Import bugs", - f"{REPO_LOCATION}/src/tools/import_bugs.py", - "*-*-* 01,04,07,10,13,16,19,22:00:00", # every three hours - ) - setup_systemd_timer( - "et-import-team-packages", - "Error Tracker - Import team packages", - f"{REPO_LOCATION}/src/tools/import_team_packages.py", - "*-*-* 02:30:00", # every day at 02:30 - ) - setup_systemd_timer( - "et-swift-corrupt-core-check", - "Error Tracker - Swift - Check for corrupt cores", - f"{REPO_LOCATION}/src/tools/swift_corrupt_core_check.py", - "*-*-* 04:30:00", # every day at 04:30 - ) - setup_systemd_timer( - "et-swift-handle-old-cores", - "Error Tracker - Swift - Handle old cores", - f"{REPO_LOCATION}/src/tools/swift_handle_old_cores.py", - "*-*-* *:45:00", # every hour at minute 45 - ) - self.unit.set_workload_version(self._getWorkloadVersion()) - self.unit.status = ops.ActiveStatus("Ready") - - def _getWorkloadVersion(self): - """Get the error tracker version from the git repository""" - try: - version = check_output( - [ - "sudo", - "-u", - "ubuntu", - "git", - "-C", - REPO_LOCATION, - "describe", - "--tags", - "--always", - "--dirty", - ] - ) - return version.decode() - except CalledProcessError as e: - logger.debug( - "Unable to get workload version (%d, %s)", e.returncode, e.stderr - ) - self.unit.status = ops.BlockedStatus("Failed git describe.") - - -if __name__ == "__main__": # pragma: nocover - ops.main(TimersCharm) # type: ignore diff --git a/charms/timers/tests/test_timers.py b/charms/timers/tests/test_timers.py deleted file mode 100644 index 9fcc1ad..0000000 --- a/charms/timers/tests/test_timers.py +++ /dev/null @@ -1,37 +0,0 @@ -import json -import logging - -import jubilant -from conftest import charm_path - -logger = logging.getLogger() - - -def test_deploy( - juju: jubilant.Juju, amqp: dict[str, str], cassandra: dict[str, str], error_tracker_config: str -): - juju.deploy( - charm=charm_path("timers"), - app="timers", - config={"configuration": error_tracker_config}, - ) - - juju.wait(lambda status: jubilant.all_active(status, "timers"), timeout=600) - - # Check local config - task = juju.exec("cat", "/home/ubuntu/config/local_config.py", unit="timers/0") - config = task.stdout - assert f"amqp_host = '{amqp['host']}'" in config, ( - "missing or wrong amqp entries in configuration" - ) - assert f"cassandra_hosts = [ '{cassandra['host']}' ]" in config, ( - "missing or wrong cassandra entry in configuration" - ) - assert "swift_bucket = " in config, "missing swift entries in configuration" - - # Check deployed systemd units - task = juju.exec("systemctl", "list-units", "-o", "json", unit="timers/0") - units = json.loads(task.stdout) - et_units = [u for u in units if u["unit"].startswith("et-")] - assert len(et_units) == 5, "wrong number of error tracker systemd units" - assert all([u["active"] == "active" for u in et_units]), "not all systemd units are active" diff --git a/pyproject.toml b/pyproject.toml index 36a8cf5..6acee4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,14 @@ name = "error-tracker" requires-python = ">=3.8" dynamic = ["version"] -dependencies = [ - "jubilant>=1.3.0", - "pytest>=8.3.5", -] +[project.optional-dependencies] +dev = [ + "jubilant", + "pytest", + "requests", + "tenacity", +] [tool.setuptools.dynamic] version = {attr = "src.errortracker.__version__"} @@ -19,3 +22,6 @@ extend-exclude = ["__pycache__", "*.egg_info"] [tool.ruff.lint] select = ["E", "F", "W", "Q", "I"] ignore = ["E501"] + +[tool.pytest] +pythonpath = ["./src", "./local_config"] diff --git a/spread.yaml b/spread.yaml new file mode 100644 index 0000000..505c0e5 --- /dev/null +++ b/spread.yaml @@ -0,0 +1,93 @@ +project: error-tracker +kill-timeout: 90m +workers: 1 + +environment: + CI: "$(HOST: echo $CI)" + +backends: + lxd: + type: adhoc + # This allocate block can be removed once the LXD VM support is landed in + # spread: https://github.com/canonical/spread/pull/185 + allocate: | + CONTAINER_NAME="error-tracker-${SPREAD_SYSTEM/./-}-${RANDOM}" + DISK="${DISK:-20}" + CPU="${CPU:-4}" + MEM="${MEM:-5}" + + lxc launch --vm -e \ + "${SPREAD_SYSTEM/-/:}" \ + "${CONTAINER_NAME}" \ + -c user.user-data="$(sed "s|SPREAD_PASSWORD|$SPREAD_PASSWORD|g" src/tests/cloud-config.yaml)" \ + -c limits.cpu="${CPU}" \ + -c limits.memory="${MEM}GiB" \ + -d root,size="${DISK}GiB" + + # Wait for the spread user + while ! lxc exec "${CONTAINER_NAME}" -- id -u spread &>/dev/null; do sleep 0.5; done + + # Set the instance address for spread + ADDRESS "$(lxc ls -f csv | grep "${CONTAINER_NAME}" | cut -d"," -f3 | cut -d" " -f1)" + discard: | + instance_name="$(lxc ls -f csv | grep -B1 -A1 $SPREAD_SYSTEM_ADDRESS | grep error-tracker | cut -f1 -d",")" + lxc delete -f $instance_name + + systems: + - ubuntu-24.04: + username: spread + workers: 1 + +suites: + tests/errortracker/: + summary: Spread tests - error tracker tests + prepare: | + apt-get update + apt-get install -y --no-install-recommends docker.io + apt-get install -y --no-install-recommends linux-modules-extra-$(uname -r) zram-tools + apt-get install -y --no-install-recommends \ + apport-retrace \ + git \ + python3-amqp \ + python3-bson \ + python3-cassandra \ + python3-flask \ + python3-pygit2 \ + python3-pytest \ + python3-pytest-cov \ + python3-swiftclient \ + ubuntu-dbgsym-keyring \ + whoopsie + docker run --name cassandra --network host --rm -d -e HEAP_NEWSIZE=10M -e MAX_HEAP_SIZE=200M docker.io/cassandra + docker run --name rabbitmq --network host --rm -d docker.io/rabbitmq + docker run --name swift --network host --rm -d docker.io/openstackswift/saio + bash -c 'while ! echo "Hello there, still waiting for Cassandra" >/dev/tcp/localhost/9042; do sleep 5; done' + bash -c 'while ! echo "Hello there, still waiting for RabbitMQ" >/dev/tcp/localhost/5672; do sleep 5; done' + bash -c 'while ! echo "Hello there, still waiting for swift" >/dev/tcp/localhost/8080; do sleep 5; done' + restore: | + docker stop cassandra + docker stop rabbitmq + docker stop swift + charm/tests/: + summary: Spread tests - charm tests + prepare: | + snap install astral-uv --classic + snap install concierge --classic + concierge prepare \ + --juju-channel=3/stable \ + --charmcraft-channel=3.x/stable \ + --preset machine + +exclude: + - .coverage + - .git + - .github + - .pytest_cache + - .ruff_cache + - .tox + - .venv + +# this needs to be under /root because spread executes the test scripts +# as root, which means that all snaps can only see files in root's +# home directory due to snap confinement. +path: /root/proj \ No newline at end of file diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index d01f799..0000000 --- a/src/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -check: - ./test/pyflakes - ./test/run - -version: - bzr version-info --python > daisy/version_info.py - -all: check version diff --git a/src/daisy/__init__.py b/src/daisy/__init__.py deleted file mode 100644 index 28aa907..0000000 --- a/src/daisy/__init__.py +++ /dev/null @@ -1,168 +0,0 @@ -from functools import reduce - -# !/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright © 2011-2013 Canonical Ltd. -# Author: Evan Dandrea -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero Public License for more details. -# -# You should have received a copy of the GNU Affero Public License -# along with this program. If not, see . - -config = None -try: - import local_config as config -except ImportError: - pass -if not config: - from daisy import configuration as config - - -def validate_and_set_configuration(): - """Validate the set configuration at module import time, to prevent - exceptions at random points during the lifetime of the application. - - This will modify the in-memory configuration data if deprecated parameters - are used, to structure them in the non-deprecated format.""" - - write_weights = getattr(config, "storage_write_weights", "") - core_storage = getattr(config, "core_storage", "") - if core_storage and not write_weights: - msg = "storage_write_weights must be set alongside core_storge." - raise ImportError(msg) - if not core_storage: - swift = getattr(config, "swift_bucket", "") - ec2 = getattr(config, "ec2_bucket", "") - local = getattr(config, "san_path", "") - if ec2 and swift: - raise ImportError("ec2_bucket and swift_bucket cannot both be set.") - - # Match the old behaviour. Put everything on swift, if available. - # Failing that, fall back to EC2, then local. - if swift: - # If there is no swift configuration set in local_config or the - # default config check swift_config as that's what the charms - # write to. Yes, this is messy and there should be a better - # solution. - try: - import swift_config - except ImportError: - pass - os_auth_url = getattr(config, "os_auth_url", "") - if not os_auth_url: - os_auth_url = getattr(swift_config, "os_auth_url", "") - os_username = getattr(config, "os_username", "") - if not os_username: - os_username = getattr(swift_config, "os_username", "") - os_password = getattr(config, "os_password", "") - if not os_password: - os_password = getattr(swift_config, "os_password", "") - os_tenant_name = getattr(config, "os_tenant_name", "") - if not os_tenant_name: - os_tenant_name = getattr(swift_config, "os_tenant_name", "") - os_region_name = getattr(config, "os_region_name", "") - if not os_region_name: - os_region_name = getattr(swift_config, "os_region_name", "") - config.storage_write_weights = {"swift": 1.0} - config.core_storage = { - "default": "swift", - "swift": { - "type": "swift", - "bucket": swift, - "os_auth_url": os_auth_url, - "os_username": os_username, - "os_password": os_password, - "os_tenant_name": os_tenant_name, - "os_region_name": os_region_name, - }, - } - elif ec2: - host = getattr(config, "ec2_host", "") - aws_access_key = getattr(config, "aws_access_key", "") - aws_secret_key = getattr(config, "aws_secret_key", "") - if not (host and aws_access_key and aws_secret_key): - msg = ( - "EC2 provider set but host, bucket, aws_access_key, or" - " aws_secret_key not set." - ) - raise ImportError(msg) - config.storage_write_weights = {"s3": 1.0} - config.core_storage = { - "default": "s3", - "s3": { - "type": "s3", - "host": host, - "bucket": ec2, - "aws_access_key": aws_access_key, - "aws_secret_key": aws_secret_key, - }, - } - elif local: - config.storage_write_weights = {"local": 1.0} - config.core_storage = { - "default": "local", - "local": {"type": "local", "path": local}, - } - else: - raise ImportError("no core storage provider is set.") - - if not getattr(config, "storage_write_weights", ""): - d = config.core_storage.get("default", "") - if not d: - msg = "No storage_write_weights set, but no default set in core" " storage" - raise ImportError(msg) - config.storage_write_weights = {d: 1.0} - - for k, v in config.core_storage.items(): - if k == "default": - continue - t = v.get("type", "") - if not t: - raise ImportError("You must set a type for %s." % k) - if t == "swift": - keys = [ - "bucket", - "os_auth_url", - "os_username", - "os_password", - "os_tenant_name", - "os_region_name", - ] - elif t == "s3": - keys = ["host", "bucket", "aws_access_key", "aws_secret_key"] - elif t == "local": - keys = ["path"] - missing = set(keys) - set(v.keys()) - if missing: - missing = ", ".join(missing) - raise ImportError("Missing keys for %s: %s." % (k, missing)) - - if reduce(lambda x, y: x + y, list(config.storage_write_weights.values())) != 1.0: - msg = "storage_write_weights values do not add up to 1.0." - raise ImportError(msg) - - -def gen_write_weight_ranges(d): - total = 0 - r = {} - for key, val in d.items(): - r[key] = (total, total + val) - total += val - return r - - -validate_and_set_configuration() - -config.write_weight_ranges = None -if getattr(config, "storage_write_weights", ""): - ranges = gen_write_weight_ranges(config.storage_write_weights) - config.write_weight_ranges = ranges diff --git a/src/daisy/app.py b/src/daisy/app.py new file mode 100644 index 0000000..ce66d2f --- /dev/null +++ b/src/daisy/app.py @@ -0,0 +1,28 @@ +from flask import Flask, request +from flask.logging import default_handler + +from daisy.submit import submit +from daisy.submit_core import submit_core +from errortracker import cassandra, config + +config.logger.addHandler(default_handler) +app = Flask(__name__) + + +@app.route("/", methods=["POST"]) +def handle_submit(system_token): + return submit(request, system_token) + + +@app.route("//submit-core//", methods=["POST"]) +def handle_submit_core(oopsid, architecture, system_token): + return submit_core(request, oopsid, architecture, system_token) + + +def __main__(): + cassandra.setup_cassandra() + app.run(host="0.0.0.0", debug=True) + + +if __name__ == "__main__": + __main__() diff --git a/src/daisy/cassandra_schema.py b/src/daisy/cassandra_schema.py deleted file mode 100644 index 4020bb1..0000000 --- a/src/daisy/cassandra_schema.py +++ /dev/null @@ -1,79 +0,0 @@ -from cassandra.cqlengine import columns, models - -DoesNotExist = models.Model.DoesNotExist - - -class ErrorTrackerTable(models.Model): - __keyspace__ = "crashdb" - __table_name_case_sensitive__ = True - __abstract__ = True - - -class Indexes(ErrorTrackerTable): - __table_name__ = "Indexes" - key = columns.Blob(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Blob(db_field="value") - - def get_as_dict(*args, **kwargs) -> dict: - query = Indexes.objects.filter(*args, **kwargs) - d = {} - for result in query: - d[result["column1"]] = result["value"] - return d - - -class OOPS(ErrorTrackerTable): - __table_name__ = "OOPS" - key = columns.Blob(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Text(db_field="value") - - def get_as_dict(*args, **kwargs) -> dict: - query = OOPS.objects.filter(*args, **kwargs) - d = {} - for result in query: - d[result["column1"]] = result["value"] - return d - - -class Stacktrace(ErrorTrackerTable): - __table_name__ = "Stacktrace" - key = columns.Blob(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Text(db_field="value") - - -class RetraceStats(ErrorTrackerTable): - __table_name__ = "RetraceStats" - key = columns.Blob(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Counter(db_field="value") - - -class Bucket(ErrorTrackerTable): - __table_name__ = "Bucket" - key = columns.Text(db_field="key", primary_key=True) - column1 = columns.TimeUUID(db_field="column1", primary_key=True) - value = columns.Blob(db_field="value") - - -class BucketRetraceFailureReason(ErrorTrackerTable): - __table_name__ = "BucketRetraceFailureReason" - key = columns.Blob(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Text(db_field="value") - - def get_as_dict(*args, **kwargs) -> dict: - query = BucketRetraceFailureReason.objects.filter(*args, **kwargs) - d = {} - for result in query: - d[result["column1"]] = result["value"] - return d - - -class AwaitingRetrace(ErrorTrackerTable): - __table_name__ = "AwaitingRetrace" - key = columns.Text(db_field="key", primary_key=True) - column1 = columns.Text(db_field="column1", primary_key=True) - value = columns.Text(db_field="value") diff --git a/src/daisy/configuration.py b/src/daisy/configuration.py deleted file mode 100644 index 70eed07..0000000 --- a/src/daisy/configuration.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright © 2011-2013 Canonical Ltd. -# Author: Evan Dandrea -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero Public License for more details. -# -# You should have received a copy of the GNU Affero Public License -# along with this program. If not, see . - -# The Cassandra keyspace to use. -cassandra_keyspace = "crashdb" - -# The addresses of the Cassandra database nodes. -# cassandra_hosts = ['192.168.10.2:9160'] -cassandra_hosts = ["192.168.10.2:9042"] - -# The username to connect with. -cassandra_username = "" - -# The password to use. -cassandra_password = "" - -# should be a multiple of cassandra_hosts -cassandra_pool_size = 9 - -# allow the pool to overflow -cassandra_max_overflow = 18 - -# list of strings representing the host/domain names that will be served -allowed_hosts = ["127.0.0.1", "localhost"] - -# The AMQP host to receive messages from. -amqp_host = "127.0.0.1" - -# The AMQP username. -amqp_username = "" - -# The AMQP username. -amqp_password = "" - -# The AMQP host to receive messages from for OOPS reports. -oops_amqp_host = "127.0.0.1" - -# The AMQP username for OOPS reports. -oops_amqp_username = "" - -# The AMQP username for OOPS reports. -oops_amqp_password = "" - -# The AMQP exchange name for OOPS reports. -oops_amqp_exchange = "" - -# The AMQP virtual host for OOPS reports. -oops_amqp_vhost = "" - -# The AMQP routing key (queue) for OOPS reports. -oops_amqp_routing_key = "oopses" - -# The path to the SAN for storing core dumps (deprecated). -san_path = "/srv/cores" - -# The path to store OOPS reports in for http://errors.ubuntu.com. -oops_repository = "/srv/local-oopses-whoopsie" - -# The host and port of the txstatsd server. -statsd_host = "localhost" - -statsd_port = 8125 - -# Use Launchpad staging instead of production. -lp_use_staging = False - -# Launchpad OAuth tokens. -# See https://wiki.ubuntu.com/ErrorTracker/Contributing/Errors -lp_oauth_token = "" -lp_oauth_secret = "" - -# Directory for httplib2's request cache. -http_cache_dir = "/tmp/errors.ubuntu.com-httplib2-cache" - -# Database configuration for the Errors Django application. This database is -# used to store OpenID login information. -django_databases = { - "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": "django_db", - "USER": "django_login", - "PASSWORD": "", - "HOST": "localhost", - "PORT": "5432", - } -} - -# S3 configuration (deprecated). Only set these if you're using S3 for storing -# the core files. -aws_access_key = "" -aws_secret_key = "" -ec2_host = "" - -# The bucket to place core files in when using S3 (deprecated). -ec2_bucket = "" - -# Swift configuration (deprecated). Only set these if you're using Swift for -# storing the core files. -os_auth_url = "" -os_username = "" -os_password = "" -os_tenant_name = "" -os_region_name = "" - -# The bucket to place core files in when using Swift (deprecated). -swift_bucket = "" - -# The core_storage parameter lists the available providers for saving the -# uploaded core files until they're retraced. -# -# If no storage_write_weights are provided below, it is assumed that the -# 'default' member should receive all the core files. It is an error to not -# provide a default or storage_write_weights, but you can omit a default if -# storage_write_weights is set. -# -# Example: -# core_storage = { -# 'default': 'swift-storage', -# 'local-s1': {'type': 'local', -# 'path': '/srv/cores'} -# 'swift-storage': {'type': 'swift', -# 'bucket': 'cores', -# 'os_auth_url': 'http://keystone.example.com/', -# 'os_username': 'ostack', -# 'os_password': 'secret', -# 'os_tenant_name': 'ostack_project', -# 'os_region_name': 'region01'} -# 's3-east-1': {'type': 's3', -# 'bucket': 'cores', -# 'host': 's3.amazonaws.com', -# 'aws_secret_key': 'secret', -# 'aws_access_key': 'access-key'} -# } - -core_storage = {} - -# The storage_write_weights parameter specifies the rough percentage of -# incoming reports that should go to each provider (0.0, 1.0]. You do not have -# to specify percentages for all providers. Any provider without a percentage -# will not be used. All the percentages must total up to 1.0. -# -# Example: -# storage_write_weights = {'local-s1': 0.25, 'swift-storage': 0.75} - -storage_write_weights = {} - -# The domain name for the Errors service. -openid_trust_root = "https://errors.ubuntu.com/" - -# The base URL for static content for https://errors.ubuntu.com -errors_static_url = "https://assets.ubuntu.com/sites/errors/398/" - -# Configuration for OOPS reports. -oops_config = { - "publishers": [ - { - "type": "amqp", - "host": oops_amqp_host, - "user": oops_amqp_username, - "password": oops_amqp_password, - "vhost": oops_amqp_vhost, - "exchange_name": oops_amqp_exchange, - "routing_key": oops_amqp_routing_key, - }, - { - "type": "datedir", - "error_dir": oops_repository, - "new_only": True, - }, - ], -} - -# The upper bound for the time it takes from receiving a core to processing it. -# Beyond this point, we'll start alerting. - -time_to_retrace_alert = 86400 # 1 day in seconds - - -# Secret key for errors.ubuntu.com -errors_secret_key = "" - -# Hooks for relations in charms to contribute their configuration settings. -try: - from db_settings import * # noqa: F403 -except ImportError: - pass - -try: - from amqp_settings import * # noqa: F403 -except ImportError: - pass - -try: - from postgres_settings import * # noqa: F403 -except ImportError: - pass - -try: - from local_settings import * # noqa: F403 -except ImportError: - pass - -allow_bug_filing = "True" diff --git a/src/daisy/gunicorn_config.py b/src/daisy/gunicorn_config.py new file mode 100644 index 0000000..cc4c182 --- /dev/null +++ b/src/daisy/gunicorn_config.py @@ -0,0 +1,9 @@ +import multiprocessing + +workers = 2 * multiprocessing.cpu_count() + +bind = "0.0.0.0:8000" + +accesslog = "-" +errorlog = "-" +loglevel = "info" diff --git a/src/daisy/launchpad.py b/src/daisy/launchpad.py deleted file mode 100644 index 996f0db..0000000 --- a/src/daisy/launchpad.py +++ /dev/null @@ -1,684 +0,0 @@ -import json -import sys -import urllib.error -import urllib.parse -import urllib.request -from functools import cmp_to_key - -import apt -import httplib2 -from lazr.restfulclient._browser import AtomicFileCache -from oauth import oauth - -from daisy import config - -if ( - not hasattr(config, "lp_oauth_token") - or not hasattr(config, "lp_oauth_secret") - or config.lp_oauth_token is None - or config.lp_oauth_secret is None -): - raise ImportError( - "You must set lp_oauth_token and " "lp_oauth_secret in local_config" - ) - -if config.lp_use_staging == "True": - _create_bug_url = "https://api.qastaging.launchpad.net/devel/bugs" - _ubuntu_target = "https://api.qastaging.launchpad.net/devel/ubuntu" - _oauth_realm = "https://api.qastaging.launchpad.net" - _launchpad_base = "https://api.qastaging.launchpad.net/devel" -else: - _create_bug_url = "https://api.launchpad.net/devel/bugs" - _ubuntu_target = "https://api.launchpad.net/devel/ubuntu" - _oauth_realm = "https://api.launchpad.net" - _launchpad_base = "https://api.launchpad.net/devel" - -# TODO: replace hardcoding of 'ubuntu' in all these urls e.g. so we can use -# ubuntu-rtm too -_get_published_binaries_url = ( - _launchpad_base + "/ubuntu/+archive/primary" - "?ws.op=getPublishedBinaries&binary_name=%s" - "&exact_match=true&ordered=true&status=Published" -) -_get_published_binary_version_url = ( - _launchpad_base + "/ubuntu/+archive/primary" - "?ws.op=getPublishedBinaries&binary_name=%s&version=%s" - "&exact_match=true&ordered=true&status=Published" -) -_get_published_source_version_url = ( - _launchpad_base + "/ubuntu/+archive/primary" - "?ws.op=getPublishedSources&source_name=%s&version=%s" - "&exact_match=true&ordered=true" -) -_get_bug_tasks_url = _launchpad_base + "/bugs/%s/bug_tasks" -_get_bug_dupof_url = _launchpad_base + "/bugs/%s/duplicate_of_link" - -_get_published_binaries_for_release_url = ( - _launchpad_base - + "/ubuntu/+archive/primary/?ws.op=getPublishedBinaries&binary_name=%s" - "&exact_match=true&distro_arch_series=%s" -) -_get_packageset_url = _launchpad_base + "/package-sets/ubuntu/%s/%s" - -_person_url = _launchpad_base + "/~" -_source_target = _launchpad_base + "/ubuntu/+source/" -_distro_arch_series = _launchpad_base + "/ubuntu/%s/i386" - -# Bug and package lookup. - -_file_cache = AtomicFileCache(config.http_cache_dir) -_http = httplib2.Http(_file_cache) - - -def json_request_entries(url): - try: - return json_request(url)["entries"] - except (KeyError, TypeError, ValueError): - return "" - - -def json_request(url): - try: - response, content = _http.request(url) - except httplib2.ServerNotFoundError: - return "" - - try: - return json.loads(content) - except ValueError: - return "" - - -def get_all_codenames(): - url = _launchpad_base + "/ubuntu/series" - return [entry["name"] for entry in json_request_entries(url)] - - -def get_codename_for_version(version): - release_codenames = { - "12.04": "precise", - "12.10": "quantal", - "13.04": "raring", - "13.10": "saucy", - "14.04": "trusty", - "14.10": "utopic", - "15.04": "vivid", - "15.10": "wily", - "16.04": "xenial", - "16.10": "yakkety", - "17.04": "zesty", - "17.10": "artful", - "18.04": "bionic", - "18.10": "cosmic", - "19.04": "disco", - "19.10": "eoan", - "20.04": "focal", - "20.10": "groovy", - "21.04": "hirsute", - "21.10": "impish", - "22.04": "jammy", - "22.10": "kinetic", - "23.04": "lunar", - "23.10": "mantic", - "24.04": "noble", - "24.10": "oracular", - "25.04": "plucky", - "25.10": "questing", - } - if not version: - return None - if version in list(release_codenames.values()): - return version - elif version.startswith("Ubuntu "): - version = version.replace("Ubuntu ", "") - if version in release_codenames: - return release_codenames[version] - elif version == "Ubuntu RTM 14.09": - return "14.09" - else: - url = _launchpad_base + "/ubuntu/series" - for entry in json_request_entries(url): - if "name" in entry and entry.get("version", None) == version: - return entry["name"] - return None - - -def get_devel_series_codename(): - from datetime import datetime - - import distro_info - - di = distro_info.UbuntuDistroInfo() - today = datetime.today().date() - try: - codename = di.devel(today) - # this can happen on release and before - # distro-info-data is SRU'ed - except distro_info.DistroDataOutdated: - codename = di.stable() - return codename - - -def get_version_for_codename(codename): - url = _launchpad_base + "/ubuntu/series" - for entry in json_request_entries(url): - if entry["name"] == codename: - return entry["version"] - return None - - -def get_versions_for_binary(binary_package, ubuntu_version): - if not ubuntu_version: - codename = get_devel_series_codename() - else: - codename = get_codename_for_version(ubuntu_version) - if not codename: - return [] - if is_source_package(binary_package): - package_name = urllib.parse.quote_plus(binary_package) - ma_url = _launchpad_base + "/ubuntu/" + codename + "/main_archive" - ma = json_request(ma_url) - if ma: - ma_link = ma["self_link"] - else: - return "" - series_url = _launchpad_base + "/ubuntu/" + codename - ps_url = ma_link + ( - "/?ws.op=getPublishedSources&exact_match=true&status=Published&source_name=%s&distro_series=%s" - % (package_name, series_url) - ) - # use the first one, since they are unordered - try: - ps = json_request_entries(ps_url)[0]["self_link"] - except IndexError: - return "" - pb_url = ps + "/?ws.op=getPublishedBinaries" - json_data = urllib2_request_json( - pb_url, config.lp_oauth_token, config.lp_oauth_secret - ) - entries = json.loads(json_data)["entries"] - for entry in entries: - # use the first binary package since all versions should be the - # same - binary_package = entry["binary_package_name"] - break - # i386 and amd64 versions should be the same, hopefully. - results = set() - dist_arch = urllib.parse.quote(_distro_arch_series % codename) - url = _get_published_binaries_for_release_url % (binary_package, dist_arch) - results |= set( - [ - x["binary_package_version"] - for x in json_request_entries(url) - if "binary_package_version" in x - ] - ) - return sorted(results, key=cmp_to_key(apt.apt_pkg.version_compare)) - - -def get_release_for_binary(binary_package, version): - results = set() - url = _get_published_binary_version_url % ( - urllib.parse.quote_plus(binary_package), - urllib.parse.quote_plus(version), - ) - results |= set( - [ - get_version_for_codename(x["display_name"].split(" ")[3]) - for x in json_request_entries(url) - ] - ) - return results - - -def binaries_are_most_recent(specific_packages, release=None): - """For each (package, version) tuple supplied, determine if that is the - most recent version of the binary package. - - This method lets us cache repeated lookups of the most recent version of - the same binary package.""" - - _cache = {} - result = [] - for package, version in specific_packages: - if not package or not version: - result.append(True) - continue - - if package in _cache: - latest_version = _cache[package] - else: - latest_version = _get_most_recent_binary_version(package, release) - # We cache this even if _get_most_recent_binary_version returns - # None, as packages like Skype will always return None and we - # shouldn't keep asking. - _cache[package] = latest_version - - if latest_version: - r = apt.apt_pkg.version_compare(version, latest_version) != -1 - result.append(r) - else: - result.append(True) - return result - - -def _get_most_recent_binary_version(package, release): - url = _get_published_binaries_url % urllib.parse.quote(package) - if release: - # TODO cache this by pushing it into the above function and instead - # passing the distro_arch_series url. - version = get_codename_for_version(release.split()[1]) - distro_arch_series = _distro_arch_series % version - url += "&distro_arch_series=" + urllib.parse.quote(distro_arch_series) - try: - return json_request_entries(url)[0]["binary_package_version"] - except (KeyError, IndexError): - return "" - - -def pocket_for_binaries(specific_packages): - """For each (package, version, release) tuple supplied, determine the - pocket in which the package version appears. - - This method lets us cache repeated lookups of the pocket for the same - binary package.""" - - _cache = {} - result = [] - for package, version, release in specific_packages: - if not package or not version or not release: - result.append("Not Found") - continue - - if (package, version, release) in _cache: - pocket = _cache[package, version, release] - else: - pocket = _get_pocket_for_binary_version(package, version, release) - # We cache this even if _get_pocket_for_binary_version returns - # None, as packages like Skype will always return None and we - # shouldn't keep asking. - _cache[package, version, release] = "%s" % (pocket) - result.append(pocket) - return result - - -def _get_pocket_for_binary_version(package, version, release): - if release == "Ubuntu RTM 14.09": - url = _get_published_binaries_url.replace( - "/ubuntu/", "/ubuntu-rtm/" - ) % urllib.parse.quote(package) - else: - url = _get_published_binaries_url % urllib.parse.quote(package) - # the package version may be Superseded or Obsolete - url = url.replace("&status=Published", "") - url += "&version=" + urllib.parse.quote_plus(version) - # TODO cache this by pushing it into the above function and instead - # passing the distro_arch_series url. - version = get_codename_for_version(release.split()[-1]) - distro_arch_series = _distro_arch_series % version - url += "&distro_arch_series=" + urllib.parse.quote(distro_arch_series) - try: - pocket = json_request_entries(url)[0]["pocket"] - return "%s" % pocket - except (KeyError, IndexError): - return "" - - -def binary_is_most_recent(package, version): - # FIXME we need to factor in the release, otherwise this is often going to - # look like the issue has disappeared when filtering the most common - # problems view to a since-passed release. - latest_version = _get_most_recent_binary_version(package) - if not latest_version: - return True - # If the version we've been provided is older than the latest version, - # return False; it's not the newest. We will then assume that because - # we haven't seen it in the new version it may be fixed. - return apt.apt_pkg.version_compare(version, latest_version) != -1 - - -def bug_is_fixed(bug, release=None): - url = _get_bug_tasks_url % urllib.parse.quote(bug) - if release: - release = release.split()[1] - codename = get_codename_for_version(release) - if codename: - codename_task = " (ubuntu %s)" % codename - else: - codename_task = "" - - try: - entries = json_request_entries(url) - if len(entries) == 0: - # We will presume that this is a private bug. - return None - - for entry in entries: - name = entry["bug_target_name"] - if release and codename and name.lower().endswith(codename_task): - if not entry["is_complete"]: - return False - elif entry["is_complete"]: - return True - - # Lets iterate again and see if we can find the Ubuntu task. - for entry in entries: - name = entry["bug_target_name"] - # Do not look at upstream bug tasks. - if name.endswith(" (Ubuntu)"): - # We also consider bugs that are Invalid as complete. I am not - # entirely sure that is correct in this context. - if not entry["is_complete"]: - # As the bug itself may be in a library package bug task, - # it is not sufficient to return True at the first complete - # bug task. - return False - return True - except (ValueError, KeyError): - return False - - -def bug_get_master_id(bug): - """Return master bug (of which given bug is a duplicate) - - Return None if bug is not a duplicate. - """ - url = _get_bug_dupof_url % urllib.parse.quote(bug) - try: - res = json_request(url) - if res: - return res.split("/")[-1] - except (ValueError, KeyError, AttributeError): - pass - return None - - -def is_source_package(package_name): - dev_series = get_devel_series_codename() - url = ( - _launchpad_base - + "/ubuntu/" - + dev_series - + ("/?ws.op=getSourcePackage&name=%s" % package_name) - ) - request = json_request(url) - if request: - return True - else: - return False - - -def get_binaries_in_source_package(package_name, release=None): - # FIXME: in the event that a package does not exist in the devel release - # an empty set will be returned and binary packages from previous releases - # will be missed e.g. synaptiks in trusty - if not release: - dev_series = get_devel_series_codename() - else: - dev_series = get_codename_for_version(release) - package_name = urllib.parse.quote_plus(package_name) - ma_url = _launchpad_base + "/ubuntu/" + dev_series + "/main_archive" - ma = json_request(ma_url) - if ma: - ma_link = ma["self_link"] - else: - return "" - dev_series_url = _launchpad_base + "/ubuntu/" + dev_series - ps_url = ma_link + ( - "/?ws.op=getPublishedSources&exact_match=true&status=Published&source_name=%s&distro_series=%s" - % (package_name, dev_series_url) - ) - # just use the first one, since they are unordered - try: - ps = json_request_entries(ps_url)[0]["self_link"] - except IndexError: - return "" - pb_url = ps + "/?ws.op=getPublishedBinaries" - pbs = [] - json_data = urllib2_request_json( - pb_url, config.lp_oauth_token, config.lp_oauth_secret - ) - try: - tsl = json.loads(json_data)["total_size_link"] - total_size = int( - urllib2_request_json(tsl, config.lp_oauth_token, config.lp_oauth_secret) - ) - while len(pbs) < total_size: - entries = json.loads(json_data)["entries"] - for entry in entries: - pbs.append(entry["binary_package_name"]) - try: - ncl = json.loads(json_data)["next_collection_link"] - except KeyError: - break - json_data = urllib2_request_json( - ncl, config.lp_oauth_token, config.lp_oauth_secret - ) - except KeyError: - entries = json.loads(json_data)["entries"] - for entry in entries: - pbs.append(entry["binary_package_name"]) - return set(pbs) - - -def urllib2_request_json(url, token, secret): - headers = _generate_headers(token, secret) - request = urllib.request.Request(url, None, headers) - response = urllib.request.urlopen(request) - content = response.read() - return content - - -def get_subscribed_packages(user): - """return binary packages to which a user is subscribed""" - src_pkgs = [] - bin_pkgs = [] - url = _person_url + user + "?ws.op=getBugSubscriberPackages" - json_data = urllib2_request_json(url, config.lp_oauth_token, config.lp_oauth_secret) - try: - tsl = json.loads(json_data)["total_size_link"] - total_size = int( - urllib2_request_json(tsl, config.lp_oauth_token, config.lp_oauth_secret) - ) - while len(src_pkgs) < total_size: - entries = json.loads(json_data)["entries"] - for entry in entries: - src_pkgs.append(entry["name"]) - try: - ncl = json.loads(json_data)["next_collection_link"] - except KeyError: - break - json_data = urllib2_request_json( - ncl, config.lp_oauth_token, config.lp_oauth_secret - ) - except KeyError: - entries = json.loads(json_data)["entries"] - for entry in entries: - src_pkgs.append(entry["name"]) - for src_pkg in src_pkgs: - bin_pkgs.extend(list(get_binaries_in_source_package(src_pkg))) - return bin_pkgs - - -def get_packages_in_packageset_name(release, name): - if not release: - series = get_devel_series_codename() - else: - series = get_codename_for_version(release) - url = _get_packageset_url % (series, name) + "?ws.op=getSourcesIncluded" - pkg_set = json_request(url) - return pkg_set - - -def is_valid_source_version(src_package, version): - url = _get_published_source_version_url % ( - urllib.parse.quote_plus(src_package), - urllib.parse.quote_plus(version), - ) - json_data = json_request(url) - if "total_size" not in list(json_data.keys()): - return False - if json_data["total_size"] == 0: - return False - elif json_data["total_size"] >= 1: - return True - - -def get_pocket_for_source_version(src_package, version, release): - # hack for packages from RTM - if release == "Ubuntu RTM 14.09": - url = _get_published_source_version_url.replace("/ubuntu/", "/ubuntu-rtm/") % ( - urllib.parse.quote_plus(src_package), - urllib.parse.quote_plus(version), - ) - else: - url = _get_published_source_version_url % ( - urllib.parse.quote_plus(src_package), - urllib.parse.quote_plus(version), - ) - series = get_codename_for_version(release.split()[-1]) - distro_arch_series = _distro_arch_series % series - url += "&distro_arch_series=" + urllib.parse.quote(distro_arch_series) - try: - pocket = json_request_entries(url)[0]["pocket"] - return "%s" % pocket - except (KeyError, IndexError): - return "?" - - -# Bug creation. - - -def _generate_operation(title, description, target=_ubuntu_target, tags=[""]): - # tags need to be a list with each tag double quoted because LP checks for - # invalid characters like single quotes in tags - tags = str(tags).replace("'", '"') - operation = { - "ws.op": "createBug", - "description": description, - "target": target, - "title": title, - "tags": tags, - } - return urllib.parse.urlencode(operation) - - -def _generate_headers(oauth_token, oauth_secret): - a = ( - 'OAuth realm="%s", ' - 'oauth_consumer_key="testing", ' - 'oauth_token="%s", ' - 'oauth_signature_method="PLAINTEXT", ' - 'oauth_signature="%%26%s", ' - 'oauth_timestamp="%d", ' - 'oauth_nonce="%s", ' - 'oauth_version="1.0"' - ) % ( - _oauth_realm, - oauth_token, - oauth_secret, - int(oauth.time.time()), - oauth.generate_nonce(), - ) - - headers = {"Authorization": a, "Content-Type": "application/x-www-form-urlencoded"} - return headers - - -def create_bug(signature, source="", releases=[], hashed=None, lastseen=""): - """Returns a tuple of (bug number, url)""" - - # temporary solution to LP: #1322325 by creating short titles - if signature.startswith("Traceback (most recent call last"): - lines = signature.splitlines() - title = "%s crashed with %s" % (source, lines[-1]) - else: - title = "%s" % signature - details = ( - "details, including versions of packages affected, " - "stacktrace or traceback, and individual crash reports" - ) - - if not hashed: - href = "https://errors.ubuntu.com/bucket/?id=%s" % urllib.parse.quote(signature) - else: - href = "https://errors.ubuntu.com/problem/%s" % hashed - - if source and lastseen: - description = ( - "The Ubuntu Error Tracker has been receiving reports " - "about a problem regarding %s. This problem was " - "most recently seen with package version %s, the " - "problem page at %s contains more %s." % (source, lastseen, href, details) - ) - else: - description = ( - "The Ubuntu Error Tracker has been receiving reports " - "about a problem regarding %s. The problem page at " - "%s contains more %s." % (source, href, details) - ) - description += ( - "\nIf you do not have access to the Ubuntu Error Tracker " - "and are a software developer, you can request it at " - "http://forms.canonical.com/reports/." - ) - - release_codenames = [] - for release in releases: - # release_codenames need to be strings not unicode - codename = get_codename_for_version(release) - if codename: - release_codenames.append("%s" % str(codename)) - else: - # can't use capital letters or spaces in a tag e.g. 'RTM 14.09' - release = release.lower() - release_codenames.append("%s" % str(release).replace(" ", "-")) - # print >>sys.stderr, 'code names:', release_codenames - tags = release_codenames - if source: - target = _source_target + source - operation = _generate_operation(title, description, target, tags) - else: - operation = _generate_operation(title, description, tags) - headers = _generate_headers(config.lp_oauth_token, config.lp_oauth_secret) - - # TODO Record the source packages and Ubuntu releases this crash has been - # seen in, so we can add tasks for each relevant release. - request = urllib.request.Request(_create_bug_url, operation, headers) - # print >>sys.stderr, 'operation:', str(operation) - try: - response = urllib.request.urlopen(request) - except urllib.error.HTTPError as e: - print("Could not create bug:", str(e), file=sys.stderr) - return (None, None) - - response.read() - try: - number = response.headers["Location"].rsplit("/", 1)[1] - if config.lp_use_staging == "True": - return (number, "https://qastaging.launchpad.net/bugs/" + number) - else: - return (number, "https://bugs.launchpad.net/bugs/" + number) - except KeyError: - return (None, None) - - -def _generate_subscription(user): - operation = { - "ws.op": "subscribe", - "level": "Discussion", - "person": _person_url + user, - } - return urllib.parse.urlencode(operation) - - -def subscribe_user(bug, user): - operation = _generate_subscription(user) - headers = _generate_headers(config.lp_oauth_token, config.lp_oauth_secret) - url = "%s/%s" % (_create_bug_url, bug) - request = urllib.request.Request(url, operation, headers) - try: - urllib.request.urlopen(request) - except urllib.error.HTTPError as e: - msg = "Could not subscribe %s to bug %s:" % (user, bug) - print(msg, str(e), e.read(), file=sys.stderr) diff --git a/src/daisy/metrics.py b/src/daisy/metrics.py index 3a4563b..72a0860 100644 --- a/src/daisy/metrics.py +++ b/src/daisy/metrics.py @@ -1,9 +1,3 @@ -from cassandra import ConsistencyLevel -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster - -from daisy import config - METRICS = None @@ -32,23 +26,3 @@ def get_metrics(namespace="daisy"): namespace = "whoopsie-daisy." + namespace METRICS = Metrics(namespace=namespace) return METRICS - - -def cassandra_session(): - auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password - ) - cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) - cassandra_session = cluster.connect(config.cassandra_keyspace) - cassandra_session.default_consistency_level = ConsistencyLevel.LOCAL_ONE - return cassandra_session - - -def record_revno(namespace="daisy"): - import socket - - from daisy.version import version_info - - if "revno" in version_info: - m = "%s.version.daisy" % socket.gethostname() - get_metrics(namespace).gauge(m, version_info["revno"]) diff --git a/src/daisy/schema.py b/src/daisy/schema.py deleted file mode 100644 index 193cc02..0000000 --- a/src/daisy/schema.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright © 2011-2013 Canonical Ltd. -# Author: Evan Dandrea -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero Public License for more details. -# -# You should have received a copy of the GNU Affero Public License -# along with this program. If not, see . - -from pycassa.system_manager import ( - ASCII_TYPE, - FLOAT_TYPE, - INT_TYPE, - LONG_TYPE, - TIME_UUID_TYPE, - UTF8_TYPE, - SystemManager, -) -from pycassa.types import ( - CounterColumnType, - DateType, -) - -from daisy import config -from oopsrepository.cassandra_shim import workaround_1779 - - -def create(): - keyspace = config.cassandra_keyspace - creds = { - "username": config.cassandra_username, - "password": config.cassandra_password, - } - mgr = SystemManager(config.cassandra_hosts[0], credentials=creds) - cfs = list(mgr.get_keyspace_column_families(keyspace).keys()) - try: - if "Indexes" not in cfs: - workaround_1779( - mgr.create_column_family, keyspace, "Indexes", comparator_type=UTF8_TYPE - ) - if "Stacktrace" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "Stacktrace", - comparator_type=UTF8_TYPE, - default_validation_class=UTF8_TYPE, - ) - if "AwaitingRetrace" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "AwaitingRetrace", - key_validation_class=UTF8_TYPE, - comparator_type=UTF8_TYPE, - default_validation_class=UTF8_TYPE, - ) - if "RetraceStats" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "RetraceStats", - comparator_type=UTF8_TYPE, - default_validation_class=CounterColumnType(), - ) - if "UniqueUsers90Days" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "UniqueUsers90Days", - comparator_type=UTF8_TYPE, - key_validation_class=UTF8_TYPE, - default_validation_class=LONG_TYPE, - ) - if "BadRequest" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "BadRequest", - default_validation_class=CounterColumnType(), - ) - if "UserBinaryPackages" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "UserBinaryPackages", - # The key_validation_class is a launchpad team ID, which is - # always ascii. - # The comparator is a binary package name, which is always - # ascii according to Debian policy. - # default_validation_class is bytes as it's always NULL. - key_validation_class=ASCII_TYPE, - comparator_type=ASCII_TYPE, - ) - if "BugToCrashSignatures" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "BugToCrashSignatures", - key_validation_class=INT_TYPE, - comparator_type=UTF8_TYPE, - ) - if "CouldNotBucket" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "CouldNotBucket", - comparator_type=TIME_UUID_TYPE, - ) - if "TimeToRetrace" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "TimeToRetrace", - default_validation_class=FLOAT_TYPE, - ) - if "UniqueSystemsForErrorsByRelease" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "UniqueSystemsForErrorsByRelease", - comparator_type=DateType(), - default_validation_class=LONG_TYPE, - ) - if "SystemImages" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "SystemImages", - key_validation_class=UTF8_TYPE, - comparator_type=UTF8_TYPE, - ) - finally: - mgr.close() - - -if __name__ == "__main__": - create() diff --git a/src/daisy/submit.py b/src/daisy/submit.py index 6b02175..4707d65 100644 --- a/src/daisy/submit.py +++ b/src/daisy/submit.py @@ -1,9 +1,10 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright © 2011-2013 Canonical Ltd. +# Copyright © 2011-2025 Canonical Ltd. # Author: Evan Dandrea # Brian Murray +# Florent 'Skia' Jacquet # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by @@ -19,52 +20,36 @@ import hashlib import logging -import os import socket import time import uuid -from binascii import hexlify, unhexlify import bson from apport import Report from cassandra import WriteTimeout -from cassandra.query import SimpleStatement +from cassandra.cqlengine.query import DoesNotExist -from daisy import config, utils from daisy.metrics import get_metrics -from oopsrepository import config as oopsconfig -from oopsrepository import oopses - -os.environ["OOPS_KEYSPACE"] = config.cassandra_keyspace -oops_config = oopsconfig.get_config() -oops_config["host"] = config.cassandra_hosts -oops_config["username"] = config.cassandra_username -oops_config["password"] = config.cassandra_password +from errortracker import cassandra_schema, oopses, utils metrics = get_metrics("daisy.%s" % socket.gethostname()) -logger = logging.getLogger("gunicorn.error") - -counters_update = None -proposed_counters_update = None +logger = logging.getLogger("daisy") -def update_counters(_session, release, src_package, date, src_version=None): - cql_release = release.replace("'", "''") - cql_src_package = src_package.encode("ascii", errors="backslashreplace") +def update_counters(release, src_package, date, src_version=None): if src_version: - key = "%s:%s:%s" % (cql_release, cql_src_package, src_version) + key = "%s:%s:%s" % (release, src_package, src_version) else: - key = "%s:%s" % (cql_release, cql_src_package) - _session.execute(counters_update, [key, date]) + key = "%s:%s" % (release, src_package) + cassandra_schema.Counters(key=key.encode(), column1=date).update(value=1) -def update_proposed_counters(_session, release, src_package, date, src_version=None): - cql_release = release.replace("'", "''") +def update_proposed_counters(release, src_package, date, src_version=None): if src_version: - key = "%s:%s:%s" % (cql_release, src_package, src_version) + key = "%s:%s:%s" % (release, src_package, src_version) else: - key = "%s:%s" % (cql_release, src_package) - _session.execute(proposed_counters_update, [key, date]) + key = "%s:%s" % (release, src_package) + cassandra_schema.CountersForProposed(key=key.encode(), column1=date).update(value=1) def create_minimal_report_from_bson(data): @@ -72,7 +57,7 @@ def create_minimal_report_from_bson(data): for key in data: # we don't need to add every key to the apport report to be able to # call crash_signature_addresses() or crash_signature() - if key not in ( + if key in ( "ProcMaps", "Stacktrace", "Signal", @@ -88,94 +73,50 @@ def create_minimal_report_from_bson(data): "dmi.bios.version", "OopsText", ): - continue - try: - report[key.encode("UTF-8")] = data[key].encode("UTF-8") - except ValueError: - # apport raises an ValueError if a key is invalid, given that - # the crash has already been written to the OOPS CF, skip the key - # and continue bucketing - metrics.meter("invalid.invalid_key") - msg = "Invalid key (%s) in report" % (key) - logger.info(msg) - continue + try: + report[key] = data[key] + except ValueError: + # apport raises an ValueError if a key is invalid, given that + # the crash has already been written to the OOPS CF, skip the key + # and continue bucketing + metrics.meter("invalid.invalid_key") + logger.info("Invalid key (%s) in report", key) + continue return report -def try_to_repair_sas(data): - """Try to repair the StacktraceAddressSignature, if this is a binary - crash.""" - - if all(x in data for x in ("Stacktrace", "Signal", "ProcMaps")): - if "StacktraceAddressSignature" not in data: - metrics.meter("repair.tried_sas") - report = create_minimal_report_from_bson(data) - sas = report.crash_signature_addresses() - if sas: - data["StacktraceAddressSignature"] = sas - metrics.meter("repair.succeeded_sas") - else: - metrics.meter("repair.failed_sas") - - -def submit(_session, environ, system_token): - # N.B. prepared statements do the hexlify() conversion on their own - global counters_update - if not counters_update: - counters_update = _session.prepare( - 'UPDATE "Counters" SET value = value +1 WHERE key = ? and column1 = ?' - ) - global proposed_counters_update - if not proposed_counters_update: - proposed_counters_update = _session.prepare( - 'UPDATE "CountersForProposed" SET value = value +1 WHERE key = ? and column1 = ?' - ) - try: - data = environ["wsgi.input"].read() - except IOError as e: - if e.message == "request data read error": - # The client disconnected while sending the report. - metrics.meter("invalid.connection_dropped") - return (False, "Connection dropped.") - else: - raise +def submit(request, system_token): + logger.info("Submit handler") + logger.info(f"request: {request}") + data = request.data try: if not bson.is_valid(data): metrics.meter("invalid.invalid_bson") - return (False, "Invalid BSON.") + return "Invalid BSON.", 400 data = bson.BSON(data).decode() except bson.errors.InvalidBSON: metrics.meter("invalid.invalid_bson") - return (False, "Invalid BSON.") + return "Invalid BSON.", 400 except bson.errors.InvalidDocument: metrics.meter("invalid.invalid_bson_doc") - return (False, "Invalid BSON Document.") + return "Invalid BSON Document.", 400 except MemoryError: metrics.meter("invalid.memory_error_bson") - return (False, "Invalid BSON.") - - # Keep a reference to the decoded report data. If we crash, we'll - # potentially attach it to the OOPS report. - environ["wsgi.input.decoded"] = data + return "Invalid BSON.", 400 oops_id = str(uuid.uuid1()) - # In theory one should be able to use map_environ with make_app in - # oops_wsgi to write arbitary data to the OOPS report but I couldn't get - # that to work so cheat and uses HTTP_ which always gets written. - environ["HTTP_Z_CRASH_ID"] = oops_id - day_key = time.strftime("%Y%m%d", time.gmtime()) if "KernelCrash" in data or "VmCore" in data: # We do not process these yet, but we keep track of how many reports # we're receiving to determine when it's worth solving. metrics.meter("unsupported.kernel_crash") - return (False, "Kernel crashes are not handled yet.") + return "Kernel crashes are not handled yet.", 400 if len(data) == 0: metrics.meter("invalid.empty_report") - return (False, "Empty report.") + return "Empty report.", 400 # Write the SHA512 hash of the system UUID in with the report. if system_token: @@ -184,10 +125,8 @@ def submit(_session, environ, system_token): # we want to try and find out which releases are sending reports with # a missing SystemIdentifier try: - whoopsie_version = environ["HTTP_X_WHOOPSIE_VERSION"] - metrics.meter( - "missing.missing_system_token_%s" % whoopsie_version.replace(".", "_") - ) + whoopsie_version = request.headers["X-Whoopsie-Version"] + metrics.meter("missing.missing_system_token_%s" % whoopsie_version.replace(".", "_")) except KeyError: pass metrics.meter("missing.missing_system_token") @@ -195,62 +134,52 @@ def submit(_session, environ, system_token): release = data.get("DistroRelease", "") if release in utils.EOL_RELEASES: metrics.meter("unsupported.eol_%s" % utils.EOL_RELEASES[release]) - return (False, "%s is End of Life" % str(release)) + return f"{release} is End of Life", 400 arch = data.get("Architecture", "") # We cannot retrace without an architecture to do it on if not arch: metrics.meter("missing.missing_arch") if arch == "armel": metrics.meter("unsupported.armel") - return (False, "armel architecture is obsoleted.") + return "armel architecture is obsoleted.", 400 # Check to see if the crash has already been reported date = data.get("Date", "") exec_path = data.get("ExecutablePath", "") proc_status = data.get("ProcStatus", "") if date and exec_path and proc_status and system_token: try: - cql_system_token = "0x" + hexlify(system_token) - results = _session.execute( - SimpleStatement( - 'SELECT column1 FROM "%s" WHERE key = %s LIMIT 1' - % ("SystemOOPSHashes", cql_system_token) - ) + results = list( + cassandra_schema.SystemOOPSHashes.objects.filter(key=system_token.encode()) ) - reported_crash_ids = (row[0] for row in results) - crash_id = "%s:%s:%s" % (date, exec_path, proc_status) - if isinstance(crash_id, str): - crash_id = crash_id.encode("utf-8") - crash_id = hashlib.md5(crash_id).hexdigest() + reported_crash_ids = (row.column1 for row in results) + crash_id = f"{date}:{exec_path}:{proc_status}" + crash_id = hashlib.md5(crash_id.encode()).hexdigest() if crash_id in reported_crash_ids: - return (False, "Crash already reported.") + return "Crash already reported.", 409 try: - whoopsie_version = environ["HTTP_X_WHOOPSIE_VERSION"] + whoopsie_version = request.headers["X-Whoopsie-Version"] metrics.meter( - "invalid.duplicate_report.whoopise_%s" - % whoopsie_version.replace(".", "_") + "invalid.duplicate_report.whoopise_%s" % whoopsie_version.replace(".", "_") ) except KeyError: pass metrics.meter("invalid.duplicate_report") except IndexError: pass - package = data.get("Package", "") + # according to debian policy neither the package or version should have + # utf8 in it but either some archives do not know that or something is + # wonky with apport + package = data.get("Package", "").encode("ascii", errors="replace").decode() + src_package = data.get("SourcePackage", "").encode("ascii", errors="replace").decode() # If the crash report does not have a package then it will not be # retraceable and we should not write it to the OOPS table. It seems that # apport is flagging crashes for submission even though data collection is # incomplete. if not package: logger.info("Crash report did not contain a package.") - return (False, "Incomplete crash report.") + return "Incomplete crash report.", 400 pkg_arch = utils.get_package_architecture(data) - # according to debian policy neither the package or version should have - # utf8 in it but either some archives do not know that or something is - # wonky with apport - src_package = data.get("SourcePackage", "") - src_package = src_package.encode("ascii", errors="replace") - environ["HTTP_Z_SRC_PKG"] = src_package problem_type = data.get("ProblemType", "") - environ["HTTP_Z_PROBLEMTYPE"] = problem_type apport_version = data.get("ApportVersion", "") third_party = False if not utils.retraceable_package(package): @@ -271,7 +200,7 @@ def submit(_session, environ, system_token): # LP: #1316841 bad duplicate signatures if release == "Ubuntu 14.04" and apport_version == "2.14.1-0ubuntu3.1": metrics.meter("missing.missing_suspend_resume_data") - return (False, "Incomplete suspend resume data found in report.") + return "Incomplete suspend resume data found in report.", 400 failure = data.get("Failure", "") if failure == "suspend/resume" and "ProcMaps" in data: # this is not useful as it is from the resuming system @@ -283,10 +212,9 @@ def submit(_session, environ, system_token): # we aren't really writing it to the database. This is a stop gap measure # while database size issues are addressed. if problem_type == "Snap": - return (True, "Crash report successfully submitted.") + return "Crash report successfully submitted.", 200 package, version = utils.split_package_and_version(package) - environ["HTTP_Z_PKG"] = package # src_version is None and is never used, nor should it be. src_package, src_version = utils.split_package_and_version(src_package) fields = utils.get_fields_for_bucket_counters( @@ -297,21 +225,17 @@ def submit(_session, environ, system_token): # phased-updater and only includes official Ubuntu packages and not those # crahses from systems under auto testing. if not third_party and not automated_testing and problem_type == "Crash": - update_counters( - _session, release=release, src_package=src_package, date=day_key - ) + update_counters(release=release, src_package=src_package, date=day_key) if version == "": metrics.meter("missing.missing_package_version") else: update_counters( - _session, release=release, src_package=src_package, src_version=version, date=day_key, ) - try_to_repair_sas(data) # ProcMaps is useful for creating a crash sig, not after that if "Traceback" in data and "ProcMaps" in data: data.pop("ProcMaps") @@ -342,12 +266,9 @@ def submit(_session, environ, system_token): # the phased-updater and only includes official Ubuntu packages and # not those from systems under auto testing. if not third_party and not automated_testing and problem_type == "Crash": - update_proposed_counters( - _session, release=release, src_package=src_package, date=day_key - ) + update_proposed_counters(release=release, src_package=src_package, date=day_key) if version != "": update_proposed_counters( - _session, release=release, src_package=src_package, src_version=version, @@ -359,10 +280,8 @@ def submit(_session, environ, system_token): if utils.blocklisted_device(system_token): # If the device stops appearing in the log file then the offending # crash file may have been removed and it could be unblocklisted. - logger.info( - "Blocklisted device %s disallowed from sending a crash." % system_token - ) - return (False, "Device blocked from sending crash reports.") + logger.info("Blocklisted device %s disallowed from sending a crash." % system_token) + return "Device blocked from sending crash reports.", 401 try: if problem_type == "Snap": @@ -370,7 +289,6 @@ def submit(_session, environ, system_token): else: expire = False oopses.insert_dict( - _session, oops_id, data, system_token, @@ -382,9 +300,7 @@ def submit(_session, environ, system_token): msg = "%s: WriteTimeout with %s keys." % (system_token, len(list(data.keys()))) logger.info(msg) logger.info("%s: The keys are %s" % (system_token, list(data.keys()))) - logger.info( - "%s: The crash has a ProblemType of: %s" % (system_token, problem_type) - ) + logger.info("%s: The crash has a ProblemType of: %s" % (system_token, problem_type)) if "Traceback" in data: logger.info("%s: The crash has a python traceback." % system_token) raise @@ -394,55 +310,39 @@ def submit(_session, environ, system_token): if arch: metrics.meter("success.oopses.%s" % arch) - success, output = bucket(_session, oops_config, oops_id, data, day_key) - return (success, output) + output, code = bucket(oops_id, data, day_key) + return (output, code) -def bucket(_session, oops_config, oops_id, data, day_key): +def bucket(oops_id, data, day_key): """Bucket oops_id. If the report was malformed, return (False, failure_msg) If a core file is to be requested, return (True, 'UUID CORE') If no further action is needed, return (True, 'UUID OOPSID') """ - indexes_select = None - if not indexes_select: - indexes_select = _session.prepare( - 'SELECT value FROM "Indexes" WHERE key = ? and column1 = ? LIMIT 1' - ) - stacktrace_select = None - if not stacktrace_select: - stacktrace_select = _session.prepare( - 'SELECT value FROM "Stacktrace" WHERE key = ? and column1 = ? LIMIT 1' - ) - release = data.get("DistroRelease", "") # Recoverable Problem, Package Install Failure, Suspend Resume crash_signature = data.get("DuplicateSignature", "") if crash_signature: crash_signature = utils.format_crash_signature(crash_signature) - utils.bucket(_session, oops_id, crash_signature, data) + utils.bucket(oops_id, crash_signature, data) metrics.meter("success.duplicate_signature") - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 # Python if "Traceback" in data: report = create_minimal_report_from_bson(data) crash_signature = report.crash_signature() if crash_signature: - hex_oopsid = "0x" + hexlify(oops_id) - cql_crash_sig = crash_signature.replace("'", "''") - _session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, 'DuplicateSignature', '%s')" - % ("OOPS", hex_oopsid, cql_crash_sig) - ) + cassandra_schema.OOPS.create( + key=oops_id.encode(), column1="DuplicateSignature", value=crash_signature ) formatted_crash_sig = utils.format_crash_signature(crash_signature) cql_formatted_crash_sig = formatted_crash_sig.replace("'", "''") - utils.bucket(_session, oops_id, cql_formatted_crash_sig, data) + utils.bucket(oops_id, cql_formatted_crash_sig, data) metrics.meter("success.python_bucketed") - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 # Crashing binary if "StacktraceTop" in data and "Signal" in data: @@ -451,23 +351,12 @@ def bucket(_session, oops_config, oops_id, data, day_key): addr_sig = data.get("StacktraceAddressSignature", None) crash_sig = "" if addr_sig: - cql_addr_sig = addr_sig.replace("'", "''") - cql_addr_sig = cql_addr_sig.encode("ascii", errors="backslashreplace") - # TODO: create a method to set retry = True for specific SASes - # LP: #1505818 try: - key = "crash_signature_for_stacktrace_address_signature" - results = _session.execute(indexes_select, [key, cql_addr_sig]) - cql_crash_sig = [row[0] for row in results][0] - # remove the 0x in the beginning - if cql_crash_sig.startswith("0x"): - crash_sig = unhexlify(cql_crash_sig[2:]) - else: - crash_sig = cql_crash_sig - except IndexError: + crash_sig = cassandra_schema.Indexes.get( + key=b"crash_signature_for_stacktrace_address_signature", column1=addr_sig + ).value.decode() + except DoesNotExist: pass - else: - cql_addr_sig = addr_sig failed_to_retrace = False if crash_sig.startswith("failed:"): failed_to_retrace = True @@ -475,19 +364,17 @@ def bucket(_session, oops_config, oops_id, data, day_key): # successfully retraced report, we need to retry these even though # there is a crash_sig stacktrace = False - if cql_addr_sig: + if addr_sig: try: - stacktraces = _session.execute( - stacktrace_select, [cql_addr_sig, "Stacktrace"] - ) - stacktrace = [stacktrace[0] for stacktrace in stacktraces][0] - threadstacktraces = _session.execute( - stacktrace_select, [cql_addr_sig, "ThreadStacktrace"] - ) - tstacktrace = [tstacktrace[0] for tstacktrace in threadstacktraces][0] + stacktrace = cassandra_schema.Stacktrace.get( + key=addr_sig.encode(), column1="Stacktrace" + ).value + tstacktrace = cassandra_schema.Stacktrace.get( + key=addr_sig.encode(), column1="ThreadStacktrace" + ).value if stacktrace and tstacktrace: stacktrace = True - except IndexError: + except DoesNotExist: metrics.meter("missing.missing_retraced_stacktrace") pass retry = False @@ -500,15 +387,12 @@ def bucket(_session, oops_config, oops_id, data, day_key): # retry amd64 crashes which failed to retrace for LTS and current # development releases, don't do it for every release b/c we have # a limited number of retracers - if arch == "amd64" and release in ("Ubuntu 24.04", "Ubuntu 24.10"): + if arch == "amd64" and release in ("Ubuntu 24.04", "Ubuntu 25.10"): retry = True if crash_sig and not retry: # The crash is a duplicate so we don't need this data. # Stacktrace, and ThreadStacktrace were already not accepted if "ProcMaps" in data: - oops_delete = _session.prepare( - 'DELETE FROM "OOPS" WHERE key = ? AND column1 = ?' - ) unneeded_columns = ( "Disassembly", "ProcMaps", @@ -517,10 +401,10 @@ def bucket(_session, oops_config, oops_id, data, day_key): "StacktraceTop", ) for unneeded_column in unneeded_columns: - _session.execute(oops_delete, [oops_id, unneeded_column]) + cassandra_schema.OOPS.delete(key=oops_id.encode(), column1=unneeded_column) # We have already retraced for this address signature, so this # crash can be immediately bucketed. - utils.bucket(_session, oops_id, crash_sig, data) + utils.bucket(oops_id, crash_sig, data) metrics.meter("success.ready_binary_bucketed") if arch: metrics.meter("success.ready_binary_bucketed.%s" % arch) @@ -528,32 +412,21 @@ def bucket(_session, oops_config, oops_id, data, day_key): # apport requires the following fields to be able to retrace a # crash so do not ask for a CORE file if they don't exist if not release: - return (True, "%s OOPSID" % oops_id) - # do not ask for a core file for crashes using the old version of - # libc6 since they won't be retraceable. LP: #1760207 - if release == "Ubuntu 18.04": - deps = data.get("Dependencies", "") - libc = [d for d in deps.split("\n") if d.startswith("libc6 2.26-0")] - try: - if libc[0].startswith("libc6 2.26-0"): - return (True, "%s OOPSID" % oops_id) - except (KeyError, IndexError): - pass + return f"{oops_id} OOPSID", 200 package = data.get("Package", "") if not package: - return (True, "%s OOPSID" % oops_id) + return f"{oops_id} OOPSID", 200 exec_path = data.get("ExecutablePath", "") if not exec_path: - return (True, "%s OOPSID" % oops_id) + return f"{oops_id} OOPSID", 200 # Are we already waiting for this stacktrace address signature to # be retraced? waiting = False - if cql_addr_sig: + if addr_sig: try: - key = "retracing" - results = _session.execute(indexes_select, [key, cql_addr_sig]) + cassandra_schema.Indexes.get(key="retracing".encode(), column1=addr_sig) waiting = True - except IndexError: + except DoesNotExist: pass if not waiting and utils.retraceable_release(release): @@ -561,13 +434,13 @@ def bucket(_session, oops_config, oops_id, data, day_key): # don't ask for a CORE if not utils.retraceable_package(package): metrics.meter("missing.retraceable_origin") - return (True, "%s OOPSID" % oops_id) + return f"{oops_id} OOPSID", 200 # Don't ask for cores from things like google-chrome-stable # which will appear as "not installed" if installed from a # .deb if "(not installed)" in package: metrics.meter("missing.package_version") - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 # retry SASes that failed to retrace as new dbgsym packages # may be available if crash_sig and retry: @@ -577,12 +450,12 @@ def bucket(_session, oops_config, oops_id, data, day_key): elif crash_sig and not retry: # Do not ask for a core for crashes we don't want to retry metrics.meter("success.not_retry_failure") - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 elif not addr_sig and not retry: # Do not ask for a core for crashes without a SAS as they # are likely corrupt cores. metrics.meter("success.not_retry_no_sas") - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 # We do not have a core file in the queue, so ask for one. Do # not assume we're going to get one, so also add this ID the # the AwaitingRetrace CF queue as well. @@ -601,23 +474,14 @@ def bucket(_session, oops_config, oops_id, data, day_key): if release: metrics.meter("success.asked_for_core.%s" % release) if addr_sig: - _session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES ('%s', '%s', '%s')" - % ("AwaitingRetrace", cql_addr_sig, oops_id, "") - ) - ) + cassandra_schema.AwaitingRetrace.create(key=addr_sig, column1=oops_id, value="") metrics.meter("success.awaiting_binary_bucket") if not output: output = "%s OOPSID" % oops_id - return (True, output) + return output, 200 # Could not bucket - hex_day_key = "0x" + hexlify(day_key) - _session.execute( - SimpleStatement( - 'INSERT INTO "%s" (key, column1, value) VALUES (%s, %s, %s)' - % ("CouldNotBucket", hex_day_key, uuid.UUID(oops_id), "0x") - ) + cassandra_schema.CouldNotBucket.create( + key=day_key.encode(), column1=uuid.UUID(oops_id), value=b"" ) - return (True, "%s OOPSID" % oops_id) + return "%s OOPSID" % oops_id, 200 diff --git a/src/daisy/submit_core.py b/src/daisy/submit_core.py index a6b90d7..87d7932 100644 --- a/src/daisy/submit_core.py +++ b/src/daisy/submit_core.py @@ -17,26 +17,19 @@ # along with this program. If not, see . import logging -import os import random -import shutil import socket -from datetime import datetime +from datetime import datetime, timezone -import amqplib.client_0_8 as amqp +import amqp +from cassandra.cqlengine.query import DoesNotExist -from daisy import config +# from daisy import config from daisy.metrics import get_metrics - -from . import utils +from errortracker import amqp_utils, cassandra_schema, config, swift_utils metrics = get_metrics("daisy.%s" % socket.gethostname()) -logger = logging.getLogger("gunicorn.error") - -oops_select = None -indexes_insert = None -_cached_swift = None -_cached_s3 = None +logger = logging.getLogger("daisy") def write_policy_allow(oops_id, bytes_used, provider_data): @@ -69,194 +62,26 @@ def swift_delete_ignoring_error(swift_cmd, bucket, oops_id): pass -def write_to_swift(environ, fileobj, oops_id, provider_data): +def write_to_swift(fileobj: bytes, oops_id: str): """Write the core file to OpenStack Swift.""" - from subprocess import CalledProcessError, check_call - - swift_cmd = [ - "/usr/bin/swift", - "--os-auth-url", - "%s" % provider_data["os_auth_url"], - "--os-username", - "%s" % provider_data["os_username"], - "--os-password", - "%s" % provider_data["os_password"], - "--os-tenant-name", - "%s" % provider_data["os_tenant_name"], - "--os-region-name", - "%s" % provider_data["os_region_name"], - "--os-project-name", - "%s" % provider_data["os_tenant_name"], - "--auth-version", - "3.0", - ] - bucket = provider_data["bucket"] - swift_post_cmd = swift_cmd + ["post", bucket] - try: - check_call(swift_post_cmd) - except CalledProcessError: - return False try: - coredir = "/tmp/cores-%s" % os.getpid() - if not os.path.exists(coredir): - os.mkdir(coredir) - import tempfile - - with tempfile.NamedTemporaryFile(dir=coredir) as t: - while True: - chunk = fileobj.read(1024 * 1024) - if not chunk: - break - t.write(chunk) - t.flush() - t.seek(0) - t_size = os.path.getsize(t.name) - msg = "%s has a %i byte core file" % (oops_id, t_size) - logger.info(msg) - swift_upload_cmd = swift_cmd + [ - "upload", - "--object-name", + swift_utils.get_swift_client().put_object(config.swift_bucket, oops_id, fileobj) + except Exception as e: + logger.error( + "error when trying to add (%s) to bucket: %s" + % ( oops_id, - bucket, - os.path.join(coredir, t.name), - ] - check_call(swift_upload_cmd) - except CalledProcessError as e: - swift_delete_ignoring_error(swift_cmd, bucket, oops_id) - msg = "CalledProcessError when trying to add (%s) to bucket: %s" % ( - oops_id, - str(e.returncode), + str(e), + ) ) - logger.info(msg) return False - except IOError as e: - swift_delete_ignoring_error(swift_cmd, bucket, oops_id) - msg = "IOError when trying to add (%s) to bucket: %s" % (oops_id, str(e)) - logger.info(msg) - return False - msg = "CORE for (%s) written to bucket" % (oops_id) - logger.info(msg) + logger.info("CORE for (%s) written to bucket" % (oops_id)) return True -def s3_delete_ignoring_error(bucket, oops_id): - from boto.exception import S3ResponseError - - try: - bucket.delete_key(oops_id) - except S3ResponseError: - pass - - -def write_to_s3(fileobj, oops_id, provider_data): - global _cached_s3 - """Write the core file to Amazon S3.""" - from boto.exception import S3ResponseError - from boto.s3.connection import S3Connection - - if not _cached_s3: - _cached_s3 = S3Connection( - aws_access_key_id=provider_data["aws_access_key"], - aws_secret_access_key=provider_data["aws_secret_key"], - host=provider_data["host"], - ) - try: - bucket = _cached_s3.get_bucket(provider_data["bucket"]) - except S3ResponseError: - bucket = _cached_s3.create_bucket(provider_data["bucket"]) - - key = bucket.new_key(oops_id) - try: - key.set_contents_from_stream(fileobj) - except IOError as e: - s3_delete_ignoring_error(bucket, oops_id) - if e.message == "request data read error": - return False - else: - raise - except S3ResponseError: - s3_delete_ignoring_error(bucket, oops_id) - return False - - return True - - -def write_to_local(fileobj, oops_id, provider_data): - """Write the core file to local.""" - - path = os.path.join(provider_data["path"], oops_id) - if provider_data.get("usage_max_mb"): - fs_stats = os.statvfs(provider_data["path"]) - bytes_used = (fs_stats.f_blocks - fs_stats.f_bavail) * fs_stats.f_bsize - if not write_policy_allow(oops_id, bytes_used, provider_data): - return False - copied = False - with open(path, "w") as fp: - try: - shutil.copyfileobj(fileobj, fp, 512) - copied = True - except IOError as e: - if e.message != "request data read error": - raise - - if copied: - os.chmod(path, 0o666) - return True - else: - try: - os.remove(path) - except OSError: - pass - return False - - -def write_to_storage_provider(environ, fileobj, uuid): - # We trade deteriminism for forgetfulness and make up for it by passing the - # decision along with the UUID as part of the queue message. - r = random.randint(1, 100) - provider = None - for key, ranges in config.write_weight_ranges.items(): - if r >= (ranges[0] * 100) and r <= (ranges[1] * 100): - provider = key - provider_data = config.core_storage[key] - break - - written = False - message = uuid - t = provider_data["type"] - if t == "swift": - written = write_to_swift(environ, fileobj, uuid, provider_data) - elif t == "s3": - written = write_to_s3(fileobj, uuid, provider_data) - elif t == "local": - written = write_to_local(fileobj, uuid, provider_data) - - message = "%s:%s" % (message, provider) - - if written: - return message - else: - metrics.meter("storage_write_error") - return None - - def write_to_amqp(message, arch): queue = "retrace_%s" % arch - try: - if config.amqp_username and config.amqp_password: - channel = amqp.Connection( - host=config.amqp_host, - userid=config.amqp_username, - password=config.amqp_password, - ).channel() - else: - channel = amqp.Connection(host=config.amqp_host).channel() - except utils.amqplib_error_types as e: - if utils.is_amqplib_connection_error(e): - # Could not connect - return False - # Unknown error mode : don't hide it. - raise + channel = amqp_utils.get_connection().channel() if not channel: return False try: @@ -264,15 +89,15 @@ def write_to_amqp(message, arch): # We'll use this timestamp to measure how long it takes to process a # retrace, from receiving the core file to writing the data back to # Cassandra. - body = amqp.Message(message, timestamp=datetime.utcnow()) + body = amqp.Message(message, timestamp=int(datetime.now(timezone.utc).timestamp())) # Persistent body.properties["delivery_mode"] = 2 channel.basic_publish(body, exchange="", routing_key=queue) msg = "%s added to %s queue" % (message.split(":")[0], queue) logger.info(msg) queued = True - except utils.amqplib_error_types as e: - if utils.is_amqplib_connection_error(e): + except amqp_utils.amqplib_error_types as e: + if amqp_utils.is_amqplib_connection_error(e): # Could not connect / interrupted connection queued = False # Unknown error mode : don't hide it. @@ -282,17 +107,11 @@ def write_to_amqp(message, arch): return queued -def submit(_session, environ, fileobj, uuid, arch): - global oops_select - if not oops_select: - oops_select = _session.prepare( - 'SELECT value FROM "OOPS" WHERE key = ? AND column1 = ? LIMIT 1' - ) +def submit_core(request, oopsid, arch, system_token): try: # every OOPS will have a SystemIdentifier - sys_id_results = _session.execute(oops_select, [uuid, "SystemIdentifier"]) - _sys_id = [row[0] for row in sys_id_results][0] # noqa: F841 - except IndexError: + _ = cassandra_schema.OOPS.get(key=oopsid.encode(), column1="SystemIdentifier") + except DoesNotExist: # Due to Cassandra's eventual consistency model, we may receive # the core dump before the OOPS has been written to all the # nodes. This is acceptable, as we'll just ask the next user @@ -300,47 +119,41 @@ def submit(_session, environ, fileobj, uuid, arch): msg = "No OOPS found for this core dump." logger.info(msg) metrics.meter("submit_core.no_matching_oops") - return (False, msg) + return msg, 400 # Only accept core files for architectures for which we have retracers, # this also won't write weird things like, (md64 or md64, which happened # in 2021. if arch not in ("amd64", "arm64", "armhf", "i386"): - return (False, "") + return "Unsupported architecture", 400 - message = write_to_storage_provider(environ, fileobj, uuid) + message = write_to_swift(request.data, oopsid) if not message: # If not written to storage then write to log file - msg = "Failure to write OOPS %s to storage provider" % (uuid) + msg = "Failure to write OOPS %s to storage provider" % (oopsid) logger.info(msg) # Return False and whoopsie will not try and upload it again. # However, we'll ask for a core file for a different crash with the # same SAS. - return (False, "") - queued = write_to_amqp(message, arch) + return msg, 500 + + queued = write_to_amqp(f"{oopsid}:swift", arch) if not queued: # If not written to amqp then write to log file msg = "Failure to write to amqp retrace queue %s %s" % (arch, message) logger.info(msg) + metrics.meter("failure.unable_to_queue_retracing_request") try: - addr_sig_results = _session.execute( - oops_select, [uuid, "StacktraceAddressSignature"] - ) - addr_sig = [row[0] for row in addr_sig_results][0] - except IndexError: + addr_sig = cassandra_schema.OOPS.get( + key=oopsid.encode(), column1="StacktraceAddressSignature" + ).value + except DoesNotExist: addr_sig = "" - pass # N.B. a report without an initial StacktraceAddressSignature won't be # written to the retracing index which is correct because there isn't a # way to identify similar ones without a SAS. if addr_sig and queued: - global indexes_insert - if not indexes_insert: - indexes_insert = _session.prepare( - 'INSERT INTO "Indexes" \ - (key, column1, value) VALUES (?, ?, ?)' - ) - _session.execute(indexes_insert, ["retracing", addr_sig, "0x"]) + cassandra_schema.Indexes.create(key=b"retracing", column1=addr_sig, value=b"") - return (True, uuid) + return oopsid, 200 diff --git a/src/daisy/version.py b/src/daisy/version.py deleted file mode 100644 index 03c6d64..0000000 --- a/src/daisy/version.py +++ /dev/null @@ -1,5 +0,0 @@ -version_info = {} -try: - from daisy.version_info import version_info # noqa: F401 -except: # noqa: E722 - pass diff --git a/src/daisy/version_middleware.py b/src/daisy/version_middleware.py deleted file mode 100644 index 4e4f3bf..0000000 --- a/src/daisy/version_middleware.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright © 2011-2013 Canonical Ltd. -# Author: Evan Dandrea -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero Public License as published by -# the Free Software Foundation; version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero Public License for more details. -# -# You should have received a copy of the GNU Affero Public License -# along with this program. If not, see . - -from daisy.version import version_info -from oopsrepository import __version__ - - -class VersionMiddleware(object): - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - def custom_start_response(status, headers, exc_info=None): - if "revno" in version_info: - rev = version_info["revno"] - headers.append(("X-Daisy-Revision-Number", str(rev))) - ver = ".".join(str(component) for component in __version__[0:3]) - headers.append(("X-Oops-Repository-Version", ver)) - return start_response(status, headers, exc_info) - - return self.app(environ, custom_start_response) diff --git a/src/daisy/wsgi.py b/src/daisy/wsgi.py deleted file mode 100644 index c9e6ca2..0000000 --- a/src/daisy/wsgi.py +++ /dev/null @@ -1,129 +0,0 @@ -import errno -import logging -import os -import re -import shutil - -from daisy import metrics, submit, submit_core, utils -from daisy.version import version_info -from daisy.version_middleware import VersionMiddleware - -_session = None -path_filter = re.compile(r"[^a-zA-Z0-9-_]") -logger = logging.getLogger("gunicorn.error") - - -def ok_response(start_response, data=""): - if data: - start_response("200 OK", [("Content-type", "text/plain")]) - else: - start_response("200 OK", []) - return [data] - - -def bad_request_response(start_response, text=""): - start_response("400 Bad Request", []) - return [text] - - -def handle_core_dump(_session, environ, fileobj, components, content_type): - operation = "" - if len(components) >= 4: - # We also accept a system_hash parameter on the end of the URL, but do - # not actually do anything with it. - uuid, operation, arch = components[1:4] - else: - return (False, "Invalid parameters") - - if not operation or operation != "submit-core": - # Unknown operation. - return (False, "Unknown operation") - if content_type != "application/octet-stream": - # No data POSTed. - # 'Incorrect Content-Type.' - return (False, "Incorrect Content-Type") - - uuid = path_filter.sub("", uuid) - arch = path_filter.sub("", arch) - - return submit_core.submit(_session, environ, fileobj, uuid, arch) - - -def app(environ, start_response): - # clean up core files in directories for which there is no pid - # this might be better done by worker_abort (need newer gunicorn) - for d in os.listdir("/tmp/"): - if "cores-" not in d: - continue - pid = int(d.split("-")[1]) - try: - os.kill(pid, 0) - except OSError as error: - # that e-t-daisy-app process is no longer running - if error.errno == errno.ESRCH: - shutil.rmtree("/tmp/cores-%s" % pid, ignore_errors=True) - continue - # there is process running with this pid but its not e-t-daisy-app - with open("/proc/%s/cmdline" % pid, "r") as cmdline: - if "e-t-daisy-app" not in cmdline: - continue - shutil.rmtree("/tmp/cores-%s" % pid, ignore_errors=True) - - global _session - if not _session: - logger.info("running daisy revision: %s" % version_info["revno"]) - _session = metrics.cassandra_session() - - method = environ.get("REQUEST_METHOD", "") - path = environ.get("PATH_INFO", "") - if method == "GET" and path == "/nagios-check": - return ok_response(start_response) - if path == "/oops-please": - if environ.get("REMOTE_ADDR") == "127.0.0.1": - raise Exception("User requested OOPS.") - else: - return bad_request_response(start_response, "Not allowed.") - - components = path.split("/") - length = len(components) - - # There is only one path component with slashes either side. - if (length == 2 and not components[0]) or ( - length == 3 and not components[0] and not components[2] - ): - # An error report submission. - if len(components[1]) == 128: - system_hash = components[1] - else: - system_hash = "" - # We pass a reference to the wsgi environment so we can possibly attach - # the decoded report to an OOPS report if an exception is raised. - response = submit.submit(_session, environ, system_hash) - else: - # A core dump submission. - content_type = environ.get("CONTENT_TYPE", "") - fileobj = environ["wsgi.input"] - response = handle_core_dump( - _session, environ, fileobj, components, content_type - ) - - if response[0]: - return ok_response(start_response, response[1]) - else: - return bad_request_response(start_response, response[1]) - - -try: - import django - - # use a version check so this'll still work with precise - if django.get_version() == "1.8.7": - from django.conf import settings - - settings.configure() - django.setup() -except ImportError: - pass - -metrics.record_revno() -application = utils.wrap_in_oops_wsgi(VersionMiddleware(app)) diff --git a/src/errortracker/__init__.py b/src/errortracker/__init__.py index f102a9c..dd38fc8 100644 --- a/src/errortracker/__init__.py +++ b/src/errortracker/__init__.py @@ -1 +1,4 @@ +# Pre-load config to parse it before runtime +from .config import * # noqa: F403 + __version__ = "0.0.1" diff --git a/src/errortracker/amqp_utils.py b/src/errortracker/amqp_utils.py new file mode 100644 index 0000000..193e78f --- /dev/null +++ b/src/errortracker/amqp_utils.py @@ -0,0 +1,63 @@ +import socket + +import amqp +from amqp import ConnectionError as AMQPConnectionException + +from errortracker import config + +# From oops-amqp +# These exception types always indicate an AMQP connection error/closure. +# However you should catch amqplib_error_types and post-filter with +# is_amqplib_connection_error. +amqplib_connection_errors = (socket.error, AMQPConnectionException) +# A tuple to reduce duplication in different code paths. Lists the types of +# exceptions legitimately raised by amqplib when the AMQP server goes down. +# Not all exceptions *will* be such errors - use is_amqplib_connection_error to +# do a second-stage filter after catching the exception. +amqplib_error_types = amqplib_connection_errors + (IOError,) + +_connection = None + + +# From oops-amqp +def is_amqplib_ioerror(e): + """Returns True if e is an amqplib internal exception.""" + # Raised by amqplib rather than socket.error on ssl issues and short reads. + if type(e) is not IOError: + return False + if e.args == ("Socket error",) or e.args == ("Socket closed",): + return True + return False + + +# From oops-amqp +def is_amqplib_connection_error(e): + """Return True if e was (probably) raised due to a connection issue.""" + return isinstance(e, amqplib_connection_errors) or is_amqplib_ioerror(e) + + +def get_connection(): + global _connection + if _connection: + return _connection + try: + if "username" in config.amqp_creds and "password" in config.amqp_creds: + _connection = amqp.Connection( + host=config.amqp_creds["host"], + userid=config.amqp_creds["username"], + password=config.amqp_creds["password"], + ) + else: + _connection = amqp.Connection(host=config.amqp_creds["host"]) + _connection.connect() + config.logger.info("amqp connected") + return _connection + except amqplib_error_types as e: + if is_amqplib_connection_error(e): + config.logger.warning("amqp connection issue: %s", e) + # Reset the connection singleton so that next attempt retries connecting + _connection = None + # Could not connect + return None + # Unknown error mode : don't hide it. + raise diff --git a/src/errortracker/cassandra.py b/src/errortracker/cassandra.py new file mode 100644 index 0000000..040a4ea --- /dev/null +++ b/src/errortracker/cassandra.py @@ -0,0 +1,79 @@ +import inspect +import os + +from cassandra.auth import PlainTextAuthProvider +from cassandra.cqlengine import connection, management +from cassandra.policies import RoundRobinPolicy + +import errortracker.cassandra_schema +from errortracker import config + +_connected = False +_session = None +KEYSPACE = config.cassandra_creds["keyspace"] +REPLICATION_FACTOR = 3 + + +def setup_cassandra(): + global _connected + if config.cassandra_creds["username"]: + auth_provider = PlainTextAuthProvider( + username=config.cassandra_creds["username"], + password=config.cassandra_creds["password"], + ) + else: + auth_provider = None + if _connected is False: + connection.setup( + config.cassandra_creds["hosts"], + KEYSPACE, + auth_provider=auth_provider, + load_balancing_policy=RoundRobinPolicy(), + protocol_version=4, + ) + _connected = True + sync_schema() + # workaround some weirdness in keyspace handling + connection.get_session().keyspace = KEYSPACE + + +def sync_schema(): + skip_sync = False + results = connection.get_session().execute( + f"SELECT * FROM system_schema.keyspaces WHERE keyspace_name='{KEYSPACE}'" + ) + for row in results: + skip_sync = True + if skip_sync: + config.logger.info("Cassandra keyspace already exists, not syncing schema") + return + config.logger.info("Cassandra keyspace does not exists, syncing schema") + + # cassandra wants this environment variable to be set, otherwise issues a + # warning. Let's please it. + # This is because there might be issues where multiple processes concurently + # try to modify the schema, and this is DANGEROUS for Cassandra. + # TODO: find a way to create the keyspace and sync_schema() only once + # atomically, even in development. + + os.environ["CQLENG_ALLOW_SCHEMA_MANAGEMENT"] = "1" + management.create_keyspace_simple(name=KEYSPACE, replication_factor=REPLICATION_FACTOR) + + def _find_subclasses(module, clazz): + return [ + cls + for name, cls in inspect.getmembers(module) + if inspect.isclass(cls) and issubclass(cls, clazz) and cls is not clazz + ] + + for klass in _find_subclasses( + errortracker.cassandra_schema, errortracker.cassandra_schema.ErrorTrackerTable + ): + management.sync_table(klass) + + +def cassandra_session(): + global _session + if not _session: + _session = connection.get_session() + return _session diff --git a/src/errortracker/cassandra_schema.py b/src/errortracker/cassandra_schema.py new file mode 100644 index 0000000..4a28b86 --- /dev/null +++ b/src/errortracker/cassandra_schema.py @@ -0,0 +1,197 @@ +import struct + +from cassandra.cqlengine import columns, models + +DoesNotExist = models.Model.DoesNotExist + + +class ErrorTrackerTable(models.Model): + # __table_name_case_sensitive__ is deprecated already, but let's keep it in case we run on older machines. + __table_name_case_sensitive__ = True + __abstract__ = True + + +class Counters(ErrorTrackerTable): + __table_name__ = "Counters" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Counter(db_field="value") + + +class CountersForProposed(ErrorTrackerTable): + __table_name__ = "CountersForProposed" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Counter(db_field="value") + + +class Indexes(ErrorTrackerTable): + __table_name__ = "Indexes" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + def get_as_dict(*args, **kwargs) -> dict: + query = Indexes.objects.filter(*args, **kwargs) + d = {} + for result in query: + # XXX: cassandra should be able to deserialize more properly by itself + if result.key == b"mean_retracing_time" and not result.column1.endswith("count"): + d[result.column1] = struct.unpack("!f", result.value)[0] + elif result.key == b"mean_retracing_time" and result.column1.endswith("count"): + d[result.column1] = struct.unpack("!i", result.value)[0] + else: + d[result.column1] = result.value + if not d: + raise Indexes.DoesNotExist + return d + + +class CouldNotBucket(ErrorTrackerTable): + __table_name__ = "CouldNotBucket" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.TimeUUID(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class DayOOPS(ErrorTrackerTable): + __table_name__ = "DayOOPS" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.TimeUUID(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class DayUsers(ErrorTrackerTable): + __table_name__ = "DayUsers" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class UserOOPS(ErrorTrackerTable): + __table_name__ = "UserOOPS" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class OOPS(ErrorTrackerTable): + __table_name__ = "OOPS" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") + + def get_as_dict(*args, **kwargs) -> dict: + query = OOPS.objects.filter(*args, **kwargs) + d = {} + for result in query: + d[result["column1"]] = result["value"] + return d + + +class Stacktrace(ErrorTrackerTable): + __table_name__ = "Stacktrace" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") + + +class SystemOOPSHashes(ErrorTrackerTable): + __table_name__ = "SystemOOPSHashes" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class BucketMetadata(ErrorTrackerTable): + __table_name__ = "BucketMetadata" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") + + def get_as_dict(*args, **kwargs) -> dict: + query = BucketMetadata.objects.filter(*args, **kwargs) + d = {} + for result in query: + d[result["column1"]] = result["value"] + return d + + +class Hashes(ErrorTrackerTable): + __table_name__ = "Hashes" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Blob(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") + + +class RetraceStats(ErrorTrackerTable): + __table_name__ = "RetraceStats" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Counter(db_field="value") + + def get_as_dict(*args, **kwargs) -> dict: + query = RetraceStats.objects.filter(*args, **kwargs) + d = {} + for result in query: + d[result["column1"]] = result["value"] + return d + + +class Bucket(ErrorTrackerTable): + __table_name__ = "Bucket" + key = columns.Text(db_field="key", primary_key=True) + column1 = columns.TimeUUID(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class DayBuckets(ErrorTrackerTable): + __table_name__ = "DayBuckets" + key = columns.Text(db_field="key", primary_key=True) + key2 = columns.Text(db_field="key2", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class DayBucketsCount(ErrorTrackerTable): + __table_name__ = "DayBucketsCount" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Counter(db_field="value") + + +class SourceVersionBuckets(ErrorTrackerTable): + __table_name__ = "SourceVersionBuckets" + key = columns.Ascii(db_field="key", primary_key=True) + key2 = columns.Ascii(db_field="key2", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class BucketVersionSystems2(ErrorTrackerTable): + __table_name__ = "BucketVersionSystems2" + key = columns.Text(db_field="key", primary_key=True) + key2 = columns.Ascii(db_field="key2", primary_key=True) + column1 = columns.Ascii(db_field="column1", primary_key=True) + value = columns.Blob(db_field="value") + + +class BucketRetraceFailureReason(ErrorTrackerTable): + __table_name__ = "BucketRetraceFailureReason" + key = columns.Blob(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") + + def get_as_dict(*args, **kwargs) -> dict: + query = BucketRetraceFailureReason.objects.filter(*args, **kwargs) + d = {} + for result in query: + d[result["column1"]] = result["value"] + return d + + +class AwaitingRetrace(ErrorTrackerTable): + __table_name__ = "AwaitingRetrace" + key = columns.Text(db_field="key", primary_key=True) + column1 = columns.Text(db_field="column1", primary_key=True) + value = columns.Text(db_field="value") diff --git a/src/errortracker/config.py b/src/errortracker/config.py new file mode 100644 index 0000000..bf5666b --- /dev/null +++ b/src/errortracker/config.py @@ -0,0 +1,61 @@ +# Error Tracker settings +import logging +from logging.config import dictConfig + +dictConfig({"version": 1, "disable_existing_loggers": False}) +logger = logging.getLogger("errortracker") + +log_level = logging.INFO + +# TODO: convert that to a TOML/YAML/JSON/else real configuration file + +amqp_creds = { + "host": "127.0.0.1:5672", + "username": "guest", + "password": "guest", +} + +cassandra_creds = { + "keyspace": "crashdb", + # The addresses of the Cassandra database nodes. + "hosts": ["localhost"], + "username": "", + "password": "", + # should be a multiple of cassandra_hosts + "pool_size": 9, + # allow the pool to overflow + "max_overflow": 18, +} + +# Example: +# swift_creds = { +# "os_auth_url": "http://keystone.example.com/", +# "os_username": "ostack", +# "os_password": "secret", +# "os_tenant_name": "ostack_project", +# "os_region_name": "region01", +# "auth_version": "3.0", +# } +# Default value is good for local dev with saio +swift_creds = { + "os_auth_url": "http://127.0.0.1:8080/auth/v1.0", + "os_username": "test:tester", + "os_password": "testing", + "auth_version": "1.0", +} + +# The swift container to store cores +swift_bucket = "cores" + +# Path used to keep some crashes in case of failure, for manual investigation +failure_storage = None + +try: + from local_config import * # noqa: F403 + + logger.info("loaded local settings") +except ImportError: + logger.info("didn't find local settings") + + +logger.setLevel(log_level) diff --git a/src/errortracker/oopses.py b/src/errortracker/oopses.py new file mode 100644 index 0000000..37880cf --- /dev/null +++ b/src/errortracker/oopses.py @@ -0,0 +1,301 @@ +# oops-repository is Copyright 2011 Canonical Ltd. +# +# Canonical Ltd ("Canonical") distributes the oops-repository source code under +# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file +# LICENSE in the source tree for more information. + +"""basic operations on oopses in the db.""" + +import json +import re +import time +import uuid +from hashlib import md5, sha1 + +from cassandra.cqlengine.query import BatchQuery + +from errortracker import cassandra_schema +from errortracker.cassandra import cassandra_session + +DAY = 60 * 60 * 24 +MONTH = DAY * 30 + +_cassandra_session = None + + +def prune(): + """Remove OOPSES that are over 30 days old.""" + # Find days to prune + days = set() + prune_to = time.strftime("%Y%m%d", time.gmtime(time.time() - MONTH)) + for dayoops in cassandra_schema.DayOOPS.objects.distinct(["key"]): + key = dayoops.key.decode() + if key < prune_to: + days.add(key) + if not days: + return + # collect all the oopses (buffers all in memory; may want to make + # incremental in future) + for day in days: + oops_ids = list( + set( + dayoops.value + for dayoops in cassandra_schema.DayOOPS.filter(key=day.encode()).only(["value"]) + ) + ) + with BatchQuery() as b: + cassandra_schema.DayOOPS.objects.batch(b).filter(key=day.encode()).delete() + for id in oops_ids: + cassandra_schema.OOPS.objects.batch(b).filter(key=id).delete() + + +def insert(oopsid, oops_json, user_token=None, fields=None, proposed_pkg=False) -> str: + """Insert an OOPS into the system. + + :return: The day which the oops was filed under. + """ + # make sure the oops report is a json dict, and break out each key to a + # separate column. For now, rather than worrying about typed column values + # we just coerce them all to strings. + oops_dict = json.loads(oops_json) + assert isinstance(oops_dict, dict) + insert_dict = {} + for key, value in list(oops_dict.items()): + insert_dict[key] = str(value) + return _insert(oopsid, insert_dict, user_token, fields, proposed_pkg) + + +def insert_dict( + oopsid, + oops_dict, + user_token=None, + fields=None, + proposed_pkg=False, + ttl=None, +) -> str: + """Insert an OOPS into the system. + + :return: The day which the oops was filed under. + """ + assert isinstance(oops_dict, dict) + return _insert(oopsid, oops_dict, user_token, fields, proposed_pkg, ttl) + + +def _insert( + oopsid, + insert_dict, + user_token=None, + fields=None, + proposed_pkg=False, + ttl=None, +) -> str: + """Internal function. Do not call this directly. + + :param oopsid: The identifier for this OOPS. + :param insert_dict: A dictionary containing the data to associate this OOPS + with. + :param user_token: An identifier for the user who experienced this OOPS. + :param fields: A dictionary containing keys to increment counters for. + :param proposed_pkg: True if the report's Tags contain package-from-proposed + :param ttl: boolean for setting the time to live for the column + :return: The day which the oops was filed under. + """ + day_key = time.strftime("%Y%m%d", time.gmtime()) + now_uuid = uuid.uuid1() + + if ttl: + ttl = 2592000 + + for key, value in list(insert_dict.items()): + # try to avoid an OOPS re column1 being missing + if not key: + continue + cassandra_schema.OOPS.create(key=oopsid.encode(), column1=key, value=value).ttl(ttl) + + automated_testing = False + if user_token and user_token.startswith("deadbeef"): + automated_testing = True + + cassandra_schema.DayOOPS.create(key=day_key.encode(), column1=now_uuid, value=oopsid.encode()) + + # Systems running automated tests should not be included in the OOPS count. + if not automated_testing: + # Provide quick lookups of the total number of oopses for the day by + # maintaining a counter. + cassandra_schema.Counters.filter(key=b"oopses", column1=day_key).update(value=1) + if fields: + for field in fields: + field = field.encode("ascii", errors="replace").decode() + cassandra_schema.Counters.filter( + key=f"oopses:{field}".encode(), column1=day_key + ).update(value=1) + if proposed_pkg: + for field in fields: + field = field.encode("ascii", errors="replace").decode() + cassandra_schema.CountersForProposed.filter( + key=f"oopses:{field}".encode(), column1=day_key + ).update(value=1) + + if user_token: + cassandra_schema.UserOOPS.create(key=user_token.encode(), column1=oopsid, value=b"") + # Build a unique identifier for crash reports to prevent the same + # crash from being reported multiple times. + date = insert_dict.get("Date", "") + exec_path = insert_dict.get("ExecutablePath", "") + proc_status = insert_dict.get("ProcStatus", "") + if date and exec_path and proc_status: + crash_id = f"{date}:{exec_path}:{proc_status}" + crash_id = md5(crash_id.encode()).hexdigest() + cassandra_schema.SystemOOPSHashes.create( + key=user_token.encode(), column1=crash_id, value=b"" + ) + # TODO we can drop this once we're successfully using ErrorsByRelease. + # We'll have to first ensure that all the calculated historical data is + # in UniqueUsers90Days. + cassandra_schema.DayUsers.create(key=day_key.encode(), column1=user_token, value=b"") + if fields: + for field in fields: + field = field.encode("ascii", errors="replace").decode() + field_day = f"{field}:{day_key}" + cassandra_schema.DayUsers.create( + key=field_day.encode(), column1=user_token, value=b"" + ) + + return day_key + + +def bucket(oopsid, bucketid, fields=None, proposed_fields=False): + """Adds an OOPS to a bucket, a collection of OOPSes that form a single + issue. If the bucket does not exist, it will be created. + + :return: The day which the bucket was filed under. + """ + session = cassandra_session() + # Get the timestamp. + try: + results = session.execute( + session.prepare( + f'SELECT WRITETIME (value) FROM {session.keyspace}."OOPS" WHERE key = ? LIMIT 1' + ), + [oopsid.encode()], + ) + timestamp = list(results)[0]["writetime(value)"] + day_key = time.strftime("%Y%m%d", time.gmtime(timestamp / 1000000)) + except IndexError: + # Eventual consistency. This OOPS probably occurred today. + day_key = time.strftime("%Y%m%d", time.gmtime()) + + cassandra_schema.Bucket.create(key=bucketid, column1=uuid.UUID(oopsid), value=b"") + cassandra_schema.DayBuckets.create(key=day_key, key2=bucketid, column1=oopsid, value=b"") + + if fields is not None: + resolutions = (day_key[:4], day_key[:6], day_key) + # All buckets for the given resolution. + for field in fields: + for resolution in resolutions: + # We have no way of knowing whether an increment has been + # performed if the write fails unexpectedly (CASSANDRA-2495). + # We will apply eventual consistency to this problem and + # tolerate slightly inaccurate counts for the span of a single + # day, cleaning up once this period has passed. This will be + # done by counting the number of columns in DayBuckets for the + # day and bucket ID. + field_resolution = ":".join((field, resolution)) + cassandra_schema.DayBucketsCount( + key=field_resolution.encode(), column1=bucketid + ).update(value=1) + for resolution in resolutions: + cassandra_schema.DayBucketsCount(key=resolution.encode(), column1=bucketid).update( + value=1 + ) + return day_key + + +def update_bucket_metadata(bucketid, source, version, comparator, release=""): + # We only update the first and last seen version fields. We do not update + # the current version field as talking to Launchpad is an expensive + # operation, and we can do that out of band. + metadata = {} + release_re = re.compile(r"^Ubuntu \d\d.\d\d$") + + bucketmetadata = cassandra_schema.BucketMetadata.get_as_dict(key=bucketid.encode()) + # TODO: Drop the FirstSeen and LastSeen fields once BucketVersionsCount + # is deployed, since we can just do a get(column_count=1) for the first + # seen version and get(column_reversed=True, column_count=1) for the + # last seen version. + # N.B.: This presumes that we are using the DpkgComparator which we + # won't be when we move to DSE. + firstseen = bucketmetadata.get("FirstSeen", None) + if firstseen and comparator(firstseen, version) < 0: + metadata["FirstSeen"] = firstseen + else: + metadata["FirstSeen"] = version + firstseen_release = bucketmetadata.get("FirstSeenRelease", None) + # Some funny releases were already written to FirstSeenRelease, + # see LP: #1805912, ensure they are overwritten. + if firstseen_release and not release_re.match(firstseen_release): + firstseen_release = None + if not firstseen_release or (release.split()[-1] < firstseen_release.split()[-1]): + metadata["FirstSeenRelease"] = release + lastseen = bucketmetadata.get("LastSeen", None) + if lastseen and comparator(lastseen, version) > 0: + metadata["LastSeen"] = lastseen + else: + metadata["LastSeen"] = version + lastseen_release = bucketmetadata.get("LastSeenRelease", None) + # Some funny releases were already written to LastSeenRelease, + # see LP: #1805912, ensure they are overwritten. + if lastseen_release and not release_re.match(lastseen_release): + lastseen_release = None + if not lastseen_release or (lastseen_release.split()[-1] < release.split()[-1]): + metadata["LastSeenRelease"] = release + + if release: + k = "~%s:FirstSeen" % release + firstseen = bucketmetadata.get(k, None) + if firstseen and comparator(firstseen, version) < 0: + metadata[k] = firstseen + else: + metadata[k] = version + k = "~%s:LastSeen" % release + lastseen = bucketmetadata.get(k, None) + if lastseen and comparator(lastseen, version) > 0: + metadata[k] = lastseen + else: + metadata[k] = version + + if metadata: + metadata["Source"] = source + for k, v in metadata.items(): + cassandra_schema.BucketMetadata.create(key=bucketid.encode(), column1=k, value=v) + + +def update_bucket_systems(bucketid, system, version=None): + """Keep track of the unique systems in a bucket with a specific version of + software.""" + if not system or not version: + return + if not version: + return + cassandra_schema.BucketVersionSystems2.create( + key=bucketid, key2=version, column1=system, value=b"" + ) + + +def update_source_version_buckets(source, version, bucketid): + # according to debian policy neither the package or version should have + # utf8 in it but either some archives do not know that or something is + # wonky with apport + source = source.encode("ascii", errors="replace").decode() + version = version.encode("ascii", errors="replace").decode() + cassandra_schema.SourceVersionBuckets.create( + key=source, key2=version, column1=bucketid, value=b"" + ) + + +def update_bucket_hashes(bucketid): + """Keep a mapping of SHA1 hashes to the buckets they represent. + These hashes will be used for shorter bucket URLs.""" + bucket_sha1 = sha1(bucketid.encode()).hexdigest() + k = "bucket_%s" % bucket_sha1[0] + cassandra_schema.Hashes.create(key=k.encode(), column1=bucket_sha1.encode(), value=bucketid) diff --git a/src/errortracker/swift_utils.py b/src/errortracker/swift_utils.py new file mode 100644 index 0000000..9c9ab80 --- /dev/null +++ b/src/errortracker/swift_utils.py @@ -0,0 +1,27 @@ +import swiftclient + +from errortracker import config + +_client = None + + +def get_swift_client(): + global _client + if _client: + return _client + + opts = {} + for key in ["os_region_name", "os_tenant_name"]: + if key in config.swift_creds: + opts[key] = config.swift_creds[key] + + _client = swiftclient.client.Connection( + config.swift_creds["os_auth_url"], + config.swift_creds["os_username"], + config.swift_creds["os_password"], + os_options=opts, + auth_version=config.swift_creds["auth_version"], + ) + _client.put_container(config.swift_bucket) + config.logger.info("swift connected and container '%s' exists", config.swift_bucket) + return _client diff --git a/src/daisy/utils.py b/src/errortracker/utils.py similarity index 56% rename from src/daisy/utils.py rename to src/errortracker/utils.py index 13dcd91..426fe92 100644 --- a/src/daisy/utils.py +++ b/src/errortracker/utils.py @@ -1,23 +1,9 @@ import logging import re -import socket -import uuid import apt -from amqp import ConnectionError as AMQPConnectionException -from oopsrepository import oopses - -# From oops-amqp -# These exception types always indicate an AMQP connection error/closure. -# However you should catch amqplib_error_types and post-filter with -# is_amqplib_connection_error. -amqplib_connection_errors = (socket.error, AMQPConnectionException) -# A tuple to reduce duplication in different code paths. Lists the types of -# exceptions legitimately raised by amqplib when the AMQP server goes down. -# Not all exceptions *will* be such errors - use is_amqplib_connection_error to -# do a second-stage filter after catching the exception. -amqplib_error_types = amqplib_connection_errors + (IOError,) +from errortracker import oopses EOL_RELEASES = { "Ubuntu 10.04": "lucid", @@ -47,6 +33,7 @@ "Ubuntu 22.10": "kinetic", "Ubuntu 23.04": "lunar", "Ubuntu 23.10": "mantic", + "Ubuntu 24.10": "oracular", } @@ -100,11 +87,6 @@ def split_package_and_version(package): if version == "(not": # The version is set to '(not installed)' version = "" - # according to debian policy neither the package or version should have - # utf8 in it but either some archives do not know that or something is - # wonky with apport - package = package.encode("ascii", errors="replace").decode() - version = version.encode("ascii", errors="replace").decode() return (package, version) @@ -137,22 +119,13 @@ def format_crash_signature(crash_signature) -> str: return crash_signature -def bucket(oops_config, oops_id, crash_signature, report_dict): +def bucket(oops_id, crash_signature, report_dict): release = report_dict.get("DistroRelease", "") package = report_dict.get("Package", "") src_package = report_dict.get("SourcePackage", "") problem_type = report_dict.get("ProblemType", "") - dependencies = report_dict.get("Dependencies", "") system_uuid = report_dict.get("SystemIdentifier", "") - if "[origin:" in package or "[origin:" in dependencies: - # This package came from a third-party source. We do not want to show - # its version as the Last Seen field on the most common problems table, - # so skip updating the bucket metadata. - third_party = True - else: - third_party = False - version = None if package: package, version = split_package_and_version(package) @@ -165,71 +138,27 @@ def bucket(oops_config, oops_id, crash_signature, report_dict): if automated_testing: fields = None else: - fields = get_fields_for_bucket_counters( - problem_type, release, package, version, pkg_arch - ) + fields = get_fields_for_bucket_counters(problem_type, release, package, version, pkg_arch) if version: - oopses.update_bucket_systems( - oops_config, crash_signature, system_uuid, version=version - ) + oopses.update_bucket_systems(crash_signature, system_uuid, version=version) # DayBucketsCount is only added to if fields is not None, so set fields to # None for crashes from systems running automated tests. - oopses.bucket(oops_config, oops_id, crash_signature, fields) + oopses.bucket(oops_id, crash_signature, fields) - if hasattr(oopses, "update_bucket_hashes"): - oopses.update_bucket_hashes(oops_config, crash_signature) + oopses.update_bucket_hashes(crash_signature) # BucketMetadata is displayed on the main page and shouldn't include # derivative or custom releases, so don't write them to the table. release_re = re.compile(r"^Ubuntu \d\d.\d\d$") - if (package and version) and release_re.match(release): + if (src_package and package and version) and release_re.match(release): oopses.update_bucket_metadata( - oops_config, crash_signature, package, version, apt.apt_pkg.version_compare, release, ) - if hasattr(oopses, "update_source_version_buckets"): - oopses.update_source_version_buckets( - oops_config, src_package, version, crash_signature - ) - if version and release: - oopses.update_bucket_versions( - oops_config, crash_signature, version, release=release, oopsid=oops_id - ) - - if hasattr(oopses, "update_errors_by_release"): - if (system_uuid and release) and not third_party: - oops_uuid = uuid.UUID(oops_id) - oopses.update_errors_by_release( - oops_config, oops_uuid, system_uuid, release - ) - - -def attach_error_report(report, context): - # We only attach error report that was submitted by the client if we've hit - # a MaximumRetryException from Cassandra. - if "type" in report and report["type"] == "MaximumRetryException": - env = context["wsgi_environ"] - if "wsgi.input.decoded" in env: - data = env["wsgi.input.decoded"] - if "req_vars" not in report: - report["req_vars"] = {} - report["req_vars"]["wsgi.input.decoded"] = data - - -def wrap_in_oops_wsgi(wsgi_handler): - import oops_dictconfig - from oops_wsgi import install_hooks, make_app - - from daisy import config - - cfg = oops_dictconfig.config_from_dict(config.oops_config) - cfg.template["reporter"] = "daisy" - install_hooks(cfg) - return make_app(wsgi_handler, cfg, oops_on_status=["500"]) + oopses.update_source_version_buckets(src_package, version, crash_signature) def retraceable_release(release): @@ -264,31 +193,7 @@ def blocklisted_device(system_token): Used for devices that have repeatedly failed to submit a crash. """ - blocklist = [ - # 20150814 - OOPS count was at 43 - "2f175cea621bda810f267f1da46409a111f58011435f410aa198362e9372da78b6fafe6827ff26e025a5ab7d2859346de6b188f0622118c15a119c58ca538acb", - # 20150826 - OOPS count was at 18 - "81b75a0bdd531a5c02a4455b05674ea45fbb65324bcae5fe51659bce850aa40bcd1388e3eed4d46ce9abb4e56d1dd7dde45ded473995feb0ac2c01518a841efe", - # 20150903 - OOPS count was at 27 - "b5329547bdab8adea4245399ff9656ca608e825425fbb0ad2c68e182b75ce80c13f9186e4e9b8e7a17dd15dd196b12a65e1b7f513184296320dad50c587754f5", - ] + blocklist = [] if system_token in blocklist: return True return False - - -# From oops-amqp -def is_amqplib_ioerror(e): - """Returns True if e is an amqplib internal exception.""" - # Raised by amqplib rather than socket.error on ssl issues and short reads. - if type(e) is not IOError: - return False - if e.args == ("Socket error",) or e.args == ("Socket closed",): - return True - return False - - -# From oops-amqp -def is_amqplib_connection_error(e): - """Return True if e was (probably) raised due to a connection issue.""" - return isinstance(e, amqplib_connection_errors) or is_amqplib_ioerror(e) diff --git a/src/oopsrepository/DESIGN.txt b/src/oopsrepository/DESIGN.txt deleted file mode 100644 index f4bfb12..0000000 --- a/src/oopsrepository/DESIGN.txt +++ /dev/null @@ -1,79 +0,0 @@ -============================ -OOPS Repository design notes -============================ - -Design goals -============ - -OOPS Repository is intended to scale up to 1 million OOPS reports a day (and -possibly further). This is based on a 1% soft failre rate needing collection. - -It needs to supports an extensible model, aggregation, automated garbage -collection, emitting messages for trend and fault detection systems and finally -realtime insertion and display of individual OOPSes. - -Components -========== - -Cassandra ---------- - -Cassandra was chosen because of the drop-dead simple method for increasing -write and read bandwidth available in the system. - -OOPS Model -========== - -An OOPS is an abstract server fault report. An OOPS has a mandatory identifier -assigned by the creator of the OOPS. An OOPS also has a json collection of -attributes, all of which are stored in the repostory, and some of which are -treated specially by front ends and reports. See the Schema for the attributes -known to the repository. While OOPSes are indexed by time in the repository, -and may have a datestamp, the system always indexes by the time the OOPS is -received rather than when it [may] have been generated. This is to simplify -the requirements for clients (they don't need to generate a datestamp or have -syncronised clocks). - -The OOPS ID must be unique - one suggested way to generate them is to hash the -json of the fault report. - -Schema -====== - -OOPS : Individual OOPSes are in this column family. - row key : the oops ID supplied by the inserter - mandatory columns: - 'date': LONG Used to build an index for garbage collection. - optional known columns (all strings): - 'bug.*': Maps to bugs. - 'HTTP.*': HTTP variables. e.g. HTTP.method is PUT/POST/GET etc. - 'REQUEST.*': arbitrary request variables. - 'context': The context for the fault report. E.g. a page template, - particular API call - that sort of thing. - 'exception': The class of the exception causing the fault. - 'URL': The URL of the request. - 'username': the username. - 'userid': A database id for the user. - 'branch': Source code branch for the server. - 'revision': Revision of the server. - 'duration': The duration of the request in ms. - 'timeline': A json sequence describing the actions taken during the - request. This may be split out to a separate CF in future. For now - an example would be [{"start":"0", "length": "34", "database": "main", - "statment":"SELECT ...", "callstack": "...."}, {....} ] - -Summaries : various aggregates, kept up to date during inserts to permit live queries. - row key : period - currently a iso8601 day - e.g. '20110227' - columns: 'duration' | 'statement count' | 'volume.*' - For 'duration', the value is the N longest oopses inserted during the day. Writers write at Q and readback to ensure its consistent. [[duration, id], ...] - For statement count, likewise [[count, id]]. - For volume., each column contains the occurences for one aggregate: - frequency.context:exception -> count - Once the period is over, the volumes are replaced with a single top-N list. - -DayOOPS : day -> oops mappping - row key : period - currently a iso8601 day - e.g. '20110227' - columns : TimeUUID -> OOPS ID, [future, suggested by mdennis: minuteinday, - with a range index] - - diff --git a/src/oopsrepository/LICENSE b/src/oopsrepository/LICENSE deleted file mode 100644 index 7e147e5..0000000 --- a/src/oopsrepository/LICENSE +++ /dev/null @@ -1,676 +0,0 @@ -oops-repository is Copyright 2011 Canonical Ltd. - -Canonical Ltd ("Canonical") distributes the oops-repository source code -under the GNU Affero General Public License, version 3 ("AGPLv3"). -The full text of this licence is given below. - -Third-party copyright in this distribution is noted where applicable. - -All rights not expressly granted are reserved. - -========================================================================= - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - (http://www.gnu.org/licenses/agpl.html) - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. - -========================================================================= diff --git a/src/oopsrepository/README b/src/oopsrepository/README deleted file mode 100644 index bf168ce..0000000 --- a/src/oopsrepository/README +++ /dev/null @@ -1,73 +0,0 @@ -**2024-12-05** -I've now vendored this as part of the new error-tracker repository here: -https://github.com/ubuntu/error-tracker - -This is in an effort to make all this stack work again on a Noble machine. As of -now, this is still running on Bionic, has never seen a Python 3 interpreter, and -nobody has been maintaining that for like at least 5 years: -https://code.launchpad.net/~daisy-pluckers/+recipe/oops-repository-daily -last modified on 2019-07-10 - -I'll do my best to put that up to modern standards. - --- Skia out - ------ OLD README STARTS HERE ----- -========================== -README for oops-repository -========================== - - -OOPS repository is a storage and reporting system for the server fault reports -- 'OOPSes' that Launchpad and other systems at Canonical use. - -OOPS repository is maintained by the Launchpad - team @ Canonical . -The project uses Python 2.6 as its main development language, Cassandra for -scalable storage. - -Runtime Dependencies -==================== - -* Cassandra (0.7) -* python-pycassa -* python-thrift 0.5 (for pycassa) -* Python - -Build Dependencies -================== - -* python-fixtures -* python-testtools -* testrepository - -Home page, code etc -=================== - -https://launchpad.net/oops-repository has the project bug tracker, source code, -FAQs etc. - -The code can be branch using bzr:: - - $ bzr branch lp:oops-repository - -Getting started -=============== - -Install cassanda. Choose a keyspace for oopsrepository and export OOPS_KEYSPACE -with that value. For instance:: - - $ export OOPS_KEYSPACE=oopses - -Create the schema:: - - $ python -m oopsrepository.schema - -Code structure -============== - -Tests are in oopsrepository/tests. -Test helpers are in oopsrepository/testing. -Actual code is in oopsrepository. - -Enjoy! diff --git a/src/oopsrepository/__init__.py b/src/oopsrepository/__init__.py deleted file mode 100644 index 1fb1039..0000000 --- a/src/oopsrepository/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -__version__ = (0, 0, 0, "dev", 0) diff --git a/src/oopsrepository/cassandra_shim.py b/src/oopsrepository/cassandra_shim.py deleted file mode 100644 index b4b8a0d..0000000 --- a/src/oopsrepository/cassandra_shim.py +++ /dev/null @@ -1,31 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""Things to ease working with cassandra.""" - -from pycassa.cassandra.ttypes import InvalidRequestException - - -def workaround_1779(callable, *args, **kwargs): - """Workaround cassandra not being able to do concurrent schema edits. - - The callable is tried until it does not raised InvalidRequestException - with why = "Previous version mismatch. cannot apply." - - :param callable: The callable to call. - :param args: The args for it. - :param kwargs: The kwargs for it. - :return: The result of calling the callable. - """ - while True: - # Workaround https://issues.apache.org/jira/browse/CASSANDRA-1779: - # Cassandra cannot do concurrent schema changes. - try: - return callable(*args, **kwargs) - break - except InvalidRequestException as e: - if e.why != "Previous version mismatch. cannot apply.": - raise diff --git a/src/oopsrepository/config.py b/src/oopsrepository/config.py deleted file mode 100644 index f208b0c..0000000 --- a/src/oopsrepository/config.py +++ /dev/null @@ -1,22 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""The config for oopsrepository.""" - -import os - - -def get_config(): - """Get a dict of the config variables controlling oopsrepository.""" - result = dict( - keyspace=os.environ.get("OOPS_KEYSPACE"), - host=[os.environ.get("OOPS_HOST", "localhost")], - username=os.environ.get("OOPS_USERNAME", ""), - password=os.environ.get("OOPS_PASSWORD", ""), - ) - if not result["keyspace"]: - raise Exception("No keyspace set - set via OOPS_KEYSPACE") - return result diff --git a/src/oopsrepository/oopses.py b/src/oopsrepository/oopses.py deleted file mode 100644 index e40bcaf..0000000 --- a/src/oopsrepository/oopses.py +++ /dev/null @@ -1,515 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""basic operations on oopses in the db.""" - -import datetime -import json -import re -import time -import uuid -from binascii import hexlify -from hashlib import md5, sha1 - -from cassandra import ConsistencyLevel -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster -from cassandra.protocol import InvalidRequestException -from cassandra.query import SimpleStatement - -DAY = 60 * 60 * 24 -MONTH = DAY * 30 - -_cassandra_session = None - - -def cassandra_session(config): - """Caching constructor of cassandra connection""" - global _cassandra_session - if _cassandra_session: - return _cassandra_session - auth_provider = PlainTextAuthProvider( - username=config["username"], password=config["password"] - ) - cluster = Cluster(config["host"], auth_provider=auth_provider) - _cassandra_session = cluster.connect(config["keyspace"]) - _cassandra_session.default_consistency_level = ConsistencyLevel.LOCAL_ONE - return _cassandra_session - - -def insert(config, oopsid, oops_json, user_token=None, fields=None, proposed_pkg=False): - """Insert an OOPS into the system. - - :return: The day which the oops was filed under. - """ - # make sure the oops report is a json dict, and break out each key to a - # separate column. For now, rather than worrying about typed column values - # we just coerce them all to strings. - oops_dict = json.loads(oops_json) - assert isinstance(oops_dict, dict) - insert_dict = {} - for key, value in list(oops_dict.items()): - insert_dict[key] = json.dumps(value) - return _insert(config, oopsid, insert_dict, user_token, fields, proposed_pkg) - - -def insert_dict( - session, - oopsid, - oops_dict, - user_token=None, - fields=None, - proposed_pkg=False, - ttl=False, -): - """Insert an OOPS into the system. - - :return: The day which the oops was filed under. - """ - assert isinstance(oops_dict, dict) - return _insert(session, oopsid, oops_dict, user_token, fields, proposed_pkg, ttl) - - -def _insert( - session, - oopsid, - insert_dict, - user_token=None, - fields=None, - proposed_pkg=False, - ttl=False, -): - """Internal function. Do not call this directly. - - :param oopsid: The identifier for this OOPS. - :param insert_dict: A dictionary containing the data to associate this OOPS - with. - :param user_token: An identifier for the user who experienced this OOPS. - :param fields: A dictionary containing keys to increment counters for. - :param proposed_pkg: True if the report's Tags contain package-from-proposed - :param ttl: boolean for setting the time to live for the column - :return: The day which the oops was filed under. - """ - if isinstance(session, dict): - session = cassandra_session(session) - day_key = time.strftime("%Y%m%d", time.gmtime()) - hex_day_key = "0x" + hexlify(day_key.encode()) - now_uuid = uuid.uuid1() - - hex_oopsid = "0x" + hexlify(oopsid.encode()) - for key, value in list(insert_dict.items()): - # try to avoid an OOPS re column1 being missing - if not key: - continue - cql_key = key.replace("'", "''") - cql_value = value.replace("'", "''") - cql_query = ( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, '%s', '%s')" - % ("OOPS", hex_oopsid, cql_key, cql_value) - ) - # delete the column after 30 days - if ttl: - cql_query += " USING TTL 2592000" - try: - session.execute(SimpleStatement(cql_query)) - except InvalidRequestException: - continue - automated_testing = False - if user_token.startswith("deadbeef"): - automated_testing = True - - session.execute( - SimpleStatement( - 'INSERT INTO "%s" (key, column1, value) VALUES (%s, %s, %s)' - % ("DayOOPS", hex_day_key, now_uuid, hex_oopsid) - ) - ) - # Systems running automated tests should not be included in the OOPS count. - if not automated_testing: - # Provide quick lookups of the total number of oopses for the day by - # maintaining a counter. - hex_oopses = "0x" + hexlify(b"oopses") - session.execute( - SimpleStatement( - "UPDATE \"%s\" SET value = value + 1 WHERE key = %s AND column1 ='%s'" - % ("Counters", hex_oopses, day_key) - ) - ) - if fields: - for field in fields: - field = field.encode("ascii", errors="replace") - cql_field = field.replace("'", "''") - hex_oopses_field = "0x" + hexlify(("oopses:%s" % cql_field).encode()) - session.execute( - SimpleStatement( - "UPDATE \"%s\" SET value = value + 1 WHERE key = %s AND column1 ='%s'" - % ("Counters", hex_oopses_field, day_key) - ) - ) - if proposed_pkg: - for field in fields: - field = field.encode("ascii", errors="replace") - cql_field = field.replace("'", "''") - hex_oopses_field = "0x" + hexlify(("oopses:%s" % cql_field).encode()) - session.execute( - SimpleStatement( - "UPDATE \"%s\" SET value = value + 1 WHERE key = %s AND column1 ='%s'" - % ("CountersForProposed", hex_oopses_field, day_key) - ) - ) - - if user_token: - hex_user_token = "0x" + hexlify(user_token.encode()) - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, '%s', %s)" - % ("UserOOPS", hex_user_token, oopsid, "0x") - ) - ) - # Build a unique identifier for crash reports to prevent the same - # crash from being reported multiple times. - date = insert_dict.get("Date", "") - exec_path = insert_dict.get("ExecutablePath", "") - proc_status = insert_dict.get("ProcStatus", "") - if date and exec_path and proc_status: - crash_id = "%s:%s:%s" % (date, exec_path, proc_status) - if isinstance(crash_id, str): - crash_id = crash_id.encode("utf-8") - crash_id = md5(crash_id).hexdigest() - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, '%s', %s)" - % ("SystemOOPSHashes", hex_user_token, crash_id, "0x") - ) - ) - # TODO we can drop this once we're successfully using ErrorsByRelease. - # We'll have to first ensure that all the calculated historical data is - # in UniqueUsers90Days. - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, '%s', %s)" - % ("DayUsers", hex_day_key, user_token, "0x") - ) - ) - if fields: - for field in fields: - field = field.encode("ascii", errors="replace") - cql_field = field.replace("'", "''") - hex_field_day = b"0x" + hexlify(("%s:%s" % (field, day_key)).encode()) - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES (%s, '%s', %s)" - % ("DayUsers", hex_field_day, user_token, "0x") - ) - ) - - return day_key - - -def bucket(session, oopsid, bucketid, fields=None, proposed_fields=False): - """Adds an OOPS to a bucket, a collection of OOPSes that form a single - issue. If the bucket does not exist, it will be created. - - :return: The day which the bucket was filed under. - """ - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - cql_bucketid = bucketid.replace("'", "''") - # Get the timestamp. - try: - results = session.execute( - session.prepare('SELECT WRITETIME (value) FROM "OOPS" WHERE key = ?'), - [oopsid.encode()], - ) - timestamp = [r[0] for r in results][0] - day_key = time.strftime("%Y%m%d", time.gmtime(timestamp / 1000000)) - except IndexError: - # Eventual consistency. This OOPS probably occurred today. - day_key = time.strftime("%Y%m%d", time.gmtime()) - - session.execute( - session.prepare( - 'INSERT INTO "Bucket" \ - (key, column1, value) VALUES (?, ?, ?)' - ), - [cql_bucketid, uuid.UUID(oopsid), b""], - ) - session.execute( - session.prepare( - 'INSERT INTO "DayBuckets" \ - (key, key2, column1, value) VALUES (?, ?, ?, ?)' - ), - [day_key, cql_bucketid, oopsid, b""], - ) - - if fields is not None: - resolutions = (day_key[:4], day_key[:6], day_key) - # All buckets for the given resolution. - dbc_update = session.prepare( - 'UPDATE "DayBucketsCount" SET value = value + 1 WHERE key = ? and column1 = ?' - ) - for field in fields: - for resolution in resolutions: - # We have no way of knowing whether an increment has been - # performed if the write fails unexpectedly (CASSANDRA-2495). - # We will apply eventual consistency to this problem and - # tolerate slightly inaccurate counts for the span of a single - # day, cleaning up once this period has passed. This will be - # done by counting the number of columns in DayBuckets for the - # day and bucket ID. - field_resolution = ":".join((field, resolution)) - session.execute(dbc_update, [field_resolution.encode(), cql_bucketid]) - for resolution in resolutions: - session.execute(dbc_update, [resolution.encode(), cql_bucketid]) - return day_key - - -def update_bucket_versions(session, bucketid, version, release=None, oopsid=None): - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - if release: - # Use the current day, rather than the day of the OOPS because this is - # specifically used for cleaning up counters nightly. If a very old - # OOPS gets processed here, we should still clean it up when we're - # handling the data for today. - day_key = time.strftime("%Y%m%d", time.gmtime()) - cql_bucketid = bucketid.replace("'", "''") - - uuid_oopsid = uuid.UUID(oopsid) - release = release.replace("'", "''") - cql_release = release.encode("ascii", errors="ignore").decode() - - if version: - version = version.encode("ascii", errors="replace").decode() - - # When correcting the counts in bv_count, we'll iterate - # BucketVersionsDay for day_key. For each of these columns, we'll look - # up the correct value by calling bv_full.get_count(...). - session.execute( - session.prepare( - 'INSERT INTO "BucketVersionsFull" (key, key2, key3, column1, value) VALUES (?, ?, ?, ?, ?)' - ), - [ - cql_bucketid, - cql_release, - version, - uuid_oopsid, - b"", - ], - ) - session.execute( - session.prepare( - 'INSERT INTO "BucketVersionsDay" (key, column1, column2, column3, value) VALUES (?, ?, ?, ?, ?)' - ), - [ - day_key.encode(), - cql_bucketid, - cql_release, - version, - b"", - ], - ) - session.execute( - session.prepare( - 'UPDATE "BucketVersionsCount" SET value = value + 1 WHERE key = ? AND column1 = ? AND column2 = ?' - ), - [cql_bucketid, cql_release, version], - ) - else: - session.execute( - session.prepare( - 'UPDATE "BucketVersions" SET value = value + 1 WHERE key = ? AND column1 = ?' - ), - [cql_bucketid.encode(), version], - ) - - -def update_errors_by_release(session, oops_id, system_token, release): - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - cql_release = release.replace("'", "''") - today = datetime.datetime.today() - today = today.replace(hour=0, minute=0, second=0, microsecond=0) - - results = session.execute( - SimpleStatement( - "SELECT value FROM \"%s\" WHERE key = '%s' and column1 = '%s'" - % ("FirstError", cql_release, system_token) - ) - ) - try: - first_error_date = [row[0] for row in results][0] - except IndexError: - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, column1, value) VALUES ('%s', '%s', '%s')" - % ("FirstError", cql_release, system_token, today) - ) - ) - first_error_date = today - - # We use the OOPS ID rather than the system identifier here because we want - # each crash from a system to take up a new column in this column family. - # Each one of those columns should be associated with the date of the first - # error for the system in this release. - # - # Remember, we're ultimately tracking errors here, not systems, but we need - # the system identifier to know the first occurrence of an error in the - # release for that machine. - # - # For the given release for today, the crash should be weighted by the - # first time an error occurred in the release for the system this came - # from. Multiplied by their weight and summed together, these form the - # numerator of our average errors per calendar day calculation. - - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, key2, column1, value) VALUES ('%s', '%s', %s, '%s')" - % ("ErrorsByRelease", cql_release, today, oops_id, first_error_date) - ) - ) - session.execute( - SimpleStatement( - "INSERT INTO \"%s\" (key, key2, column1, value) VALUES ('%s', '%s', '%s', %s)" - % ("SystemsForErrorsByRelease", cql_release, today, system_token, "0x") - ) - ) - - -def update_bucket_metadata(session, bucketid, source, version, comparator, release=""): - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - # We only update the first and last seen version fields. We do not update - # the current version field as talking to Launchpad is an expensive - # operation, and we can do that out of band. - metadata = {} - bucketmetadata = {} - release_re = re.compile(r"^Ubuntu \d\d.\d\d$") - cql_bucketid = bucketid.replace("'", "''") - - bucketmetadata_rows = session.execute( - session.prepare('SELECT column1, value FROM "BucketMetadata" WHERE key = ?'), - [bucketid.encode()], - ) - for row in bucketmetadata_rows: - bucketmetadata[row[0]] = row[1] - try: - # TODO: Drop the FirstSeen and LastSeen fields once BucketVersionsCount - # is deployed, since we can just do a get(column_count=1) for the first - # seen version and get(column_reversed=True, column_count=1) for the - # last seen version. - # N.B.: This presumes that we are using the DpkgComparator which we - # won't be when we move to DSE. - lastseen = bucketmetadata["LastSeen"] - if not lastseen or comparator(lastseen, version) < 0: - metadata["LastSeen"] = version - lastseen_release = bucketmetadata["LastSeenRelease"] - # Some funny releases were already written to LastSeenRelease, - # see LP: #1805912, ensure they are overwritten. - if lastseen_release and not release_re.match(lastseen_release): - lastseen_release = None - if not lastseen_release or (lastseen_release.split()[-1] < release.split()[-1]): - metadata["LastSeenRelease"] = release - firstseen = bucketmetadata["FirstSeen"] - if not firstseen or comparator(firstseen, version) > 0: - metadata["FirstSeen"] = version - firstseen_release = bucketmetadata["FirstSeenRelease"] - # Some funny releases were already written to FirstSeenRelease, - # see LP: #1805912, ensure they are overwritten. - if firstseen_release and not release_re.match(firstseen_release): - firstseen_release = None - if not firstseen_release or ( - release.split()[-1] < firstseen_release.split()[-1] - ): - metadata["FirstSeenRelease"] = release - except KeyError: - metadata["FirstSeen"] = version - metadata["LastSeen"] = version - if release: - metadata["FirstSeenRelease"] = release - metadata["LastSeenRelease"] = release - - if release: - k = "~%s:FirstSeen" % release - firstseen = metadata.get(k, None) - if not firstseen or comparator(firstseen, version) > 0: - metadata[k] = version - k = "~%s:LastSeen" % release - lastseen = metadata.get(k, None) - if not lastseen or comparator(lastseen, version) < 0: - metadata[k] = version - - if metadata: - metadata["Source"] = source - bmd_insert = session.prepare( - 'INSERT INTO "BucketMetadata" \ - (key, column1, value) VALUES (?, ?, ?)' - ) - for k in metadata: - # a prepared statement seems to convert into hex so using hexlify - # with bucketid is not needed - session.execute(bmd_insert, [cql_bucketid.encode(), k, metadata[k]]) - - -def update_bucket_systems(session, bucketid, system, version=None): - """Keep track of the unique systems in a bucket with a specific version of - software.""" - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - if not system or not version: - return - if not version: - return - cql_bucketid = bucketid.replace("'", "''") - session.execute( - session.prepare( - 'INSERT INTO "BucketVersionSystems2" \ - (key, key2, column1, value) VALUES (?, ?, ?, ?)' - ), - [cql_bucketid, version, system, b""], - ) - - -def update_source_version_buckets(session, source, version, bucketid): - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - cql_bucketid = bucketid.replace("'", "''") - # according to debian policy neither the package or version should have - # utf8 in it but either some archives do not know that or something is - # wonky with apport - source = source.encode("ascii", errors="replace").decode() - version = version.encode("ascii", errors="replace").decode() - session.execute( - session.prepare( - 'INSERT INTO "SourceVersionBuckets" \ - (key, key2, column1, value) VALUES (?, ?, ?, ?)' - ), - [source, version, cql_bucketid, b""], - ) - - -def update_bucket_hashes(session, bucketid): - """Keep a mapping of SHA1 hashes to the buckets they represent. - These hashes will be used for shorter bucket URLs.""" - # retracer.py hasn't been updated to pass in a python-cassandra session - if isinstance(session, dict): - session = cassandra_session(session) - cql_bucketid = bucketid.replace("'", "''") - bucket_sha1 = sha1(bucketid.encode()).hexdigest() - k = "bucket_%s" % bucket_sha1[0] - session.execute( - session.prepare( - 'INSERT INTO "Hashes" \ - (key, column1, value) VALUES (?, ?, ?)' - ), - [k.encode(), bucket_sha1.encode(), cql_bucketid], - ) diff --git a/src/oopsrepository/schema.py b/src/oopsrepository/schema.py deleted file mode 100644 index 8432021..0000000 --- a/src/oopsrepository/schema.py +++ /dev/null @@ -1,229 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""The schema for oopsrepository.""" - -from pycassa.system_manager import ( - ASCII_TYPE, - TIME_UUID_TYPE, - UTF8_TYPE, - SystemManager, -) -from pycassa.types import ( - AsciiType, - CompositeType, - CounterColumnType, - DateType, - UTF8Type, -) - -from oopsrepository.cassandra_shim import workaround_1779 - -from . import config - - -def create(config): - """Create a schema. - - See DESIGN.txt for the schema description. - - :param config: The config (per oopsrepository.config.get_config) for - oopsrepository. - """ - keyspace = config["keyspace"] - creds = {"username": config["username"], "password": config["password"]} - mgr = SystemManager(config["host"][0], credentials=creds) - cfs = list(mgr.get_keyspace_column_families(keyspace).keys()) - try: - if "OOPS" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "OOPS", - comparator_type=UTF8_TYPE, - default_validation_class=UTF8_TYPE, - ) - if "DayOOPS" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "DayOOPS", - comparator_type=TIME_UUID_TYPE, - ) - if "UserOOPS" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "UserOOPS", - comparator_type=UTF8_TYPE, - ) - if "SystemOOPSHashes" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "SystemOOPSHashes", - comparator_type=UTF8_TYPE, - ) - if "DayUsers" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "DayUsers", - comparator_type=UTF8_TYPE, - ) - if "Bucket" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "Bucket", - comparator_type=TIME_UUID_TYPE, - key_validation_class=UTF8_TYPE, - ) - # TODO It might be more performant to use just the date for the key and - # a composite key of the bucket_id and the oops_id as the column name. - composite = CompositeType(UTF8Type(), UTF8Type()) - if "DayBuckets" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "DayBuckets", - comparator_type=UTF8_TYPE, - key_validation_class=composite, - ) - if "DayBucketsCount" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "DayBucketsCount", - comparator_type=UTF8_TYPE, - default_validation_class=CounterColumnType(), - ) - if "BucketMetadata" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketMetadata", - comparator_type=UTF8_TYPE, - default_validation_class=UTF8_TYPE, - ) - if "BucketVersions" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketVersions", - comparator_type=UTF8_TYPE, - default_validation_class=CounterColumnType(), - ) - if "BucketVersionsCount" not in cfs: - composite = CompositeType(AsciiType(), AsciiType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketVersionsCount", - key_validation_class=UTF8_TYPE, - comparator_type=composite, - default_validation_class=CounterColumnType(), - ) - if "BucketVersionsFull" not in cfs: - composite = CompositeType(UTF8Type(), AsciiType(), AsciiType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketVersionsFull", - key_validation_class=composite, - comparator_type=TIME_UUID_TYPE, - ) - if "BucketVersionsDay" not in cfs: - composite = CompositeType(UTF8Type(), AsciiType(), AsciiType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketVersionsDay", - comparator_type=composite, - ) - if "BucketVersionSystems2" not in cfs: - composite = CompositeType(UTF8Type(), AsciiType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketVersionSystems2", - key_validation_class=composite, - comparator_type=AsciiType(), - ) - if "BucketRetraceFailureReason" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "BucketRetraceFailureReason", - comparator_type=UTF8_TYPE, - default_validation_class=UTF8_TYPE, - ) - if "Counters" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "Counters", - comparator_type=UTF8_TYPE, - default_validation_class=CounterColumnType(), - ) - if "CountersForProposed" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "CountersForProposed", - comparator_type=UTF8_TYPE, - default_validation_class=CounterColumnType(), - ) - if "FirstError" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "FirstError", - key_validation_class=ASCII_TYPE, - comparator_type=ASCII_TYPE, - default_validation_class=DateType(), - ) - if "ErrorsByRelease" not in cfs: - composite = CompositeType(AsciiType(), DateType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "ErrorsByRelease", - default_validation_class=DateType(), - key_validation_class=composite, - comparator_type=TIME_UUID_TYPE, - ) - if "SourceVersionBuckets" not in cfs: - composite = CompositeType(AsciiType(), AsciiType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "SourceVersionBuckets", - key_validation_class=composite, - comparator_type=UTF8_TYPE, - ) - if "Hashes" not in cfs: - workaround_1779( - mgr.create_column_family, - keyspace, - "Hashes", - default_validation_class=UTF8_TYPE, - ) - if "SystemsForErrorsByRelease" not in cfs: - composite = CompositeType(AsciiType(), DateType()) - workaround_1779( - mgr.create_column_family, - keyspace, - "SystemsForErrorsByRelease", - key_validation_class=composite, - comparator_type=UTF8_TYPE, - ) - finally: - mgr.close() - - -if __name__ == "__main__": - create(config.get_config()) diff --git a/src/oopsrepository/testing/__init__.py b/src/oopsrepository/testing/__init__.py deleted file mode 100644 index fe50c94..0000000 --- a/src/oopsrepository/testing/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""Test support for oopsrepository.""" diff --git a/src/oopsrepository/testing/cassandra.py b/src/oopsrepository/testing/cassandra.py deleted file mode 100644 index 455b8bd..0000000 --- a/src/oopsrepository/testing/cassandra.py +++ /dev/null @@ -1,54 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""Test helpers for working with cassandra.""" - -import os -import os.path - -import pycassa -from fixtures import Fixture, TempDir -from pycassa.system_manager import SystemManager - -from oopsrepository.cassandra_shim import workaround_1779 -from oopsrepository.config import get_config -from oopsrepository.schema import create - - -class TemporaryKeyspace(Fixture): - """Create a temporary keyspace. - - The keyspace is named after a tempdir. - """ - - def setUp(self): - super(TemporaryKeyspace, self).setUp() - tempdir = self.useFixture(TempDir()) - self.keyspace = os.path.basename(tempdir.path) - os.environ["OOPS_KEYSPACE"] = self.keyspace - c = get_config() - creds = {"username": c["username"], "password": c["password"]} - self.mgr = SystemManager(c["host"][0], credentials=creds) - workaround_1779( - self.mgr.create_keyspace, - self.keyspace, - pycassa.SIMPLE_STRATEGY, - {"replication_factor": "1"}, - ) - self.addCleanup(workaround_1779, self.mgr.drop_keyspace, self.keyspace) - - -class TemporaryOOPSDB(Fixture): - """Create a temporary usable OOPS DB. - - The keyspace for it is at self.keyspace. - """ - - def setUp(self): - super(TemporaryOOPSDB, self).setUp() - self.keyspace = self.useFixture(TemporaryKeyspace()).keyspace - os.environ["OOPS_KEYSPACE"] = self.keyspace - create(get_config()) diff --git a/src/oopsrepository/testing/matchers.py b/src/oopsrepository/testing/matchers.py deleted file mode 100644 index b513bea..0000000 --- a/src/oopsrepository/testing/matchers.py +++ /dev/null @@ -1,50 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -"""Various oopsrepository specific matchers.""" - -import json -import os -import time -import uuid - -import pycassa -from pycassa.cassandra.ttypes import NotFoundException -from testtools.matchers import Matcher, Mismatch - -from oopsrepository import config - - -class HasOOPSSchema(Matcher): - """Matches if a keyspace has a usable OOPS schema. - - This will write to the keyspace. - """ - - def match(self, keyspace): - os.environ["OOPS_KEYSPACE"] = keyspace - c = config.get_config() - pool = pycassa.ConnectionPool( - c["keyspace"], c["host"], username=c["username"], password=c["password"] - ) - try: - cf = pycassa.ColumnFamily(pool, "OOPS") - cf.insert("key", {"date": json.dumps(time.time()), "URL": "a bit boring"}) - cf = pycassa.ColumnFamily(pool, "DayOOPS") - cf.insert("20100212", {uuid.uuid1(): "key"}) - cf = pycassa.ColumnFamily(pool, "UserOOPS") - cf.insert("user-token", {"key": ""}) - - cf = pycassa.ColumnFamily(pool, "Bucket") - cf.insert( - "/bin/bash:11:x86_64:[vdso]+70c:...", {pycassa.util.uuid.uuid1(): ""} - ) - cf = pycassa.ColumnFamily(pool, "DayBuckets") - cf.insert(("20100212", "/bin/bash:11:x86_64:[vdso]+70c:..."), {"key": ""}) - cf = pycassa.ColumnFamily(pool, "DayBucketsCount") - cf.add("20100212", "/bin/bash:11:x86_64:[vdso]+70c:...", 13) - except NotFoundException as e: - return Mismatch(e.why) diff --git a/src/oopsrepository/tests/__init__.py b/src/oopsrepository/tests/__init__.py deleted file mode 100644 index 46f0020..0000000 --- a/src/oopsrepository/tests/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -from unittest import TestLoader - - -def test_suite(): - test_names = [ - "cassandra_fixture", - "config", - "matchers", - "oopses", - "schema", - ] - tests = ["oopsrepository.tests.test_" + test for test in test_names] - loader = TestLoader() - return loader.loadTestsFromNames(tests) diff --git a/src/oopsrepository/tests/test_cassandra_fixture.py b/src/oopsrepository/tests/test_cassandra_fixture.py deleted file mode 100644 index b24d7dd..0000000 --- a/src/oopsrepository/tests/test_cassandra_fixture.py +++ /dev/null @@ -1,38 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -import os - -from pycassa.system_manager import SystemManager -from testtools import TestCase - -from oopsrepository import config -from oopsrepository.testing.cassandra import TemporaryKeyspace, TemporaryOOPSDB -from oopsrepository.testing.matchers import HasOOPSSchema - - -class TestTemporaryKeyspace(TestCase): - - def test_manages_keyspace(self): - fixture = TemporaryKeyspace() - with fixture: - os.environ["OOPS_KEYSPACE"] = fixture.keyspace - c = config.get_config() - creds = {"username": c["username"], "password": c["password"]} - mgr = SystemManager(c["host"][0], credentials=creds) - keyspace = fixture.keyspace - # The keyspace should be accessible. - self.assertTrue(keyspace in mgr.list_keyspaces()) - # And deleted after the fixture is finished with. - self.assertFalse(keyspace in mgr.list_keyspaces()) - - -class TestTemporaryOOPSDB(TestCase): - - def test_usable(self): - with TemporaryOOPSDB() as db: - keyspace = db.keyspace - self.assertThat(keyspace, HasOOPSSchema()) diff --git a/src/oopsrepository/tests/test_config.py b/src/oopsrepository/tests/test_config.py deleted file mode 100644 index d1e8e68..0000000 --- a/src/oopsrepository/tests/test_config.py +++ /dev/null @@ -1,21 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -from fixtures import EnvironmentVariableFixture -from testtools import TestCase - -from oopsrepository import config - - -class TestConfig(TestCase): - - def test_environmentvariables_setting(self): - with EnvironmentVariableFixture("OOPS_KEYSPACE", "foo"): - self.assertEqual("foo", config.get_config()["keyspace"]) - - def test_unset_variables_raise(self): - with EnvironmentVariableFixture("OOPS_KEYSPACE"): - self.assertRaises(Exception, config.get_config) diff --git a/src/oopsrepository/tests/test_matchers.py b/src/oopsrepository/tests/test_matchers.py deleted file mode 100644 index 487e09b..0000000 --- a/src/oopsrepository/tests/test_matchers.py +++ /dev/null @@ -1,23 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -import os - -from testtools import TestCase - -from oopsrepository import config, schema -from oopsrepository.testing.cassandra import TemporaryKeyspace -from oopsrepository.testing.matchers import HasOOPSSchema - - -class TestHasOOPSSchema(TestCase): - - def test_creates_columnfamily(self): - keyspace = self.useFixture(TemporaryKeyspace()).keyspace - self.assertNotEqual(None, HasOOPSSchema().match(keyspace)) - os.environ["OOPS_KEYSPACE"] = keyspace - schema.create(config.get_config()) - self.assertThat(keyspace, HasOOPSSchema()) diff --git a/src/oopsrepository/tests/test_oopses.py b/src/oopsrepository/tests/test_oopses.py deleted file mode 100644 index a6ac788..0000000 --- a/src/oopsrepository/tests/test_oopses.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8; -*- -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - -import datetime -import json -import os -import time -import uuid - -import pycassa -from testtools import TestCase - -from oopsrepository import config, oopses -from oopsrepository.testing.cassandra import TemporaryOOPSDB - - -class ClearCache(TestCase): - def setUp(self): - super(ClearCache, self).setUp() - # oopsrepository.oopses has a cache of the connection pool, which we - # need to clear to prevent previous test runs from bleeding through to - # the next. - oopses._connection_pool = None - - keyspace = self.useFixture(TemporaryOOPSDB()).keyspace - os.environ["OOPS_KEYSPACE"] = keyspace - self.config = config.get_config() - self.pool = pycassa.ConnectionPool( - self.config["keyspace"], - self.config["host"], - username=self.config["username"], - password=self.config["password"], - ) - - -class TestPrune(ClearCache): - - def test_fresh_oops_kept(self): - day_key = oopses.insert( - self.config, "key", json.dumps({"date": time.time(), "URL": "a bit boring"}) - ) - dayoopses_cf = pycassa.ColumnFamily(self.pool, "DayOOPS") - oopses_cf = pycassa.ColumnFamily(self.pool, "OOPS") - oopses.prune(self.config) - # The oops is still readable and the day hasn't been purged. - oopses_cf.get("key") - dayoopses_cf.get(day_key) - - def test_old_oops_deleted(self): - dayoopses_cf = pycassa.ColumnFamily(self.pool, "DayOOPS") - datestamp = time.time() - oopses.MONTH - oopses.DAY - day_key = time.strftime("%Y%m%d", time.gmtime(datestamp)) - now_uuid = uuid.uuid1() - dayoopses_cf.insert(day_key, {now_uuid: "key"}) - oopses_cf = pycassa.ColumnFamily(self.pool, "OOPS") - oopses_cf.insert("key", {"date": json.dumps(datestamp), "URL": "a bit boring"}) - oopses.prune(self.config) - self.assertRaises(pycassa.NotFoundException, oopses_cf.get, "key") - # The day index is cleared out too. - self.assertRaises(pycassa.NotFoundException, dayoopses_cf.get, day_key) - - -class TestInsert(ClearCache): - - def _test_insert_check(self, oopsid, day_key, value=None): - oopses_cf = pycassa.ColumnFamily(self.pool, "OOPS") - if value is None: - value = "13000" - # The oops is retrievable - columns = oopses_cf.get(oopsid) - self.assertEqual(value, columns["duration"]) - # The oops has been indexed by day - dayoops_cf = pycassa.ColumnFamily(self.pool, "DayOOPS") - oops_refs = dayoops_cf.get(day_key) - self.assertEqual([oopsid], list(oops_refs.values())) - # TODO - the aggregates for the OOPS have been updated. - - def test_insert_oops(self): - oopsid = str(uuid.uuid1()) - oops = json.dumps({"duration": 13000}) - day_key = oopses.insert(self.config, oopsid, oops) - self._test_insert_check(oopsid, day_key) - - def test_insert_oops_dict(self): - oopsid = str(uuid.uuid1()) - oops = {"duration": "13000"} - day_key = oopses.insert_dict(self.config, oopsid, oops) - self._test_insert_check(oopsid, day_key) - - def test_insert_unicode(self): - oopsid = str(uuid.uuid1()) - oops = {"duration": "♥"} - day_key = oopses.insert_dict(self.config, oopsid, oops) - self._test_insert_check(oopsid, day_key, value="♥") - - def test_insert_updates_counters(self): - counters_cf = pycassa.ColumnFamily(self.pool, "Counters") - oopsid = str(uuid.uuid1()) - oops = {"duration": "13000"} - user_token = "user1" - - day_key = oopses.insert_dict(self.config, oopsid, oops, user_token) - oops_count = counters_cf.get("oopses", [day_key]) - self.assertEqual([1], list(oops_count.values())) - - oopsid = str(uuid.uuid1()) - day_key = oopses.insert_dict(config, oopsid, oops, user_token) - oops_count = counters_cf.get("oopses", [day_key]) - self.assertEqual([2], list(oops_count.values())) - - -class TestBucket(ClearCache): - - def test_insert_bucket(self): - fields = [ - "Ubuntu 12.04", - "Ubuntu 12.04:whoopsie", - "Ubuntu 12.04:whoopsie:3.04", - "whoopsie:3.04", - ] - oopsid = str(uuid.uuid1()) - oops = json.dumps({"duration": 13000}) - oopses.insert(self.config, oopsid, oops) - day_key = oopses.bucket(self.config, oopsid, "bucket-key", fields) - - bucket_cf = pycassa.ColumnFamily(self.pool, "Bucket") - daybucketcount_cf = pycassa.ColumnFamily(self.pool, "DayBucketsCount") - - oops_refs = bucket_cf.get("bucket-key") - self.assertEqual([pycassa.util.uuid.UUID(oopsid)], list(oops_refs.keys())) - self.assertEqual( - list(daybucketcount_cf.get(day_key, ["bucket-key"]).values()), [1] - ) - - oopsid = str(uuid.uuid1()) - oops = json.dumps({"wibbles": 12}) - oopses.insert(self.config, oopsid, oops) - day_key = oopses.bucket(self.config, oopsid, "bucket-key", fields) - - # Check that the counters all exist and have two crashes. - resolutions = (day_key[:4], day_key[:6], day_key) - for field in fields: - for resolution in resolutions: - k = "%s:%s" % (field, resolution) - self.assertEqual( - list(daybucketcount_cf.get(k, ["bucket-key"]).values()), [2] - ) - for resolution in resolutions: - self.assertEqual( - list(daybucketcount_cf.get(resolution, ["bucket-key"]).values()), [2] - ) - - def test_update_bucket_metadata(self): - import apt - - bucketmetadata_cf = pycassa.ColumnFamily(self.pool, "BucketMetadata") - # Does not exist yet. - oopses.update_bucket_metadata( - self.config, - "bucket-id", - "whoopsie", - "1.2.3", - apt.apt_pkg.version_compare, - "Ubuntu 12.04", - ) - metadata = bucketmetadata_cf.get("bucket-id") - self.assertEqual(metadata["Source"], "whoopsie") - self.assertEqual(metadata["FirstSeen"], "1.2.3") - self.assertEqual(metadata["LastSeen"], "1.2.3") - self.assertEqual(metadata["~Ubuntu 12.04:FirstSeen"], "1.2.3") - self.assertEqual(metadata["~Ubuntu 12.04:LastSeen"], "1.2.3") - - oopses.update_bucket_metadata( - config, - "bucket-id", - "whoopsie", - "1.2.4", - apt.apt_pkg.version_compare, - "Ubuntu 12.04", - ) - metadata = bucketmetadata_cf.get("bucket-id") - self.assertEqual(metadata["Source"], "whoopsie") - self.assertEqual(metadata["FirstSeen"], "1.2.3") - self.assertEqual(metadata["LastSeen"], "1.2.4") - self.assertEqual(metadata["~Ubuntu 12.04:FirstSeen"], "1.2.3") - self.assertEqual(metadata["~Ubuntu 12.04:LastSeen"], "1.2.4") - - # Earlier version than the newest - oopses.update_bucket_metadata( - config, - "bucket-id", - "whoopsie", - "1.2.4~ev1", - apt.apt_pkg.version_compare, - "Ubuntu 12.04", - ) - metadata = bucketmetadata_cf.get("bucket-id") - self.assertEqual(metadata["Source"], "whoopsie") - self.assertEqual(metadata["FirstSeen"], "1.2.3") - self.assertEqual(metadata["LastSeen"], "1.2.4") - self.assertEqual(metadata["~Ubuntu 12.04:FirstSeen"], "1.2.3") - self.assertEqual(metadata["~Ubuntu 12.04:LastSeen"], "1.2.4") - - # Earlier version than the earliest - oopses.update_bucket_metadata( - config, - "bucket-id", - "whoopsie", - "1.2.2", - apt.apt_pkg.version_compare, - "Ubuntu 12.04", - ) - metadata = bucketmetadata_cf.get("bucket-id") - self.assertEqual(metadata["Source"], "whoopsie") - self.assertEqual(metadata["FirstSeen"], "1.2.2") - self.assertEqual(metadata["LastSeen"], "1.2.4") - self.assertEqual(metadata["~Ubuntu 12.04:FirstSeen"], "1.2.2") - self.assertEqual(metadata["~Ubuntu 12.04:LastSeen"], "1.2.4") - - def test_bucket_hashes(self): - # Test hashing - from hashlib import sha1 - - hashes_cf = pycassa.ColumnFamily(self.pool, "Hashes") - h = sha1("bucket-id").hexdigest() - oopses.update_bucket_hashes(self.config, "bucket-id") - v = list(hashes_cf.get("bucket_%s" % h[0], columns=[h]).values())[0] - self.assertEqual(v, "bucket-id") - - def test_update_bucket_versions(self): - bucketversions_cf = pycassa.ColumnFamily(self.pool, "BucketVersions") - oopses.update_bucket_versions(self.config, "bucket-id", "1.2.3") - self.assertEqual(bucketversions_cf.get("bucket-id")["1.2.3"], 1) - - bv_full = pycassa.ColumnFamily(self.pool, "BucketVersionsFull") - bv_count = pycassa.ColumnFamily(self.pool, "BucketVersionsCount") - u = uuid.uuid1() - args = (self.config, "bucket-id", "1.2.3", "Ubuntu 12.04", str(u)) - oopses.update_bucket_versions(*args) - d = list(bv_full.get(("bucket-id", "Ubuntu 12.04", "1.2.3")).items())[0] - self.assertEqual((u, ""), d) - c = bv_count.get("bucket-id", columns=[("Ubuntu 12.04", "1.2.3")]) - c = list(c.values())[0] - self.assertEqual(1, c) - - def test_dpkg_comparator(self): - bv_count = pycassa.ColumnFamily(self.pool, "BucketVersionsCount") - bv_count.add("bucket-id", ("release", "1.0")) - bv_count.get("bucket-id", columns=[("release", "1.0")]) - bv_count.get("bucket-id", column_start=[("release", "1.0")]) - bv_count.get("bucket-id", column_finish=[("release", "1.0")]) - bv_count.add("bucket-id", ("release", "1.0~ev1")) - self.assertEqual( - 1, bv_count.get("bucket-id", column_count=1)[("release", "1.0~ev1")] - ) - bv_count.add("bucket-id", ("release", "1.0+ev1")) - c = [("release", "1.0+")] - self.assertEqual( - 1, bv_count.get("bucket-id", column_start=c)[("release", "1.0+ev1")] - ) - - def test_update_errors_by_release(self): - firsterror = pycassa.ColumnFamily(self.pool, "FirstError") - errorsbyrelease = pycassa.ColumnFamily(self.pool, "ErrorsByRelease") - release = "Ubuntu 12.04" - system_token = "system-id" - oops_id = uuid.uuid1() - today = datetime.datetime.today() - today = today.replace(hour=0, minute=0, second=0, microsecond=0) - oopses.update_errors_by_release(self.config, oops_id, system_token, release) - - d = firsterror.get(release, columns=[system_token])[system_token] - self.assertEqual(today, d) - d = list(errorsbyrelease.get((release, today)).values())[0] - self.assertEqual(today, d) - - def test_update_source_version_buckets(self): - srcversbuckets = pycassa.ColumnFamily(self.pool, "SourceVersionBuckets") - src_package = "whoopsie" - version = "1.2.3" - oops_id = str(uuid.uuid1()) - oopses.update_source_version_buckets(self.config, src_package, version, oops_id) - - bucket_id = list(srcversbuckets.get((src_package, version)).keys())[0] - self.assertEqual(oops_id, bucket_id) - - def test_update_bucket_systems(self): - bv_systems = pycassa.ColumnFamily(self.pool, "BucketVersionSystems2") - bucketid = "foo" - system_token = "system-id" - version = "1.0" - oopses.update_bucket_systems( - self.config, bucketid, system_token, version=version - ) - - system = list(bv_systems.get((bucketid, version)).keys())[0] - self.assertEqual(system, system_token) diff --git a/src/oopsrepository/tests/test_schema.py b/src/oopsrepository/tests/test_schema.py deleted file mode 100644 index 5c4a9fb..0000000 --- a/src/oopsrepository/tests/test_schema.py +++ /dev/null @@ -1,23 +0,0 @@ -# oops-repository is Copyright 2011 Canonical Ltd. -# -# Canonical Ltd ("Canonical") distributes the oops-repository source code under -# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file -# LICENSE in the source tree for more information. - - -import os - -from testtools import TestCase - -from oopsrepository import config, schema -from oopsrepository.testing.cassandra import TemporaryKeyspace -from oopsrepository.testing.matchers import HasOOPSSchema - - -class TestCreateSchema(TestCase): - - def test_creates_columnfamily(self): - keyspace = self.useFixture(TemporaryKeyspace()).keyspace - os.environ["OOPS_KEYSPACE"] = keyspace - schema.create(config.get_config()) - self.assertThat(keyspace, HasOOPSSchema()) diff --git a/src/retracer.py b/src/retracer.py index 8448d51..fabf813 100755 --- a/src/retracer.py +++ b/src/retracer.py @@ -39,16 +39,14 @@ # external libs from apport import Report -from cassandra.auth import PlainTextAuthProvider -from cassandra.cqlengine import connection -from cassandra.policies import RoundRobinPolicy +from problem_report import CompressedValue, _base64_decoder -from daisy import cassandra_schema, config, utils +from daisy.metrics import get_metrics # internal libs -from daisy.metrics import get_metrics, record_revno -from daisy.version import version_info as daisy_version_info -from oopsrepository import config as oopsconfig +from errortracker import amqp_utils, cassandra_schema, config, utils +from errortracker.cassandra import setup_cassandra +from errortracker.swift_utils import get_swift_client apport_version_info = {} try: @@ -57,15 +55,10 @@ pass -LOGGING_FORMAT = ( - "%(asctime)s:%(process)d:%(thread)d" ":%(levelname)s:%(name)s:%(message)s" -) - -_cached_swift = None -_cached_s3 = None -_swift_auth_failure = False +LOGGING_FORMAT = "%(asctime)s:%(process)d:%(thread)d:%(levelname)s:%(name)s:%(message)s" metrics = get_metrics("retracer.%s" % socket.gethostname()) +logger = logging.getLogger("retracer") def ensure_str(var): @@ -75,7 +68,7 @@ def ensure_str(var): def log(message, level=logging.INFO): - logging.log(level, message) + logger.log(level, message) def rm_eff(path): @@ -110,12 +103,15 @@ def shutdown(): def prefix_log_with_amqp_message(func): def wrapped(obj, msg): try: + amqp_msg = msg.body + if type(amqp_msg) is bytes: + amqp_msg = amqp_msg.decode() # This is a terrible hack to include the UUID for the core file and # OOPS report as well as the storage provider name with the log # message. format_string = ( "%(asctime)s:%(process)d:%(thread)d:%(levelname)s" - ":%(name)s:" + str(msg.body) + ":%(message)s" + ":%(name)s:" + amqp_msg + ":%(message)s" ) formatter = logging.Formatter(format_string) logging.getLogger().handlers[0].setFormatter(formatter) @@ -139,18 +135,19 @@ def __init__( config_dir, sandbox_dir, architecture, - verbose, - cache_debs, - use_sandbox, - cleanup_sandbox, - cleanup_debs, - stacktrace_source, + verbose=False, + cache_debs=False, + use_sandbox=False, + cleanup_sandbox=False, + cleanup_debs=False, + stacktrace_source=False, failed=False, ): signal.signal(signal.SIGTERM, self.exit_gracefully) + setup_cassandra() + self.swift = get_swift_client() self._stop_now = False self._processing_callback = False - self.setup_cassandra() self.config_dir = config_dir self.sandbox_dir = sandbox_dir self.verbose = verbose @@ -189,54 +186,19 @@ def exit_gracefully(self, signal, frame): # I think we'd need to make msg and oops_ids globals self._stop_now = True if not self._processing_callback: - if self.connection: - self.connection.close() if self.channel: self.channel.close() + if self.connection: + self.connection.close() sys.exit() - def setup_cassandra(self): - os.environ["OOPS_KEYSPACE"] = config.cassandra_keyspace - self.oops_config = oopsconfig.get_config() - auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password - ) - connection.setup( - config.cassandra_hosts, - "crashdb", - auth_provider=auth_provider, - load_balancing_policy=RoundRobinPolicy(), - protocol_version=4, - ) - self.oops_config["host"] = config.cassandra_hosts - self.oops_config["username"] = config.cassandra_username - self.oops_config["password"] = config.cassandra_password - - def listen(self): + def run_forever(self): if self.failed: - retrace = "failed_retrace_%s" + queue = f"failed_retrace_{self.architecture}" else: - retrace = "retrace_%s" - retrace = retrace % self.architecture - try: - if config.amqp_username and config.amqp_password: - self.connection = amqp.Connection( - host=config.amqp_host, - userid=config.amqp_username, - password=config.amqp_password, - ) - else: - self.connection = amqp.Connection(host=config.amqp_host) - self.run_forever(queue=retrace) - finally: - if self.connection: - self.connection.close() - if self.channel: - self.channel.close() - - def run_forever(self, queue): + queue = f"retrace_{self.architecture}" try: - self.connection.connect() + self.connection = amqp_utils.get_connection() self.channel = self.connection.channel() self.channel.queue_declare(queue=queue, durable=True, auto_delete=False) self.channel.basic_qos(0, 1, False) @@ -246,6 +208,10 @@ def run_forever(self, queue): self.connection.drain_events() except KeyboardInterrupt: log("Keyboard interrupt received, exiting properly") + self._stop_now = True + finally: + self.channel.close() + self.connection.close() if self.channel and self.channel.is_open: self.channel.basic_cancel(tag) @@ -258,7 +224,10 @@ def update_retrace_stats(self, release, day_key, retracing_time, result): """ # This is kept around for legacy reasons. The integration tests # currently depend on this being exposed in the API. - status = result + if result: + status = "success" + else: + status = "failure" # Increment the counters. This will create the rows if they don't exist yet. cassandra_schema.RetraceStats( key=day_key.encode(), column1="%s:%s" % (release, status) @@ -279,13 +248,7 @@ def update_retrace_stats(self, release, day_key, retracing_time, result): count_key, ], ) - # keep those two hex values to display in logging when something goes wrong - mean[mean_key + "_hex"] = mean[mean_key] - mean[count_key + "_hex"] = mean[count_key] - - mean[mean_key] = struct.unpack("!f", mean[mean_key])[0] - mean[count_key] = int.from_bytes(mean[count_key]) - except (cassandra_schema.DoesNotExist, KeyError): + except cassandra_schema.DoesNotExist: mean = {mean_key: 0.0, count_key: 0} new_mean = float( @@ -368,10 +331,8 @@ def move_to_failed_queue(self, msg): def failed_to_process(self, msg, oops_id, old=False): # Try to remove the core file from the storage provider - parts = self.msg_body.split(":", 1) - oops_id = None - oops_id, provider = parts - removed = self.remove(*parts) + oops_id, provider = self.msg_body.split(":", 1) + removed = self.remove(oops_id) if removed: # We've processed this. Delete it off the MQ. msg.channel.basic_ack(msg.delivery_tag) @@ -391,222 +352,62 @@ def failed_to_process(self, msg, oops_id, old=False): addr_sig = cassandra_schema.OOPS.objects.get( key=oops_id.encode(), column1="StacktraceAddressSignature" )["value"] - cassandra_schema.Indexes.objects.filter( - key=b"retracing", column1=addr_sig - ).delete() + cassandra_schema.Indexes.objects.filter(key=b"retracing", column1=addr_sig).delete() except cassandra_schema.DoesNotExist as e: - log( - "Could not remove from the retracing row (%s) (%s):" - % (oops_id, repr(e)) - ) + log("Could not remove from the retracing row (%s) (%s):" % (oops_id, repr(e))) - def write_swift_bucket_to_disk(self, key, provider_data): - global _cached_swift - global _swift_auth_failure - import swiftclient - - opts = { - "tenant_name": provider_data["os_tenant_name"], - "region_name": provider_data["os_region_name"], - } - if not _cached_swift: - _cached_swift = swiftclient.client.Connection( - provider_data["os_auth_url"], - provider_data["os_username"], - provider_data["os_password"], - os_options=opts, - auth_version="3.0", - ) - if self.verbose: - log("swift token: %s" % str(_cached_swift.token)) - fmt = "-{}.{}.oopsid".format(provider_data["type"], key) + def write_swift_bucket_to_disk(self, key): + fmt = f"-swift.{key}.oopsid" fd, path = tempfile.mkstemp(fmt) os.close(fd) - bucket = provider_data["bucket"] try: - _cached_swift.http_conn = None - headers, body = _cached_swift.get_object(bucket, key, resp_chunk_size=65536) + _, body = self.swift.get_object(config.swift_bucket, key, resp_chunk_size=65536) with open(path, "wb") as fp: for chunk in body: fp.write(chunk) return path - except swiftclient.client.ClientException as e: - if "Unauthorized" in str(e): - metrics.meter("swift_client_exception.auth_failure") - log("Authorization failure connecting to swift.") - _swift_auth_failure = True - elif "404 Not Found" in str(e): - return "Missing" - else: - metrics.meter("swift_client_exception") - log("Could not retrieve %s (swift):" % key) + except Exception as e: + log("Could not get %s from swift: %s" % (key, e)) log(traceback.format_exc()) # This will still exist if we were partway through a write. rm_eff(path) return None - def remove_from_swift(self, key, provider_data): - global _cached_swift - global _swift_auth_failure - import swiftclient - - opts = { - "tenant_name": provider_data["os_tenant_name"], - "region_name": provider_data["os_region_name"], - } - # test that the connection to swift still works - if _cached_swift: - try: - _cached_swift.get_account() - except swiftclient.client.ClientException as e: - if "Unauthorized" in str(e): - log("Authorization failure getting account info") - _cached_swift = "" - if not _cached_swift: - _cached_swift = swiftclient.client.Connection( - provider_data["os_auth_url"], - provider_data["os_username"], - provider_data["os_password"], - os_options=opts, - auth_version="3.0", - ) - if self.verbose: - log("swift token: %s" % str(_cached_swift.token)) - bucket = provider_data["bucket"] + def remove_from_swift(self, key): + log("Removing core from swift") try: - _cached_swift.http_conn = None - _cached_swift.delete_object(bucket, key) + self.swift.delete_object(config.swift_bucket, key) # 404s are handled when we write the bucket to disk - except swiftclient.client.ClientException as e: - if "Unauthorized" in str(e): - metrics.meter("swift_client_exception.auth_failure") - log("Authorization failure connecting to swift.") - log(traceback.format_exc()) - # if there is a failure to receive and a failure to delete - # stop the retracing process - if _swift_auth_failure: - log("Two swift auth failures, stopping.") - sys.exit(1) - _swift_auth_failure = True - else: - log("Could not remove %s (swift):" % key) - log(traceback.format_exc()) - metrics.meter("swift_delete_error") - return False - return True - - def write_s3_bucket_to_disk(self, key, provider_data): - global _cached_s3 - from boto.exception import S3ResponseError - from boto.s3.connection import S3Connection - - if not _cached_s3: - _cached_s3 = S3Connection( - aws_access_key_id=provider_data["aws_access_key"], - aws_secret_access_key=provider_data["aws_secret_key"], - host=provider_data["host"], - ) - try: - bucket = _cached_s3.get_bucket(provider_data["bucket"]) - key = bucket.get_key(key) - except S3ResponseError: - log("Could not retrieve %s (s3):" % key) - log(traceback.format_exc()) - return None - fmt = "-{}.{}.oopsid".format(provider_data["type"], key) - fd, path = tempfile.mkstemp(fmt) - os.close(fd) - with open(path, "wb") as fp: - for data in key: - # 8K at a time. - fp.write(data) - return path - - def remove_from_s3(self, key, provider_data): - global _cached_s3 - from boto.exception import S3ResponseError - from boto.s3.connection import S3Connection - - try: - if not _cached_s3: - _cached_s3 = S3Connection( - aws_access_key_id=provider_data["aws_access_key"], - aws_secret_access_key=provider_data["aws_secret_key"], - host=provider_data["host"], - ) - bucket = _cached_s3.get_bucket(provider_data["bucket"]) - key = bucket.get_key(key) - key.delete() - except S3ResponseError: - log("Could not remove %s (s3):" % key) + except Exception as e: + log("Could not remove %s from swift: %s" % (key, e)) + if "404 Not Found" in str(e): + return True + log("Could not remove %s from swift: %s" % (key, e)) log(traceback.format_exc()) return False return True - def write_local_to_disk(self, key, provider_data): - path = os.path.join(provider_data["path"], key) - fmt = "-{}.{}.oopsid".format(provider_data["type"], key) - fd, new_path = tempfile.mkstemp(fmt) - os.close(fd) - if not os.path.exists(path): - return None - else: - shutil.copyfile(path, new_path) - return new_path + def write_bucket_to_disk(self, oops_id): + return self.write_swift_bucket_to_disk(oops_id) - def remove_from_local(self, key, provider_data): - path = os.path.join(provider_data["path"], key) - rm_eff(path) - return True + def remove(self, oops_id): + return self.remove_from_swift(oops_id) - def write_bucket_to_disk(self, oops_id, provider): - path = "" - cs = getattr(config, "core_storage", "") - if not cs: - log("core_storage not set.") - sys.exit(1) - provider_data = cs[provider] - t = provider_data["type"] - if t == "swift": - path = self.write_swift_bucket_to_disk(oops_id, provider_data) - elif t == "s3": - path = self.write_s3_bucket_to_disk(oops_id, provider_data) - elif t == "local": - path = self.write_local_to_disk(oops_id, provider_data) - return path - - def remove(self, oops_id, provider): - cs = getattr(config, "core_storage", "") - if not cs: - log("core_storage not set.") - sys.exit(1) - provider_data = cs[provider] - t = provider_data["type"] - if t == "swift": - removed = self.remove_from_swift(oops_id, provider_data) - elif t == "s3": - removed = self.remove_from_s3(oops_id, provider_data) - elif t == "local": - removed = self.remove_from_local(oops_id, provider_data) - if removed: - return True - return False - - def save_crash(self, failure_storage, report, oops_id, core_file): - log("Saved OOPS %s for manual investigation." % oops_id) - # create a new crash with the CoreDump for investigation - report["CoreDump"] = (core_file,) - failed_crash = "%s/%s.crash" % (failure_storage, oops_id) - with open(failed_crash, "wb") as fp: - report.write(fp) + def save_crash(self, report, oops_id, core_file): + if config.failure_storage: + log("Saved OOPS %s for manual investigation." % oops_id) + # create a new crash with the CoreDump for investigation + report["CoreDump"] = (core_file,) + failed_crash = "%s/%s.crash" % (config.failure_storage, oops_id) + with open(failed_crash, "wb") as fp: + report.write(fp) @prefix_log_with_amqp_message def callback(self, msg): self._processing_callback = True log("Processing.") self.msg_body = ensure_str(msg.body) - parts = self.msg_body.split(":", 1) - oops_id, provider = parts + oops_id, provider = self.msg_body.split(":", 1) try: col = cassandra_schema.OOPS.get_as_dict(key=oops_id.encode()) except cassandra_schema.DoesNotExist: @@ -623,9 +424,9 @@ def callback(self, msg): # retraced, check for this and ack the message. # N.B.: This only works in some cases because we don't mark a report as # having been retraced e.g. there is no Retrace column in keys - if "RetraceFailureReason" in list( + if "RetraceFailureReason" in list(col.keys()) or "RetraceOutdatedPackages" in list( col.keys() - ) or "RetraceOutdatedPackages" in list(col.keys()): + ): log("Ack'ing already retraced OOPS.") msg.channel.basic_ack(msg.delivery_tag) # 2016-05-19 - this failed to delete cores and ack'ing of msgs @@ -639,7 +440,7 @@ def callback(self, msg): if "UnreportableReason" in list(col.keys()): unreportable_reason = col["UnreportableReason"] - path = self.write_bucket_to_disk(*parts) + path = self.write_bucket_to_disk(oops_id) if path == "Missing": log("Ack'ing OOPS with missing core file.") @@ -651,21 +452,17 @@ def callback(self, msg): return core_file = "%s.core" % path - with open(core_file, "wb") as fp: - log("Decompressing to %s" % core_file) - p1 = Popen(["base64", "-d", path], stdout=PIPE) - # Set stderr to PIPE so we get output in the result tuple. - p2 = Popen(["zcat"], stdin=p1.stdout, stdout=fp, stderr=PIPE) - ret = p2.communicate() - rm_eff(path) - - if p2.returncode != 0: - log("Error processing %s:" % path) - if unreportable_reason: - log("UnreportableReason is: %s" % unreportable_reason) - if ret[1]: - for line in ret[1].splitlines(): - log(line) + try: + with open(core_file, "wb") as fp: + log("Decompressing to %s" % core_file) + with open(path) as path_fp: + for block in CompressedValue.decode_compressed_stream( + _base64_decoder(path_fp) + ): + fp.write(block) + rm_eff(path) + except Exception as e: + log("Failed to decompress core: %s" % str(e)) # We couldn't decompress this, so there's no value in trying again. self.processed(msg) # probably incomplete cores from armhf? @@ -675,11 +472,10 @@ def callback(self, msg): metrics.meter("retrace.failure.decompression.%s" % self.architecture) rm_eff(core_file) return + # confirm that gdb thinks the core file is good gdb_cmd = [self.gdb_path, "--batch", "--ex", "target core %s" % core_file] - proc = Popen( - gdb_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, errors="ignore" - ) + proc = Popen(gdb_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, errors="ignore") (out, err) = proc.communicate() if "is truncated: expected core file size" in err or "not a core dump" in err: # Not a core file, there's no value in trying again. @@ -836,28 +632,19 @@ def callback(self, msg): give_up = True retrace_result = "missing_execpath" break - if ( - "Cannot find package which ships InterpreterPath" - in line - ): + if "Cannot find package which ships InterpreterPath" in line: give_up = True retrace_result = "missing_intpath" break if "failed with exit code -9" in line: metrics.meter("retrace.failed.gdb_failure.minus_nine") - if failure_storage: - self.save_crash( - failure_storage, report, oops_id, core_file - ) + self.save_crash(report, oops_id, core_file) give_up = True retrace_result = "gdb_crash" break if "failed with exit code -11" in line: metrics.meter("retrace.failed.gdb_failure.minus_eleven") - if failure_storage: - self.save_crash( - failure_storage, report, oops_id, core_file - ) + self.save_crash(report, oops_id, core_file) give_up = True retrace_result = "gdb_crash" break @@ -942,9 +729,7 @@ def callback(self, msg): metrics.meter("retrace.failed.invalid_core") metrics.meter("retrace.failed.invalid_core.%s" % release) metrics.meter("retrace.failed.invalid_core.%s" % architecture) - metrics.meter( - "retrace.failed.invalid_core.%s.%s" % (release, architecture) - ) + metrics.meter("retrace.failed.invalid_core.%s.%s" % (release, architecture)) if apport_vers: metrics.meter( "retrace.failed.invalid_core.%s.%s" @@ -955,18 +740,13 @@ def callback(self, msg): # another core sas = report.get("StacktraceAddressSignature", "") if sas: - cassandra_schema.Indexes.objects.filter( - key=b"retracing", column1=sas - ).delete() - self.update_retrace_stats( - release, day_key, retracing_time, result=retrace_result - ) + cassandra_schema.Indexes.objects.filter(key=b"retracing", column1=sas).delete() + self.update_retrace_stats(release, day_key, retracing_time, result=retrace_result) metrics.meter("retrace.failed") metrics.meter("retrace.failed.%s" % release) metrics.meter("retrace.failed.%s" % architecture) metrics.meter("retrace.failed.%s.%s" % (release, architecture)) - if failure_storage: - self.save_crash(failure_storage, report, oops_id, core_file) + self.save_crash(report, oops_id, core_file) rm_eff("%s.new" % report_path) # TODO 2024-12-16: Skia: let's see what to do with that later, # but for now we don't want the retracer to choke on this. @@ -1012,9 +792,7 @@ def callback(self, msg): metrics.meter("retrace.missing.crash_signature") metrics.meter("retrace.missing.%s.crash_signature" % architecture) metrics.meter("retrace.missing.%s.crash_signature" % release) - metrics.meter( - "retrace.missing.%s.%s.crash_signature" % (release, architecture) - ) + metrics.meter("retrace.missing.%s.%s.crash_signature" % (release, architecture)) if missing_dbgsym_pkg: metrics.meter( "retrace.missing.crash_signature. \ @@ -1062,8 +840,7 @@ def callback(self, msg): else: log("Gave up requeueing after %s attempts." % count) if architecture == "armhf" and "RetraceOutdatedPackages" not in report: - if failure_storage: - self.save_crash(failure_storage, report, oops_id, core_file) + self.save_crash(report, oops_id, core_file) if stacktrace_addr_sig and not original_sas: # if the OOPS doesn't already have a SAS add one @@ -1077,15 +854,10 @@ def callback(self, msg): ) else: metrics.meter("retrace.missing.stacktrace_address_signature") + metrics.meter("retrace.missing.%s.stacktrace_address_signature" % architecture) + metrics.meter("retrace.missing.%s.stacktrace_address_signature" % release) metrics.meter( - "retrace.missing.%s.stacktrace_address_signature" % architecture - ) - metrics.meter( - "retrace.missing.%s.stacktrace_address_signature" % release - ) - metrics.meter( - "retrace.missing.%s.%s.stacktrace_address_signature" - % (release, architecture) + "retrace.missing.%s.%s.stacktrace_address_signature" % (release, architecture) ) cassandra_schema.OOPS.objects.create( key=oops_id.encode(), column1="RetraceStatus", value="Failure" @@ -1137,11 +909,8 @@ def callback(self, msg): metrics.meter("retrace.missing.stacktrace") metrics.meter("retrace.missing.%s.stacktrace" % architecture) metrics.meter("retrace.missing.%s.stacktrace" % release) - metrics.meter( - "retrace.missing.%s.%s.stacktrace" % (release, architecture) - ) - if failure_storage: - self.save_crash(failure_storage, report, oops_id, core_file) + metrics.meter("retrace.missing.%s.%s.stacktrace" % (release, architecture)) + self.save_crash(report, oops_id, core_file) cassandra_schema.OOPS.objects.create( key=oops_id.encode(), column1="RetraceStatus", value="Failure" @@ -1162,16 +931,12 @@ def callback(self, msg): if unreportable_reason: log("UnreportableReason is: %s" % unreportable_reason) metrics.meter("retrace.missing.stacktrace_addr_sig") - metrics.meter( - "retrace.missing.%s.stacktrace_addr_sig" % architecture - ) + metrics.meter("retrace.missing.%s.stacktrace_addr_sig" % architecture) metrics.meter("retrace.missing.%s.stacktrace_addr_sig" % release) metrics.meter( - "retrace.missing.%s.%s.stacktrace_addr_sig" - % (release, architecture) + "retrace.missing.%s.%s.stacktrace_addr_sig" % (release, architecture) ) - if failure_storage: - self.save_crash(failure_storage, report, oops_id, core_file) + self.save_crash(report, oops_id, core_file) if "Stacktrace" not in report: failure_reason = "No stacktrace after retracing" @@ -1199,8 +964,7 @@ def callback(self, msg): missing_ddebs.append(line.split(" ")[-1]) log("%s (%s)" % (line, release)) if architecture == "armhf" and missing_ddebs and not outdated_pkgs: - if failure_storage: - self.save_crash(failure_storage, report, oops_id, core_file) + self.save_crash(report, oops_id, core_file) if not outdated_pkgs: failure_reason += " and missing ddebs." else: @@ -1234,21 +998,15 @@ def callback(self, msg): missing_ddeb_count = 0 if crash_signature: try: - rf_reason = ( - cassandra_schema.BucketRetraceFailureReason.get_as_dict( - key=crash_signature.encode() - ) + rf_reason = cassandra_schema.BucketRetraceFailureReason.get_as_dict( + key=crash_signature.encode() ) if "missing_ddeb_count" in rf_reason: - least_missing_ddeb_count = int( - rf_reason["missing_ddeb_count"] - ) + least_missing_ddeb_count = int(rf_reason["missing_ddeb_count"]) else: least_missing_ddeb_count = 9999 if "outdated_pkg_count" in rf_reason: - least_outdated_pkg_count = int( - rf_reason["outdated_pkg_count"] - ) + least_outdated_pkg_count = int(rf_reason["outdated_pkg_count"]) else: least_outdated_pkg_count = 9999 except cassandra_schema.DoesNotExist: @@ -1271,12 +1029,9 @@ def callback(self, msg): ) metrics.meter("retrace.failure.outdated_packages") metrics.meter("retrace.failure.%s.outdated_packages" % release) + metrics.meter("retrace.failure.%s.outdated_packages" % architecture) metrics.meter( - "retrace.failure.%s.outdated_packages" % architecture - ) - metrics.meter( - "retrace.failure.%s.%s.outdated_packages" - % (release, architecture) + "retrace.failure.%s.%s.outdated_packages" % (release, architecture) ) else: pass @@ -1317,9 +1072,9 @@ def callback(self, msg): # This will contain the OOPS ID we're currently processing as # well. ids = list( - cassandra_schema.AwaitingRetrace.objects.filter( - key=original_sas - ).values_list("column1", flat=True) + cassandra_schema.AwaitingRetrace.objects.filter(key=original_sas).values_list( + "column1", flat=True + ) ) oops_ids = ids else: @@ -1343,7 +1098,14 @@ def callback(self, msg): except cassandra_schema.DoesNotExist: # An oops may not exist in awaiting_retrace if the initial # report didn't have a SAS - pass + # Still make sure all others are cleared the slow way + for id in oops_ids: + try: + cassandra_schema.AwaitingRetrace.objects.filter( + key=original_sas, column1=id + ).delete() + except cassandra_schema.DoesNotExist: + pass if crash_signature: self.bucket(oops_ids, crash_signature) @@ -1367,21 +1129,13 @@ def callback(self, msg): sys.exit() def processed(self, msg): - parts = self.msg_body.split(":", 1) - oops_id = None - oops_id, provider = parts - removed = self.remove(*parts) + oops_id, provider = self.msg_body.split(":", 1) + removed = self.remove(oops_id) if removed: # We've processed this. Delete it off the MQ. msg.channel.basic_ack(msg.delivery_tag) self.update_time_to_retrace(msg) return True - # 2016-05-18 This was added due to intermittent issues removing core - # files from swift. Requeue the oops_id and on the second pass we will - # try to remove the core again or just retrace it. - else: - log("Requeued an OOPS (%s) whose core file was not removed." % oops_id) - self.requeue(msg, oops_id) return False def requeue(self, msg, oops_id): @@ -1423,7 +1177,7 @@ def update_time_to_retrace(self, msg): if not timestamp: return - time_taken = int(datetime.datetime.now(datetime.UTC).timestamp()) - timestamp + time_taken = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) - timestamp # This needs to be at a global level since it's dealing with the time # items have been sitting on a queue shared by all retracers. m = get_metrics("retracer.all") @@ -1437,9 +1191,9 @@ def rebucket(self, crash_signature): failed_key = "failed:" + crash_signature ids = [ str(id).encode() - for id in cassandra_schema.Bucket.objects.filter( - key=failed_key - ).values_list("column1", flat=True) + for id in cassandra_schema.Bucket.objects.filter(key=failed_key).values_list( + "column1", flat=True + ) ] if not ids: @@ -1470,7 +1224,7 @@ def bucket(self, ids, crash_signature): except cassandra_schema.DoesNotExist: log("Could not find %s for %s." % (oops_id, crash_signature)) o = {} - utils.bucket(self.oops_config, oops_id, crash_signature, o) + utils.bucket(oops_id, crash_signature, o) metrics.meter("success.binary_bucketed") if not crash_signature.startswith("failed:") and o: self.cleanup_oops(oops_id) @@ -1533,7 +1287,7 @@ def parse_options(): ) parser.add_argument( "--core-storage", - help="Directory in which to store cores for manual " "investigation.", + help="Directory in which to store cores for manual investigation.", ) parser.add_argument("-o", "--output", help="Log messages to a file.") parser.add_argument( @@ -1542,21 +1296,12 @@ def parse_options(): dest="stacktrace_source", help="Do not have apport create a StacktraceSource.", ) - parser.add_argument( - "--retrieve-core", - help=( - "Debug processing a single uuid:provider_id." - "This does not touch Cassandra or the queue." - ), - ) return parser.parse_args() def main(): global log_output global root_handler - # should move to a configuration option - global failure_storage options = parse_options() if options.output: @@ -1566,19 +1311,10 @@ def main(): sys.stderr.close() sys.stderr = sys.stdout - failure_storage = "" - if options.core_storage: - if os.path.exists(options.core_storage): - failure_storage = options.core_storage - logging.basicConfig(format=LOGGING_FORMAT, level=logging.INFO) try: msg = "Running" - if "revno" in daisy_version_info: - revno = daisy_version_info["revno"] - msg += " daisy revision number: %s" % revno - record_revno() if "revno" in apport_version_info: revno = apport_version_info["revno"] msg += " apport_revision number: %s" % revno @@ -1600,12 +1336,7 @@ def main(): options.stacktrace_source, failed=options.failed, ) - if options.retrieve_core: - parts = options.one_off.split(":", 1) - path, oops_id = retracer.write_bucket_to_disk(parts[0], parts[1]) - log("Wrote %s to %s. Exiting." % (path, oops_id)) - else: - retracer.listen() + retracer.run_forever() except: # log if you want raise diff --git a/src/test/test_core_providers.py b/src/test/test_core_providers.py deleted file mode 100644 index 741b1b2..0000000 --- a/src/test/test_core_providers.py +++ /dev/null @@ -1,74 +0,0 @@ -import unittest -import uuid - -import mock -from testtools import TestCase - -import daisy - - -class TestSubmitCore(TestCase): - def test_write_weights(self): - d = {"local": 0.25, "s1": 0.25, "s2": 0.5} - result = daisy.gen_write_weight_ranges(d) - self.assertEqual(result["local"][1] - result["local"][0], 0.25) - self.assertEqual(result["s1"][1] - result["s1"][0], 0.25) - self.assertEqual(result["s2"][1] - result["s2"][0], 0.5) - - def test_verify_configuration(self): - # Existing local configuration gets mapped to a sole storage_provider. - with mock.patch("daisy.config", autospec=True) as cfg: - cfg.san_path = "/foo" - cfg.swift_bucket = "" - cfg.ec2_bucket = "" - daisy.validate_and_set_configuration() - self.assertEqual(cfg.storage_write_weights["local"], 1.0) - self.assertEqual(cfg.core_storage["default"], "local") - self.assertEqual(cfg.core_storage["local"]["type"], "local") - - # Existing Swift configuration gets mapped to a sole storage_provider. - with mock.patch("daisy.config", autospec=True) as cfg: - cfg.san_path = "/foo" - cfg.swift_bucket = "cores" - cfg.ec2_bucket = "" - daisy.validate_and_set_configuration() - self.assertEqual(cfg.storage_write_weights["swift"], 1.0) - self.assertEqual(cfg.core_storage["default"], "swift") - self.assertEqual(cfg.core_storage["swift"]["type"], "swift") - - # You cannot set both swift_bucket and ec2_bucket. - with mock.patch("daisy.config", autospec=True) as cfg: - cfg.swift_bucket = "cores" - cfg.ec2_bucket = "cores" - self.assertRaises(ImportError, daisy.validate_and_set_configuration) - - @mock.patch("daisy.submit_core.write_to_swift") - @mock.patch("daisy.submit_core.write_to_s3") - @mock.patch("random.randint") - def test_write_to_storage_provider(self, randint, s3, swift): - s3.return_value = True - swift.return_value = True - randint.return_value = 100 - ranges = {"swift-host1": 0.5, "s3-host1": 0.5} - cs = { - "default": "swift-host1", - "s3-host1": {"type": "s3"}, - "swift-host1": {"type": "swift"}, - } - ranges = daisy.gen_write_weight_ranges(ranges) - obj = "daisy.config.write_weight_ranges" - with mock.patch.dict(obj, ranges, clear=True): - obj = "daisy.config.core_storage" - with mock.patch.dict(obj, cs, clear=True): - u = str(uuid.uuid1()) - result = daisy.submit_core.write_to_storage_provider(None, u) - if 1.0 in ranges["s3-host1"]: - s3.assert_called_with(None, u, cs["s3-host1"]) - self.assertEqual(result, "%s:s3-host1" % u) - else: - swift.assert_called_with(None, u, cs["swift-host1"]) - self.assertEqual(result, "%s:swift-host1" % u) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/test_retracer.py b/src/test/test_retracer.py deleted file mode 100644 index 77dc6b6..0000000 --- a/src/test/test_retracer.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf8 -*- -import datetime - -# Actually show the messages (info) with the retracer format. -import logging -import os -import shutil -import tempfile -import time -import unittest -import uuid - -import amqp -import mock -import pycassa -from pycassa.types import FloatType, IntegerType -from testtools import TestCase - -from daisy import config, retracer, schema -from oopsrepository import config as oopsconfig -from oopsrepository import schema as oopsschema -from oopsrepository.testing.cassandra import TemporaryOOPSDB - -logging.basicConfig(format=retracer.LOGGING_FORMAT, level=logging.INFO) - - -class TestSubmission(TestCase): - def setUp(self): - super(TestSubmission, self).setUp() - # We need to set the config before importing. - os.environ["OOPS_HOST"] = config.cassandra_hosts[0] - self.keyspace = self.useFixture(TemporaryOOPSDB()).keyspace - os.environ["OOPS_KEYSPACE"] = self.keyspace - creds = { - "username": config.cassandra_username, - "password": config.cassandra_password, - } - self.pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=creds - ) - config.cassandra_keyspace = self.keyspace - schema.create() - oops_config = oopsconfig.get_config() - oops_config["username"] = config.cassandra_username - oops_config["password"] = config.cassandra_password - oopsschema.create(oops_config) - self.temp = tempfile.mkdtemp() - config_dir = os.path.join(self.temp, "config") - sandbox_dir = os.path.join(self.temp, "sandbox") - os.makedirs(config_dir) - os.makedirs(sandbox_dir) - self.architecture = "amd64" - # Don't depend on apport-retrace being installed. - with mock.patch("daisy.retracer.Popen") as popen: - popen.return_value.returncode = 0 - popen.return_value.communicate.return_value = ["/bin/false"] - self.retracer = retracer.Retracer( - config_dir, sandbox_dir, self.architecture, False, False - ) - - def tearDown(self): - super(TestSubmission, self).tearDown() - shutil.rmtree(self.temp) - - def test_update_retrace_stats(self): - retrace_stats_fam = pycassa.ColumnFamily(self.pool, "RetraceStats") - indexes_fam = pycassa.ColumnFamily(self.pool, "Indexes") - release = "Ubuntu 12.04" - day_key = time.strftime("%Y%m%d", time.gmtime()) - - self.retracer.update_retrace_stats(release, day_key, 30.5, True) - result = retrace_stats_fam.get(day_key) - self.assertEqual(result["Ubuntu 12.04:success"], 1) - mean_key = "%s:%s:%s" % (day_key, release, self.architecture) - counter_key = "%s:count" % mean_key - indexes_fam.column_validators = { - mean_key: FloatType(), - counter_key: IntegerType(), - } - result = indexes_fam.get("mean_retracing_time") - self.assertEqual(result[mean_key], 30.5) - self.assertEqual(result[counter_key], 1) - - self.retracer.update_retrace_stats(release, day_key, 30.5, True) - result = indexes_fam.get("mean_retracing_time") - self.assertEqual(result[mean_key], 30.5) - self.assertEqual(result[counter_key], 2) - - def test_chunked_insert(self): - # UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in - # position 487: ordinal not in range(128) - stack_fam = pycassa.ColumnFamily(self.pool, "Stacktrace") - stack_fam.default_validation_class = pycassa.types.UTF8Type() - - # Non-chunked version. - data = {"Package": "apport", "ProblemType": "Crash"} - retracer.chunked_insert(stack_fam, "foo", data) - results = next(stack_fam.get_range()) - self.assertEqual(results[0], "foo") - self.assertEqual(results[1]["Package"], "apport") - self.assertEqual(results[1]["ProblemType"], "Crash") - - # Chunked. - stack_fam.truncate() - data["Big"] = "a" * (1024 * 1024 * 4 + 1) - retracer.chunked_insert(stack_fam, "foo", data) - results = next(stack_fam.get_range()) - self.assertEqual(results[0], "foo") - self.assertEqual(results[1]["Package"], "apport") - self.assertEqual(results[1]["ProblemType"], "Crash") - self.assertEqual(results[1]["Big"], "a" * 1024 * 1024 * 4) - self.assertEqual(results[1]["Big-1"], "a") - - # Unicode. As generated in callback(), oops_fam.get() - stack_fam.truncate() - data = {"☃".encode("utf8"): "☕".encode("utf8")} - retracer.chunked_insert(stack_fam, "foo", data) - results = next(stack_fam.get_range()) - - def test_retracer_logging(self): - msg = mock.Mock() - u = uuid.uuid1() - msg.body = "%s:%s" % (str(u), "local") - from io import StringIO - - stream = StringIO() - handler = logging.StreamHandler(stream) - root = logging.getLogger() - root.handlers = [] - try: - root.addHandler(handler) - with mock.patch.object(retracer, "config") as cfg: - cfg.core_storage = {"local": {"type": "local", "path": "/tmp"}} - path = os.path.join("/tmp", str(u)) - with open(path, "w") as fp: - fp.write("fake core file") - self.retracer.callback(msg) - self.assertFalse(os.path.exists(path)) - - # Test that pycassa can still log correctly. - self.retracer.pool.listeners[0].logger.info("pycassa-message") - finally: - # Don't leave logging set up. - root.handlers = [] - stream.seek(0) - contents = stream.read() - self.assertIn("%s:%s" % (str(u), "local"), contents) - self.assertIn(":pycassa.pool:pycassa-message", contents) - - def test_update_time_to_retrace(self): - time_to_retrace = pycassa.ColumnFamily(self.pool, "TimeToRetrace") - - oops_id = str(uuid.uuid1()) - ts = datetime.datetime.utcnow() - ts = ts - datetime.timedelta(minutes=5) - - msg = amqp.Message(oops_id, timestamp=ts) - - self.retracer.update_time_to_retrace(oops_id, msg) - date, vals = next(time_to_retrace.get_range()) - day_key = time.strftime("%Y%m%d", time.gmtime()) - self.assertEqual(date, day_key) - # Tolerate it being 9.9 seconds off at most. - self.assertAlmostEqual(vals[oops_id], 60 * 5, places=-1) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/test_submit.py b/src/test/test_submit.py deleted file mode 100644 index 0804e49..0000000 --- a/src/test/test_submit.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/python - -import os -import shutil -import tempfile -import time -import unittest -from io import StringIO - -import apport -import bson -import mock -import pycassa -from testtools import TestCase - -from daisy import config, schema, submit, wsgi -from oopsrepository import config as oopsconfig -from oopsrepository import oopses -from oopsrepository import schema as oopsschema -from oopsrepository.testing.cassandra import TemporaryOOPSDB - -# SHA-512 of the system-uuid -sha512_system_uuid = ( - "/d78abb0542736865f94704521609c230dac03a2f369d043ac212d6" - "933b91410e06399e37f9c5cc88436a31737330c1c8eccb2c2f9f374" - "d62f716432a32d50fac" -) - - -class TestSubmission(TestCase): - def setUp(self): - super(TestSubmission, self).setUp() - self.start_response = mock.Mock() - - # Set up daisy schema. - os.environ["OOPS_HOST"] = config.cassandra_hosts[0] - self.keyspace = self.useFixture(TemporaryOOPSDB()).keyspace - os.environ["OOPS_KEYSPACE"] = self.keyspace - config.cassandra_keyspace = self.keyspace - self.creds = { - "username": config.cassandra_username, - "password": config.cassandra_password, - } - schema.create() - - # Set up oopsrepository schema. - oops_config = oopsconfig.get_config() - oops_config["username"] = config.cassandra_username - oops_config["password"] = config.cassandra_password - oopsschema.create(oops_config) - - # Clear singletons. - wsgi._pool = None - oopses._connection_pool = None - submit.oops_config = oops_config - - -class TestCrashSubmission(TestSubmission): - - def test_bogus_submission(self): - environ = {"PATH_INFO": "/", "wsgi.input": StringIO("")} - wsgi.app(environ, self.start_response) - self.assertEqual(self.start_response.call_args[0][0], "400 Bad Request") - - def test_python_submission(self): - """Ensure that a Python crash is accepted, bucketed, and that the - retracing ColumnFamilies remain untouched.""" - - report = apport.Report() - report["ProblemType"] = "Crash" - report["InterpreterPath"] = "/usr/bin/python" - report["ExecutablePath"] = "/usr/bin/foo" - report["DistroRelease"] = "Ubuntu 12.04" - report["Package"] = "ubiquity 2.34" - report["Traceback"] = ( - "Traceback (most recent call last):\n" - ' File "/usr/bin/foo", line 1, in \n' - " sys.exit(1)" - ) - report_bson = bson.BSON.encode(report.data) - report_io = StringIO(report_bson) - environ = { - "CONTENT_TYPE": "application/octet-stream", - "PATH_INFO": sha512_system_uuid, - "wsgi.input": report_io, - } - - wsgi.app(environ, self.start_response) - self.assertEqual(self.start_response.call_args[0][0], "200 OK") - - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - oops_cf = pycassa.ColumnFamily(pool, "OOPS") - bucket_cf = pycassa.ColumnFamily(pool, "Bucket") - # Ensure the crash was bucketed: - oops_id = oops_cf.get_range().next()[0] - crash_signature = "/usr/bin/foo: sys.exit(1):/usr/bin/foo@1" - self.assertEqual( - pycassa.util.uuid.UUID(oops_id), - list(bucket_cf.get(crash_signature).keys())[0], - ) - - # A Python crash shouldn't touch the retracing CFs: - for fam in ("AwaitingRetrace", "Stacktrace", "Indexes"): - cf = pycassa.ColumnFamily(pool, fam) - self.assertEqual([x for x in cf.get_range()], []) - cf = pycassa.ColumnFamily(pool, "DayBucketsCount") - counts = [x for x in cf.get_range()] - day_key = time.strftime("%Y%m%d", time.gmtime()) - resolutions = (day_key, day_key[:4], day_key[:6]) - release = report["DistroRelease"] - keys = [] - for resolution in resolutions: - keys.append("%s:%s" % (release, resolution)) - for resolution in resolutions: - keys.append("%s:ubiquity:%s" % (release, resolution)) - for resolution in resolutions: - keys.append("%s:ubiquity:2.34:%s" % (release, resolution)) - for resolution in resolutions: - keys.append("ubiquity:2.34:%s" % resolution) - "ubiquity:2.34" - - for key in keys: - found = False - for count in counts: - if count[0] == key: - found = True - self.assertTrue(found, "Could not find %s" % key) - for count in counts: - self.assertEqual(list(count[1].values()), [1]) - - def test_kerneloops_submission(self): - oops_text = """BUG: unable to handle kernel paging request at ffffb4ff -IP: [] ext4_get_acl+0x80/0x210 -*pde = 01874067 *pte = 00000000 -Oops: 0000 [#1] SMP -Modules linked in: bnep rfcomm bluetooth dm_crypt olpc_xo1 scx200_acb snd_cs5535audio snd_ac97_codec ac97_bus snd_pcm snd_seq_midi snd_rawmidi snd_seq_midi_event snd_seq snd_timer snd_seq_device snd cs5535_gpio soundcore snd_page_alloc binfmt_misc geode_aes cs5535_mfd geode_rng msr vesafb usbhid hid 8139too pata_cs5536 8139cp - -Pid: 1798, comm: gnome-session-c Not tainted 3.0.0-11-generic #17-Ubuntu First International Computer, Inc. ION603/ION603 -EIP: 0060:[] EFLAGS: 00010286 CPU: 0 -EIP is at ext4_get_acl+0x80/0x210 -EAX: f5d3009c EBX: f5d30088 ECX: 00000000 EDX: f5d301d8 -ESI: ffffb4ff EDI: 00008000 EBP: f29b3dc8 ESP: f29b3da4 - DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068 -Process gnome-session-c (pid: 1798, ti=f29b2000 task=f2bd72c0 task.ti=f29b2000) -Stack: - f29b3db0 c113bb90 f5d301d8 f29b3de4 c11b9016 f5d3009c f5d30088 f5d30088 - 00000001 f29b3ddc c11e4cca 00000001 f5d30088 000081ed f29b3df0 c11313b7 - 00000021 00000021 f5d30088 f29b3e08 c1131b45 c11e4c80 f5d30088 00000021 -Call Trace: - [] ? d_splice_alias+0x40/0x50 - [] ? ext4_lookup.part.30+0x56/0x120 - [] ext4_check_acl+0x4a/0x90 - [] acl_permission_check+0x97/0xa0 - [] generic_permission+0x25/0xc0 - [] ? ext4_xattr_set_acl+0x160/0x160 - [] inode_permission+0x99/0xd0 - [] ? ext4_xattr_set_acl+0x160/0x160 - [] may_open+0x6b/0x110 - [] do_last+0x1a6/0x640 - [] path_openat+0x9d/0x350 - [] ? unlock_page+0x42/0x50 - [] ? __do_fault+0x3b0/0x4b0 - [] do_filp_open+0x31/0x80 - [] ? aa_dup_task_context+0x33/0x60 - [] ? apparmor_cred_prepare+0x2d/0x50 - [] open_exec+0x2f/0x110 - [] ? check_unsafe_exec+0xb7/0xf0 - [] do_execve_common+0x8a/0x270 - [] do_execve+0x17/0x20 - [] sys_execve+0x37/0x70 - [] ptregs_execve+0x12/0x18 - [] ? syscall_call+0x7/0xb -Code: 8d 76 00 8d 93 54 01 00 00 8b 32 85 f6 74 e2 8d 43 14 89 55 e4 89 45 f0 e8 2e 7e 34 00 8b 55 e4 8b 32 83 fe ff 74 07 85 f6 74 03 <3e> ff 06 8b 45 f0 e8 25 19 e4 ff 90 83 fe ff 75 b5 81 ff 00 40 -EIP: [] ext4_get_acl+0x80/0x210 SS:ESP 0068:f29b3da4 -CR2: 00000000ffffb4ff ----[ end trace b567e6a3070ffb42 ]---""" - bucket = "kernel paging request:ext4_get_acl+0x80/0x210:ext4_check_acl+0x4a/0x90:acl_permission_check+0x97/0xa0:generic_permission+0x25/0xc0:inode_permission+0x99/0xd0:may_open+0x6b/0x110:do_last+0x1a6/0x640:path_openat+0x9d/0x350:do_filp_open+0x31/0x80:open_exec+0x2f/0x110:do_execve_common+0x8a/0x270:do_execve+0x17/0x20:sys_execve+0x37/0x70:ptregs_execve+0x12/0x18" - report = apport.Report() - report["ProblemType"] = "KernelOops" - report["OopsText"] = oops_text - report_bson = bson.BSON.encode(report.data) - report_io = StringIO(report_bson) - environ = { - "CONTENT_TYPE": "application/octet-stream", - "PATH_INFO": sha512_system_uuid, - "wsgi.input": report_io, - } - - wsgi.app(environ, self.start_response) - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - bucket_cf = pycassa.ColumnFamily(pool, "Bucket") - bucket_id, unused_oops_id = next(bucket_cf.get_range()) - self.assertEqual(bucket_id, bucket) - - -class TestBinarySubmission(TestCrashSubmission): - def setUp(self): - super(TestBinarySubmission, self).setUp() - self.stack_addr_sig = ( - "/usr/bin/foo:11:x86_64/lib/x86_64-linux-gnu/libc-2.15.so+e4d93:" - "/usr/bin/foo+1e071" - ) - report = apport.Report() - report["ProblemType"] = "Crash" - report["StacktraceAddressSignature"] = self.stack_addr_sig - report["ExecutablePath"] = "/usr/bin/foo" - report["Package"] = "whoopsie 1.2.3" - report["DistroRelease"] = "Ubuntu 12.04" - report["StacktraceTop"] = "raise () from /lib/i386-linux-gnu/libc.so.6" - report["Signal"] = "11" - report_bson = bson.BSON.encode(report.data) - report_io = StringIO(report_bson) - self.environ = { - "CONTENT_TYPE": "application/octet-stream", - "PATH_INFO": sha512_system_uuid, - "wsgi.input": report_io, - } - - def test_binary_submission_not_retraced(self): - """If a binary crash has been submitted that we do not have a core file - for, either already retraced or awaiting to be retraced.""" - - resp = wsgi.app(self.environ, self.start_response)[0] - self.assertEqual(self.start_response.call_args[0][0], "200 OK") - # We should get back a request for the core file: - self.assertTrue(resp.endswith(" CORE")) - - # It should end up in the AwaitingRetrace CF queue. - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - awaiting_retrace_cf = pycassa.ColumnFamily(pool, "AwaitingRetrace") - oops_cf = pycassa.ColumnFamily(pool, "OOPS") - oops_id = oops_cf.get_range().next()[0] - self.assertEqual( - list(awaiting_retrace_cf.get(self.stack_addr_sig).keys())[0], oops_id - ) - - def test_binary_submission_retrace_queued(self): - """If a binary crash has been submitted that we do have a core file - for, but it has not been retraced yet.""" - # Lets pretend we've seen the stacktrace address signature before, and - # have received a core file for it, but have not finished retracing it: - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - awaiting_retrace_cf = pycassa.ColumnFamily(pool, "AwaitingRetrace") - oops_cf = pycassa.ColumnFamily(pool, "OOPS") - indexes_cf = pycassa.ColumnFamily(pool, "Indexes") - indexes_cf.insert("retracing", {self.stack_addr_sig: ""}) - - resp = wsgi.app(self.environ, self.start_response)[0] - self.assertEqual(self.start_response.call_args[0][0], "200 OK") - # We should not get back a request for the core file: - self.assertEqual(resp, "") - # Ensure the crash was bucketed and added to the AwaitingRetrace CF - # queue: - oops_id = oops_cf.get_range().next()[0] - self.assertEqual( - list(awaiting_retrace_cf.get(self.stack_addr_sig).keys())[0], oops_id - ) - - def test_binary_submission_already_retraced(self): - """If a binary crash has been submitted that we have a fully-retraced - core file for.""" - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - indexes_cf = pycassa.ColumnFamily(pool, "Indexes") - bucket_cf = pycassa.ColumnFamily(pool, "Bucket") - oops_cf = pycassa.ColumnFamily(pool, "OOPS") - - indexes_cf.insert( - "crash_signature_for_stacktrace_address_signature", - {self.stack_addr_sig: "fake-crash-signature"}, - ) - - resp = wsgi.app(self.environ, self.start_response)[0] - self.assertEqual(self.start_response.call_args[0][0], "200 OK") - # We should not get back a request for the core file: - self.assertEqual(resp, "") - - # Make sure 'foo' ends up in the bucket. - oops_id = oops_cf.get_range().next()[0] - bucket_contents = list(bucket_cf.get("fake-crash-signature").keys()) - self.assertEqual(bucket_contents, [pycassa.util.uuid.UUID(oops_id)]) - - -class TestCoreSubmission(TestSubmission): - def setUp(self): - super(TestCoreSubmission, self).setUp() - self.conn_mock = mock.MagicMock() - # TODO in the future, we may want to just set up a local Rabbit MQ, - # like we do with Cassandra. - amqp_connection = mock.patch("amqplib.client_0_8.Connection", self.conn_mock) - amqp_connection.start() - self.msg_mock = mock.MagicMock() - amqp_msg = mock.patch("amqplib.client_0_8.Message", self.msg_mock) - amqp_msg.start() - self.addCleanup(amqp_msg.stop) - self.addCleanup(amqp_connection.stop) - - def test_core_submission(self): - data = "I am an ELF binary. No, really." - core_io = StringIO(data) - uuid = "12345678-1234-5678-1234-567812345678" - environ = { - "QUERY_STRING": "uuid=%s&arch=amd64" % uuid, - "CONTENT_TYPE": "application/octet-stream", - "wsgi.input": core_io, - "PATH_INFO": "/%s/submit-core/amd64" % uuid, - } - stack_addr_sig = ( - "/usr/bin/foo:11:x86_64/lib/x86_64-linux-gnu/libc-2.15.so+e4d93:" - "/usr/bin/foo+1e071" - ) - path = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, path) - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - oops_cf = pycassa.ColumnFamily(pool, "OOPS") - oops_cf.insert(uuid, {"StacktraceAddressSignature": stack_addr_sig}) - - with mock.patch("daisy.submit_core.config", autospec=True) as cfg: - cfg.core_storage = {"local": {"type": "local", "path": path}} - cfg.storage_write_weights = {"local": 1.0} - cfg.write_weight_ranges = {"local": (0.0, 1.0)} - wsgi.app(environ, self.start_response) - self.assertEqual(self.start_response.call_args[0][0], "200 OK") - - # Did we actually write the core file to disk? - with open(os.path.join(path, uuid)) as fp: - contents = fp.read() - self.assertEqual(contents, data) - - # Did we put the crash on the retracing queue? - channel = self.conn_mock.return_value.channel - basic_publish_call = channel.return_value.basic_publish.call_args - kwargs = basic_publish_call[1] - self.assertEqual(kwargs["routing_key"], "retrace_amd64") - self.assertEqual(kwargs["exchange"], "") - msg = "%s:local" % uuid - self.assertEqual(self.msg_mock.call_args[0][0], msg) - self.assertTrue(os.path.exists(os.path.join(path, uuid))) - - # did we mark this as retracing in Cassandra? - indexes_cf = pycassa.ColumnFamily(pool, "Indexes") - indexes_cf.get("retracing", [stack_addr_sig]) - - def test_core_submission_s3(self): - from daisy import submit_core - - provider_data = { - "aws_access_key": "access", - "aws_secret_key": "secret", - "host": "does-not-exist.ubuntu.com", - "bucket": "core_files", - } - with tempfile.NamedTemporaryFile(mode="w") as fp: - fp.write("Core file contents.") - fp.flush() - with open(fp.name, "r") as f: - with mock.patch("boto.s3.connection.S3Connection") as s3con: - get_bucket = s3con.return_value.get_bucket - create_bucket = s3con.return_value.create_bucket - submit_core.write_to_s3(f, "oops-id", provider_data) - # Did we grab from the correct bucket? - get_bucket.assert_called_with("core_files") - new_key = get_bucket.return_value.new_key - # Did we create a new key in the bucket for the OOPS ID? - new_key.assert_called_with("oops-id") - - # Bucket does not exist. - from boto.exception import S3ResponseError - - get_bucket.side_effect = S3ResponseError("400", "No reason") - submit_core.write_to_s3(f, "oops-id", provider_data) - get_bucket.assert_called_with("core_files") - # Did we create the non-existent bucket? - create_bucket.assert_called_with("core_files") - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/test_weighting.py b/src/test/test_weighting.py deleted file mode 100644 index 2bea12b..0000000 --- a/src/test/test_weighting.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/python - -import datetime -import os -import time -import unittest -import uuid -from hashlib import sha512 - -import mock -import pycassa -from testtools import TestCase - -from daisy import config, schema -from oopsrepository import config as oopsconfig -from oopsrepository import schema as oopsschema -from oopsrepository.testing.cassandra import TemporaryOOPSDB - - -class T(TestCase): - def setUp(self): - super(T, self).setUp() - self.start_response = mock.Mock() - - # Set up daisy schema. - os.environ["OOPS_HOST"] = config.cassandra_hosts[0] - self.keyspace = self.useFixture(TemporaryOOPSDB()).keyspace - os.environ["OOPS_KEYSPACE"] = self.keyspace - config.cassandra_keyspace = self.keyspace - self.creds = { - "username": config.cassandra_username, - "password": config.cassandra_password, - } - schema.create() - - # Set up oopsrepository schema. - oops_config = oopsconfig.get_config() - oops_config["username"] = config.cassandra_username - oops_config["password"] = config.cassandra_password - oopsschema.create(oops_config) - - def test_weighting(self): - """Test the weighting of errors per calendar day. - The first error ever seen for a system running a given release - should be 0. - Subsequent errors should be the number of days since that first - error, divided by 90, up to 1.0. - """ - - # This has to go here and there can't be any other tests in this file, - # as these modules set up the Cassandra connections at import time. - from tools import ( - build_errors_by_release, - unique_systems_for_errors_by_release, - weight_errors_per_day, - ) - - # Configure the script that back populates the data to use our test - # Cassandra keyspace for writing data into. - pool = pycassa.ConnectionPool( - self.keyspace, config.cassandra_hosts, credentials=self.creds - ) - build_errors_by_release.write_pool = pool - args = (pool, "FirstError") - build_errors_by_release.firsterror = pycassa.ColumnFamily(*args) - args = (pool, "ErrorsByRelease") - build_errors_by_release.errorsbyrelease = pycassa.ColumnFamily(*args) - args = (pool, "SystemsForErrorsByRelease") - build_errors_by_release.systems = pycassa.ColumnFamily(*args) - oops = pycassa.ColumnFamily(pool, "OOPS") - - # Create three reports. The first one week ago, the second a single day - # after the first, and the third a day after that. - last_week = datetime.datetime.today() - datetime.timedelta(days=7) - last_week = last_week.replace(hour=0, minute=0, second=0, microsecond=0) - timestamps = [ - # Convert to microseconds for Cassandra. - time.mktime(last_week.timetuple()) * 1e6, - time.mktime((last_week + datetime.timedelta(days=1)).timetuple()) * 1e6, - time.mktime((last_week + datetime.timedelta(days=2)).timetuple()) * 1e6, - ] - - # All the reports for this test will be from the same machine, running - # Ubuntu 12.04. - ident = sha512("To be filled by OEM").hexdigest() - for timestamp in timestamps: - u = str(uuid.uuid1()) - d = {"DistroRelease": "Ubuntu 12.04", "SystemIdentifier": ident} - oops.insert(u, d, timestamp=timestamp) - - # We will process each error report to find the first occurance for - # each system for each release. Because we will not process these in - # time order, we'll need to go through a second time to write the - # correct values from FirstError (which isn't correct until we've seen - # all the data) into ErrorsByRelease. - # - # As an example, if we see a report from a day ago and write it into - # FirstError and ErrorsByRelease, then we see a report from a week ago - # and write it into FirstError and ErrorsByRelease, the data in - # FirstError will be correct, but that first error report value in - # ErrorsByRelease will be inaccurate, because it was based on a report - # from a week ago being the first error report seen for the given - # release. - build_errors_by_release.main() - build_errors_by_release.main() - - # Now actually produce the weights for all of last week (it will only - # process the three days there were reports). - start = last_week - end = datetime.datetime.today() - unique_systems_for_errors_by_release.main("Ubuntu 12.04", start, end) - weights = weight_errors_per_day.weight() - - # On the first day we had any error reports, the weighting would be 0 - # because 0 days have passed since the first report. - self.assertEqual(weights[timestamps[0] / 1e6], 0.0) - - # The second report is one day after the first, and the only report of - # the day. - self.assertEqual(weights[timestamps[1] / 1e6], 1 / 90.0) - - # The third report is two days after the first, and the only report of - # the day. - self.assertEqual(weights[timestamps[2] / 1e6], 2 / 90.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/tests/cloud-config.yaml b/src/tests/cloud-config.yaml new file mode 100644 index 0000000..12a092d --- /dev/null +++ b/src/tests/cloud-config.yaml @@ -0,0 +1,8 @@ +#cloud-config +ssh_pwauth: true +users: + - default + - name: spread + plain_text_passwd: SPREAD_PASSWORD + lock_passwd: false + sudo: ALL=(ALL) NOPASSWD:ALL diff --git a/src/tests/conftest.py b/src/tests/conftest.py new file mode 100644 index 0000000..c4a198c --- /dev/null +++ b/src/tests/conftest.py @@ -0,0 +1,47 @@ +# oops-repository is Copyright 2011 Canonical Ltd. +# +# Canonical Ltd ("Canonical") distributes the oops-repository source code under +# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file +# LICENSE in the source tree for more information. + +"""Test helpers for working with cassandra.""" + +import shutil +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest +from cassandra.cqlengine import management + +import retracer as et_retracer +from errortracker import cassandra + + +@pytest.fixture(scope="function") +def temporary_db(): + cassandra.KEYSPACE = "tmp" + cassandra.REPLICATION_FACTOR = 1 + cassandra.setup_cassandra() + yield + management.drop_keyspace(cassandra.KEYSPACE) + + +@pytest.fixture(scope="function") +def retracer(temporary_db): + temp = Path(tempfile.mkdtemp()) + config_dir = temp / "config" + sandbox_dir = temp / "sandbox" + config_dir.mkdir() + sandbox_dir.mkdir() + architecture = "amd64" + # Don't depend on apport-retrace being installed. + with patch("retracer.Popen") as popen: + popen.return_value.returncode = 0 + popen.return_value.communicate.return_value = ["/bin/false"] + yield et_retracer.Retracer( + config_dir=config_dir, + sandbox_dir=sandbox_dir, + architecture=architecture, + ) + shutil.rmtree(temp) diff --git a/src/test/pyflakes b/src/tests/pyflakes similarity index 100% rename from src/test/pyflakes rename to src/tests/pyflakes diff --git a/src/test/run b/src/tests/run similarity index 100% rename from src/test/run rename to src/tests/run diff --git a/src/tests/test_oopses.py b/src/tests/test_oopses.py new file mode 100644 index 0000000..7dc886b --- /dev/null +++ b/src/tests/test_oopses.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8; -*- +# oops-repository is Copyright 2011 Canonical Ltd. +# +# Canonical Ltd ("Canonical") distributes the oops-repository source code under +# the GNU Affero General Public License, version 3 ("AGPLv3"). See the file +# LICENSE in the source tree for more information. + +import json +import time +import uuid + +import pytest +from cassandra.cqlengine.query import DoesNotExist + +from errortracker import cassandra_schema, oopses + + +class TestPrune: + def test_fresh_oops_kept(self, temporary_db): + ts = time.time() + day_key = oopses.insert_dict("key", {"date": json.dumps(ts), "URL": "a bit boring"}) + + # Prune OOPSes + oopses.prune() + + # The oops is still readable and the day hasn't been purged. + oops = cassandra_schema.OOPS.get_as_dict(key=b"key") + assert oops == {"date": str(ts), "URL": "a bit boring"} + dayoopses = cassandra_schema.DayOOPS.filter(key=day_key.encode()) + found = False + for dayoops in dayoopses: + if dayoops.value.decode() == "key": + found = True + assert found is True, "DayOOPS object not found" + + def test_old_oops_deleted(self, temporary_db): + ts = time.time() + very_old_date = ts - 90 * oopses.DAY + old_date = ts - 31 * oopses.DAY + not_old_date = ts - 20 * oopses.DAY + very_old_day_key = time.strftime("%Y%m%d", time.gmtime(very_old_date)) + old_day_key = time.strftime("%Y%m%d", time.gmtime(old_date)) + not_old_day_key = time.strftime("%Y%m%d", time.gmtime(not_old_date)) + data = [ + { + "datestamp": very_old_date, + "day_key": very_old_day_key, + "key": "very_old_key", + "URL": "very boring", + }, + { + "datestamp": old_date, + "day_key": old_day_key, + "key": "old_key", + "URL": "boring", + }, + { + "datestamp": not_old_date, + "day_key": not_old_day_key, + "key": "not_old_key", + "URL": "not boring", + }, + ] + for oops in data: + cassandra_schema.DayOOPS.create( + key=oops["day_key"].encode(), column1=uuid.uuid1(), value=oops["key"].encode() + ) + cassandra_schema.OOPS.create( + key=oops["key"].encode(), column1="date", value=json.dumps(oops["datestamp"]) + ) + cassandra_schema.OOPS.create( + key=oops["key"].encode(), column1="URL", value=oops["URL"] + ) + + # Prune OOPSes + oopses.prune() + + with pytest.raises(DoesNotExist) as _: + cassandra_schema.OOPS.get(key=b"very_old_key") + with pytest.raises(DoesNotExist) as _: + cassandra_schema.OOPS.get(key=b"old_key") + assert ( + cassandra_schema.OOPS.get(key=b"not_old_key", column1="URL").value == "not boring" + ), "OOPS was pruned, but was too recent" + # The day index is cleared out too. + with pytest.raises(DoesNotExist) as _: + cassandra_schema.DayOOPS.get(key=very_old_day_key.encode()) + with pytest.raises(DoesNotExist) as _: + cassandra_schema.DayOOPS.get(key=old_day_key.encode()) + assert ( + cassandra_schema.DayOOPS.get(key=not_old_day_key.encode()).value.decode() + == "not_old_key" + ) + + +class TestInsert: + def _test_insert_check(self, oopsid, day_key, value=None): + if value is None: + value = "13000" + # The oops is retrievable + result = cassandra_schema.OOPS.get_as_dict(key=oopsid.encode()) + assert value == result["duration"] + # The oops has been indexed by day + oops_refs = cassandra_schema.DayOOPS.filter(key=day_key.encode()).only(["value"]) + assert [oopsid] == [day_oops.value.decode() for day_oops in oops_refs] + # TODO - the aggregates for the OOPS have been updated. + + def test_insert_oops_dict(self, temporary_db): + oopsid = str(uuid.uuid1()) + oops = {"duration": "13000"} + day_key = oopses.insert_dict(oopsid, oops) + self._test_insert_check(oopsid, day_key) + + def test_insert_unicode(self, temporary_db): + oopsid = str(uuid.uuid1()) + oops = {"duration": "♥"} + day_key = oopses.insert_dict(oopsid, oops) + self._test_insert_check(oopsid, day_key, value="♥") + + def test_insert_updates_counters(self, temporary_db): + oopsid = str(uuid.uuid1()) + oops = {"duration": "13000"} + user_token = "user1" + + day_key = oopses.insert_dict(oopsid, oops, user_token) + oops_count = cassandra_schema.Counters.filter(key=b"oopses", column1=day_key) + assert [1] == [count.value for count in oops_count] + + oopsid = str(uuid.uuid1()) + day_key = oopses.insert_dict(oopsid, oops, user_token) + oops_count = cassandra_schema.Counters.filter(key=b"oopses", column1=day_key) + assert [2] == [count.value for count in oops_count] + + +class TestBucket: + def test_insert_bucket(self, temporary_db): + fields = [ + "Ubuntu 12.04", + "Ubuntu 12.04:whoopsie", + "Ubuntu 12.04:whoopsie:3.04", + "whoopsie:3.04", + ] + oopsid = str(uuid.uuid1()) + oops = json.dumps({"duration": 13000}) + oopses.insert(oopsid, oops) + day_key = oopses.bucket(oopsid, "bucket-key", fields) + + assert [oopsid] == [ + str(bucket_entry.column1) + for bucket_entry in cassandra_schema.Bucket.filter(key="bucket-key") + ] + assert [1] == [ + counter.value + for counter in cassandra_schema.DayBucketsCount.filter( + key=day_key.encode(), column1="bucket-key" + ) + ] + + oopsid = str(uuid.uuid1()) + oops = json.dumps({"wibbles": 12}) + oopses.insert(oopsid, oops) + day_key = oopses.bucket(oopsid, "bucket-key", fields) + + # Check that the counters all exist and have two crashes. + resolutions = (day_key[:4], day_key[:6], day_key) + for field in fields: + for resolution in resolutions: + k = "%s:%s" % (field, resolution) + assert [2] == [ + counter.value + for counter in cassandra_schema.DayBucketsCount.filter( + key=k.encode(), column1="bucket-key" + ) + ] + for resolution in resolutions: + assert [2] == [ + counter.value + for counter in cassandra_schema.DayBucketsCount.filter( + key=resolution.encode(), column1="bucket-key" + ) + ] + + def test_update_bucket_metadata(self, temporary_db): + import apt + + # Does not exist yet. + oopses.update_bucket_metadata( + "bucket-id", + "whoopsie", + "1.2.3", + apt.apt_pkg.version_compare, + "Ubuntu 12.04", + ) + metadata = cassandra_schema.BucketMetadata.get_as_dict(key=b"bucket-id") + assert metadata["Source"] == "whoopsie" + assert metadata["FirstSeen"] == "1.2.3" + assert metadata["LastSeen"] == "1.2.3" + assert metadata["~Ubuntu 12.04:FirstSeen"] == "1.2.3" + assert metadata["~Ubuntu 12.04:LastSeen"] == "1.2.3" + + oopses.update_bucket_metadata( + "bucket-id", + "whoopsie", + "1.2.4", + apt.apt_pkg.version_compare, + "Ubuntu 12.04", + ) + metadata = cassandra_schema.BucketMetadata.get_as_dict(key=b"bucket-id") + assert metadata["Source"] == "whoopsie" + assert metadata["FirstSeen"] == "1.2.3" + assert metadata["LastSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.04:FirstSeen"] == "1.2.3" + assert metadata["~Ubuntu 12.04:LastSeen"] == "1.2.4" + + # Earlier version than the newest + oopses.update_bucket_metadata( + "bucket-id", + "whoopsie", + "1.2.4~ev1", + apt.apt_pkg.version_compare, + "Ubuntu 12.04", + ) + metadata = cassandra_schema.BucketMetadata.get_as_dict(key=b"bucket-id") + assert metadata["Source"] == "whoopsie" + assert metadata["FirstSeen"] == "1.2.3" + assert metadata["LastSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.04:FirstSeen"] == "1.2.3" + assert metadata["~Ubuntu 12.04:LastSeen"] == "1.2.4" + + # Earlier version than the earliest + oopses.update_bucket_metadata( + "bucket-id", + "whoopsie", + "1.2.2", + apt.apt_pkg.version_compare, + "Ubuntu 12.04", + ) + metadata = cassandra_schema.BucketMetadata.get_as_dict(key=b"bucket-id") + assert metadata["Source"] == "whoopsie" + assert metadata["FirstSeen"] == "1.2.2" + assert metadata["LastSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.04:FirstSeen"] == "1.2.2" + assert metadata["~Ubuntu 12.04:LastSeen"] == "1.2.4" + + # Same crash in newer Ubuntu release + oopses.update_bucket_metadata( + "bucket-id", + "whoopsie", + "1.2.4", + apt.apt_pkg.version_compare, + "Ubuntu 12.10", + ) + metadata = cassandra_schema.BucketMetadata.get_as_dict(key=b"bucket-id") + assert metadata["Source"] == "whoopsie" + assert metadata["FirstSeen"] == "1.2.2" + assert metadata["LastSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.04:FirstSeen"] == "1.2.2" + assert metadata["~Ubuntu 12.04:LastSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.10:FirstSeen"] == "1.2.4" + assert metadata["~Ubuntu 12.10:LastSeen"] == "1.2.4" + + def test_bucket_hashes(self, temporary_db): + # Test hashing + from hashlib import sha1 + + h = sha1(b"bucket-id").hexdigest() + oopses.update_bucket_hashes("bucket-id") + assert ( + "bucket-id" + == cassandra_schema.Hashes.get(key=f"bucket_{h[0]}".encode(), column1=h.encode()).value + ) + + def test_update_source_version_buckets(self, temporary_db): + src_package = "whoopsie" + version = "1.2.3" + oops_id = str(uuid.uuid1()) + oopses.update_source_version_buckets(src_package, version, oops_id) + + assert ( + oops_id + == cassandra_schema.SourceVersionBuckets.get(key=src_package, key2=version).column1 + ) + + def test_update_bucket_systems(self, temporary_db): + bucketid = "foo" + system_token = "system-id" + version = "1.0" + oopses.update_bucket_systems(bucketid, system_token, version=version) + + assert ( + system_token + == cassandra_schema.BucketVersionSystems2.get(key=bucketid, key2=version).column1 + ) diff --git a/src/tests/test_retracer.py b/src/tests/test_retracer.py new file mode 100644 index 0000000..1422826 --- /dev/null +++ b/src/tests/test_retracer.py @@ -0,0 +1,24 @@ +# -*- coding: utf8 -*- +import time + +from errortracker import cassandra_schema + + +class TestSubmission: + def test_update_retrace_stats(self, retracer): + release = "Ubuntu 12.04" + day_key = time.strftime("%Y%m%d", time.gmtime()) + + retracer.update_retrace_stats(release, day_key, 30, True) + result = cassandra_schema.RetraceStats.get_as_dict(key=day_key.encode()) + assert result["Ubuntu 12.04:success"] == 1 + mean_key = f"{day_key}:{release}:{retracer.architecture}" + counter_key = f"{mean_key}:count" + result = cassandra_schema.Indexes.get_as_dict(key=b"mean_retracing_time") + assert result[mean_key] == 30 + assert result[counter_key] == 1 + + retracer.update_retrace_stats(release, day_key, 40, True) + result = cassandra_schema.Indexes.get_as_dict(key=b"mean_retracing_time") + assert result[mean_key] == 35 + assert result[counter_key] == 2 diff --git a/src/tests/test_submit.py b/src/tests/test_submit.py new file mode 100644 index 0000000..f741b94 --- /dev/null +++ b/src/tests/test_submit.py @@ -0,0 +1,320 @@ +#!/usr/bin/python + +import shutil +import tempfile +import time +import uuid +from pathlib import Path + +import apport +import bson +import pytest + +from daisy.app import app as daisy_flask_app +from errortracker import amqp_utils, cassandra_schema, swift_utils + +# SHA-512 of the system-uuid +sha512_system_uuid = ( + "d78abb0542736865f94704521609c230dac03a2f369d043ac212d6" + "933b91410e06399e37f9c5cc88436a31737330c1c8eccb2c2f9f374" + "d62f716432a32d50fac" +) + + +@pytest.fixture() +def app(): + daisy_flask_app.config.update( + { + "TESTING": True, + } + ) + yield daisy_flask_app + + +@pytest.fixture() +def client(app): + return app.test_client() + + +@pytest.fixture() +def path(): + p = tempfile.mkdtemp() + yield Path(p) + shutil.rmtree(p) + + +class TestCrashSubmission: + def test_index_not_found(self, client): + response = client.post("/") + assert response.status_code == 404 + + def test_bogus_submission(self, client, temporary_db): + response = client.post("/my_super_awesome_system_id", data=b"") + assert response.status_code == 400 + assert b"Invalid BSON" in response.data + + def test_submission_eol_release(self, client, temporary_db): + """Ensure that a Python crash is accepted, bucketed, and that the + retracing ColumnFamilies remain untouched.""" + + report = apport.Report() + report["ProblemType"] = "Crash" + report["InterpreterPath"] = "/usr/bin/python" + report["ExecutablePath"] = "/usr/bin/foo" + report["DistroRelease"] = "Ubuntu 12.04" + report["Package"] = "ubiquity 2.34" + report["Traceback"] = ( + "Traceback (most recent call last):\n" + ' File "/usr/bin/foo", line 1, in \n' + " sys.exit(1)" + ) + report_bson = bson.BSON.encode(report.data) + response = client.post(f"/{sha512_system_uuid}", data=report_bson) + assert b"Ubuntu 12.04 is End of Life" in response.data + assert response.status_code == 400 + + def test_python_submission(self, client, temporary_db): + """Ensure that a Python crash is accepted, bucketed, and that the + retracing ColumnFamilies remain untouched.""" + report = apport.Report() + report["ProblemType"] = "Crash" + report["InterpreterPath"] = "/usr/bin/python" + report["ExecutablePath"] = "/usr/bin/foo" + report["DistroRelease"] = "Ubuntu 24.04" + report["Package"] = "ubiquity 2.34" + report["Traceback"] = ( + "Traceback (most recent call last):\n" + ' File "/usr/bin/foo", line 1, in \n' + " sys.exit(1)" + ) + report_bson = bson.BSON.encode(report.data) + response = client.post(f"/{sha512_system_uuid}", data=report_bson) + assert b" OOPSID" in response.data + assert response.status_code == 200 + + oops_id = response.data.decode().split(" ")[0] + crash_signature = "/usr/bin/foo: sys.exit(1):/usr/bin/foo@1" + + # Ensure the crash was bucketed: + bucket = cassandra_schema.Bucket.all()[0] + assert bucket.key == crash_signature + assert str(bucket.column1) == oops_id + + # A Python crash shouldn't touch the retracing CFs: + assert [] == list(cassandra_schema.AwaitingRetrace.all()) + assert [] == list(cassandra_schema.Stacktrace.all()) + assert [] == list(cassandra_schema.Indexes.all()) + + now = time.gmtime() + day_key = time.strftime("%Y%m%d", now) + month_key = time.strftime("%Y%m", now) + year_key = time.strftime("%Y", now) + release = report["DistroRelease"] + time_keys = (day_key, month_key, year_key) + keys = [] + for time_key in time_keys: + keys.append(f"{release}:{time_key}") + keys.append(f"{release}:ubiquity:{time_key}") + keys.append(f"{release}:ubiquity:2.34:{time_key}") + keys.append(f"ubiquity:2.34:{time_key}") + + for key in keys: + assert cassandra_schema.DayBucketsCount.get(key=key.encode()).value == 1 + + def test_kerneloops_submission(self, client, temporary_db): + oops_text = """BUG: unable to handle kernel paging request at ffffb4ff +IP: [] ext4_get_acl+0x80/0x210 +*pde = 01874067 *pte = 00000000 +Oops: 0000 [#1] SMP +Modules linked in: bnep rfcomm bluetooth dm_crypt olpc_xo1 scx200_acb snd_cs5535audio snd_ac97_codec ac97_bus snd_pcm snd_seq_midi snd_rawmidi snd_seq_midi_event snd_seq snd_timer snd_seq_device snd cs5535_gpio soundcore snd_page_alloc binfmt_misc geode_aes cs5535_mfd geode_rng msr vesafb usbhid hid 8139too pata_cs5536 8139cp + +Pid: 1798, comm: gnome-session-c Not tainted 3.0.0-11-generic #17-Ubuntu First International Computer, Inc. ION603/ION603 +EIP: 0060:[] EFLAGS: 00010286 CPU: 0 +EIP is at ext4_get_acl+0x80/0x210 +EAX: f5d3009c EBX: f5d30088 ECX: 00000000 EDX: f5d301d8 +ESI: ffffb4ff EDI: 00008000 EBP: f29b3dc8 ESP: f29b3da4 + DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068 +Process gnome-session-c (pid: 1798, ti=f29b2000 task=f2bd72c0 task.ti=f29b2000) +Stack: + f29b3db0 c113bb90 f5d301d8 f29b3de4 c11b9016 f5d3009c f5d30088 f5d30088 + 00000001 f29b3ddc c11e4cca 00000001 f5d30088 000081ed f29b3df0 c11313b7 + 00000021 00000021 f5d30088 f29b3e08 c1131b45 c11e4c80 f5d30088 00000021 +Call Trace: + [] ? d_splice_alias+0x40/0x50 + [] ? ext4_lookup.part.30+0x56/0x120 + [] ext4_check_acl+0x4a/0x90 + [] acl_permission_check+0x97/0xa0 + [] generic_permission+0x25/0xc0 + [] ? ext4_xattr_set_acl+0x160/0x160 + [] inode_permission+0x99/0xd0 + [] ? ext4_xattr_set_acl+0x160/0x160 + [] may_open+0x6b/0x110 + [] do_last+0x1a6/0x640 + [] path_openat+0x9d/0x350 + [] ? unlock_page+0x42/0x50 + [] ? __do_fault+0x3b0/0x4b0 + [] do_filp_open+0x31/0x80 + [] ? aa_dup_task_context+0x33/0x60 + [] ? apparmor_cred_prepare+0x2d/0x50 + [] open_exec+0x2f/0x110 + [] ? check_unsafe_exec+0xb7/0xf0 + [] do_execve_common+0x8a/0x270 + [] do_execve+0x17/0x20 + [] sys_execve+0x37/0x70 + [] ptregs_execve+0x12/0x18 + [] ? syscall_call+0x7/0xb +Code: 8d 76 00 8d 93 54 01 00 00 8b 32 85 f6 74 e2 8d 43 14 89 55 e4 89 45 f0 e8 2e 7e 34 00 8b 55 e4 8b 32 83 fe ff 74 07 85 f6 74 03 <3e> ff 06 8b 45 f0 e8 25 19 e4 ff 90 83 fe ff 75 b5 81 ff 00 40 +EIP: [] ext4_get_acl+0x80/0x210 SS:ESP 0068:f29b3da4 +CR2: 00000000ffffb4ff +---[ end trace b567e6a3070ffb42 ]---""" + report = apport.Report() + report["ProblemType"] = "KernelOops" + report["Package"] = "linux" + report["OopsText"] = oops_text + report_bson = bson.BSON.encode(report.data) + response = client.post(f"/{sha512_system_uuid}", data=report_bson) + assert b" OOPSID" in response.data + assert response.status_code == 200 + + # XXX kernel crash bucketing not working yet + # oops_id = response.data.decode().split(" ")[0] + # crash_signature = "kernel paging request:ext4_get_acl+0x80/0x210:ext4_check_acl+0x4a/0x90:acl_permission_check+0x97/0xa0:generic_permission+0x25/0xc0:inode_permission+0x99/0xd0:may_open+0x6b/0x110:do_last+0x1a6/0x640:path_openat+0x9d/0x350:do_filp_open+0x31/0x80:open_exec+0x2f/0x110:do_execve_common+0x8a/0x270:do_execve+0x17/0x20:sys_execve+0x37/0x70:ptregs_execve+0x12/0x18" + + # Ensure the crash was bucketed: + # bucket = cassandra_schema.Bucket.all()[0] + # assert bucket.key == crash_signature + # assert str(bucket.column1) == oops_id + + +class TestBinarySubmission: + def setup_method(self): + self.stack_addr_sig = ( + "/usr/bin/foo:11:x86_64/lib/x86_64-linux-gnu/libc-2.15.so+e4d93:/usr/bin/foo+1e071" + ) + self.report = apport.Report() + self.report["ProblemType"] = "Crash" + self.report["StacktraceAddressSignature"] = self.stack_addr_sig + self.report["ExecutablePath"] = "/usr/bin/foo" + self.report["Package"] = "whoopsie 1.2.3" + self.report["DistroRelease"] = "Ubuntu 24.04" + self.report["StacktraceTop"] = "raise () from /lib/i386-linux-gnu/libc.so.6" + self.report["Signal"] = "11" + self.report_bson = bson.BSON.encode(self.report.data) + + def test_binary_submission_not_retraced(self, client, temporary_db): + """If a binary crash has been submitted that we do not have a core file + for, either already retraced or awaiting to be retraced.""" + self.setup_method() + + response = client.post(f"/{sha512_system_uuid}", data=self.report_bson) + # We should get back a request for the core file: + assert response.data.decode().endswith(" CORE") + assert response.status_code == 200 + + oops_id = response.data.decode().split(" ")[0] + + # It should end up in the AwaitingRetrace queue. + retrace_queue = cassandra_schema.AwaitingRetrace.all()[0] + assert retrace_queue.key == self.stack_addr_sig + assert retrace_queue.column1 == oops_id + + def test_binary_submission_retrace_queued(self, client, temporary_db): + """If a binary crash has been submitted that we do have a core file + for, but it has not been retraced yet.""" + self.setup_method() + + # Lets pretend we've seen the stacktrace address signature before, and + # have received a core file for it, but have not finished retracing it: + cassandra_schema.AwaitingRetrace.create(key=self.stack_addr_sig, column1=str(uuid.uuid1())) + cassandra_schema.Indexes.create(key=b"retracing", column1=self.stack_addr_sig, value=b"") + + response = client.post(f"/{sha512_system_uuid}", data=self.report_bson) + # We should not get back a request for the core file: + assert b" OOPSID" in response.data + assert not response.data.decode().endswith(" CORE") + assert response.status_code == 200 + + oops_id = response.data.decode().split(" ")[0] + + # It should end up in the AwaitingRetrace queue. + retrace_queue = cassandra_schema.AwaitingRetrace.all() + found = False + for r in retrace_queue: + assert r.key == self.stack_addr_sig + if r.column1 == oops_id: + found = True + assert found, f"{oops_id} not found in AwaitingRetrace" + + def test_binary_submission_already_retraced(self, client, temporary_db): + """If a binary crash has been submitted that we have a fully-retraced + core file for.""" + self.setup_method() + + cassandra_schema.Indexes.create( + key=b"crash_signature_for_stacktrace_address_signature", + column1=self.stack_addr_sig, + value=b"fake-crash-signature", + ) + cassandra_schema.Stacktrace.create( + key=self.stack_addr_sig.encode(), column1="Stacktrace", value="fake full stacktrace" + ) + cassandra_schema.Stacktrace.create( + key=self.stack_addr_sig.encode(), + column1="ThreadStacktrace", + value="fake thread stacktrace", + ) + + response = client.post(f"/{sha512_system_uuid}", data=self.report_bson) + # We should not get back a request for the core file: + assert b" OOPSID" in response.data + assert not response.data.decode().endswith(" CORE") + assert response.status_code == 200 + + oops_id = response.data.decode().split(" ")[0] + + # Make sure 'foo' ends up in the bucket. + buckets = list(cassandra_schema.Bucket.filter(key="fake-crash-signature")) + + assert len(buckets) == 1 + assert oops_id == str(buckets[0].column1) + + +class TestCoreSubmission: + def test_core_submission(self, client, temporary_db, path): + data = "I am an ELF binary. No, really." + uuid = "12345678-1234-5678-1234-567812345678" + stack_addr_sig = ( + "/usr/bin/foo:11:x86_64/lib/x86_64-linux-gnu/libc-2.15.so+e4d93:/usr/bin/foo+1e071" + ) + cassandra_schema.OOPS.create( + key=uuid.encode(), column1="StacktraceAddressSignature", value=stack_addr_sig + ) + cassandra_schema.OOPS.create( + key=uuid.encode(), column1="SystemIdentifier", value=stack_addr_sig + ) + + response = client.post(f"/{uuid}/submit-core/amd64/{sha512_system_uuid}", data=data) + + # We should not get back a request for the core file: + assert uuid.encode() == response.data + assert response.status_code == 200 + + # Did we actually write the core file to swift? + _, contents = swift_utils.get_swift_client().get_object("cores", uuid) + assert contents.decode() == data + + with amqp_utils.get_connection() as c: + assert c is not None, "Could not connect to RabbitMQ" + + ch = c.channel() + + def on_message(message): + assert message.body == f"{uuid}:swift" + ch.basic_ack(message.delivery_tag) + + ch.basic_consume(queue="retrace_amd64", callback=on_message) + c.drain_events() # only one event, and in the tests, no need for the usual infinite loop + ch.close() + + # did we mark this as retracing in Cassandra? + assert cassandra_schema.Indexes.get(key=b"retracing").column1 == stack_addr_sig diff --git a/src/tools/import_bugs.py b/src/tools/import_bugs.py index a16c0dc..f02d477 100755 --- a/src/tools/import_bugs.py +++ b/src/tools/import_bugs.py @@ -3,20 +3,10 @@ import sqlite3 from urllib.request import urlretrieve -from cassandra import ConsistencyLevel -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster +from errortracker import cassandra -from daisy import config +session = cassandra.cassandra_session() -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) -session.default_consistency_level = ( - ConsistencyLevel.LOCAL_ONE -) # TODO: do something about that deprecation warning bm_table_insert = session.prepare( "INSERT INTO \"BucketMetadata\" (key, column1, value) VALUES (?, 'LaunchpadBug', ?)" ) diff --git a/src/tools/import_team_packages.py b/src/tools/import_team_packages.py index d318465..38f2f86 100755 --- a/src/tools/import_team_packages.py +++ b/src/tools/import_team_packages.py @@ -3,24 +3,16 @@ from contextlib import suppress import requests -from cassandra import ConsistencyLevel -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster from launchpadlib.errors import ResponseError from launchpadlib.launchpad import Launchpad -from daisy import config +from errortracker import cassandra -SRC_PACKAGE_TEAM_MAPPING = ( - "https://ubuntu-archive-team.ubuntu.com/package-team-mapping.json" -) +SRC_PACKAGE_TEAM_MAPPING = "https://ubuntu-archive-team.ubuntu.com/package-team-mapping.json" + + +session = cassandra.cassandra_session() -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) -session.default_consistency_level = ConsistencyLevel.LOCAL_ONE user_binary_packages_insert = session.prepare( 'INSERT INTO "UserBinaryPackages" (key, column1, value) VALUES (?, ?, 0x)' ) @@ -52,9 +44,7 @@ def import_user_binary_packages(team_name, src_pkgs): with suppress(IndexError, ResponseError): binary_packages = get_binary_packages(src_pkg) for binary_package in binary_packages: - session.execute( - user_binary_packages_insert, [team_name, binary_package] - ) + session.execute(user_binary_packages_insert, [team_name, binary_package]) if __name__ == "__main__": diff --git a/src/tools/remove_an_oops.py b/src/tools/remove_an_oops.py index f4e39a3..b269e40 100755 --- a/src/tools/remove_an_oops.py +++ b/src/tools/remove_an_oops.py @@ -5,18 +5,12 @@ import sys from time import sleep -from cassandra import ConsistencyLevel, OperationTimedOut -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, NoHostAvailable +from cassandra import OperationTimedOut +from cassandra.cluster import NoHostAvailable -from daisy import config +from errortracker import cassandra -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) -session.default_consistency_level = ConsistencyLevel.LOCAL_ONE +session = cassandra.cassandra_session() URL = "https://errors.ubuntu.com/oops/" @@ -33,9 +27,7 @@ oops_lookup_stmt = session.prepare('SELECT * FROM "OOPS" WHERE key=?') oops_delete_stmt = session.prepare('DELETE FROM "OOPS" WHERE key=?') - useroops_delete_stmt = session.prepare( - 'DELETE FROM "UserOOPS" WHERE key=? AND column1=?' - ) + useroops_delete_stmt = session.prepare('DELETE FROM "UserOOPS" WHERE key=? AND column1=?') max_retries = 5 for i in range(max_retries): diff --git a/src/tools/remove_old_data.py b/src/tools/remove_old_data.py index 762f567..99b7025 100755 --- a/src/tools/remove_old_data.py +++ b/src/tools/remove_old_data.py @@ -6,23 +6,16 @@ from time import sleep from cassandra import OperationTimedOut -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, NoHostAvailable +from cassandra.cluster import NoHostAvailable -from daisy import config +from errortracker import cassandra -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) +session = cassandra.cassandra_session() # use a prepared statement which is less resource intensive oops_lookup_stmt = session.prepare('SELECT * FROM "OOPS" WHERE key = ?') oops_delete_stmt = session.prepare('DELETE FROM "OOPS" WHERE key = ?') -dayoops_delete_stmt = session.prepare( - 'DELETE FROM "DayOOPS" WHERE key = ? AND column1 = ?' -) +dayoops_delete_stmt = session.prepare('DELETE FROM "DayOOPS" WHERE key = ? AND column1 = ?') URL = "https://errors.ubuntu.com/oops/" @@ -79,9 +72,7 @@ def remove_oops(oops_id): sys.argv.remove("--no-dry-run") else: dry_run = True - print( - "Running by default in dry-run mode. Pass --no-dry-run to really delete stuff." - ) + print("Running by default in dry-run mode. Pass --no-dry-run to really delete stuff.") # Range of dates for which all OOPSes start_date = datetime.strptime(sys.argv[1], "%Y-%m-%d").date() @@ -107,9 +98,7 @@ def remove_oops(oops_id): % (URL, oops_id.value.decode(), start_date), end="", ) - if remove_oops(oops_id.value) and remove_dayoops( - start_date, oops_id.column1 - ): + if remove_oops(oops_id.value) and remove_dayoops(start_date, oops_id.column1): print(" SUCCESS") count_success += 1 else: @@ -118,6 +107,5 @@ def remove_oops(oops_id): except KeyboardInterrupt: pass print( - "Finishing cleaning OOPSes: %s successes and %s failures" - % (count_success, count_failure) + "Finishing cleaning OOPSes: %s successes and %s failures" % (count_success, count_failure) ) diff --git a/src/tools/remove_old_release_data.py b/src/tools/remove_old_release_data.py index 8e39493..9c7be22 100755 --- a/src/tools/remove_old_release_data.py +++ b/src/tools/remove_old_release_data.py @@ -6,18 +6,13 @@ from time import sleep import distro_info -from cassandra import ConsistencyLevel, OperationTimedOut -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster, NoHostAvailable +from cassandra import OperationTimedOut +from cassandra.cluster import NoHostAvailable -from daisy import config +from errortracker import cassandra + +session = cassandra.cassandra_session() -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) -session.default_consistency_level = ConsistencyLevel.LOCAL_ONE oops_lookup_stmt = session.prepare('SELECT * FROM "OOPS" WHERE key=?') oops_delete_stmt = session.prepare('DELETE FROM "OOPS" WHERE key=? AND column1=?') diff --git a/src/tools/swift_corrupt_core_check.py b/src/tools/swift_corrupt_core_check.py index b842355..56c115b 100755 --- a/src/tools/swift_corrupt_core_check.py +++ b/src/tools/swift_corrupt_core_check.py @@ -10,7 +10,7 @@ import swiftclient -from daisy import config +from errortracker import config, swift_utils # get container returns a max of 10000 listings, if an integer is not given # lets get everything not 10k. @@ -21,30 +21,12 @@ else: unlimited = True -cs = getattr(config, "core_storage", "") -if not cs: - print("core_storage not set.") - sys.exit(1) - -provider_data = cs["swift"] -opts = { - "tenant_name": provider_data["os_tenant_name"], - "region_name": provider_data["os_region_name"], -} -_cached_swift = swiftclient.client.Connection( - provider_data["os_auth_url"], - provider_data["os_username"], - provider_data["os_password"], - os_options=opts, - auth_version="3.0", -) -bucket = provider_data["bucket"] +swift_client = swift_utils.get_swift_client() +bucket = config.swift_bucket gdb_which = Popen(["which", "gdb"], stdout=PIPE, universal_newlines=True) gdb_path = gdb_which.communicate()[0].strip() -_cached_swift.http_conn = None - count = 0 unqueued_count = 0 @@ -58,9 +40,7 @@ def rm_eff(path): raise -for container in _cached_swift.get_container( - container=bucket, limit=limit, full_listing=unlimited -): +for container in swift_client.get_container(container=bucket, limit=limit, full_listing=unlimited): # the dict is the metadata for the container if isinstance(container, dict): continue @@ -75,9 +55,7 @@ def rm_eff(path): fd, path = tempfile.mkstemp(fmt) os.close(fd) try: - headers, body = _cached_swift.get_object( - bucket, uuid, resp_chunk_size=65536 - ) + headers, body = swift_client.get_object(bucket, uuid, resp_chunk_size=65536) with open(path, "wb") as fp: for chunk in body: fp.write(chunk) @@ -101,7 +79,7 @@ def rm_eff(path): print(line) # We couldn't decompress this, so there's no value in trying again. try: - _cached_swift.delete_object(bucket, uuid) + swift_client.delete_object(bucket, uuid) except swiftclient.client.ClientException as e: if "404 Not Found" in str(e): rm_eff(core_file) @@ -112,14 +90,12 @@ def rm_eff(path): continue # confirm that gdb thinks the core file is good gdb_cmd = [gdb_path, "--batch", "--ex", "target core %s" % core_file] - proc = Popen( - gdb_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, errors="replace" - ) + proc = Popen(gdb_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, errors="replace") (out, err) = proc.communicate() if "is truncated: expected core file size" in err or "not a core dump" in err: # Not a core file, there's no value in trying again. try: - _cached_swift.delete_object(bucket, uuid) + swift_client.delete_object(bucket, uuid) except swiftclient.client.ClientException as e: if "404 Not Found" in str(e): rm_eff(core_file) diff --git a/src/tools/swift_handle_old_cores.py b/src/tools/swift_handle_old_cores.py index 8457acc..bca3f4c 100755 --- a/src/tools/swift_handle_old_cores.py +++ b/src/tools/swift_handle_old_cores.py @@ -6,56 +6,28 @@ import atexit import sys -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import amqp import swiftclient -from cassandra import ConsistencyLevel -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster -from daisy import config, utils +from errortracker import amqp_utils, cassandra, config, swift_utils, utils limit = None if len(sys.argv) == 2: limit = int(sys.argv[1]) -cs = getattr(config, "core_storage", "") -if not cs: - print("core_storage not set.") - sys.exit(1) +swift_client = swift_utils.get_swift_client() +bucket = config.swift_bucket -provider_data = cs["swift"] -opts = { - "tenant_name": provider_data["os_tenant_name"], - "region_name": provider_data["os_region_name"], -} -_cached_swift = swiftclient.client.Connection( - provider_data["os_auth_url"], - provider_data["os_username"], - provider_data["os_password"], - os_options=opts, - auth_version="3.0", -) -bucket = provider_data["bucket"] +session = cassandra.cassandra_session() -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) -session.default_consistency_level = ConsistencyLevel.LOCAL_ONE - -_cached_swift.http_conn = None -connection = amqp.Connection( - host=config.amqp_host, userid=config.amqp_username, password=config.amqp_password -) -connection.connect() +connection = amqp_utils.get_connection() channel = connection.channel() atexit.register(connection.close) atexit.register(channel.close) -now = datetime.now(UTC) +now = datetime.now(timezone.utc) abitago = now - timedelta(7) count = 0 queued_count = 0 @@ -65,7 +37,7 @@ def remove_core(bucket, core): global removed_count try: - _cached_swift.delete_object(bucket, core) + swift_client.delete_object(bucket, core) removed_count += 1 print("removed %s from swift" % core, file=sys.stderr) except swiftclient.client.ClientException as e: @@ -74,7 +46,7 @@ def remove_core(bucket, core): print("%s not found in swift" % core, file=sys.stderr) -for container in _cached_swift.get_container(container=bucket, limit=limit): +for container in swift_client.get_container(container=bucket, limit=limit): # the dict is the metadata for the container if isinstance(container, dict): continue @@ -83,9 +55,7 @@ def remove_core(bucket, core): uuid = core["name"] count += 1 try: - arch = session.execute(oops_lookup, [uuid.encode(), "Architecture"]).one()[ - 0 - ] + arch = session.execute(oops_lookup, [uuid.encode(), "Architecture"]).one()[0] except (IndexError, TypeError): arch = "" if not arch: @@ -93,9 +63,7 @@ def remove_core(bucket, core): remove_core(bucket, uuid) continue try: - release = session.execute( - oops_lookup, [uuid.encode(), "DistroRelease"] - ).one()[0] + release = session.execute(oops_lookup, [uuid.encode(), "DistroRelease"]).one()[0] except (IndexError, TypeError): release = "" if not release: diff --git a/src/tools/unique_users_daily_update.py b/src/tools/unique_users_daily_update.py index 3be0323..caa6738 100755 --- a/src/tools/unique_users_daily_update.py +++ b/src/tools/unique_users_daily_update.py @@ -4,17 +4,11 @@ import sys import distro_info -from cassandra.auth import PlainTextAuthProvider -from cassandra.cluster import Cluster from cassandra.query import SimpleStatement -from daisy import config +from errortracker import cassandra -auth_provider = PlainTextAuthProvider( - username=config.cassandra_username, password=config.cassandra_password -) -cluster = Cluster(config.cassandra_hosts, auth_provider=auth_provider) -session = cluster.connect(config.cassandra_keyspace) +session = cassandra.cassandra_session() d = distro_info.UbuntuDistroInfo() @@ -38,9 +32,7 @@ def _date_range_iterator(start, finish): releases = [ "Ubuntu " + r.replace(" LTS", "") - for r in sorted( - set(d.supported(result="release") + d.supported_esm(result="release")) - ) + for r in sorted(set(d.supported(result="release") + d.supported_esm(result="release"))) ] try: releases.append("Ubuntu " + d.devel(result="release")) @@ -71,9 +63,7 @@ def _date_range_iterator(start, finish): print(f"found {user_count} users") # value is the number of users uu_results = session.execute( - SimpleStatement( - 'SELECT value from "UniqueUsers90Days" WHERE key=%s and column1=%s' - ), + SimpleStatement('SELECT value from "UniqueUsers90Days" WHERE key=%s and column1=%s'), [release, formatted], ) try: diff --git a/tests/errortracker/functional/task.yaml b/tests/errortracker/functional/task.yaml new file mode 100644 index 0000000..8a62683 --- /dev/null +++ b/tests/errortracker/functional/task.yaml @@ -0,0 +1,6 @@ +summary: Run error tracker tests +execute: | + pushd "$SPREAD_PATH"/src + python3 -m pytest -ra --cov=$(pwd) --cov-branch --cov-report=xml --durations=0 tests +# artifacts: +# - coverage.xml diff --git a/tests/errortracker/integration/data/config/Ubuntu 24.04/codename b/tests/errortracker/integration/data/config/Ubuntu 24.04/codename new file mode 100644 index 0000000..465786f --- /dev/null +++ b/tests/errortracker/integration/data/config/Ubuntu 24.04/codename @@ -0,0 +1 @@ +noble diff --git a/tests/errortracker/integration/data/config/Ubuntu 24.04/sources.list b/tests/errortracker/integration/data/config/Ubuntu 24.04/sources.list new file mode 100644 index 0000000..694c5d8 --- /dev/null +++ b/tests/errortracker/integration/data/config/Ubuntu 24.04/sources.list @@ -0,0 +1,13 @@ +deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse +deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse +deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble-proposed main restricted universe multiverse +deb [trusted=yes] http://security.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse + +deb-src [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse +deb-src [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse +deb-src [trusted=yes] http://archive.ubuntu.com/ubuntu/ noble-proposed main restricted universe multiverse +deb-src [trusted=yes] http://security.ubuntu.com/ubuntu/ noble-security main restricted universe multiverse + +deb [trusted=yes] http://ddebs.ubuntu.com noble main restricted universe multiverse +deb [trusted=yes] http://ddebs.ubuntu.com noble-updates main restricted universe multiverse +deb [trusted=yes] http://ddebs.ubuntu.com noble-proposed main restricted universe multiverse diff --git a/tests/errortracker/integration/data/config/crashdb.conf b/tests/errortracker/integration/data/config/crashdb.conf new file mode 100644 index 0000000..7cb9030 --- /dev/null +++ b/tests/errortracker/integration/data/config/crashdb.conf @@ -0,0 +1,12 @@ +# map crash database names to CrashDatabase implementations and URLs + +default = 'debug' + +databases = { + 'debug': { + # for debugging + 'impl': 'memory', + 'bug_pattern_url': 'file:///tmp/bugpatterns.xml', + 'distro': 'debug' + }, +} diff --git a/tests/errortracker/integration/data/crash/_usr_bin_cat.2001.crash b/tests/errortracker/integration/data/crash/_usr_bin_cat.2001.crash new file mode 100644 index 0000000..5594ef8 --- /dev/null +++ b/tests/errortracker/integration/data/crash/_usr_bin_cat.2001.crash @@ -0,0 +1,259 @@ +ProblemType: Crash +ApportVersion: 2.28.1-0ubuntu3.8 +Architecture: amd64 +CasperMD5CheckResult: unknown +CloudBuildName: ubuntu-base:buildd +CloudSerial: 20250206 +Date: Wed Oct 22 14:40:48 2025 +Dependencies: + gcc-14-base 14.2.0-4ubuntu2~24.04 + libacl1 2.3.2-1build1.1 + libattr1 1:2.5.2-1build1.1 + libc6 2.39-0ubuntu8.6 + libgcc-s1 14.2.0-4ubuntu2~24.04 + libgmp10 2:6.3.0+dfsg-2ubuntu6.1 + libidn2-0 2.3.7-2build1.1 + libpcre2-8-0 10.42-4ubuntu2.1 + libselinux1 3.5-2ubuntu2.1 + libssl3t64 3.0.13-0ubuntu3.6 + libunistring5 1.1-2build1.1 +Disassembly: + => 0x7ffff7d1ba91 : cmp $0xfffffffffffff000,%rax + 0x7ffff7d1ba97 : ja 0x7ffff7d1bae8 + 0x7ffff7d1ba99 : ret + 0x7ffff7d1ba9a : nopw 0x0(%rax,%rax,1) + 0x7ffff7d1baa0 : push %rbp + 0x7ffff7d1baa1 : mov %rsp,%rbp + 0x7ffff7d1baa4 : sub $0x20,%rsp + 0x7ffff7d1baa8 : mov %rdx,-0x18(%rbp) + 0x7ffff7d1baac : mov %rsi,-0x10(%rbp) + 0x7ffff7d1bab0 : mov %edi,-0x8(%rbp) + 0x7ffff7d1bab3 : call 0x7ffff7c98550 + 0x7ffff7d1bab8 : mov -0x18(%rbp),%rdx + 0x7ffff7d1babc : mov -0x10(%rbp),%rsi + 0x7ffff7d1bac0 : mov %eax,%r8d + 0x7ffff7d1bac3 : mov -0x8(%rbp),%edi + 0x7ffff7d1bac6 : xor %eax,%eax +DistroRelease: Ubuntu 24.04 +ExecutablePath: /usr/bin/cat +ExecutableTimestamp: 1750609299 +Package: coreutils 9.4-3ubuntu6.1 +PackageArchitecture: amd64 +ProcCmdline: /usr/bin/cat +ProcCpuinfoMinimal: + processor : 3 + vendor_id : AuthenticAMD + cpu family : 23 + model : 49 + model name : AMD EPYC-Rome Processor + stepping : 0 + microcode : 0x1000065 + cpu MHz : 2499.998 + cache size : 512 KB + physical id : 3 + siblings : 1 + core id : 0 + cpu cores : 1 + apicid : 3 + initial apicid : 3 + fpu : yes + fpu_exception : yes + cpuid level : 13 + wp : yes + flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm rep_good nopl cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm svm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext perfctr_core ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 clzero xsaveerptr wbnoinvd arat npt nrip_save umip rdpid + bugs : sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass retbleed smt_rsb srso ibpb_no_ret + bogomips : 4999.99 + TLB size : 1024 4K pages + clflush size : 64 + cache_alignment : 64 + address sizes : 40 bits physical, 48 bits virtual + power management: +ProcCwd: /build/apport-test-crashes-XYncgo/apport-test-crashes-0.20251022 +ProcEnviron: + LANG=C.UTF-8 + LC_COLLATE=C.UTF-8 + SHELL=/bin/sh + TERM=unknown +ProcMaps: + 555555554000-555555556000 r--p 00000000 fd:01 528040 /usr/bin/cat + 555555556000-55555555b000 r-xp 00002000 fd:01 528040 /usr/bin/cat + 55555555b000-55555555d000 r--p 00007000 fd:01 528040 /usr/bin/cat + 55555555d000-55555555e000 r--p 00008000 fd:01 528040 /usr/bin/cat + 55555555e000-55555555f000 rw-p 00009000 fd:01 528040 /usr/bin/cat + 55555555f000-555555580000 rw-p 00000000 00:00 0 [heap] + 7ffff7c00000-7ffff7c28000 r--p 00000000 fd:01 527844 /usr/lib/x86_64-linux-gnu/libc.so.6 + 7ffff7c28000-7ffff7db0000 r-xp 00028000 fd:01 527844 /usr/lib/x86_64-linux-gnu/libc.so.6 + 7ffff7db0000-7ffff7dff000 r--p 001b0000 fd:01 527844 /usr/lib/x86_64-linux-gnu/libc.so.6 + 7ffff7dff000-7ffff7e03000 r--p 001fe000 fd:01 527844 /usr/lib/x86_64-linux-gnu/libc.so.6 + 7ffff7e03000-7ffff7e05000 rw-p 00202000 fd:01 527844 /usr/lib/x86_64-linux-gnu/libc.so.6 + 7ffff7e05000-7ffff7e12000 rw-p 00000000 00:00 0 + 7ffff7f2d000-7ffff7f4f000 rw-p 00000000 00:00 0 + 7ffff7f4f000-7ffff7fa8000 r--p 00000000 fd:01 525231 /usr/lib/locale/C.utf8/LC_CTYPE + 7ffff7fa8000-7ffff7fa9000 r--p 00000000 fd:01 525261 /usr/lib/locale/C.utf8/LC_NUMERIC + 7ffff7fa9000-7ffff7faa000 r--p 00000000 fd:01 525279 /usr/lib/locale/C.utf8/LC_TIME + 7ffff7faa000-7ffff7fab000 r--p 00000000 fd:01 525230 /usr/lib/locale/C.utf8/LC_COLLATE + 7ffff7fab000-7ffff7fac000 r--p 00000000 fd:01 525257 /usr/lib/locale/C.utf8/LC_MONETARY + 7ffff7fac000-7ffff7fad000 r--p 00000000 fd:01 525252 /usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES + 7ffff7fad000-7ffff7fae000 r--p 00000000 fd:01 525264 /usr/lib/locale/C.utf8/LC_PAPER + 7ffff7fae000-7ffff7fb5000 r--s 00000000 fd:01 527597 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache + 7ffff7fb5000-7ffff7fb8000 rw-p 00000000 00:00 0 + 7ffff7fb8000-7ffff7fb9000 r--p 00000000 fd:01 525259 /usr/lib/locale/C.utf8/LC_NAME + 7ffff7fb9000-7ffff7fba000 r--p 00000000 fd:01 525229 /usr/lib/locale/C.utf8/LC_ADDRESS + 7ffff7fba000-7ffff7fbb000 r--p 00000000 fd:01 525271 /usr/lib/locale/C.utf8/LC_TELEPHONE + 7ffff7fbb000-7ffff7fbc000 r--p 00000000 fd:01 525249 /usr/lib/locale/C.utf8/LC_MEASUREMENT + 7ffff7fbc000-7ffff7fbd000 r--p 00000000 fd:01 525232 /usr/lib/locale/C.utf8/LC_IDENTIFICATION + 7ffff7fbd000-7ffff7fbf000 rw-p 00000000 00:00 0 + 7ffff7fbf000-7ffff7fc3000 r--p 00000000 00:00 0 [vvar] + 7ffff7fc3000-7ffff7fc5000 r-xp 00000000 00:00 0 [vdso] + 7ffff7fc5000-7ffff7fc6000 r--p 00000000 fd:01 527841 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffff7fc6000-7ffff7ff1000 r-xp 00001000 fd:01 527841 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffff7ff1000-7ffff7ffb000 r--p 0002c000 fd:01 527841 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffff7ffb000-7ffff7ffd000 r--p 00036000 fd:01 527841 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffff7ffd000-7ffff7fff000 rw-p 00038000 fd:01 527841 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 + 7ffffffdd000-7ffffffff000 rw-p 00000000 00:00 0 [stack] + ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] +ProcStatus: + Name: cat + Umask: 0022 + State: t (tracing stop) + Tgid: 8029 + Ngid: 0 + Pid: 8029 + PPid: 8023 + TracerPid: 8023 + Uid: 2001 2001 2001 2001 + Gid: 2501 2501 2501 2501 + FDSize: 64 + Groups: 100 112 2501 + NStgid: 8029 + NSpid: 8029 + NSpgid: 8029 + NSsid: 7944 + Kthread: 0 + VmPeak: 3272 kB + VmSize: 3272 kB + VmLck: 0 kB + VmPin: 0 kB + VmHWM: 1536 kB + VmRSS: 1536 kB + RssAnon: 0 kB + RssFile: 1536 kB + RssShmem: 0 kB + VmData: 360 kB + VmStk: 136 kB + VmExe: 20 kB + VmLib: 1748 kB + VmPTE: 32 kB + VmSwap: 0 kB + HugetlbPages: 0 kB + CoreDumping: 0 + THP_enabled: 1 + untag_mask: 0xffffffffffffffff + Threads: 1 + SigQ: 0/63820 + SigPnd: 0000000000000000 + ShdPnd: 0000000000000000 + SigBlk: 0000000000000000 + SigIgn: 0000000000000000 + SigCgt: 0000000000000000 + CapInh: 0000000000000000 + CapPrm: 0000000000000000 + CapEff: 0000000000000000 + CapBnd: 000001ffffffffff + CapAmb: 0000000000000000 + NoNewPrivs: 0 + Seccomp: 0 + Seccomp_filters: 0 + Speculation_Store_Bypass: vulnerable + SpeculationIndirectBranch: conditional enabled + Cpus_allowed: f + Cpus_allowed_list: 0-3 + Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001 + Mems_allowed_list: 0 + voluntary_ctxt_switches: 6 + nonvoluntary_ctxt_switches: 0 + x86_Thread_features: + x86_Thread_features_locked: +ProcVersionSignature: Ubuntu 6.8.0-85.85-generic 6.8.12 +Registers: + rax 0xfffffffffffffe00 -512 + rbx 0x20000 131072 + rcx 0x7ffff7d1ba91 140737351105169 + rdx 0x20000 131072 + rsi 0x7ffff7f2e000 140737353277440 + rdi 0x0 0 + rbp 0x7fffffffd4b0 0x7fffffffd4b0 + rsp 0x7fffffffd488 0x7fffffffd488 + r8 0x0 0 + r9 0x7ffff7ffb440 140737354118208 + r10 0x22 34 + r11 0x246 582 + r12 0x20000 131072 + r13 0x7ffff7f2e000 140737353277440 + r14 0x0 0 + r15 0x0 0 + rip 0x7ffff7d1ba91 0x7ffff7d1ba91 + eflags 0x246 [ PF ZF IF ] + cs 0x33 51 + ss 0x2b 43 + ds 0x0 0 + es 0x0 0 + fs 0x0 0 + gs 0x0 0 + fs_base 0x7ffff7fb5740 140737353832256 + gs_base 0x0 0 +SegvAnalysis: + Segfault happened at: 0x7ffff7d1ba91 : cmp $0xfffffffffffff000,%rax + PC (0x7ffff7d1ba91) ok + source "$0xfffffffffffff000" ok + destination "%rax" ok + SP (0x7fffffffd488) ok + Reason could not be automatically determined. +Signal: 11 +SignalName: SIGSEGV +SourcePackage: coreutils +Stacktrace: + #0 0x00007ffff7d1ba91 in read () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #1 0x0000555555557f66 in ?? () + No symbol table info available. + #2 0x0000555555557285 in ?? () + No symbol table info available. + #3 0x00007ffff7c2a1ca in ?? () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #4 0x00007ffff7c2a28b in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #5 0x0000555555557ae5 in ?? () + No symbol table info available. +StacktraceAddressSignature: /usr/bin/cat:11:/usr/lib/x86_64-linux-gnu/libc.so.6+f3a91:/usr/bin/cat+1f66:/usr/bin/cat+1285:/usr/lib/x86_64-linux-gnu/libc.so.6+21ca:/usr/lib/x86_64-linux-gnu/libc.so.6+228b:/usr/bin/cat+1ae5 +StacktraceTop: + read () from /lib/x86_64-linux-gnu/libc.so.6 + ?? () + ?? () + ?? () from /lib/x86_64-linux-gnu/libc.so.6 + __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6 +Tags: cloud-image noble +ThreadStacktrace: + . + Thread 1 (Thread 0x7ffff7fb5740 (LWP 8029)): + #0 0x00007ffff7d1ba91 in read () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #1 0x0000555555557f66 in ?? () + No symbol table info available. + #2 0x0000555555557285 in ?? () + No symbol table info available. + #3 0x00007ffff7c2a1ca in ?? () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #4 0x00007ffff7c2a28b in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6 + No symbol table info available. + #5 0x0000555555557ae5 in ?? () + No symbol table info available. +Uname: Linux 6.8.0-85-generic x86_64 +UpgradeStatus: No upgrade log present (probably fresh install) +UserGroups: sbuild +_HooksRun: no +separator: +CoreDump: base64 + H4sICAAAAAAC/0NvcmVEdW1wAA== + 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5/X7YEfPGs8D85CPPl/nY43O0TMwLh8H+dg2Jun4bikgKwpbgWLbal9W7tD5Xfd9Ff3zZj38/mDyZzCfW+TX12P6B9nzaMaeaanV8/hFX7rdFPX+ouThyzKvHvlLJseKtcf189EfWprzoC5us9Z6sE92+0Ym50HyfuJ29fP9pOh1sZHZc4Csb8x9nXfB+YBJPl4/tR4W9i4373R5Y1Be/xUu98g73K4rTRW0x1WvbVeRdJUiz0I88/EwYRLPs8+lrwN8vqbPVCaczO5BkEec6XLKsr7lZ85ndfHMx+M3XRc18/WDrxjXj8/v/edy51sOmXPE79BrEAq8bvr56A8tzbl9o/H9A2ewnenR/Xval7dmpGneRYh7/r3Wkte06Q/pvv+qvG9Vw97Fq5toaa5pbqzZtbHO8HADfVMHnjsGQE6DXJwVuz9DEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEOTKUwRy5NGlpaV9kuQ6yeRHX7h4nkqVyQJFHElppVSltWfT5Zek9yv/f+Pi+zRS1YN8xaoFll9avE4Rd81rZbq892vLg/xSwKGIx7czeeSD12v1wE6hhPwhs+mat7TxzZldUo73P/3m+czxcPDf2HhYIbEx44LsellVqJWlcLwQ9KaHP6ik9fIDkI9LHq42KT1GjTAbj1RPGVN8HOrkG3vXamSmHisPxqFO3jVfpJGZetQ3/efY+Oq/qJUjH2X56q+3afQKQM/1XoeSdm3QSag2l9yfRfAvAH7Wy3pJK/V6k5BPL9slreS+L4HPZu3j/XVQV17PT+XBfOoZAL3jEB96GdslaSSv58eI3gpJHF7vbijPrH3TGyWN5PMuHx//5yQbH7weOy+Q43Yy3cJ5XhC1R5s+ewKOA/o01+fztwvG47/6+wj0+EvqeKT2Zh5h+ra3mP5PVjFPZqXB/i/3m/gj7tA4pgXkP5y7MELlj7dcVI68+uTJkUw9fv6G0c/proha+sEfvB3zL7P55rGr2Xz0sfZHtPPPUyz/fT9m/rXp7M2e1PrNDOo/pXywq7eTidF57v/ZOW15LojjfR7/9gKbTb2sFkpbFRtHvsYGrguOV33KkdaVWqRK8n+X9BGcV3FeNWzf7+u8emRWO45++NhJzXpEnQdg/NfcoV2P89X1ke+bjP9/fe2l9/Z84/ZjoD/z4XlFn+sdXGLw9GLJTcp8tAj5v/Uiq8+x57XroSxKmR6v/yTU5+jKxyC/46u0HJ59BsopMDClnD8F7YH63/8PL5wXqf9MGbN77Pva+rvAz49VsPo8ckp33SnT1p/n5/XnE5rjuUfOZ9bfbP60qj/X09f/yIeY3bFZbf2lp3PXfxr05j/IJM+f9j9A6kOFmk8Hbz+vf/8OVt9+74uqnUx/8HGl78f6p7Xtn3laG39cbwbaz/XWguExyP/6c6w+PE1lZv3VC83ib5R8KyfZHSQv765PPaocX4T0MWgXH5efIRPCl8m/APhv+pQ23xaQ/PzMdWcUew44/sIAs8/XF1fbtJK3X+0nnX2ePgLyqUfZeP/75n9qzuwPDp976qE+JZA+oosHfX+oaahP/2ltf3B4f8ye79PGx2ltf6jjTafP59fF68GfoMfLk/+W+Wsl5OP1bznJxsfT/+OUIjf8EmIC6kvTSjkOKHn2dU25R8B+/Te0/nJVFWjqWQ9pxzMsP/cvr+/kB6Cf38tkP+SX7DBfwWMEHkCOP9WOZ9W/z+j8C/n/5u+5X7X1GoP815xi9TnyDI9Tlu/+40wv7e8CQ3ucY6A/fgbmb0gP7dPmV+s9/7rG7rHnWP57kix//7Og3zd3nrZl7g6mVplZaFXa3hysD2dsko0emwF7X5p42/j6oauP62Z2nZk7S+YbKX394fVcxasN62Zej3t2Sxo7k6tJTJEgO1FmVKaU9dzqHKzfb3y9rTYzveNrP1tvlDaDrodz/VPJtQC/guyDOhR/nskjuvW8FGDrjAnoN95/Z375lmb88/W0A+S1ILlbXSD5vEAs6Q5AelLNYNPKUs15Xh9ajtOijQq6+ZfHpd7tR5+HcQHPfQKQ7+R/Y/OxBGmu73L19VEb9cNMjx//9urvjnzt53991Mr+t1d/dTfN9+3V1335387/9VGax2hccDvq/RZfz0H60H9n9TsLaV4OT/P66Zm5gx3X38fp+Snkexzytejq9dEXtfo3Q3oc5L0gvwry2yCfAvk8yF+C/OKPmPwOyGdAngN51TyTx19i8lmQ50AWvszkB0C6QN4E8l6QXwP5EMiyHzN5PUgXyADIEMg/B/lFkN8CeRrkL0FeBLnmJ0zeANIFsgfk7SDvBDkF8jDIvwH5IMg5kG+C9P6UyVGQ94Ks+TmTm0F+EuQdIG0LEB8gj4E8DnLkFehHkPeB/C7IH4D8NcjSc+B3kB6QnwIZA3kXyC+B/F8gHwf5IsifgSyD6/46kE6QnSD/K8hvgjwFcudrTH4a5OdA/iXIb4L83yCffU0bx/wyL9WzeRAed0mLsADj89vSm4eUJYx6TwgnVkNyECSf8um8QccTv5/k44nve/DrGl8X8Xk4PX8y+OWMj+MKSD8Niit1aW6XXwcrdWk+b3PeC3IDjEvYrVHn/etAvgJ+s0N6M+TnTzX0z10ckgX3wgRXGatdeewfzxycaGkONjdmZKgbTyXrBqLxunBIlto924Lerp7e4I7OvmDPLT29no7WWDQ+PlEzHB+X2rf73Tt6WmuGkpFYJJSKKNm39fn87UF3d5s3uM3X29NKjNPDve7uHR5mpy3Q1wrFqvaV/F09zLjU1+Ppbh0Yj8YGByV3j1JKcHtXN7PdKrXt3AklDztquhocNUPxRE1iNCqTmoRGIzVjiWhcjiQdNaP8OKnekP7k0FA0RpLJyFB0omY0NNZap5RYFxobSyTlGjmSkmvCyVBqJJKq2XlLPDycMDxVX9tQ39DkrG9oaK0lRmNyojU0Lieo/RApWCY1GPizSFhOkSMpORS+jRSZkMmBRLImJScT8WH1RDhGTPLT0UTcUXPzUCI5GpLJh0gymUi2smRNKhIeT0blPUQzPKRRGBqMDIwPX9ZWKQGRSoYt8tU479y86c7xgfG4PN7QWFvfWOvMiAfa7723BDzQ8TUZQaTmCXR3bff5PT2t8cQ4aYa0Xd/xrLszw6mjz9/ro7GTbbdr20fbMEreXVGizAJ0egiNDmbNDn7ftrZWHjHpw+5tvtYBOvl4uzo8rXUppSE18UQ8MhFNyZG4LLkDvcG2rs7tvh2tdbtCybpYdACykcrKteFEfEgKeLr9QTLtBPs6e9zbPUFfZ1urU+pp83Z3dRFtJjrdpARmP+Buu9G9w6NUu2ajc+PmxpamzfqWeDrbfe5OMqfJciwCs2VmOKvzpp/PpDfHbqjZltozOpCIRcM1Q+NxpcNSFhFDte64gRhLJjIHR9YcnB6KWVN5h/tGD6vDbkfNn6Vb3udrb22or3eqc272cFQHmvFIzfCHOr/rLgjmE0NmS9Jx4e/ake6KQX0pmjjJOK4GiiZ+uMms3skqX9ubvZ7ujtbx+G3xxO641Nbl7+vo7GltqZduanVqr5oZVzt/G4lDv9/d62ltq+3r3V7TovdEVr+4A4Gu7t5gt0cR7b7u1jp5dIz+Gxkd3hMdHwxKAXevlw2/WCIcipHQjsY/nJFWk+kTygeWVI8Nk2kvJXX39fRCLLaRaSQc0c6HqdY9JJPeM2q/4oT7rphw6WD3e27y+Fsb0rGa41La09XX3eYJtpPADZJAJFmcm5qdzsaNTWTO2/6H0um/233KJ+weT0+Pr6szSObtXFcq9crW1dHh7mxvHRy7bbhGURgjrQ8NRxw1NalQPCpH74jUROK7HDXjxGnjYdKP/hC5LI2MhQYd22h+R3soMkr8sYXNxq5YeE99Q40yq9bUN22uZYe3OmoGHDXJodBtkWQiQW4sfD0Bv/uW1g9vblIuILiif3dMHX535w714sWDyO33uXvEVkeZlw3NhTrzhOZKza7gXYFeEtU9rWOhZCgWi8RaG9XSd3R39QX4WkBdt2TcQbYFAnx5NRiSIzVydJSEdzu91vf6tt8SZNNb60ap3UvWfeTC3un2s2KZGhlcXo/f36rcFadGdOvP7AVWxrLCemGkWyKo19/MlQjcArdqFxbGq6btZkWSxYebNIL3Xcagy84buLn9kqNM5ybtqr67j9znkVXNx/p83fQD6bJW4t1Qck+NHEoOR+QU3P39/k8Iv9vjPR2oOdYHfIBlxqzf10nu5Bsyhim9k2gidxId0LHsRkMbJbo7Ju28APGjeUCFIAiCIAiCIAiCIMgfDvT9HvrdFfoeUFtXt0c5KEvS2xnvHPH3VD95nSTdfB2TpeSf/jnC5X7AQOsWsLF3+tS6ZVCRraLU7Yqifx/5Gf5+FktvhxeR1kF21/1L2veK315S+Nx3ntbYSdvVwvPx97y4/YNQ7p/wcm42+f6LBcVQBWqW+3hfoXHeySvtW0ECXT2+nf5bgqS+3Z62XuU97BwsZfwf+E/w359IN5D/ey3Kq9NxKXV/N6B8L4/EQAEJCr+vs2+nhDGh590eEyXWWS4pP6KFzrOT5J9vh6+Hz7P661f3den3Yt+pOo3Z2HjndcL3ixnv1PvFtP23Eyd6/L7tvA+4jvpdDfCd1K/7zgRPHwPJC+HpWZBjcJyn50FOwnGeXgR5hBcww9YUk4/ofmuHp8/Aee4dSC/+CM7z38qBdD18R4z/Vg5PB0A6oAHwnUlp8re6ciF9RHecp6d1x3n6mO44T8/ojvP0rO44T8/rjvN0QLf2moT0Ed1xnp7WHefpY7rjPD2jO87Ts7rjMGakft1vI/F01ZLmO39q+hjIGehHnobvBEn9MIp5ehEkfHles+7PO0HfLtXvkNKD4dpUorb5dzeP8rJgpK6tdlweaqmjryrSFwxynO/s6/B0+9py5Oj1deQyAO9C5sjR0dXp6XV335Iri6enx73D01PXc0tPMCOdQyXgDni6czhnOJyI72L/rxlNDI7HIqnacCg8EsnlDHfOprrb27tJxXL5yuP3BLykvTnb6u7p6/Z0eDp7c+TytZPzvu2+Njd9ZSZXEAxCgpypaW6k8dDwrs4usetyFZuWl3a0b5O2fGRiNObYFUmmool46zpnbf26j2wt33J9e5cS3g72tomDvXfrWDc8OAAvoNQOyoPrSE6W2lrucGwJJcMjUfqexngysjW6saX5w6zwLXWaMzRrIhUaiG7d0dlX56e13FLHDtBTQ5EQzeaIh0YjresSyeFa0p5aUnAtNVkbTiQj62hGmjUWGk45ooOt6+ipYERJr3OkoncQzUbIRfNFI7FBMNi2nWQglZZb19Wvc0Tig4qU94yRcwOJRGxdnaGaquQEJaeAUiBdVgOoNQioudNqjaDWKKB2a1qtGdSaBdR60mqbQG2TgFpvWq0F1FoE1Hxptc2gtllArT2t5uTd5hTpt64MRbXrRPquszetyHvBKdIN3Rkl8n5winTETR1pRd4TTpGucLelFXlfOEU646aM3nDy7nCK9MdNvkA6tHmHNIh0iK89rcg7pMGwQ7bUKeMZEsnIMFhIhibWOQaiMhvmzWqfROMyTZCM8fFROqrrslUHhFSdRqphIdUGI9VBIdWNRqqpqIhqo2GpQqpNhm4aM1QdDMmh4JicTGs3G9ZZVHuTkXaLSK1bjDQ3i2huNtKkU4lAWBiGFJ1NBHQNY8rZIKRrGFTOjUK6hlFFJzIBXcOwcjYJ6RrGVdQ4MsKJwYg2MpxGgcWv76qFjerVVHP9V40YxVfYxEBcpglV1SjAUmKqRhE2KKTaYBRgETFVo/gaElM1Ci8zP+tUjaIrJWeOppb6jD7aFIxMyBnqRgGWkp2i6kYxlpIbRNWNYiwlbxRVN4qulNwoqm4YYXKTqLpRlKXkZkH1jUaRlpI3iaobRltYTsbMoiZDlxweTibGx4hGLBGSDeNWJsetLW20tiSHhgUMNVobiqYiIpaaBCwlhoYELDVbW0qI1WmTgCWxOrUIWBoTsLPZyA5Z77FbP5HbwFRKvQvcpXyRQLkN3NUyMJSx1laM03Q4MR6XM1cNGp0RtZKRSCQ4EooNWWg0Dmk0UtH4cCyi6jQa6jQManQGE+MDGToNhjrO5mhLhgNb1OwZl0dNS6LpxpP8li1vjGbN7LnbEM263mfVfzxOv6WhZI+EnWSWM7wL2NUSTPcOMwh9Z3zXQPKzfuF5R8wyNgZZSPGcpK9McjaonQBZSReZZHU2B5n/IafSMaY1BdfzqkbNW9UYBLfzytIOMa0uuJzXlnaGcd5xWoMGtbo8qd5XKX1k/AxldCKcSlo8QvF58nqE0u7J6yHKrZ68HqJ0pdU2gtpGAbU+T17PXgJptSZQaxJxifvWvJ7Z+DryembT3pHXM5tbO/J6ZtPVkeczm76OPJ/ZBDIUeaw4RYJle7obnLz/nIYdaPZQYmJ0NHPVmzH6YBpM36sbLb2IulNQ3WjpRdQbBNWNFvtEfaOgutGCn6g3CqobLfiJepOgutGCn6g3C6obLfiJ+iZBdaMFP1FvEVQ3WvAT9c2C6kYLfho2gmHXZBZ2gnHXZBJ3TsHAazIJPKdg5DWZRJ5TMPSaTELPKRh7TUaxB1dLswcScFo1kV7Ss1VVHuteZVNnXVZFEsnocDDHg9GMSmzKZ7UdGR6NxOVUdsFDqSD9JrpAuUbhPyysvTmPWod2GXhqD5moR4y7XF02qU83jcYMMeAUNmA0aIiBBmEDRqOGGNgobMBo2BADjcIGjMYNMdAkbMBo4BADzcIGjKZtYmCTsAGjiZsYaBE2YBS7xMBmYQNGkzcNJOFQ3GQWisKxuMkkFp3CwbjJJBidwtG4ySQancLhuMkkHJ3C8bipKWsq2VLHd7ElqTY1kpKTcmhAiifkSL1Ebib5O26XAn1/jb6qpv9d9Ok17E219bo3S/W/37sajqn68K7YNH/RziGmr77HB++UTVdDOrDM8uEds2n+DQCzv7+m01dfL4Z30abvzlMf3mGb/gLX13rWsv7wztv0fbq/nyKqD++6TVfDm42uZdYf3ombHgR9xzL14V266btBf3aZ+vD79dO9EBEOsfany2fvpE3L8OZmvTa/dfmgfx/oO8T01fiF91anH1uevtpKeIdu+iegLxh/6fED+r8B/WPLjB949276vopl1T/tP9B/LF/9t5ZY+7m+WP3Tf3umX/l+y3QRvGlr4T9N2Rk83sb0rzHIn8n/Ay+DoghgSQ4A diff --git a/tests/errortracker/integration/data/crash/_usr_bin_cat.2001.upload b/tests/errortracker/integration/data/crash/_usr_bin_cat.2001.upload new file mode 100644 index 0000000..e69de29 diff --git a/tests/errortracker/integration/task.sh b/tests/errortracker/integration/task.sh new file mode 100755 index 0000000..49bff9d --- /dev/null +++ b/tests/errortracker/integration/task.sh @@ -0,0 +1,42 @@ +#!/usr/bin/bash + +set -e + +# start daisy +pushd "$SPREAD_PATH"/src +PYTHONPATH=$(pwd) python3 ./daisy/app.py >/tmp/daisy.log 2>&1 & +daisy_pid=$! +timeout 60 bash -c 'while ! echo "Hello there, still waiting for daisy" >/dev/tcp/localhost/5000; do sleep 5; done' +popd + +# start the retracer +python3 "$SPREAD_PATH"/src/retracer.py -a amd64 --sandbox-dir /tmp/sandbox -v --config-dir ./data/config >/tmp/retracer.log 2>&1 & +retracer_pid=$! + +sleep 2 # Make sure everything is started + +# upload a retraceable crash +cd ./data +# XXX workaround some weirdness of LXD/spread machinery where whoopsie hits a +# "Bad file descriptor" when trying to set its lock +cp ./crash /tmp -r +pushd /tmp/crash +CRASH_DB_URL=http://127.0.0.1:5000 APPORT_REPORT_DIR=$(pwd) CRASH_DB_IDENTIFIER=i_am_a_machine whoopsie --no-polling -f 2>&1 >/tmp/whoopsie.log +popd + +# timeout for 10min waiting for a successful retrace +if timeout 600 bash -c "tail -n0 -f /tmp/retracer.log | sed '/Successfully retraced/ q'"; then + echo "Success" + ret=0 +else + echo "Failure" + echo "============== daisy logs ==============" + cat /tmp/daisy.log + echo "============ retracer logs =============" + cat /tmp/retracer.log + echo "============ whoopsie logs =============" + cat /tmp/whoopsie.log + ret=1 +fi +kill $daisy_pid $retracer_pid +exit $ret diff --git a/tests/errortracker/integration/task.yaml b/tests/errortracker/integration/task.yaml new file mode 100644 index 0000000..6efbda8 --- /dev/null +++ b/tests/errortracker/integration/task.yaml @@ -0,0 +1,9 @@ +# XXX: this task is directly depending on the ddebs archive for Noble. This can +# make the test flaky, but it's still best to have it than not. +summary: Run daisy + retracer whole end-to-end scenario +execute: | + chmod a+rx /root + # Work around https://bugs.launchpad.net/ubuntu/+source/gdb/+bug/1818918 + # Apport will not be run as root, thus the included workaround here will hit ENOPERM + mkdir -p /usr/lib/debug/.dwz + sudo -u spread --preserve-env=SPREAD_PATH ./task.sh