pax_global_header00006660000000000000000000000064150516070050014511gustar00rootroot0000000000000052 comment=9f24322c6ff75751579f6f3975a32e825f74423d Nagstamon-master/000077500000000000000000000000001505160700500142755ustar00rootroot00000000000000Nagstamon-master/.github/000077500000000000000000000000001505160700500156355ustar00rootroot00000000000000Nagstamon-master/.github/FUNDING.yml000066400000000000000000000000661505160700500174540ustar00rootroot00000000000000custom: ['https://www.paypal.com/paypalme/nagstamon'] Nagstamon-master/.github/workflows/000077500000000000000000000000001505160700500176725ustar00rootroot00000000000000Nagstamon-master/.github/workflows/build-release-latest.yml000066400000000000000000000531231505160700500244300ustar00rootroot00000000000000name: build-release-latest on: push: tags-ignore: 'v*' branches: - '**' - '!master' - '!*.*.*' env: python_win_version: 3.13.2 repo_dir: nagstamon-jekyll/docs/repo cr_image: ghcr.io/henriwahl/build-nagstamon # release type this file is used for release: latest jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.11, 3.12, 3.13] steps: - uses: actions/checkout@v4 # docker login is needed for pushing the test image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # somehow weird way to get the hash over the requirements to be aware if they changed - id: requirements_hash run: echo "HASH=$(md5sum build/requirements/linux.txt | cut -d\ -f1)" >> $GITHUB_OUTPUT # if image defined by hash over requirements is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} --build-arg VERSION=${{ matrix.python-version }} --build-arg REQUIREMENTS="$(cat build/requirements/linux.txt | base64 --wrap=0)" -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} # - name: Lint with flake8 # run: | # # stop the build if there are Python syntax errors or undefined names # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest # using the tests in precompiled image makes them way faster instead of creating the test environment every time from scratch run: docker run --rm -v $PWD:/src --workdir /src ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} python -m unittest tests/test_*.py debian: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ github.job }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # if image defined by variable steps.dockerfile_hash.outputs.HASH is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon -e DEB_BUILD_OPTIONS=nocheck ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} - uses: actions/upload-artifact@v4 with: path: build/*.deb retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora: runs-on: ubuntu-latest needs: test strategy: matrix: version: [ 40, 41, 42, 43 ] steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ github.job }}-${{ matrix.version }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # if image defined by variable steps.dockerfile_hash.outputs.HASH is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}-${{ matrix.version }}:${{ steps.dockerfile_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}-${{ matrix.version }}:${{ steps.dockerfile_hash.outputs.HASH }} -f build/docker/Dockerfile-${{ github.job }}-${{ matrix.version }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}-${{ matrix.version }}:${{ steps.dockerfile_hash.outputs.HASH }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}-${{ matrix.version }}:${{ steps.dockerfile_hash.outputs.HASH }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }}-${{ matrix.version }} rhel-9: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ github.job }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # if image defined by variable steps.dockerfile_hash.outputs.HASH is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} # see https://github.com/HenriWahl/Nagstamon/issues/1090 linux-pyinstaller: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ github.job }} | cut -d\ -f1)" >> $GITHUB_OUTPUT - id: app_version run: echo "VERSION=$(grep VERSION\ =\ \' Nagstamon/config.py | cut -d\' -f2)" >> $GITHUB_OUTPUT # if image defined by variable steps.dockerfile_hash.outputs.HASH is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run --volume ${{ github.workspace }}:/nagstamon --env VERSION=${{ steps.app_version.outputs.VERSION }} ${{ env.cr_image }}-${{ github.job }}:${{ steps.dockerfile_hash.outputs.HASH }} - uses: actions/upload-artifact@v4 with: path: dist/* retention-days: 1 if-no-files-found: error name: ${{ github.job }} macos-intel: runs-on: macos-13 needs: test steps: - uses: actions/checkout@v4 - run : brew install create-dmg - run: pip3 install --no-warn-script-location -r build/requirements/macos.txt - run: cd ${{ github.workspace }}/build; python3 build.py env: PYTHONPATH: ${{ github.workspace }} - uses: actions/upload-artifact@v4 with: path: build/dist/*.dmg retention-days: 1 if-no-files-found: error name: ${{ github.job }} macos-arm: runs-on: macos-14 needs: test steps: - uses: actions/checkout@v4 - run : brew install create-dmg - run: pip3 install --no-warn-script-location --break-system-packages -r build/requirements/macos.txt - run: cd ${{ github.workspace }}/build; python3 build.py env: PYTHONPATH: ${{ github.workspace }} - uses: actions/upload-artifact@v4 with: path: build/dist/*.dmg retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-32: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x86 # no PyQt6 for win32 available on pypi.org - run: ((Get-Content -path build/requirements/windows.txt -Raw) -replace 'pyqt6.*','pyqt5') | Set-Content -Path build/requirements/windows.txt - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip build/dist/*.exe retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-64: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x64 - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip build/dist/*.exe retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-64-debug: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x64 - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py debug env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip retention-days: 1 if-no-files-found: error name: ${{ github.job }} # borrowed from dhcpy6d repo-debian: runs-on: ubuntu-latest # try to avoid race condition and start uploading only after the last install package has been build needs: [debian, fedora, linux-pyinstaller, macos-arm, macos-intel, rhel-9, windows-32, windows-64, windows-64-debug] env: family: debian steps: # checkout to get Dockerfiles for steps.dockerfile_hash.outputs.HASH - uses: actions/checkout@v4 # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'debian*' path: artifact merge-multiple: true # get secret signing key - run: echo "${{ secrets.PACKAGE_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for nagstamon-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/${{ env.dist }}/${{ env.release }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${{ env.dist }}/${{ env.release }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ env.family }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # create deb repo via Debian build container - run: | /usr/bin/docker run --rm \ -v ${{ github.workspace }}:/workspace \ -v $PWD/${{ env.repo_dir }}/${{ env.family }}/${{ env.release }}:/repo \ ${{ env.cr_image }}-${{ env.family }}:${{ steps.dockerfile_hash.outputs.HASH }} \ /bin/sh -c "cd /workspace && \ gpg --import signing_key.asc && \ cp -r artifact/*.deb nagstamon-jekyll/docs/repo/${{ env.family }}/${{ env.release }} && \ cd nagstamon-jekyll/docs/repo/${{ env.family }}/${{ env.release }} dpkg-scanpackages . > Packages && \ gzip -k -f Packages && \ apt-ftparchive release . > Release && \ gpg -abs -o Release.gpg Release && \ gpg --clearsign -o InRelease Release && \ gpg --output key.gpg --armor --export" # commit and push new binaries to nagstamon-jekyll - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.family }}" && git push repo-rpm-fedora: runs-on: ubuntu-latest # if not all are ready there might be trouble when downloading artifacts # maybe faster now with build containers needs: [repo-debian] env: family: fedora # which image to use for packaging version_latest: 40 steps: # checkout to get Dockerfiles for steps.dockerfile_hash.outputs.HASH - uses: actions/checkout@v4 # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'fedora*' path: artifact merge-multiple: true # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # organize SSH deploy key for nagstamon-repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/${{ env.release }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${{ env.release }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ env.family }}-${{ env.version_latest }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # if image defined by variable steps.dockerfile_hash.outputs.HASH is not pullable aka does not exist it will be created and pushed # only needed for fedora - run: docker pull ${{ env.cr_image }}-${{ env.family }}-${{ env.version_latest }}:${{ steps.dockerfile_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ env.family }}-${{ env.version_latest }}:${{ steps.dockerfile_hash.outputs.HASH }} -f build/docker/Dockerfile-${{ env.family }}-${{ env.version_latest }} . - run: docker push ${{ env.cr_image }}-${{ env.family }}-${{ env.version_latest }}:${{ steps.dockerfile_hash.outputs.HASH }} # copy *.rpm files into nagstamon-jekyll and create repodata - run: | version=${{ env.release }} && \ mkdir -p mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${version} && \ cp -r artifact/*.${{ env.family }}* ${{ env.repo_dir }}/${{ env.family }}/${version} && \ docker run --rm -v ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}:/repo \ ${{ env.cr_image }}-${{ env.family }}-${{ env.version_latest }}:${{ steps.dockerfile_hash.outputs.HASH }} \ /bin/bash -c "createrepo --verbose --workers 1 /repo" && \ ls -laR ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version} # commit and push new binaries to nagstamon-repo - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git pull && git add . && git commit -am "new latest repo ${{ env.family }}" && git push repo-rpm-rhel: runs-on: ubuntu-latest # if not all are ready there might be trouble when downloading artifacts # maybe faster now with build containers needs: [repo-rpm-fedora] env: family: rhel # currently just one version available version: 9 steps: # checkout to get Dockerfiles for steps.dockerfile_hash.outputs.HASH - uses: actions/checkout@v4 # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'rhel*' path: artifact merge-multiple: true # organize SSH deploy key for nagstamon-repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/${{ env.release }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${{ env.release }} # somehow weird way to get the hash over the Dockerfile to be aware if it changed - id: dockerfile_hash run: echo "HASH=$(md5sum build/docker/Dockerfile-${{ env.family }}-${{ env.version }} | cut -d\ -f1)" >> $GITHUB_OUTPUT # copy *.rpm files into nagstamon-jekyll and create repodata - run: | version=${{ env.release }} && \ mkdir -p mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${version} && \ cp -r artifact/*.${{ env.family }}* ${{ env.repo_dir }}/${{ env.family }}/${version} && \ docker run --rm -v ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}:/repo \ ${{ env.cr_image }}-${{ env.family }}-${{ env.version }}:${{ steps.dockerfile_hash.outputs.HASH }} \ /bin/bash -c "createrepo --verbose --workers 1 /repo" && \ ls -laR ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version} # commit and push new binaries to nagstamon-repo - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git pull && git add . && git commit -am "new latest repo ${{ env.family }}" && git push github-release: runs-on: ubuntu-latest needs: [repo-rpm-rhel] steps: - uses: actions/download-artifact@v4 with: pattern: '*' path: artifact merge-multiple: true - run: cd artifact && md5sum *agstamon* > md5sums.txt - run: cd artifact && sha256sum *agstamon* > sha256sums.txt # finally some maintained solution for creating releases # https://github.com/marketplace/actions/create-release - uses: ncipollo/release-action@v1 with: allowUpdates: true makeLatest: true prerelease: true tag: latest removeArtifacts: true replacesArtifacts: true artifacts: "artifact/*" Nagstamon-master/.github/workflows/build-release-stable.yml000066400000000000000000000553431505160700500244140ustar00rootroot00000000000000name: build-release-stable on: push: tags: 'v*' env: python_win_version: 3.12.7 repo_dir: nagstamon-jekyll/docs/repo cr_image: ghcr.io/henriwahl/build-nagstamon # to be increased if new updates of build images are necessary cr_image_version: 5 # release type this file is used for release: stable jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9, 3.11] steps: - uses: actions/checkout@v4 # somehow weird way to get the hash over the requirements to be aware if they changed - id: requirements_hash run: echo "HASH=$(md5sum build/requirements/linux.txt | cut -d\ -f1)" >> $GITHUB_OUTPUT # docker login is needed for pushing the test image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by hash over requirements is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} --build-arg VERSION=${{ matrix.python-version }} --build-arg REQUIREMENTS="$(cat build/requirements/linux.txt | base64 --wrap=0)" -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} # - name: Lint with flake8 # run: | # # stop the build if there are Python syntax errors or undefined names # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest # using the tests in precompiled image makes them way faster instead of creating the test environment every time from scratch run: docker run --rm -v $PWD:/src --workdir /src ${{ env.cr_image }}-${{ github.job }}-${{ matrix.python-version }}-${{ steps.requirements_hash.outputs.HASH }} python -m unittest tests/test_*.py debian: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon -e DEB_BUILD_OPTIONS=nocheck ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.deb retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora-38: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora-39: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora-40: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora-41: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} fedora-42: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} rhel-9: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} # building in precompiled image makes them way faster instead of creating the build environment every time from scratch - run: /usr/bin/docker run -v ${{ github.workspace }}:/nagstamon ${{ env.cr_image }}-${{ github.job }}:${{ env.cr_image_version }} - uses: actions/upload-artifact@v4 with: path: build/*.rpm retention-days: 1 if-no-files-found: error name: ${{ github.job }} macos-intel: runs-on: macos-12 needs: test steps: - uses: actions/checkout@v4 - run: pip3 install --no-warn-script-location -r build/requirements/macos.txt - run: cd ${{ github.workspace }}/build; python3 build.py env: PYTHONPATH: ${{ github.workspace }} - uses: actions/upload-artifact@v4 with: path: build/*.dmg retention-days: 1 if-no-files-found: error name: ${{ github.job }} macos-arm: runs-on: macos-14 needs: test steps: - uses: actions/checkout@v4 - run: pip3 install --no-warn-script-location --break-system-packages -r build/requirements/macos.txt - run: cd ${{ github.workspace }}/build; python3 build.py env: PYTHONPATH: ${{ github.workspace }} - uses: actions/upload-artifact@v4 with: path: build/*.dmg retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-32: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x86 # no PyQt6 for win32 available on pypi.org - run: ((Get-Content -path build/requirements/windows.txt -Raw) -replace 'pyqt6.*','pyqt5') | Set-Content -Path build/requirements/windows.txt - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip build/dist/*.exe retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-64: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x64 - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip build/dist/*.exe retention-days: 1 if-no-files-found: error name: ${{ github.job }} windows-64-debug: # better depend on stable build image runs-on: windows-2022 needs: test steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '${{ env.python_win_version }}' architecture: x64 - run: python -m pip install --no-warn-script-location -r build/requirements/windows.txt # pretty hacky but no other idea to avoid gssapi being installed which breaks requests-kerberos - run: python -m pip uninstall -y gssapi requests-gssapi - run: cd ${{ github.workspace }}/build; python build.py debug env: PYTHONPATH: ${{ github.workspace }} WIN_SIGNING_CERT_BASE64: ${{ secrets.SIGNING_CERT_BASE64 }} WIN_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - uses: actions/upload-artifact@v4 with: path: | build/dist/*.zip retention-days: 1 if-no-files-found: error name: ${{ github.job }} # borrowed from dhcpy6d repo-debian: runs-on: ubuntu-latest # try to avoid race condition and start uploading only after the last install package has been build needs: [debian, fedora-38, fedora-39, fedora-40, fedora-41, fedora-42, rhel-9, macos-intel, macos-arm, windows-32, windows-64, windows-64-debug] env: family: debian steps: - uses: actions/checkout@v4 # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'debian*' path: artifact merge-multiple: true # get secret signing key - run: echo "${{ secrets.PACKAGE_SIGNING_KEY }}" > signing_key.asc # organize SSH deploy key for nagstamon-jekyll repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/${{ env.dist }}/${{ env.release }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${{ env.dist }}/${{ env.release }} # create deb repo via Debian build container - run: | /usr/bin/docker run --rm \ -v ${{ github.workspace }}:/workspace \ -v $PWD/${{ env.repo_dir }}/${{ env.family }}/${{ env.release }}:/repo \ ${{ env.cr_image }}-${{ env.family }}:${{ env.cr_image_version }} \ /bin/sh -c "cd /workspace && \ gpg --import signing_key.asc && \ cp -r artifact/*.deb nagstamon-jekyll/docs/repo/${{ env.family }}/${{ env.release }} && \ cd nagstamon-jekyll/docs/repo/${{ env.family }}/${{ env.release }} dpkg-scanpackages . > Packages && \ gzip -k -f Packages && \ apt-ftparchive release . > Release && \ gpg -abs -o Release.gpg Release && \ gpg --clearsign -o InRelease Release && \ gpg --output key.gpg --armor --export" # commit and push new binaries to nagstamon-jekyll - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git add . && git commit -am "new ${{ env.release }} repo ${{ env.family }}" && git push repo-rpm-fedora: runs-on: ubuntu-latest # if not all are ready there might be trouble when downloading artifacts # maybe faster now with build containers needs: [repo-debian] env: family: fedora # which image to use for packaging cr_image_latest: 40 steps: # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'fedora*' path: artifact merge-multiple: true # docker login is needed for pushing the build image - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # organize SSH deploy key for nagstamon-repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/?? # if image defined by variable cr_image_version is not pullable aka does not exist it will be created and pushed - run: docker pull ${{ env.cr_image }}-${{ env.family }}-${{ env.cr_image_latest }}:${{ env.cr_image_version }} || /usr/bin/docker build -t ${{ env.cr_image }}-${{ env.family }}-${{ env.cr_image_latest }}:${{ env.cr_image_version }} -f build/docker/Dockerfile-${{ github.job }} . - run: docker push ${{ env.cr_image }}-${{ env.family }}-${{ env.cr_image_latest }}:${{ env.cr_image_version }} # copy *.rpm files into nagstamon-jekyll and create repodata - run: | for noarch_rpm in artifact/*${{ env.family }}*.noarch.rpm; \ do \ version=$(echo ${noarch_rpm} | python3 -c "file=input(); print(file.split('${{ env.family }}')[1].split('.')[0])") && \ mkdir -p mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${version} && \ cp -r artifact/*.${{ env.family }}${version}* ${{ env.repo_dir }}/${{ env.family }}/${version} && \ docker run --rm -v ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}:/repo \ ${{ env.cr_image }}-${{ env.family }}-${version}:${{ env.cr_image_version }} \ /bin/bash -c "createrepo --verbose --workers 1 /repo" && \ ls -laR ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}; \ done # commit and push new binaries to nagstamon-repo - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git pull && git add . && git commit -am "new latest repo ${{ env.family }}" && git push repo-rpm-rhel: runs-on: ubuntu-latest # if not all are ready there might be trouble when downloading artifacts # maybe faster now with build containers needs: [repo-rpm-fedora] env: family: rhel # currently just one version available version: 9 steps: # get binaries created by other jobs - uses: actions/download-artifact@v4 with: pattern: 'rhel*' path: artifact merge-multiple: true # organize SSH deploy key for nagstamon-repo - run: mkdir ~/.ssh - run: echo "${{ secrets.NAGSTAMON_REPO_KEY_WEB }}" > ~/.ssh/id_ed25519 - run: chmod -R go-rwx ~/.ssh # get and prepare nagstamon-jekyll - run: git clone git@github.com:HenriWahl/nagstamon-jekyll.git - run: rm -rf ${{ env.repo_dir }}/${{ env.family }}/${{ env.version }} - run: mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${{ env.version }} # copy *.rpm files into nagstamon-jekyll - run: cp -r artifact/*.${{ env.family }}*.rpm ${{ env.repo_dir }}/${{ env.family }}/${{ env.version }} # copy *.rpm files into nagstamon-jekyll and create repodata - run: | for noarch_rpm in artifact/*${{ env.family }}*.noarch.rpm; \ do \ version=$(echo ${noarch_rpm} | python3 -c "file=input(); print(file.split('${{ env.family }}')[1].split('.')[0])") && \ mkdir -p mkdir -p ${{ env.repo_dir }}/${{ env.family }}/${version} && \ cp -r artifact/*.${{ env.family }}${version}* ${{ env.repo_dir }}/${{ env.family }}/${version} && \ docker run --rm -v ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}:/repo \ ${{ env.cr_image }}-${{ env.family }}-${version}:${{ env.cr_image_version }} \ /bin/bash -c "createrepo --verbose --workers 1 /repo" && \ ls -laR ${PWD}/${{ env.repo_dir }}/${{ env.family }}/${version}; \ done # commit and push new binaries to nagstamon-repo - run: git config --global user.email "repo@nagstamon.de" && git config --global user.name "Nagstamon Repository" - run: cd ${{ env.repo_dir }} && git pull && git add . && git commit -am "new latest repo ${{ env.family }}" && git push github-release: runs-on: ubuntu-latest needs: [repo-rpm-rhel] steps: - uses: actions/download-artifact@v4 with: pattern: '*' path: artifact merge-multiple: true - run: cd artifact && md5sum *agstamon* > md5sums.txt - run: cd artifact && sha256sum *agstamon* > sha256sums.txt #- uses: marvinpinto/action-automatic-releases@latest # the dciborow action is outdated as well :-( - uses: dciborow/action-github-releases@v1.0.1 with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false draft: true files: | artifact/* Nagstamon-master/.github/workflows/codeql.yml000066400000000000000000000053531505160700500216720ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "master" ] pull_request: # The branches below must be a subset of the branches above branches: [ "master" ] schedule: - cron: '20 23 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{matrix.language}}" Nagstamon-master/.gitignore000066400000000000000000000002471505160700500162700ustar00rootroot00000000000000__pycache__/ .directory .DS_Store .idea/ .metadata/ .vscode/ *.conf/ *.deb *.dmg *.exe *.gz *.pyc *.rpm *.swp *.zip *~ build/bdist* dist/ nagstamon nagstamon.egg-info/Nagstamon-master/.pylintrc000066400000000000000000000000261505160700500161400ustar00rootroot00000000000000[MASTER] disable=R,C,WNagstamon-master/CONTRIBUTE.md000066400000000000000000000007371505160700500163040ustar00rootroot00000000000000Contribution ============ Of course every contribution is welcome - without contributors Nagstamon never would have been what it is today. - Some basic information is already available at https://nagstamon.de/documentation. - The Python packages required to run Nagstamon are listed in https://github.com/HenriWahl/Nagstamon/blob/master/requirements.txt. - Building a binary release is easily done by running https://github.com/HenriWahl/Nagstamon/blob/master/build/build.py Nagstamon-master/COPYRIGHT000066400000000000000000000000771505160700500155740ustar00rootroot00000000000000Copyright (C) 2008-2025 Henri Wahl et al. Nagstamon-master/LICENSE000066400000000000000000001323301505160700500153040ustar00rootroot00000000000000Nagstamon is licensed under the following GPL2: GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. 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 this service 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. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey 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 General Public License as published by the Free Software Foundation; either version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Nagstamon uses BeautifulSoup under the following license: Copyright (c) 2004-2010, Leonard Richardson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. Nagstamon's experimental Zabbix support is based on zabbix_api.py, which is licensed under LGPL 2.1: GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey 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 library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it!Nagstamon-master/Nagstamon/000077500000000000000000000000001505160700500162245ustar00rootroot00000000000000Nagstamon-master/Nagstamon/Servers/000077500000000000000000000000001505160700500176555ustar00rootroot00000000000000Nagstamon-master/Nagstamon/Servers/Alertmanager/000077500000000000000000000000001505160700500222575ustar00rootroot00000000000000Nagstamon-master/Nagstamon/Servers/Alertmanager/CHANGELOG.md000066400000000000000000000012661505160700500240750ustar00rootroot00000000000000# Changelog [1.2.0] - 2021-08-23: * changed: Removed dependencies to the Prometheus integration alertmanager is now a full module residing in its own directory * added: Support user defined severity values for critical or warning [1.1.0] - 2021-05-18: * changed: Using logging module for all outputs Some refactoring for testing support * added: Initial tests based on unittest and pylint (see tests/test_Alertmanager.py) [1.0.2] - 2021-04-10: * added: Better debug output [1.0.1] - 2020-11-27: * added: Support for hiding suppressed alerts with the scheduled downtime filter [1.0.0] - 2020-11-08: * added: Inital versionNagstamon-master/Nagstamon/Servers/Alertmanager/LICENSE000066400000000000000000000014331505160700500232650ustar00rootroot00000000000000Nagstamon - Nagios status monitor for your desktop Copyright (C) 2008-2025 Henri Wahl et al. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USANagstamon-master/Nagstamon/Servers/Alertmanager/README.md000066400000000000000000000007151505160700500235410ustar00rootroot00000000000000# alertmanager ## description The AlertmanagerServer and AlertmanagerService classes implement support for Prometheus' alertmanager. The monitor URL in the setup should be something like: `http://prometheus.example.com:9093` What the integration does: It reads the alerts from the Alertmanager's REST API and tries to fit each alert into Nagstamon's GenericServer and GenericService objects. ## author(s) Initial implementation by Stephan Schwarz (@stearz)Nagstamon-master/Nagstamon/Servers/Alertmanager/__init__.py000066400000000000000000000001751505160700500243730ustar00rootroot00000000000000# encoding: utf-8 from .alertmanagerservice import AlertmanagerService from .alertmanagerserver import AlertmanagerServer Nagstamon-master/Nagstamon/Servers/Alertmanager/alertmanagerserver.py000066400000000000000000000314221505160700500265240ustar00rootroot00000000000000import sys import json import re import time from datetime import datetime, timedelta import requests from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.helpers import webbrowser_open from .helpers import (start_logging, get_duration, convert_timestring_to_utc, detect_from_labels) from .alertmanagerservice import AlertmanagerService # TODO: support debug level switching while running log = start_logging('alertmanager', conf.debug_mode) class AlertmanagerServer(GenericServer): """ special treatment for alertmanager API """ TYPE = 'Alertmanager' # alertmanager actions are limited to visiting the monitor for now MENU_ACTIONS = ['Monitor', 'Downtime', 'Acknowledge'] BROWSER_URLS = { 'monitor': '$MONITOR$/#/alerts', 'hosts': '$MONITOR$/#/alerts', 'services': '$MONITOR$/#/alerts', 'history': '$MONITOR$/#/alerts' } API_PATH_ALERTS = "/api/v2/alerts?inhibited=false" API_PATH_SILENCES = "/api/v2/silences" API_FILTERS = '&filter=' # vars specific to alertmanager class map_to_hostname = '' map_to_servicename = '' map_to_status_information = '' map_to_critical = '' map_to_warning = '' map_to_down = '' map_to_unknown = '' map_to_ok = '' name = '' alertmanager_filter = '' def init_HTTP(self): """ things to do if HTTP is not initialized """ GenericServer.init_HTTP(self) # prepare for JSON self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) def init_config(self): """ dummy init_config, called at thread start """ def get_start_end(self, host): """ Set a default of starttime of "now" and endtime is "now + 24 hours" directly from web interface """ start = datetime.now() end = datetime.now() + timedelta(hours=24) return (str(start.strftime("%Y-%m-%d %H:%M:%S")), str(end.strftime("%Y-%m-%d %H:%M:%S"))) def map_severity(self, the_severity): """Maps a severity Args: the_severity (str): The severity that should be mapped Returns: str: The matched Nagstamon severity """ if the_severity in self.map_to_unknown.split(','): return "UNKNOWN" if the_severity in self.map_to_critical.split(','): return "CRITICAL" if the_severity in self.map_to_warning.split(','): return "WARNING" if the_severity in self.map_to_down.split(','): return "DOWN" if the_severity in self.map_to_ok.split(','): return "OK" return the_severity.upper() def _process_alert(self, alert): result = {} # alertmanager specific extensions generator_url = alert.get("generatorURL", {}) fingerprint = alert.get("fingerprint", {}) log.debug("processing alert with fingerprint '%s':", fingerprint) labels = alert.get("labels", {}) state = alert.get("status", {"state": "active"})["state"] severity = self.map_severity(labels.get("severity", "unknown")) # skip alerts with none severity if severity == "NONE": log.debug("[%s]: detected detected state '%s' and severity '%s' from labels \ -> skipping alert", fingerprint, state, severity) return False log.debug("[%s]: detected detected state '%s' and severity '%s' from labels", fingerprint, state, severity) hostname = detect_from_labels(labels,self.map_to_hostname,"unknown") hostname = re.sub(':[0-9]+', '', hostname) log.debug("[%s]: detected hostname from labels: '%s'", fingerprint, hostname) servicename = detect_from_labels(labels,self.map_to_servicename,"unknown") log.debug("[%s]: detected servicename from labels: '%s'", fingerprint, servicename) if "status" in alert: attempt = alert["status"].get("state", "unknown") else: attempt = "unknown" if attempt == "suppressed": scheduled_downtime = True acknowledged = True log.debug("[%s]: detected status: '%s' -> interpreting as silenced", fingerprint, attempt) else: scheduled_downtime = False acknowledged = False log.debug("[%s]: detected status: '%s'", fingerprint, attempt) duration = str(get_duration(alert["startsAt"])) annotations = alert.get("annotations", {}) status_information = detect_from_labels(annotations,self.map_to_status_information,'') result['host'] = str(hostname) result['name'] = servicename result['server'] = self.name result['status'] = severity result['labels'] = labels result['last_check'] = str(get_duration(alert["updatedAt"])) result['attempt'] = attempt result['scheduled_downtime'] = scheduled_downtime result['acknowledged'] = acknowledged result['duration'] = duration result['generatorURL'] = generator_url result['fingerprint'] = fingerprint result['status_information'] = status_information return result def _get_status(self): """ Get status from alertmanager Server """ log.debug("detection config (map_to_status_information): '%s'", self.map_to_status_information) log.debug("detection config (map_to_hostname): '%s'", self.map_to_hostname) log.debug("detection config (map_to_servicename): '%s'", self.map_to_servicename) log.debug("detection config (alertmanager_filter): '%s'", self.alertmanager_filter) log.debug("severity config (map_to_unknown): '%s'", self.map_to_unknown) log.debug("severity config (map_to_critical): '%s'", self.map_to_critical) log.debug("severity config (map_to_warning): '%s'", self.map_to_warning) log.debug("severity config (map_to_down): '%s'", self.map_to_down) log.debug("severity config (map_to_ok): '%s'", self.map_to_ok) # get all alerts from the API server try: if self.alertmanager_filter != '': result = self.fetch_url(self.monitor_url + self.API_PATH_ALERTS + self.API_FILTERS + self.alertmanager_filter, giveback="raw") else: result = self.fetch_url(self.monitor_url + self.API_PATH_ALERTS, giveback="raw") if result.status_code == 200: log.debug("received status code '%s' with this content in result.result: \n\ ---------------------------------------------------------------\n\ %s\ ---------------------------------------------------------------", result.status_code, result.result) else: log.error("received status code '%s'", result.status_code) try: data = json.loads(result.result) except json.decoder.JSONDecodeError: data = {} error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return errors_occured for alert in data: alert_data = self._process_alert(alert) if not alert_data: continue service = AlertmanagerService() service.host = alert_data['host'] service.name = alert_data['fingerprint'] service.display_name = alert_data['name'] service.server = alert_data['server'] service.status = alert_data['status'] service.labels = alert_data['labels'] service.scheduled_downtime = alert_data['scheduled_downtime'] service.acknowledged = alert_data['acknowledged'] service.last_check = alert_data['last_check'] service.attempt = alert_data['attempt'] service.duration = alert_data['duration'] service.generator_url = alert_data['generatorURL'] service.fingerprint = alert_data['fingerprint'] service.status_information = alert_data['status_information'] if service.host not in self.new_hosts: self.new_hosts[service.host] = GenericHost() self.new_hosts[service.host].name = str(service.host) self.new_hosts[service.host].server = self.name self.new_hosts[service.host].services[service.name] = service except Exception as the_exception: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) log.exception(the_exception) return Result(result=result, error=error) # dummy return in case all is OK return Result() def open_monitor_webpage(self, host, service): """ open monitor from tablewidget context menu """ webbrowser_open('%s' % (self.monitor_url)) def open_monitor(self, host, service=''): """ open monitor for alert """ url = self.monitor_url webbrowser_open(url) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): alert = self.hosts[host].services[service] # Convert local dates to UTC start_time_dt = convert_timestring_to_utc(start_time) end_time_dt = convert_timestring_to_utc(end_time) # API Spec: https://github.com/prometheus/alertmanager/blob/master/api/v2/openapi.yaml silence_data = {} silence_data["matchers"] = [] for name, value in alert.labels.items(): silence_data["matchers"].append({ "name": name, "value": value, "isRegex": False, "isEqual": True }) silence_data["startsAt"] = start_time_dt silence_data["endsAt"] = end_time_dt silence_data["comment"] = comment or "Nagstamon downtime" silence_data["createdBy"] = author or "Nagstamon" post = requests.post(self.monitor_url + self.API_PATH_SILENCES, json=silence_data) #silence_id = post.json()["silenceID"] # Overwrite function from generic server to add expire_time value def set_acknowledge(self, info_dict): ''' different monitors might have different implementations of _set_acknowledge ''' if info_dict['acknowledge_all_services'] is True: all_services = info_dict['all_services'] else: all_services = [] # Make sure expire_time is set #if not info_dict['expire_time']: # info_dict['expire_time'] = None self._set_acknowledge(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['sticky'], info_dict['notify'], info_dict['persistent'], all_services, info_dict['expire_time']) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None, expire_time=None): alert = self.hosts[host].services[service] ends_at = convert_timestring_to_utc(expire_time) cgi_data = {} cgi_data["matchers"] = [] for name, value in alert.labels.items(): cgi_data["matchers"].append({ "name": name, "value": value, "isRegex": False }) cgi_data["startsAt"] = datetime.utcfromtimestamp(time.time()).isoformat() cgi_data["endsAt"] = ends_at or cgi_data["startAt"] cgi_data["comment"] = comment or "Nagstamon silence" cgi_data["createdBy"] = author or "Nagstamon" cgi_data = json.dumps(cgi_data) result = self.fetch_url(self.monitor_url + self.API_PATH_SILENCES, giveback="raw", cgi_data=cgi_data) return result Nagstamon-master/Nagstamon/Servers/Alertmanager/alertmanagerservice.py000066400000000000000000000007431505160700500266600ustar00rootroot00000000000000from Nagstamon.objects import GenericService class AlertmanagerService(GenericService): """ add alertmanager specific service property to generic service class """ fingerprint = "" labels = {} def get_service_name(self): return self.display_name def get_hash(self): """ return hash for event history tracking """ return " ".join((self.server, self.site, self.host, self.name, self.status, self.fingerprint)) Nagstamon-master/Nagstamon/Servers/Alertmanager/helpers.py000066400000000000000000000055151505160700500243010ustar00rootroot00000000000000import sys import logging import dateutil.parser from datetime import datetime, timedelta, timezone def start_logging(log_name, debug_mode): logger = logging.getLogger(log_name) handler = logging.StreamHandler(sys.stdout) if debug_mode is True: LOG_LEVEL = logging.DEBUG handler.setLevel(logging.DEBUG) else: LOG_LEVEL = logging.INFO handler.setLevel(logging.INFO) logger.setLevel(LOG_LEVEL) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) return logger def get_duration(timestring): """ calculates the duration (delta) from Prometheus' activeAt (ISO8601 format) until now and returns a human friendly string Args: timestring (string): An ISO8601 time string Returns: string: A time string in human readable format """ time_object = dateutil.parser.parse(timestring) duration = datetime.now(timezone.utc) - time_object hour = int(duration.seconds / 3600) minute = int(duration.seconds % 3600 / 60) second = int(duration.seconds % 60) if duration.days > 0: return "%sd %sh %02dm %02ds" % (duration.days, hour, minute, second) if hour > 0: return "%sh %02dm %02ds" % (hour, minute, second) if minute > 0: return "%02dm %02ds" % (minute, second) return "%02ds" % (second) def convert_timestring_to_utc(timestring): """Converts time string and returns time for timezone UTC in ISO format Args: timestring (string): A time string Returns: string: A time string in ISO format """ local_time = datetime.now(timezone(timedelta(0))).astimezone().tzinfo parsed_time = dateutil.parser.parse(timestring) utc_time = parsed_time.replace(tzinfo=local_time).astimezone(timezone.utc) return utc_time.isoformat() def detect_from_labels(labels, config_label_list, default_value="", list_delimiter=","): """Returns the name of the label that first matched between `labels` and `config_label_list`. If there has not been a match it returns an empty string. Args: labels (list(str)): A list of string labels config_label_list (str): A delimiter seperated list - Delimiter can be specified with `list_delimiter`. Default delimiter is ",". default_value (str, optional): The value to return if there has not been a match. Defaults to "". list_delimiter (str, optional): The delimiter used in the value of `config_label_list`. Defaults to ",". Returns: str: The matched label name or an empty string if there was no match """ result = default_value for each_label in config_label_list.split(list_delimiter): if each_label in labels: result = labels.get(each_label) break return result Nagstamon-master/Nagstamon/Servers/Centreon/000077500000000000000000000000001505160700500214325ustar00rootroot00000000000000Nagstamon-master/Nagstamon/Servers/Centreon/CentreonAPI.py000066400000000000000000001070311505160700500241150ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import traceback import urllib.request import urllib.parse import urllib.error import sys # API V2 import pprint import json import requests from datetime import datetime, timedelta from Nagstamon.objects import * from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf from Nagstamon.helpers import webbrowser_open # This class support Centreon V2 API # Things to do : # - BROWSER_URLS -> move into define_url() to be consistent class CentreonServer(GenericServer): def __init__(self, **kwds): GenericServer.__init__(self, **kwds) self.TYPE = 'Centreon' # Centreon API uses a token self.token = None # HARD/SOFT state mapping self.HARD_SOFT = {'(H)': 'hard', '(S)': 'soft'} # Entries for monitor default actions in context menu # Removed Monitor, as I don’t know a way to show directly the service # or host details page, so i show the main ressource page # self.MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] self.MENU_ACTIONS = ['Recheck', 'Acknowledge', 'Downtime'] # URLs of the Centreon pages self.urls_centreon = None # limit number of services retrived self.limit_services_number = 9999 def init_config(self): ''' init_config, called at thread start ''' # Version check result = self.fetch_url(f'{self.monitor_cgi_url}/api/latest/platform/versions', no_auth=True, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) self.centreon_version_major = int(data["web"]["major"]) self.centreon_version_minor = int(data["web"]["minor"]) if conf.debug_mode is True: self.debug(server='[' + self.get_name() + ']', debug='Centreon version detected : ' + str(self.centreon_version_major) + '.' + str(self.centreon_version_minor)) if self.centreon_version_major >= 21: # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/monitoring/resources', 'hosts': '$MONITOR$/monitoring/resources', 'services': '$MONITOR$/monitoring/resources', 'history': '$MONITOR$/main.php?p=20301'} # RestAPI version if self.centreon_version_major == 21: self.restapi_version = "latest" elif self.centreon_version_major == 22: self.restapi_version = "v22.04" elif self.centreon_version_major == 23 and self.centreon_version_minor == 4: self.restapi_version = "v23.04" elif self.centreon_version_major == 23 and self.centreon_version_minor == 10: self.restapi_version = "v23.10" elif self.centreon_version_major == 24: self.restapi_version = "v24.04" else: self.restapi_version = "v24.04" if conf.debug_mode is True: self.debug(server='[' + self.get_name() + ']', debug='Centreon API version used : ' + self.restapi_version) else: if conf.debug_mode is True: self.debug(server='[' + self.get_name() + ']', debug='Unsupported Centreon version, must be >= 21') # Changed this because define_url was called 2 times if not self.tls_error and self.urls_centreon is None: self.define_url() def init_HTTP(self): GenericServer.init_HTTP(self) if self.session is None: self.session.headers.update({'Content-Type': 'application/json'}) self.token = self.get_token().result def define_url(self): urls_centreon_api_v2 = { 'resources': self.monitor_cgi_url + '/api/' + self.restapi_version + '/monitoring/resources', 'login': self.monitor_cgi_url + '/api/' + self.restapi_version + '/login', 'services': self.monitor_cgi_url + '/api/' + self.restapi_version + '/monitoring/resources', 'hosts': self.monitor_cgi_url + '/api/' + self.restapi_version + '/monitoring/resources' } self.urls_centreon = urls_centreon_api_v2 def open_monitor(self, host, service=''): # Used for self.MENU_ACTIONS = ['Monitor'] # Autologin seems deprecated as admin must enable it globaly and use the old pages # Ex : http://10.66.113.52/centreon/main.php?autologin=1&useralias=admin&token=xxxxxx # if self.use_autologin is True: # auth = '&autologin=1&useralias=' + self.username + '&token=' + self.autologin_key # else: # auth = '' # webbrowser_open(self.urls_centreon['resources'] + auth ) webbrowser_open(self.monitor_cgi_url) def get_token(self): try: cgi_data = { "security": { "credentials": { "login": self.username, "password": self.password } } } # Post json json_string = json.dumps(cgi_data) result = self.fetch_url(self.urls_centreon['login'], cgi_data=json_string, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server=self.get_name(), debug="Fetched JSON: " + pprint.pformat(data)) # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) token = data["security"]["token"] # ID of the user is needed by some requests user_id = data["contact"]["id"] if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='API login : ' + self.username + ' / ' + self.password + ' > Token : ' + token + ' > User ID : ' + str( user_id)) self.user_id = user_id self.session.headers.update({'X-Auth-Token': token}) return Result(result=token) except: traceback.print_exc(file=sys.stdout) result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def get_host(self, host): # https://demo.centreon.com/centreon/api/latest/monitoring/resources?page=1&limit=30&sort_by={"status_severity_code":"asc","last_status_change":"desc"}&types=["host"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"] url_hosts = self.urls_centreon['hosts'] + '?types=["host"]&search={"h.name":"' + host + '"}' try: # Get json result = self.fetch_url(url_hosts, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) fqdn = str(data["result"][0]["fqdn"]) if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Get Host FQDN or address : ' + host + " / " + fqdn) # Give back host or ip return Result(result=fqdn) except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def get_host_and_service_id(self, host, service=''): if service == "": # Hosts only # https://demo.centreon.com/centreon/api/latest/monitoring/resources?page=1&limit=30&sort_by={"status_severity_code":"asc","last_status_change":"desc"}&types=["host"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"] url_hosts = self.urls_centreon['hosts'] + '?types=["host"]&search={"h.name":"' + host + '"}' try: # Get json result = self.fetch_url(url_hosts, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) host_id = data["result"][0]["id"] if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Get Host ID : ' + host + " / " + str(host_id)) return host_id except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) else: # Host + Service if host == "Meta_Services": url_service = self.urls_centreon[ 'services'] + '?types=["metaservice"]&search={"s.name":"' + service + '"}' else: url_service = self.urls_centreon[ 'services'] + '?types=["service"]&search={"$and":[{"h.name":{"$eq":"' + host + '"}}, {"s.description":{"$eq":"' + service + '"}}]}' try: # Get json result = self.fetch_url(url_service, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) if host == "Meta_Services": host_id = 0 else: host_id = data["result"][0]["parent"]["id"] service_id = data["result"][0]["id"] if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Get Host / Service ID : ' + str(host_id) + " / " + str(service_id)) return host_id, service_id except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def get_start_end(self, host): # I don’t know how to get this info... # self.defaults_downtime_duration_hours = 2 # self.defaults_downtime_duration_minutes = 0 start = datetime.now() end = datetime.now() + timedelta(hours=2) return (str(start.strftime('%Y-%m-%d %H:%M')), str(end.strftime('%Y-%m-%d %H:%M'))) def _get_status(self): ''' Get status from Centreon Server ''' # Be sure that the session is still active result = self.check_session() if result is not None: if result.result == 'ERROR': if 'urls_centreon' in result.error: result.error = 'Connection error' return result # filter regexep to reduce network traffic # waiting to find a solution to reverse regexp # my first idea is # begin by (^(?!(.* # ending by ))) # replace | by )))(^(?!(.* # replace )( by )|( self.re_service_filter = '' self.re_host_filter = '' if conf.re_service_enabled is True and conf.re_service_reverse is True: self.re_service_filter = '&search={"s.description":{"$rg":"' + str(conf.re_service_pattern) + '"}}' if conf.re_host_enabled is True and conf.re_host_reverse is True: self.re_host_filter = '&search={"h.name":{"$rg":"' + str(conf.re_host_pattern) + '"}}' # Services URL # https://demo.centreon.com/centreon/api/latest/monitoring/resources?page=1&limit=30&sort_by={"status_severity_code":"asc","last_status_change":"desc"}&types=["service"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"] url_services = self.urls_centreon[ 'services'] + '?types=["metaservice","service"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"]' + self.re_service_filter + '&limit=' + str( self.limit_services_number) # Hosts URL # https://demo.centreon.com/centreon/api/latest/monitoring/resources?page=1&limit=30&sort_by={"status_severity_code":"asc","last_status_change":"desc"}&types=["host"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"] url_hosts = self.urls_centreon[ 'hosts'] + '?types=["host"]&statuses=["WARNING","DOWN","CRITICAL","UNKNOWN"]' + self.re_host_filter + '&limit=' + str( self.limit_services_number) # Hosts try: # Get json result = self.fetch_url(url_hosts, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) if data["meta"]["total"] == 0: self.debug(server='[' + self.get_name() + ']', debug='No host down') else: for alerts in data["result"]: new_host = alerts["name"] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = alerts["name"] self.new_hosts[new_host].server = self.name # API inconsistency, even by fixing exact version number, changed starting with 22.04 if self.centreon_version_major == 21 or (self.centreon_version_major == 22 and self.centreon_version_minor == 4): self.new_hosts[new_host].criticality = alerts["severity_level"] else: self.new_hosts[new_host].criticality = alerts["severity"] self.new_hosts[new_host].status = alerts["status"]["name"] self.new_hosts[new_host].last_check = alerts["last_check"] # last_state_change = datetime.strptime(alerts["last_status_change"], '%Y-%m-%dT%H:%M:%S%z').replace(tzinfo=None) self.new_hosts[new_host].duration = alerts["duration"] self.new_hosts[new_host].attempt = alerts["tries"] self.new_hosts[new_host].status_information = alerts["information"] # Change starting with 23.10 if (self.centreon_version_major >= 23 and self.centreon_version_minor >= 10) or self.centreon_version_major > 23: self.new_hosts[new_host].passiveonly = alerts["has_passive_checks_enabled"] self.new_hosts[new_host].notifications_disabled = not alerts["is_notification_enabled"] self.new_hosts[new_host].acknowledged = alerts["is_acknowledged"] self.new_hosts[new_host].scheduled_downtime = alerts["is_in_downtime"] else: self.new_hosts[new_host].passiveonly = alerts["passive_checks"] self.new_hosts[new_host].notifications_disabled = not alerts["notification_enabled"] self.new_hosts[new_host].acknowledged = alerts["acknowledged"] self.new_hosts[new_host].scheduled_downtime = alerts["in_downtime"] # avoid crash if flapping is not configured in Centreon # according to https://github.com/HenriWahl/Nagstamon/issues/866#issuecomment-1302257034 self.new_hosts[new_host].flapping = alerts.get("flapping", False) if "(S)" in alerts["tries"]: self.new_hosts[new_host].status_type = self.HARD_SOFT['(S)'] else: self.new_hosts[new_host].status_type = self.HARD_SOFT['(H)'] self.debug(server='[' + self.get_name() + ']', debug='Host indexed : ' + new_host) except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # Services try: # Get json result = self.fetch_url(url_services, giveback='raw') data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return (errors_occured) if data["meta"]["total"] == 0: self.debug(server='[' + self.get_name() + ']', debug='No service down') else: for alerts in data["result"]: if alerts["type"] == "metaservice": new_host = "Meta_Services" else: new_host = alerts["parent"]["name"] new_service = alerts["name"] # Needed if non-ok services are on a UP host if not new_host in self.new_hosts: self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = new_host self.new_hosts[new_host].status = 'UP' self.new_hosts[new_host].services[new_service] = GenericService() # Attributs à remplir self.debug(server='[' + self.get_name() + ']', debug='Service indexed : ' + new_host + ' / ' + new_service) self.new_hosts[new_host].services[new_service].server = self.name self.new_hosts[new_host].services[new_service].host = new_host self.new_hosts[new_host].services[new_service].name = new_service self.new_hosts[new_host].services[new_service].status = alerts["status"]["name"] self.new_hosts[new_host].services[new_service].last_check = alerts["last_check"] # last_state_change = datetime.strptime(alerts["last_state_change"], '%Y-%m-%dT%H:%M:%S%z').replace(tzinfo=None) # self.new_hosts[new_host].services[new_service].duration = datetime.now() - last_state_change self.new_hosts[new_host].services[new_service].duration = alerts["duration"] self.new_hosts[new_host].services[new_service].attempt = alerts["tries"] self.new_hosts[new_host].services[new_service].status_information = alerts["information"] # Change starting with 23.10 if (self.centreon_version_major >= 23 and self.centreon_version_minor >= 10) or self.centreon_version_major > 23: self.new_hosts[new_host].services[new_service].passiveonly = alerts["has_passive_checks_enabled"] self.new_hosts[new_host].services[new_service].notifications_disabled = not alerts["is_notification_enabled"] self.new_hosts[new_host].services[new_service].acknowledged = alerts["is_acknowledged"] self.new_hosts[new_host].services[new_service].scheduled_downtime = alerts["is_in_downtime"] else: self.new_hosts[new_host].services[new_service].passiveonly = alerts["passive_checks"] self.new_hosts[new_host].services[new_service].notifications_disabled = not alerts["notification_enabled"] self.new_hosts[new_host].services[new_service].acknowledged = alerts["acknowledged"] self.new_hosts[new_host].services[new_service].scheduled_downtime = alerts["in_downtime"] # avoid crash if flapping is not configured in Centreon # according to https://github.com/HenriWahl/Nagstamon/issues/866#issuecomment-1302257034 self.new_hosts[new_host].services[new_service].flapping = alerts.get("flapping", False) if "(S)" in alerts["tries"]: self.new_hosts[new_host].services[new_service].status_type = self.HARD_SOFT['(S)'] else: self.new_hosts[new_host].services[new_service].status_type = self.HARD_SOFT['(H)'] # API inconsistency, even by fixing exact version number, changed starting with 22.04 if self.centreon_version_major == 21 or (self.centreon_version_major == 22 and self.centreon_version_minor == 4): self.new_hosts[new_host].services[new_service].criticality = alerts["severity_level"] else: self.new_hosts[new_host].services[new_service].criticality = alerts["severity"] except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # return True if all worked well return Result() def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): try: acknowledgements = { "acknowledgement": { "comment": comment, "with_services": True, "is_notify_contacts": notify, "is_persistent_comment": persistent, "is_sticky": sticky }, "resources": [ ] } # host if service == '': host_id = self.get_host_and_service_id(host) if self.centreon_version_major >= 24: new_resource = { "type": "host", "id": host_id, "parent": { "id": host_id } } else: new_resource = { "type": "host", "id": host_id, "parent": { "id": None } } acknowledgements["resources"].append(new_resource) # Post json json_string = json.dumps(acknowledgements) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/hosts/{host_id}/acknowledgements result = self.fetch_url(self.urls_centreon['resources'] + '/acknowledge', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Set Ack on Host, status code : " + str(status_code)) # Service if service != '' or all_services: if not all_services: all_services = [service] for s in all_services: host_id, service_id = self.get_host_and_service_id(host, service) if host == "Meta_Services": new_resource = { "type": "metaservice", "id": service_id, "parent": { "id": None } } else: new_resource = { "type": "service", "id": service_id, "parent": { "id": host_id } } acknowledgements["resources"].append(new_resource) if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Stack ack for Host (" + host + ") / Service (" + service + ")") # Post json json_string = json.dumps(acknowledgements) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/services/acknowledgements result = self.fetch_url(self.urls_centreon['resources'] + '/acknowledge', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Set Acks, status code : " + str(status_code)) except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def _set_recheck(self, host, service): rechecks = { "resources": [ ] } # This new parameter was added in 23.04 if self.centreon_version_major >= 23: property_to_add = { "check": { "is_forced": True } } rechecks.update(property_to_add) try: # Host if service == '': host_id = self.get_host_and_service_id(host) new_resource = { "type": "host", "id": host_id, "parent": None } rechecks["resources"].append(new_resource) # Post json json_string = json.dumps(rechecks) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/resources/check result = self.fetch_url(self.urls_centreon['resources'] + '/check', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Recheck on Host : " + host + ", status code : " + str(status_code)) # Service else: host_id, service_id = self.get_host_and_service_id(host, service) if host == "Meta_Services": new_resource = { "type": "metaservice", "id": service_id, "parent": None } else: new_resource = { "type": "service", "id": service_id, "parent": { "id": host_id } } rechecks["resources"].append(new_resource) # Post json json_string = json.dumps(rechecks) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/resources/check result = self.fetch_url(self.urls_centreon['resources'] + '/check', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Reckeck on Host (" + host + ") / Service (" + service + "), status code : " + str( status_code)) except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): obj_start_time = datetime.strptime(start_time, '%Y-%m-%d %H:%M') obj_end_time = datetime.strptime(end_time, '%Y-%m-%d %H:%M') # Nagstamon don’t provide the TZ, we need to get it from the OS obj_start_time = obj_start_time.replace(tzinfo=datetime.now().astimezone().tzinfo) obj_end_time = obj_end_time.replace(tzinfo=datetime.now().astimezone().tzinfo) # duration unit is second duration = (hours * 3600) + (minutes * 60) #  API require boolean if fixed == 1: fixed = True else: fixed = False downtimes = { "downtime": { "comment": comment, "with_services": True, "is_fixed": fixed, "duration": duration, "start_time": obj_start_time.strftime('%Y-%m-%dT%H:%M:%S%z'), "end_time": obj_end_time.strftime('%Y-%m-%dT%H:%M:%S%z') }, "resources": [ ] } try: if service == '': # Host host_id = self.get_host_and_service_id(host) new_resource = { "type": "host", "id": host_id, "parent": None } downtimes["resources"].append(new_resource) # Post json json_string = json.dumps(downtimes) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/resources/downtime result = self.fetch_url(self.urls_centreon['resources'] + '/downtime', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Downtime on Host : " + host + ", status code : " + str(status_code)) # Service else: host_id, service_id = self.get_host_and_service_id(host, service) if host == "Meta_Services": new_resource = { "type": "metaservice", "id": service_id, "parent": { "id": None } } else: new_resource = { "type": "service", "id": service_id, "parent": { "id": host_id } } downtimes["resources"].append(new_resource) # Post json json_string = json.dumps(downtimes) # {protocol}://{server}:{port}/centreon/api/{version}/monitoring/resources/downtime result = self.fetch_url(self.urls_centreon['resources'] + '/downtime', cgi_data=json_string, giveback='raw') error = result.error status_code = result.status_code if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug="Downtime on Host (" + host + ") / Service (" + service + "), status code : " + str( status_code)) except: traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def check_session(self): if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Checking session status') try: if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Check-session, the token expire if not been used for more than one hour. Current Token = ' + str( self.token)) cgi_data = {'limit': '0'} # Get en empty service list, to check the status of the current token # This request must be done in a GET, so just encode the parameters and fetch result = self.fetch_url(self.urls_centreon['resources'] + '?' + urllib.parse.urlencode(cgi_data), giveback="raw") # If we got an 403 or 401 (and 500 for version 21.), the token expired and must be renewed if self.centreon_version_major == 21: ressources_response_list = [401, 403, 500] else: ressources_response_list = [401, 403] if result.status_code in ressources_response_list: self.token = self.get_token().result if conf.debug_mode: self.debug(server='[' + self.get_name() + ']', debug='Check-session, session renewed') result = self.fetch_url(self.urls_centreon['resources'] + '?' + urllib.parse.urlencode(cgi_data), giveback="raw") if not 'ConnectTimeoutError' in result.error and \ not 'NewConnectionError' in result.error: data = json.loads(result.result) error = result.error status_code = result.status_code else: return Result(result='ERROR', error='Connection error') if conf.debug_mode: self.debug(server=self.get_name(), debug="Check-session, Fetched JSON: " + pprint.pformat(data)) self.debug(server=self.get_name(), debug="Check-session, Error : " + error + ", Status code : " + str(status_code)) except: traceback.print_exc(file=sys.stdout) result, error = self.error(sys.exc_info()) return Result(result=result, error=error) Nagstamon-master/Nagstamon/Servers/Centreon/CentreonLegacy.py000066400000000000000000001636331505160700500247220ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import urllib.request import urllib.parse import urllib.error import socket import sys import re import copy from datetime import datetime, timedelta from Nagstamon.objects import * from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf from Nagstamon.helpers import webbrowser_open class CentreonServer(GenericServer): def __init__(self, **kwds): GenericServer.__init__(self, **kwds) self.TYPE = 'Centreon' # centreon generic web interface uses a sid which is needed to ask for news self.SID = None # HARD/SOFT state mapping self.HARD_SOFT = {'(H)': 'hard', '(S)': 'soft'} # apparently necessesary because of non-english states as in https://github.com/HenriWahl/Nagstamon/issues/91 (Centeron 2.5) self.TRANSLATIONS = {'INDISPONIBLE': 'DOWN', 'INJOIGNABLE': 'UNREACHABLE', 'CRITIQUE': 'CRITICAL', 'INCONNU': 'UNKNOWN', 'ALERTE': 'WARNING'} # Entries for monitor default actions in context menu self.MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Downtime'] # Centreon works better or at all with html.parser for BeautifulSoup self.PARSER = 'html.parser' # Needed to detect each Centreon's version self.centreon_version = None # Token that centreon use to protect the system self.centreon_token = None # To only detect broker once self.first_login = True # limit number of services retrived self.limit_services_number = 9999 # default value, applies to version 2.2 and others self.XML_PATH = 'xml' def init_config(self): ''' dummy init_config, called at thread start, not really needed here, just omit extra properties ''' # set URLs here already self.init_HTTP() if not self.tls_error and self.centreon_version is not None: self._define_url() def init_HTTP(self): """ initialize HTTP connection """ if self.session is None: GenericServer.init_HTTP(self) if self.centreon_version is None: result_versioncheck = self.fetch_url(self.monitor_cgi_url + '/index.php', giveback='raw') raw_versioncheck, error_versioncheck = result_versioncheck.result, result_versioncheck.error if error_versioncheck == '': if re.search(r'2\.2\.[0-9]', raw_versioncheck): self.centreon_version = 2.2 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 2.2') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?p=1', 'hosts': '$MONITOR$/main.php?p=20103&o=hpb', 'services': '$MONITOR$/main.php?p=20202&o=svcpb', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'2\.[3-6]\.[0-5]', raw_versioncheck): self.centreon_version = 2.3456 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 2.3 <=> 2.6.5') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?p=1', 'hosts': '$MONITOR$/main.php?p=20103&o=hpb', 'services': '$MONITOR$/main.php?p=20202&o=svcpb', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'2\.6\.[6-9]', raw_versioncheck): self.centreon_version = 2.66 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 2.6.6') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?p=1', 'hosts': '$MONITOR$/main.php?p=20103&o=hpb', 'services': '$MONITOR$/main.php?p=20202&o=svcpb', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'2\.7\.[0-9]', raw_versioncheck): # Centreon 2.7 only support C. Broker self.centreon_version = 2.7 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 2.7') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?', 'hosts': '$MONITOR$/main.php?p=20202&o=hpb', 'services': '$MONITOR$/main.php?p=20201&o=svcpb', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'2\.8\.[0-9]', raw_versioncheck): # Centreon 2.8 only support C. Broker self.centreon_version = 2.8 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 2.8') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?', 'hosts': '$MONITOR$/main.php?p=20202', 'services': '$MONITOR$/main.php?p=20201', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'18\.10\.[0-9]', raw_versioncheck): self.centreon_version = 18.10 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 18.10') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?', 'hosts': '$MONITOR$/main.php?p=20202', 'services': '$MONITOR$/main.php?p=20201', 'history': '$MONITOR$/main.php?p=203'} elif re.search(r'19\.(04|10)\.[0-9]', raw_versioncheck) or re.search(r'20\.(04|10)\.[0-9]', raw_versioncheck): self.centreon_version = 19.04 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version detected : 19.04 <=> 20.10') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?', 'hosts': '$MONITOR$/main.php?p=20202', 'services': '$MONITOR$/main.php?p=20201', 'history': '$MONITOR$/main.php?p=203'} else: # unsupported version or unable do determine self.centreon_version = 19.04 if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Centreon version unknown : supposed to be >= 19.04') # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$/main.php?', 'hosts': '$MONITOR$/main.php?p=20202&o=hpb', 'services': '$MONITOR$/main.php?p=20201&o=svcpb', 'history': '$MONITOR$/main.php?p=203'} else: if conf.debug_mode is True: self.debug(server=self.get_name(), debug='Error getting the home page : ' + error_versioncheck) if self.first_login: self.SID = self._get_sid().result self.first_login = False del result_versioncheck, raw_versioncheck, error_versioncheck def reset_HTTP(self): ''' Centreon needs deletion of SID ''' self.SID = None self.SID = self._get_sid().result def open_monitor(self, host, service=''): if self.use_autologin is True: auth = '&autologin=1&useralias=' + self.username + '&token=' + self.autologin_key else: auth = '' # Meta if host == '_Module_Meta': # Centreon < 2.7 if self.centreon_version < 2.7: webbrowser_open(self.urls_centreon['index'] + '?' + urllib.parse.urlencode({'p': 20206, 'o': 'meta'}) + auth ) # Centreon 2.7 elif self.centreon_version == 2.7: webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':20206, 'o':'meta'}) + auth ) # Centreon 2.8 elif self.centreon_version == 2.8: m = re.search(r'^.+ \((?P.+)\)$', service) if m: service = m.group('rsd') webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':20201,'o':'svcd','host_name':'_Module_Meta','service_description':service}) + auth ) # Starting from Centreon 18.10 else: m = re.search(r'^.+ \((?P.+)\)$', service) if m: service = m.group('rsd') webbrowser_open(self.urls_centreon['main_with_frames'] + '?' + urllib.parse.urlencode({'p':20201,'o':'svcd','host_name':'_Module_Meta','service_description':service}) + auth ) # must be a host if service is empty elif service == '': if self.centreon_version < 2.7: webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':201,'o':'hd', 'host_name':host}) + auth ) elif self.centreon_version in [2.7, 2.8]: webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':20202,'o':'hd', 'host_name':host}) + auth ) else: webbrowser_open(self.urls_centreon['main_with_frames'] + '?' + urllib.parse.urlencode({'p':20202,'o':'hd', 'host_name':host}) + auth ) # so it's a service else: if self.centreon_version < 2.7: webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':202, 'o':'svcd', 'host_name':host, 'service_description':service}) + auth ) elif self.centreon_version in [2.7, 2.8]: webbrowser_open(self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p':20201,'o':'svcd', 'host_name':host, 'service_description':service}) + auth ) else: webbrowser_open(self.urls_centreon['main_with_frames'] + '?' + urllib.parse.urlencode({'p':20201,'o':'svcd', 'host_name':host, 'service_description':service}) + auth ) def _get_sid(self): ''' gets a shiny new SID for XML HTTP requests to Centreon cutting it out via .partition() from raw HTML additionally get php session cookie ''' try: # Aulogin with key, BROWSER_URLS needs the key if self.use_autologin: auth = '&autologin=1&useralias=' + self.username + '&token=' + self.autologin_key self.BROWSER_URLS= { 'monitor': self.BROWSER_URLS['monitor'] + auth,\ 'hosts': self.BROWSER_URLS['hosts'] + auth,\ 'services': self.BROWSER_URLS['services'] + auth,\ 'history': self.BROWSER_URLS['history'] + auth} raw = self.fetch_url(self.monitor_cgi_url + '/index.php?p=101&autologin=1&useralias=' + self.username + '&token=' + self.autologin_key, giveback='raw') if conf.debug_mode: self.debug(server=self.get_name(), debug='Autologin : ' + self.username + ' : ' + self.autologin_key) # Gathering of the token who will be used to interact with Centreon (start with 2.66) if self.centreon_version >= 2.66 and self.centreon_version < 19.04: page = self.fetch_url(self.monitor_cgi_url + '/main.get.php') self.centreon_token = page.result.find('input', {'name': "centreon_token"})['value'] # Password auth else: login = self.fetch_url(self.monitor_cgi_url + '/index.php') if login.error == '' and login.status_code == 200: # Centreon >= 2.6.6 implement a token if self.centreon_version >= 2.66 and self.centreon_version <= 19.04: form = login.result.find('form') form_inputs = {} # Need to catch the centreon_token for login to work for form_input in ('centreon_token', 'submitLogin'): form_inputs[form_input] = form.find('input', {'name': form_input})['value'] self.centreon_token = form_inputs['centreon_token'] form_inputs['useralias'] = self.username form_inputs['password'] = self.password # fire up login button with all needed data raw = self.fetch_url(self.monitor_cgi_url + '/index.php', cgi_data=form_inputs) else: login_data = {"useralias" : self.username, "password" : self.password, "submit" : "Login"} raw = self.fetch_url(self.monitor_cgi_url + "/index.php", cgi_data=login_data, giveback="raw") if conf.debug_mode: self.debug(server=self.get_name(), debug='Password login : ' + self.username + ' : ' + self.password) sid = self.session.cookies.get('PHPSESSID', '') if conf.debug_mode: self.debug(server=self.get_name(), debug='SID : ' + sid) if self.centreon_version >= 2.66 and self.centreon_version < 19.04: self.debug(server=self.get_name(), debug='Centreon Token : ' + self.centreon_token) # those broker urls would not be changing too often so this check migth be done here if self.first_login: self._get_xml_path(sid) self.first_login = False return Result(result=sid) except: import traceback traceback.print_exc(file=sys.stdout) result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def get_start_end(self, host): ''' get start and end time for downtime from Centreon server ''' try: # It's not possible since 18.10 to get date from the webinterface # because it's set in javascript if self.centreon_version < 18.10: cgi_data = {'o':'ah',\ 'host_name':host} if self.centreon_version < 2.7: cgi_data['p'] = '20106' elif self.centreon_version == 2.7: cgi_data['p'] = '210' elif self.centreon_version == 2.8: cgi_data['o'] = 'a' cgi_data['p'] = '210' result = self.fetch_url(self.urls_centreon['main'], cgi_data = cgi_data, giveback='obj') html, error = result.result, result.error if error == '': start_date = html.find(attrs={'name':'start'}).attrs['value'] start_hour = html.find(attrs={'name':'start_time'}).attrs['value'] start_time = start_date + ' ' + start_hour end_date = html.find(attrs={'name':'end'}).attrs['value'] end_hour = html.find(attrs={'name':'end_time'}).attrs['value'] end_time = end_date + ' ' + end_hour return start_time, end_time else: start_time = datetime.now().strftime("%m/%d/%Y %H:%M") end_time = datetime.now() + timedelta(hours=2) end_time = end_time.strftime("%m/%d/%Y %H:%M") return start_time, end_time except: self.error(sys.exc_info()) return 'n/a', 'n/a' def get_host(self, host): ''' Centreonified way to get host ip - attribute 'a' in down hosts xml is of no use for up hosts so we need to get ip anyway from web page ''' # the fastest method is taking hostname as used in monitor if conf.connect_by_host == True or host == '': return Result(result=host) # do a web interface search limited to only one result - the hostname cgi_data = {'sid': self.SID, 'search': host, 'num': 0, 'limit': 1, 'sort_type':'hostname', 'order': 'ASC', 'date_time_format_status': 'd/m/Y H:i:s', 'o': 'h', 'p': 20102, 'time': 0} centreon_hosts = self.urls_centreon['xml_hosts'] + '?' + urllib.parse.urlencode(cgi_data) result = self.fetch_url(centreon_hosts, giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code # initialize ip string ip = '' if len(xmlobj) != 0: ip = str(xmlobj.l.a.text) # when connection by DNS is not configured do it by IP try: if conf.connect_by_dns: # try to get DNS name for ip (reverse DNS), if not available use ip try: address = socket.gethostbyaddr(ip)[0] except: if conf.debug_mode: self.debug(server=self.get_name(), debug='Unable to do a reverse DNS lookup on IP: ' + ip) address = ip else: address = ip except: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) else: result, error = self.error(sys.exc_info()) return Result(error=error) del xmlobj # print IP in debug mode if conf.debug_mode: self.debug(server=self.get_name(), debug='IP of %s:' % (host) + ' ' + address) # give back host or ip return Result(result=address) def _get_xml_path(self, sid): ''' Find out where this instance of Centreon is publishing the status XMLs Centreon 2.6 + ndo/c.broker - /include/monitoring/status/Hosts/xml/{ndo,broker}/hostXML.php according to configuration Centreon 2.7 + c.broker - /include/monitoring/status/Hosts/xml/hostXML.php Centreon 2.8 + c.broker - /include/monitoring/status/Hosts/xml/hostXML.php regexping HTML for Javascript ''' if self.centreon_version <= 2.66: # 2.6 support NDO and C. Broker, we must check which one is used cgi_data = {'p':201, 'sid':sid} result = self.fetch_url(self.monitor_cgi_url + '/main.php', cgi_data=cgi_data, giveback='raw') raw, error = result.result, result.error if error == '': if re.search(r'var _addrXML.*xml\/ndo\/host', raw): self.XML_PATH = 'xml/ndo' if conf.debug_mode: self.debug(server=self.get_name(), debug='Detected broker : NDO') elif re.search(r'var _addrXML.*xml\/broker\/host', raw): self.XML_PATH = 'xml/broker' if conf.debug_mode: self.debug(server=self.get_name(), debug='Detected broker : C. Broker') else: if conf.debug_mode: self.debug(server=self.get_name(), debug='Could not detect the broker for Centeron 2.[3-6]. Using Centreon Broker') self.XML_PATH = 'xml/broker' del raw else: if conf.debug_mode: self.debug(server=self.get_name(), debug='Unable to fetch the main page to detect the broker : ' + error) del result, error else: if conf.debug_mode: self.debug(server=self.get_name(), debug='Only Centreon Broker is supported in Centeon >= 2.7 -> XML_PATH=' + self.XML_PATH) def _define_url(self): urls_centreon_2_2 = { 'main': self.monitor_cgi_url + '/main.php', 'index': self.monitor_cgi_url + '/index.php', 'xml_services': self.monitor_cgi_url + '/include/monitoring/status/Services/' + self.XML_PATH + '/serviceXML.php', 'xml_hosts': self.monitor_cgi_url + '/include/monitoring/status/Hosts/' + self.XML_PATH + '/hostXML.php', 'xml_meta': self.monitor_cgi_url + '/include/monitoring/status/Meta/' + self.XML_PATH + '/metaServiceXML.php', 'xml_hostSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/hostSendCommand.php', 'xml_serviceSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/serviceSendCommand.php', 'external_cmd_cmdPopup': self.monitor_cgi_url + '/include/monitoring/external_cmd/cmdPopup.php', # no idea if this really exist in centreon < 2.7 'autologoutXMLresponse': self.monitor_cgi_url + '/include/common/javascript/autologoutXMLresponse.php' } # inconsistant url in Centreon 2.7 urls_centreon_2_7 = { 'main': self.monitor_cgi_url + '/main.php', 'index': self.monitor_cgi_url + '/index.php', 'xml_services': self.monitor_cgi_url + '/include/monitoring/status/Services/' + self.XML_PATH + '/serviceXML.php', 'xml_hosts': self.monitor_cgi_url + '/include/monitoring/status/Hosts/' + self.XML_PATH + '/broker/hostXML.php', 'xml_meta': self.monitor_cgi_url + '/include/monitoring/status/Meta/' + self.XML_PATH + '/broker/metaServiceXML.php', 'xml_hostSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/hostSendCommand.php', 'xml_serviceSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/serviceSendCommand.php', 'external_cmd_cmdPopup': self.monitor_cgi_url + '/include/monitoring/external_cmd/cmdPopup.php', 'autologoutXMLresponse': self.monitor_cgi_url + '/include/common/javascript/autologoutXMLresponse.php' } urls_centreon_2_8 = { 'main': self.monitor_cgi_url + '/main.php', 'index': self.monitor_cgi_url + '/index.php', 'xml_services': self.monitor_cgi_url + '/include/monitoring/status/Services/' + self.XML_PATH + '/serviceXML.php', 'xml_hosts': self.monitor_cgi_url + '/include/monitoring/status/Hosts/' + self.XML_PATH + '/hostXML.php', 'xml_hostSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/hostSendCommand.php', 'xml_serviceSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/serviceSendCommand.php', 'external_cmd_cmdPopup': self.monitor_cgi_url + '/include/monitoring/external_cmd/cmdPopup.php', 'autologoutXMLresponse': self.monitor_cgi_url + '/include/core/autologout/autologoutXMLresponse.php' } urls_centreon_18_10 = { 'main': self.monitor_cgi_url + '/main.get.php', # needed to get the frames around the page when opening the monitoring on a host/service 'main_with_frames': self.monitor_cgi_url + '/main.php', 'index': self.monitor_cgi_url + '/index.php', 'xml_services': self.monitor_cgi_url + '/include/monitoring/status/Services/' + self.XML_PATH + '/serviceXML.php', 'xml_hosts': self.monitor_cgi_url + '/include/monitoring/status/Hosts/' + self.XML_PATH + '/hostXML.php', 'xml_hostSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/hostSendCommand.php', 'xml_serviceSendCommand': self.monitor_cgi_url + '/include/monitoring/objectDetails/xml/serviceSendCommand.php', 'external_cmd_cmdPopup': self.monitor_cgi_url + '/include/monitoring/external_cmd/cmdPopup.php', 'keepAlive': self.monitor_cgi_url + '/api/internal.php?object=centreon_keepalive&action=keepAlive' } if self.centreon_version < 2.7: self.urls_centreon = urls_centreon_2_2 elif self.centreon_version == 2.7: self.urls_centreon = urls_centreon_2_7 elif self.centreon_version == 2.8: self.urls_centreon = urls_centreon_2_8 # 18.10 and beyond elif self.centreon_version >= 18.10: self.urls_centreon = urls_centreon_18_10 if conf.debug_mode: self.debug(server=self.get_name(), debug='URLs defined for Centreon %s' % (self.centreon_version)) def _get_host_id(self, host): ''' get host_id via parsing raw html ''' if self.centreon_version < 2.7: cgi_data = {'p': 20102, 'o': 'hd', 'host_name': host, 'sid': self.SID} else: cgi_data = {'p': 20202, 'o': 'hd', 'host_name': host, 'sid': self.SID} url = self.urls_centreon['main'] + '?' + urllib.parse.urlencode(cgi_data) result = self.fetch_url(url, giveback='raw') raw, error = result.result, result.error if error == '': host_id = raw.partition("var host_id = '")[2].partition("'")[0] del raw else: if conf.debug_mode: self.debug(server=self.get_name(), debug='Host ID could not be retrieved.') # some cleanup del result, error # only if host_id is an usable integer return it try: if int(host_id): if conf.debug_mode: self.debug(server=self.get_name(), host=host, debug='Host ID is ' + host_id) return host_id else: return '' except: return '' def _get_host_and_service_id(self, host, service): ''' parse a ton of html to get a host and a service id... ''' cgi_data = {'p':'20201',\ 'host_name':host,\ 'service_description':service,\ 'o':'svcd'} # This request must be done in a GET, so just encode the parameters and fetch result = self.fetch_url(self.urls_centreon['main'] + '?' + urllib.parse.urlencode(cgi_data), giveback="raw") raw, error = result.result, result.error if error == '': host_id = raw.partition("var host_id = '")[2].partition("'")[0] svc_id = raw.partition("var svc_id = '")[2].partition("'")[0] del raw if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='- Get host/svc ID : ' + host_id + '/' + svc_id) else: if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='- IDs could not be retrieved.') # some cleanup del result, error # only if host_id is an usable integer return it try: if int(host_id) and int(svc_id): if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='- Host & Service ID are valid (int)') return host_id,svc_id else: return '','' except: return '','' def _get_status(self): ''' Get status from Centreon Server ''' # Be sure that the session is still active result = self._check_session() if result is not None: if result.result == 'ERROR': if 'urls_centreon' in result.error: result.error = 'Connection error' return result # services (unknown, warning or critical?) if self.centreon_version < 2.7: nagcgiurl_services = self.urls_centreon['xml_services'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'svcpb', 'sort_type':'status', 'sid':self.SID}) else: nagcgiurl_services = self.urls_centreon['xml_services'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'svcpb', 'p':20201, 'nc':0, 'criticality':0, 'statusService':'svcpb', 'sSetOrderInMemory':1, 'sid':self.SID}) # hosts (up or down or unreachable) # define hosts xml URL, because of inconsistant url if self.centreon_version < 2.7: nagcgiurl_hosts = self.urls_centreon['xml_hosts'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'hpb', 'sort_type':'status', 'sid':self.SID}) elif self.centreon_version >= 2.7 and self.centreon_version < 19.04: nagcgiurl_hosts = self.urls_centreon['xml_hosts'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'hpb', 'p':20202, 'criticality':0, 'statusHost':'hpb', 'sSetOrderInMemory':1, 'sid':self.SID}) else: nagcgiurl_hosts = self.urls_centreon['xml_hosts'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'hpb', 'p':20202, 'criticality':0, 'statusHost':'hpb', 'sSetOrderInMemory':1}) # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: result = self.fetch_url(nagcgiurl_hosts, giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code # check if any error occured errors_occured = self.check_for_error(xmlobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # Check if the result is not empty if len(xmlobj) == 0: if conf.debug_mode: self.debug(server=self.get_name(), debug='Empty host XML result') return Result(result=None, error="Empty host XML result") # in case there are no children session ID is expired if xmlobj.text.lower() == 'bad session id': del xmlobj if conf.debug_mode: self.debug(server=self.get_name(), debug='Bad session ID, retrieving new one...') # try again... self.SID = self._get_sid().result result = self.fetch_url(nagcgiurl_hosts, giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code errors_occured = self.check_for_error(xmlobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # a second time a bad session id should raise an error if xmlobj.text.lower() == 'bad session id': if conf.debug_mode: self.debug(server=self.get_name(), debug='Even after renewing session ID, unable to get the XML') return Result(result='ERROR', error='Bad session ID', status_code=status_code) for l in xmlobj.findAll('l'): try: # host objects contain service objects if not l.hn.text in self.new_hosts: self.new_hosts[str(l.hn.text)] = GenericHost() self.new_hosts[str(l.hn.text)].name = str(l.hn.text) self.new_hosts[str(l.hn.text)].server = self.name self.new_hosts[str(l.hn.text)].status = str(l.cs.text) # disgusting workaround for https://github.com/HenriWahl/Nagstamon/issues/91 if self.new_hosts[str(l.hn.text)].status in self.TRANSLATIONS: self.new_hosts[str(l.hn.text)].status = self.TRANSLATIONS[self.new_hosts[str(l.hn.text)].status] self.new_hosts[str(l.hn.text)].attempt, self.new_hosts[str(l.hn.text)].status_type = str(l.tr.text).split(' ') self.new_hosts[str(l.hn.text)].status_type = self.HARD_SOFT[self.new_hosts[str(l.hn.text)].status_type] self.new_hosts[str(l.hn.text)].last_check = str(l.lc.text) self.new_hosts[str(l.hn.text)].duration = str(l.lsc.text) self.new_hosts[str(l.hn.text)].status_information = str(l.ou.text).replace('\n', ' ').strip() if l.find('cih') != None: self.new_hosts[str(l.hn.text)].criticality = str(l.cih.text) else: self.new_hosts[str(l.hn.text)].criticality = '' self.new_hosts[str(l.hn.text)].acknowledged = bool(int(str(l.ha.text))) self.new_hosts[str(l.hn.text)].scheduled_downtime = bool(int(str(l.hdtm.text))) if l.find('is') != None: self.new_hosts[str(l.hn.text)].flapping = bool(int(str(l.find('is').text))) else: self.new_hosts[str(l.hn.text)].flapping = False self.new_hosts[str(l.hn.text)].notifications_disabled = not bool(int(str(l.ne.text))) self.new_hosts[str(l.hn.text)].passiveonly = not bool(int(str(l.ace.text))) except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) del xmlobj except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: result = self.fetch_url(nagcgiurl_services, giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code # check if any error occured errors_occured = self.check_for_error(xmlobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # Check if the result is not empty if len(xmlobj) == 0: if conf.debug_mode: self.debug(server=self.get_name(), debug='Empty service XML result') return Result(result=None, error="Empty service XML result") # in case there are no children session id is invalid if xmlobj.text.lower() == 'bad session id': # debug if conf.debug_mode: self.debug(server=self.get_name(), debug='Bad session ID, retrieving new one...') # try again... self.SID = self._get_sid().result result = self.fetch_url(nagcgiurl_services, giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code errors_occured = self.check_for_error(xmlobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # a second time a bad session id should raise an error if xmlobj.text.lower() == 'bad session id': return Result(result='ERROR', error='Bad session ID', status_code=status_code) # In Centreon 2.8, Meta are merged with regular services if self.centreon_version < 2.8: # define meta-services xml URL if self.centreon_version == 2.7: nagcgiurl_meta_services = self.urls_centreon['xml_meta'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'meta', 'sort_type':'status', 'sid':self.SID}) else: nagcgiurl_meta_services = self.urls_centreon['xml_meta'] + '?' + urllib.parse.urlencode({'num':0, 'limit':self.limit_services_number, 'o':'meta', 'sort_type':'status', 'sid':self.SID}) # retrive meta-services xml STATUS result_meta = self.fetch_url(nagcgiurl_meta_services, giveback='xml') xmlobj_meta, error_meta, status_code_meta = result_meta.result, result_meta.error, result_meta.status_code # check if any error occured errors_occured = self.check_for_error(xmlobj_meta, error_meta, status_code_meta) # if there are errors return them if errors_occured is not None: return errors_occured # a second time a bad session id should raise an error if xmlobj_meta.text.lower() == 'bad session id': if conf.debug_mode: self.debug(server=self.get_name(), debug='Even after renewing session ID, unable to get the XML') return Result(result='ERROR', error='Bad session ID', status_code=status_code_meta) # INSERT META-services xml at the end of the services xml try: xmlobj.append(xmlobj_meta.reponse) except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del xmlobj_meta for l in xmlobj.findAll('l'): try: # host objects contain service objects ###if not self.new_hosts.has_key(str(l.hn.text)): if not l.hn.text in self.new_hosts: self.new_hosts[str(l.hn.text)] = GenericHost() self.new_hosts[str(l.hn.text)].name = str(l.hn.text) self.new_hosts[str(l.hn.text)].status = 'UP' # if a service does not exist create its object if not l.sd.text in self.new_hosts[str(l.hn.text)].services: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)] = GenericService() self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host = str(l.hn.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].name = str(l.sd.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].server = self.name self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status = str(l.cs.text) if self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host == '_Module_Meta': # ajusting service name for Meta services if self.centreon_version < 2.8: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].name = '{} ({})'.format(str(l.sd.text), l.rsd.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].attempt = str(l.ca.text) else: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].name = '{} ({})'.format(str(l.sdn.text), l.sdl.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].attempt, \ self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type = str(l.ca.text).split(' ') else: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].attempt, \ self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type = str(l.ca.text).split(' ') # disgusting workaround for https://github.com/HenriWahl/Nagstamon/issues/91 # Still needed in Centreon 2.8 at least : https://github.com/HenriWahl/Nagstamon/issues/344 # Need enhancement, we can do service state matching with this field service_unknown #if self.centreon_version < 2.66: if self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status in self.TRANSLATIONS: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status = self.TRANSLATIONS[\ self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status] if not (self.centreon_version < 2.8 and self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host == '_Module_Meta'): self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type =\ self.HARD_SOFT[self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type] if conf.debug_mode: self.debug(server=self.get_name(), debug='Parsing service XML (Host/Service/Status_type) ' + self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host + '/' + self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].name + '/' + self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].last_check = str(l.lc.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].duration = str(l.d.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_information = str(l.po.text).replace('\n', ' ').strip() if l.find('cih') != None: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].criticality = str(l.cih.text) else: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].criticality = '' self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].acknowledged = bool(int(str(l.pa.text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].notifications_disabled = not bool(int(str(l.ne.text))) # for features not available in centreon < 2.8 and meta services if not (self.centreon_version < 2.8 and self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host == '_Module_Meta'): self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].scheduled_downtime = bool(int(str(l.dtm.text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].flapping = bool(int(str(l.find('is').text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].passiveonly = not bool(int(str(l.ac.text))) except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del xmlobj except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # return True if all worked well return Result() def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): # decision about host or service - they have different URLs try: if service == '': # host cgi_data = {'cmd': '14', 'host_name': host, 'author': author, 'comment': comment, 'submit': 'Add', 'notify': int(notify), 'persistent': int(persistent), 'sticky': int(sticky), 'ackhostservice': '0', 'en': '1'} if self.centreon_version < 2.7: cgi_data['p'] = '20105' cgi_data['o'] = 'hpb' else: cgi_data['p'] = '20202' cgi_data['o'] = 'hpb' cgi_data['centreon_token'] = self.centreon_token # Post raw = self.fetch_url(self.urls_centreon['main'], cgi_data=cgi_data, giveback='raw') del raw # if host is acknowledged and all services should be to or if a service is acknowledged # (and all other on this host too) if service != '' or all_services: # service(s) @ host # if all_services is empty only one service has to be checked - the one clicked # otherwise if there all services should be acknowledged if not all_services: all_services = [service] # acknowledge all services on a host for s in all_services: cgi_data = {'cmd': '15', 'host_name': host, 'author': author, 'comment': comment, 'submit': 'Add', 'notify': int(notify), 'service_description': s, 'force_check': '1', 'persistent': int(persistent), # following not needed in 18.10, required in wich version ? 'persistant': int(persistent), 'sticky': int(sticky), 'o': 'svcd', 'en': '1'} if self.centreon_version < 2.7: cgi_data['p'] = '20215' else: cgi_data['p'] = '20201' cgi_data['centreon_token'] = self.centreon_token # in case of a meta-service, extract the 'rsd' field from the service name : if host == '_Module_Meta': m = re.search(r'^.+ \((?P.+)\)$', s) if m: rsd = m.group('rsd') if self.centreon_version < 2.8: cgi_data = {'p': '20206', 'o': 'meta', 'cmd': '70', 'select[' + host + ';' + rsd + ']': '1', 'limit': '0'} elif self.centreon_version in [2.8, 18.10]: cgi_data['service_description'] = rsd # POST, for some strange reason only working if giveback is 'raw' raw = self.fetch_url(self.urls_centreon['main'], cgi_data=cgi_data, giveback='raw') del raw except: self.error(sys.exc_info()) def _set_recheck(self, host, service): ''' host and service ids are needed to tell Centreon what whe want ''' try: # decision about host or service - they have different URLs # Meta if host == '_Module_Meta': if conf.debug_mode: self.debug(server=self.get_name(), debug='Recheck on a Meta service, more work to be done') m = re.search(r'^.+ \((?P.+)\)$', service) if m: rsd = m.group('rsd') if self.centreon_version < 2.8: url = self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p': '20206','o': 'meta','cmd': '3','select[' + host + ';' + rsd + ']': '1','limit':'0'}) else: url = self.urls_centreon['main'] + '?' + urllib.parse.urlencode({'p': '202','o': 'svc','cmd': '3','select[' + host + ';' + rsd + ']': '1','limit':'1','centreon_token':self.centreon_token}) elif service == '': # ... it can only be a host, so check all his services and there is a command for that host_id = self._get_host_id(host) if self.centreon_version < 2.7: url = self.urls_centreon['xml_hostSendCommand'] + '?' + urllib.parse.urlencode({'cmd':'host_schedule_check', 'actiontype':1,'host_id':host_id,'sid':self.SID}) else: url = self.urls_centreon['xml_hostSendCommand'] + '?' + urllib.parse.urlencode({'cmd':'host_schedule_check', 'actiontype':1,'host_id':host_id}) del host_id else: # service @ host host_id, service_id = self._get_host_and_service_id(host, service) # Starting from 19.04 this must be in POST if self.centreon_version < 19.04: # fill and encode URL data cgi_data = urllib.parse.urlencode({'cmd':'service_schedule_check', 'actiontype':1,\ 'host_id':host_id, 'service_id':service_id, 'sid':self.SID}) url = self.urls_centreon['xml_serviceSendCommand'] + '?' + cgi_data del host_id, service_id else: cgi_data = {'cmd': 'service_schedule_check', 'host_id': host_id, 'service_id': service_id, 'actiontype': '0'} del host_id, service_id if self.centreon_version < 19.04: # execute GET request raw = self.fetch_url(url, giveback='raw') del raw else: # running remote cgi command with POST method, for some strange reason only working if # giveback is 'raw' raw = self.fetch_url(self.urls_centreon['xml_serviceSendCommand'], cgi_data=cgi_data, giveback='raw') del raw except: self.error(sys.exc_info()) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): ''' gets actual host and service ids and apply them to downtime cgi ''' try: # duration unit is minute duration = (hours * 60) + minutes # need cmdPopup.php needs boolean if fixed == 1: fixed = 'true' else: fixed = 'false' # Host downtime if service == '': if self.centreon_version < 19.04: cgi_data = {'cmd':75,\ 'duration':duration,\ 'duration_scale':'m',\ 'start':start_time,\ 'end':end_time,\ 'comment':comment,\ 'fixed':fixed,\ 'downtimehostservice':'true',\ 'author':author,\ 'sid':self.SID,\ 'select['+host+']':1 } # Params has changed starting from 19.04 else: cgi_data = {'cmd':75, 'duration':duration, 'duration_scale':'m', 'comment':comment, 'start':start_time, 'end':end_time, 'host_or_centreon_time':0, 'fixed':fixed, 'downtimehostservice':'true', 'author':author, 'resources':'["'+host+'"]' } # Service downtime else: # Centreon 2.8 only, in case of a meta-service, extract the 'rsd' field from the service name : if host == '_Module_Meta' and self.centreon_version in [2.8, 18.10]: m = re.search(r'^.+ \((?P.+)\)$', service) if m: rsd = m.group('rsd') service = rsd if self.centreon_version < 19.04: cgi_data = {'cmd':74,\ 'duration':duration,\ 'duration_scale':'m',\ 'start':start_time,\ 'end':end_time,\ 'comment':comment,\ 'fixed':fixed,\ 'downtimehostservice':0,\ 'author':author,\ 'sid':self.SID,\ 'select['+host+';'+service+']':1 } # Params has changed starting from 19.04 else: cgi_data = {'cmd':74, 'duration':duration, 'duration_scale':'m', 'comment':comment, 'start':start_time, 'end':end_time, 'host_or_centreon_time':0, 'fixed':fixed, 'downtimehostservice':0, 'author':author, 'resources':'["'+host+'%3B'+service+'"]' } if self.centreon_version < 19.04: # This request must be done in a GET, so just encode the parameters and fetch raw = self.fetch_url(self.urls_centreon['external_cmd_cmdPopup'] + '?' + urllib.parse.urlencode(cgi_data), giveback="raw") del raw # Starting from 19.04, must be POST else: # Do it in POST raw = self.fetch_url(self.urls_centreon['external_cmd_cmdPopup'], cgi_data=cgi_data, giveback='raw') del raw except: self.error(sys.exc_info()) def _check_session(self): if conf.debug_mode: self.debug(server=self.get_name(), debug='Checking session status') if 'url_centreon' not in self.__dict__: self.init_config() if self.centreon_version: try: if self.centreon_version >= 18.10: result = self.fetch_url(self.urls_centreon['keepAlive'], giveback='raw') self.raw, self.error, self.status_code = result.result, result.error, result.status_code # Return 200 & null a session is open if conf.debug_mode: self.debug(server=self.get_name(), debug='Session status : ' + self.raw + ', http code : ' + str(self.status_code)) # 401 if no valid session is present if self.status_code == 401: self.SID = self._get_sid().result if conf.debug_mode: self.debug(server=self.get_name(), debug='Session renewed') else: result = self.fetch_url(self.urls_centreon['autologoutXMLresponse'], giveback='xml') xmlobj, error, status_code = result.result, result.error, result.status_code self.session_state = xmlobj.find("state").text.lower() if conf.debug_mode: self.debug(server=self.get_name(), debug='Session status : ' + self.session_state) if self.session_state == "nok": self.SID = self._get_sid().result if conf.debug_mode: self.debug(server=self.get_name(), debug='Session renewed') except: import traceback traceback.print_exc(file=sys.stdout) result, error = self.error(sys.exc_info()) return Result(result=result, error=error) else: return Result(result='ERROR', error='Cannot detect Centreon version')Nagstamon-master/Nagstamon/Servers/Centreon/__init__.py000066400000000000000000000063141505160700500235470ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import json from . import CentreonAPI from ..Generic import GenericServer from Nagstamon.config import conf class CentreonServer(GenericServer): """ Use this class as switch """ TYPE = 'Centreon' def __init__(self, **kwds): # like all servers we need to initialize Generic GenericServer.__init__(self, **kwds) # due to not being initialized right now we need to access config directly to get this instance's config server_conf = conf.servers.get(kwds.get('name')) if server_conf and server_conf.enabled: # because auf being very early in init process the property ignore_cert is not known yet # add it here to be able to fetch URL and ignore certs if activated self.ignore_cert = server_conf.ignore_cert self.custom_cert_use = server_conf.custom_cert_use # This URL exists on Centreon 22.x - if not accessible it must be legacy versions_raw = self.fetch_url(f'{server_conf.monitor_cgi_url}/api/latest/platform/versions', no_auth=True, giveback='raw') self.debug(server='[' + self.get_name() + ']', debug='Status code %s' % (str(versions_raw.status_code))) if versions_raw.status_code == 200: data = json.loads(versions_raw.result) ver_major = int(data["web"]["major"]) ver_minor = int(data["web"]["minor"]) # API V2 is usable only after 21.04 (not tested), ressources endpoint is buggy in 20.10 if ver_major >= 21: self.debug(server='[' + self.get_name() + ']', debug='Loading class API, Centreon version : ' + str(ver_major) + '.' + str(ver_minor)) from .CentreonAPI import CentreonServer as CentreonServerReal else: self.debug(server='[' + self.get_name() + ']', debug='Loading class LEGACY, Centreon version : ' + str(ver_major) + '.' + str(ver_minor)) from .CentreonLegacy import CentreonServer as CentreonServerReal else: from .CentreonLegacy import CentreonServer as CentreonServerReal self.debug(server='[' + self.get_name() + ']', debug='Loading class LEGACY, Centreon version will be checked later') # kind of mad but helps the Servers/__init__.py to detect if there is any other class to be used self.ClassServerReal = CentreonServerReal Nagstamon-master/Nagstamon/Servers/Generic.py000066400000000000000000002366411505160700500216170ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from collections import OrderedDict import copy import datetime import json from pathlib import Path import platform import socket import sys import traceback import urllib.parse from typing import Optional from urllib.request import getproxies from bs4 import BeautifulSoup import requests # check ECP authentication support availability try: from requests_ecp import HTTPECPAuth ECP_AVAILABLE = True except ImportError: ECP_AVAILABLE = False from Nagstamon.helpers import (host_is_filtered_out_by_re, service_is_filtered_out_by_re, status_information_is_filtered_out_by_re, duration_is_filtered_out_by_re, attempt_is_filtered_out_by_re, groups_is_filtered_out_by_re, criticality_is_filtered_out_by_re, not_empty, webbrowser_open, STATES) from Nagstamon.objects import (GenericService, GenericHost, Result) from Nagstamon.config import (AppInfo, conf, debug_queue, OS, OS_MACOS, RESOURCES) # flag to keep track of Kerberos availability KERBEROS_AVAILABLE = False if OS == OS_MACOS: # requests_gssapi is newer but not available everywhere try: # extra imports needed to get it compiled on macOS import numbers import gssapi.raw.cython_converters from requests_gssapi import HTTPSPNEGOAuth as HTTPSKerberos KERBEROS_AVAILABLE = True except ImportError as error: print(error) else: # requests_gssapi is newer but not available everywhere try: # requests_gssapi needs installation of KfW - Kerberos for Windows # requests_kerberoes doesn't from requests_kerberos import HTTPKerberosAuth as HTTPSKerberos KERBEROS_AVAILABLE = True except ImportError as error: print(error) # disable annoying SubjectAltNameWarning warnings try: from requests.packages.urllib3.exceptions import SubjectAltNameWarning requests.packages.urllib3.disable_warnings(SubjectAltNameWarning) except ImportError: # older requests version might not have the packages submodule # for example the one in Ubuntu 14.04 pass # avoid flooding logs with InsecureRequestWarnings for connections with 'no_auth' set # interestingly this is not available in requests.packages.urllib3.exceptions try: import urllib3 urllib3.disable_warnings() except ImportError: pass # add possibility for bearer auth to requests class BearerAuth(requests.auth.AuthBase): def __init__(self, token): self.token = token def __call__(self, r): r.headers["Authorization"] = "Bearer " + self.token return r class GenericServer: ''' Abstract server which serves as template for all other types Default values are for Nagios servers ''' TYPE = 'Generic' # dictionary to translate status bitmaps on webinterface into status flags # this are defaults from Nagios # 'disabled.gif' is in Nagios for hosts the same as 'passiveonly.gif' for services STATUS_MAPPING = {'ack.gif': 'acknowledged', 'passiveonly.gif': 'passiveonly', 'disabled.gif': 'passiveonly', 'ndisabled.gif': 'notifications_disabled', 'downtime.gif': 'scheduled_downtime', 'flapping.gif': 'flapping'} # Entries for monitor default actions in context menu MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ['check_output', 'performance_data'] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = {'monitor': '$MONITOR$', 'hosts': '$MONITOR-CGI$/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12', 'services': '$MONITOR-CGI$/status.cgi?host=all&servicestatustypes=253', 'history': '$MONITOR-CGI$/history.cgi?host=all'} USER_AGENT = '{0}/{1}/{2}'.format(AppInfo.NAME, AppInfo.VERSION, platform.system()) # needed to check return code of monitor server in case of false authentication STATUS_CODES_NO_AUTH = [401, 403] # default parser for BeautifulSoup - the rediscovered lxml causes trouble for Centreon so there should be choice # see https://github.com/HenriWahl/Nagstamon/issues/431 PARSER = 'lxml' def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] self.enabled = False self.type = '' self.monitor_url = '' self.monitor_cgi_url = '' self.username = '' self.password = '' self.use_proxy = False self.use_proxy_from_os = False self.proxy_address = '' self.proxy_username = '' self.proxy_password = '' self.auth_type = '' self.encoding = 'utf-8' self.hosts = dict() self.new_hosts = dict() self.isChecking = False self.CheckingForNewVersion = False # store current, last and difference of worst state for notification self.worst_status_diff = self.worst_status_current = self.worst_status_last = 'UP' self.nagitems_filtered_list = list() self.nagitems_filtered = {'services': {'DISASTER': [], 'CRITICAL': [], 'HIGH': [], 'AVERAGE': [], 'WARNING': [], 'INFORMATION': [], 'UNKNOWN': []}, 'hosts': {'DOWN': [], 'UNREACHABLE': []}} # number of filtered items self.nagitems_filtered_count = 0 self.down = 0 self.unreachable = 0 self.unknown = 0 self.critical = 0 self.warning = 0 # zabbix support self.information = 0 self.average = 0 self.high = 0 self.disaster = 0 self.all_ok = True self.status = '' self.status_description = '' self.status_code = 0 self.has_error = False self.timeout = 10 # The events_* are recycled from GUI.py # history of events to track status changes for notifications # events that came in self.events_current = {} # events that had been already displayed in popwin and need no extra mark self.events_history = {} # events to be given to custom notification, maybe to desktop notification too self.events_notification = {} # needed for looping server thread self.thread_counter = 0 # needed for RecheckAll - save start_time once for not having to get it for every recheck self.start_time = None # Requests-based connections self.session = None # flag which decides if authentication has to be renewed self.refresh_authentication = False # flag which tells GUI if there is an TLS problem self.tls_error = False # counter for login attempts - have to be treatened differently by every monitoring server type self.login_count = 0 # to handle Icinga versions this information is necessary, might be of future use for others too self.version = '' # macOS pyinstaller onefile conglomerate tends to lose cacert.pem due to macOS temp folder cleaning self.cacert_path = self.cacert_content = False if OS == OS_MACOS: # trying to find root path when run by pyinstaller onefile, must be something like # /var/folders/7w/hfvrg7v92x3gjt95cqh974240000gn/T/_MEIQ3l3u3 root_path = Path(RESOURCES).parent.parent if root_path.joinpath('certifi').is_dir() and root_path.joinpath('certifi', 'cacert.pem').is_file(): # store path of cacert... self.cacert_path = root_path.joinpath('certifi', 'cacert.pem') # ...and its content with open(self.cacert_path, mode='rb') as file: self.cacert_content = file.read() # Special FX # Centreon self.use_autologin = False self.autologin_key = '' # Icinga self.use_display_name_host = False self.use_display_name_service = False # Checkmk Multisite self.force_authuser = False # OP5 api filters self.host_filter = 'state !=0' self.service_filter = 'state !=0 or host.state != 0' # Opsview hashtag filter self.hashtag_filter = '' # Opsview can_change_only option self.can_change_only = False # Sensu/Uchiwa/??? Datacenter/Site config self.monitor_site = 'Site 1' # Zabbix self.use_description_name_service = None # IcingaDBWebNotifications self.notification_filter = None self.notification_lookback = None # Thruk self.disabled_backends = None def init_config(self): ''' set URLs for CGI - they are static and there is no need to set them with every cycle ''' # create filters like described in # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # # the following variables are not necessary anymore as with 'new' filtering # # hoststatus # hoststatustypes = 12 # servicestatus # servicestatustypes = 253 # serviceprops & hostprops both have the same values for the same states so I # group them together # hostserviceprops = 0 # services (unknown, warning or critical?) as dictionary, sorted by hard and soft state type self.cgiurl_services = { 'hard': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=262144&limit=0', 'soft': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=524288&limit=0'} # hosts (up or down or unreachable) self.cgiurl_hosts = { 'hard': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=262144&limit=0', 'soft': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=524288&limit=0'} def init_HTTP(self): """ initialize HTTP connection should return a valid session if none exists yet when reauthentication is needed the session should be removed """ if self.refresh_authentication: self.session = None return False elif self.session is None: self.session = self.create_session() return True def create_session(self): """ reusable session creation partly not constantly working Basic Authorization requires extra Authorization headers, different between various server types """ session = requests.Session() session.headers['User-Agent'] = self.USER_AGENT # support for different authentication types if self.authentication == 'basic': # basic authentication # not encoding as 'utf-8' causes trouble with special characters # https://github.com/HenriWahl/Nagstamon/issues/1126 # https://github.com/psf/requests/issues/4564#issuecomment-670771785 session.auth = (self.username.encode('utf-8'), self.password.encode('utf-8')) elif self.authentication == 'digest': session.auth = requests.auth.HTTPDigestAuth(self.username, self.password) elif self.authentication == 'ecp' and ECP_AVAILABLE: session.auth = HTTPECPAuth(self.idp_ecp_endpoint, username=self.username, password=self.password) elif self.authentication == 'kerberos' and KERBEROS_AVAILABLE: session.auth = HTTPSKerberos() elif self.authentication == 'bearer': session.auth = BearerAuth(self.password) # default to check TLS validity if self.ignore_cert: session.verify = False elif self.custom_cert_use: session.verify = self.custom_cert_ca_file else: session.verify = True # add proxy information self.proxify(session) return session def proxify(self, session): ''' add proxy information to session or single request ''' # check if proxies have to be used if self.use_proxy is True: if self.use_proxy_from_os is True: # get proxies from system directly instead of via trust_env session.proxies = getproxies() # check for missing '/' to make proxies work for scheme, proxy_url in session.proxies.items(): if not proxy_url.endswith('/'): session.proxies[scheme] = proxy_url + '/' pass else: # check if username and password are given and provide credentials if needed if self.proxy_username == self.proxy_password == '': user_pass = '' else: user_pass = '{0}:{1}'.format(self.proxy_username, self.proxy_password) # split and analyze proxy URL proxy_address_parts = self.proxy_address.split('//') scheme = proxy_address_parts[0] host_port = ''.join(proxy_address_parts[1:]) # use only valid schemes if scheme.lower() in ('http:', 'https:', 'socks5:', 'socks5h:'): # merge proxy URL proxy_url = f'{scheme}//{user_pass}@{host_port}/' # fill session.proxies for both protocols session.proxies = {'http': proxy_url, 'https': proxy_url} else: session.proxies = None session.trust_env = False def reset_HTTP(self): ''' if authentication fails try to reset any HTTP session stuff - might be different for different monitors ''' self.session = None def get_name(self): ''' return stringified name ''' return str(self.name) def get_username(self): ''' return stringified username ''' return str(self.username) def get_password(self): ''' return stringified password ''' return str(self.password) def get_server_version(self): ''' dummy function, at the moment only used by Icinga ''' pass def set_recheck(self, info_dict): self._set_recheck(info_dict['host'], info_dict['service']) def _set_recheck(self, host, service): if service != '': if self.hosts[host].services[service].is_passive_only(): # Do not check passive only checks return try: # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios result = self.fetch_url( self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '96', 'host': host})) self.start_time = dict(result.result.find(attrs={'name': 'start_time'}).attrs)['value'] # decision about host or service - they have different URLs if service == '': # host cmd_typ = '96' else: # service @ host cmd_typ = '7' # ignore empty service in case of rechecking a host cgi_data = urllib.parse.urlencode([('cmd_typ', cmd_typ), ('cmd_mod', '2'), ('host', host), ('service', service), ('start_time', self.start_time), ('force_check', 'on'), ('btnSubmit', 'Commit')]) # execute POST request self.fetch_url(self.monitor_cgi_url + '/cmd.cgi', giveback='raw', cgi_data=cgi_data) except: traceback.print_exc(file=sys.stdout) def set_acknowledge(self, info_dict): ''' different monitors might have different implementations of _set_acknowledge ''' if info_dict['acknowledge_all_services'] is True: all_services = info_dict['all_services'] else: all_services = [] self._set_acknowledge(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['sticky'], info_dict['notify'], info_dict['persistent'], all_services) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): ''' send acknowledge to monitor server - might be different on every monitor type ''' url = self.monitor_cgi_url + '/cmd.cgi' # the following flags apply to hosts and services # # according to sf.net bug #3304098 (https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3304098&group_id=236865) # the send_notification-flag must not exist if it is set to 'off', otherwise # the Nagios core interpretes it as set, regardless its real value # # for whatever silly reason Icinga depends on the correct order of submitted form items... # see sf.net bug 3428844 # # Thanks to Icinga ORDER OF ARGUMENTS IS IMPORTANT HERE! # cgi_data = OrderedDict() if service == '': cgi_data['cmd_typ'] = '33' else: cgi_data['cmd_typ'] = '34' cgi_data['cmd_mod'] = '2' cgi_data['host'] = host if service != '': cgi_data['service'] = service cgi_data['com_author'] = author cgi_data['com_data'] = comment cgi_data['btnSubmit'] = 'Commit' if notify is True: cgi_data['send_notification'] = 'on' if persistent is True: cgi_data['persistent'] = 'on' if sticky is True: cgi_data['sticky_ack'] = 'on' self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # acknowledge all services on a host if all_services: for s in all_services: cgi_data['cmd_typ'] = '34' cgi_data['service'] = s self.fetch_url(url, giveback='raw', cgi_data=cgi_data) def set_downtime(self, info_dict): ''' different monitors might have different implementations of _set_downtime ''' self._set_downtime(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['fixed'], info_dict['start_time'], info_dict['end_time'], info_dict['hours'], info_dict['minutes']) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): ''' finally send downtime command to monitor server ''' url = self.monitor_cgi_url + '/cmd.cgi' # for some reason Icinga is very fastidiuos about the order of CGI arguments, so please # here we go... it took DAYS :-( cgi_data = OrderedDict() if service == '': cgi_data['cmd_typ'] = '55' else: cgi_data['cmd_typ'] = '56' cgi_data['cmd_mod'] = '2' cgi_data['trigger'] = '0' cgi_data['host'] = host if service != '': cgi_data['service'] = service cgi_data['com_author'] = author cgi_data['com_data'] = comment cgi_data['fixed'] = fixed cgi_data['start_time'] = start_time cgi_data['end_time'] = end_time cgi_data['hours'] = hours cgi_data['minutes'] = minutes cgi_data['btnSubmit'] = 'Commit' # running remote cgi command self.fetch_url(url, giveback='raw', cgi_data=cgi_data) def set_submit_check_result(self, info_dict): """ start specific submission part """ self._set_submit_check_result(info_dict['host'], info_dict['service'], info_dict['state'], info_dict['comment'], info_dict['check_output'], info_dict['performance_data']) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): ''' worker for submitting check result ''' url = self.monitor_cgi_url + '/cmd.cgi' # decision about host or service - they have different URLs if service == '': # host cgi_data = urllib.parse.urlencode([('cmd_typ', '87'), ('cmd_mod', '2'), ('host', host), ('plugin_state', {'up': '0', 'down': '1', 'unreachable': '2'}[state]), ('plugin_output', check_output), ('performance_data', performance_data), ('btnSubmit', 'Commit')]) self.fetch_url(url, giveback='raw', cgi_data=cgi_data) if service != '': # service @ host cgi_data = urllib.parse.urlencode( [('cmd_typ', '30'), ('cmd_mod', '2'), ('host', host), ('service', service), ('plugin_state', {'ok': '0', 'warning': '1', 'critical': '2', 'unknown': '3'}[state]), ('plugin_output', check_output), ('performance_data', performance_data), ('btnSubmit', 'Commit')]) # running remote cgi command self.fetch_url(url, giveback='raw', cgi_data=cgi_data) def get_start_end(self, host): ''' for GUI to get actual downtime start and end from server - they may vary so it's better to get directly from web interface ''' try: result = self.fetch_url( self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '55', 'host': host})) start_time = dict(result.result.find(attrs={'name': 'start_time'}).attrs)['value'] end_time = dict(result.result.find(attrs={'name': 'end_time'}).attrs)['value'] # give values back as tuple return start_time, end_time except Exception: self.error(sys.exc_info()) return 'n/a', 'n/a' def open_monitor(self, host, service=''): ''' open monitor from tablewidget context menu ''' # only type is important so do not care of service '' in case of host monitor if service == '': typ = 1 else: typ = 2 if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Open host/service monitor web page ' + self.monitor_cgi_url + '/extinfo.cgi?' + urllib.parse.urlencode( {'type': typ, 'host': host, 'service': service})) webbrowser_open(self.monitor_cgi_url + '/extinfo.cgi?' + urllib.parse.urlencode( {'type': typ, 'host': host, 'service': service})) def open_monitor_webpage(self): ''' open monitor from systray/toparea context menu ''' if conf.debug_mode: self.debug(server=self.get_name(), debug='Open monitor web page ' + self.monitor_cgi_url) webbrowser_open(self.monitor_url) def _get_status(self): ''' Get status from Nagios Server ''' # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily nagitems = {'services': [], 'hosts': []} # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_hosts[status_type]) htobj, error, status_code = result.result, result.error, result.status_code # check if any error occured errors_occured = self.check_for_error(htobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # put a copy of a part of htobj into table to be able to delete htobj # too mnuch copy.deepcopy()s here give recursion crashs table = htobj('table', {'class': 'status'})[0] # access table rows # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # do some cleanup del result, error # kick out table heads trs.pop(0) # dummy tds to be deleteable tds = [] for tr in trs: try: # ignore empty rows if len(tr('td', recursive=False)) > 1: n = dict() # get tds in one tr tds = tr('td', recursive=False) # host try: n['host'] = str(tds[0].table.tr.td.table.tr.td.a.text) except Exception: n['host'] = str(nagitems[len(nagitems) - 1]['host']) # status n['status'] = str(tds[1].text) # last_check n['last_check'] = str(tds[2].text) # duration n['duration'] = str(tds[3].text) # division between Nagios and Icinga in real life... where # Nagios has only 5 columns there are 7 in Icinga 1.3... # ... and 6 in Icinga 1.2 :-) if len(tds) < 7: # the old Nagios table # status_information if len(tds[4](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[4].text).replace('\n', ' ').replace('\t', ' ').strip() # attempts are not shown in case of hosts so it defaults to 'n/a' n['attempt'] = 'n/a' else: # attempts are shown for hosts # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n['attempt'] = str(tds[4].text).strip() # status_information if len(tds[5](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[5].text).replace('\n', ' ').replace('\t', ' ').strip() # status flags n['passiveonly'] = False n['notifications_disabled'] = False n['flapping'] = False n['acknowledged'] = False n['scheduled_downtime'] = False # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this host item to nagitems nagitems['hosts'].append(n) # after collection data in nagitems create objects from its informations # host objects contain service objects if n['host'] not in self.new_hosts: new_host = n['host'] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n['host'] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n['status'] self.new_hosts[new_host].last_check = n['last_check'] self.new_hosts[new_host].duration = n['duration'] self.new_hosts[new_host].attempt = n['attempt'] # ##self.new_hosts[new_host].status_information = n['status_information'].encode('utf-8') self.new_hosts[new_host].status_information = n['status_information'] self.new_hosts[new_host].passiveonly = n['passiveonly'] self.new_hosts[new_host].notifications_disabled = n['notifications_disabled'] self.new_hosts[new_host].flapping = n['flapping'] self.new_hosts[new_host].acknowledged = n['acknowledged'] self.new_hosts[new_host].scheduled_downtime = n['scheduled_downtime'] self.new_hosts[new_host].status_type = status_type del tds, n except Exception: self.error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except Exception: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_services[status_type]) htobj, error, status_code = result.result, result.error, result.status_code # check if any error occured errors_occured = self.check_for_error(htobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # too much copy.deepcopy()s here give recursion crashs table = htobj('table', {'class': 'status'})[0] # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) del result, error # kick out table heads trs.pop(0) # dummy tds to be deleteable tds = [] for tr in trs: try: # ignore empty rows - there are a lot of them - a Nagios bug? tds = tr('td', recursive=False) if len(tds) > 1: n = dict() # host # the resulting table of Nagios status.cgi table omits the # hostname of a failing service if there are more than one # so if the hostname is empty the nagios status item should get # its hostname from the previuos item - one reason to keep 'nagitems' try: n['host'] = str(tds[0](text=not_empty)[0]) except Exception: n['host'] = str(nagitems['services'][len(nagitems['services']) - 1]['host']) # service n['service'] = str(tds[1](text=not_empty)[0]) # status n['status'] = str(tds[2](text=not_empty)[0]) # last_check n['last_check'] = str(tds[3](text=not_empty)[0]) # duration n['duration'] = str(tds[4](text=not_empty)[0]) # attempt # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n['attempt'] = str(tds[5](text=not_empty)[0]).strip() # status_information if len(tds[6](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[6].text).replace('\n', ' ').replace('\t', ' ').strip() # status flags n['passiveonly'] = False n['notifications_disabled'] = False n['flapping'] = False n['acknowledged'] = False n['scheduled_downtime'] = False # map status icons to status flags icons = tds[1].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this service item to nagitems - only if service nagitems['services'].append(n) # after collection data in nagitems create objects of its informations # host objects contain service objects if n['host'] not in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = 'UP' # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 # if host is not down but in downtime or any other flag this should be evaluated too # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: self.new_hosts[n['host']].__dict__[self.STATUS_MAPPING[icon]] = True # if a service does not exist create its object if n['service'] not in self.new_hosts[n['host']].services: new_service = n['service'] self.new_hosts[n['host']].services[new_service] = GenericService() self.new_hosts[n['host']].services[new_service].host = n['host'] self.new_hosts[n['host']].services[new_service].name = n['service'] self.new_hosts[n['host']].services[new_service].server = self.name self.new_hosts[n['host']].services[new_service].status = n['status'] self.new_hosts[n['host']].services[new_service].last_check = n['last_check'] self.new_hosts[n['host']].services[new_service].duration = n['duration'] self.new_hosts[n['host']].services[new_service].attempt = n['attempt'] self.new_hosts[n['host']].services[new_service].status_information = n['status_information'] self.new_hosts[n['host']].services[new_service].passiveonly = n['passiveonly'] self.new_hosts[n['host']].services[new_service].notifications_disabled = n[ 'notifications_disabled'] self.new_hosts[n['host']].services[new_service].flapping = n['flapping'] self.new_hosts[n['host']].services[new_service].acknowledged = n['acknowledged'] self.new_hosts[n['host']].services[new_service].scheduled_downtime = n['scheduled_downtime'] self.new_hosts[n['host']].services[new_service].status_type = status_type del tds, n except Exception: self.error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except Exception: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del(nagitems) # dummy return in case all is OK return Result() def get_status(self, output=None): ''' get nagios status information from cgiurl and give it back as dictionary output parameter is needed in case authentication failed so that popwin might ask for credentials ''' # set checking flag to be sure only one thread cares about this server self.isChecking = True # check if server is enabled, if not, do not get any status if self.enabled is False: self.worst_status_diff = 'UP' self.isChecking = False return Result() # initialize HTTP first self.init_HTTP() # get all trouble hosts/services from server specific _get_status() status = self._get_status() if status is not None: self.status = status.result self.status_description = status.error self.status_code = status.status_code else: return Result() # some monitor server seem to have a problem with too short intervals # and sometimes send a bad status line which would result in a misleading # ERROR display - it seems safe to ignore these errors # see https://github.com/HenriWahl/Nagstamon/issues/207 # Update: Another strange error to ignore is ConnectionResetError # see https://github.com/HenriWahl/Nagstamon/issues/295 if 'BadStatusLine' in self.status_description or\ 'ConnectionResetError' in self.status_description: self.status_description = '' self.isChecking = False return Result(result=self.status, error=self.status_description, status_code=self.status_code) if (self.status == 'ERROR' or self.status_description != '' or self.status_code >= 400): # ask for password if authorization failed if 'HTTP Error 401' in self.status_description or \ 'HTTP Error 403' in self.status_description or \ 'HTTP Error 500' in self.status_description or \ 'bad session id' in self.status_description.lower() or \ 'login failed' in self.status_description.lower() or \ self.status_code in self.STATUS_CODES_NO_AUTH: if conf.servers[self.name].enabled is True: # needed to get valid credentials self.refresh_authentication = True # clean existent authentication self.reset_HTTP() self.init_HTTP() status = self._get_status() self.status = status.result self.status_description = status.error self.status_code = status.status_code return status elif self.status_description.startswith('requests.exceptions.SSLError:'): self.tls_error = True else: self.isChecking = False self.tls_error = False return Result(result=self.status, error=self.status_description, status_code=self.status_code) # no new authentication needed self.refresh_authentication = False # this part has been before in GUI.RefreshDisplay() - wrong place, here it needs to be reset self.nagitems_filtered = {'services': {'DISASTER': [], 'CRITICAL': [], 'HIGH': [], 'AVERAGE': [], 'WARNING': [], 'INFORMATION': [], 'UNKNOWN': []}, 'hosts': {'DOWN': [], 'UNREACHABLE': []}} # initialize counts for various service/hosts states # count them with every miserable host/service respective to their meaning self.down = 0 self.unreachable = 0 self.unknown = 0 self.critical = 0 self.warning = 0 # zabbix support self.information = 0 self.average = 0 self.high = 0 self.disaster = 0 for host in self.new_hosts.values(): # Don't enter the loop if we don't have a problem. Jump down to your problem services if not host.status == 'UP': # add hostname for sorting host.host = host.name # Some generic filters if host.acknowledged is True and conf.filter_acknowledged_hosts_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: ACKNOWLEDGED ' + str(host.name)) host.visible = False if host.notifications_disabled is True and\ conf.filter_hosts_services_disabled_notifications is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: NOTIFICATIONS ' + str(host.name)) host.visible = False if host.passiveonly is True and conf.filter_hosts_services_disabled_checks is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: PASSIVEONLY ' + str(host.name)) host.visible = False if host.scheduled_downtime is True and conf.filter_hosts_services_maintenance is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: DOWNTIME ' + str(host.name)) host.visible = False if host.flapping is True and conf.filter_all_flapping_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: FLAPPING HOST ' + str(host.name)) host.visible = False # Checkmk and OP5 do not show the status_type so their host.status_type will be empty if host.status_type != '': if conf.filter_hosts_in_soft_state is True and host.status_type == 'soft': if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: SOFT STATE ' + str(host.name)) host.visible = False if host_is_filtered_out_by_re(host.name, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name)) host.visible = False if status_information_is_filtered_out_by_re(host.status_information, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name)) host.visible = False # The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. if self.type == 'Centreon': if criticality_is_filtered_out_by_re(host.criticality, conf): if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP Criticality ' + str(host.name)) host.visible = False # Finegrain for the specific state if host.status == 'DOWN': if conf.filter_all_down_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: DOWN ' + str(host.name)) host.visible = False if host.visible: self.nagitems_filtered['hosts']['DOWN'].append(host) self.down += 1 if host.status == 'UNREACHABLE': if conf.filter_all_unreachable_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: UNREACHABLE ' + str(host.name)) host.visible = False if host.visible: self.nagitems_filtered['hosts']['UNREACHABLE'].append(host) self.unreachable += 1 # Add host flags for status icons in treeview if host.acknowledged: host.host_flags += 'A' if host.scheduled_downtime: host.host_flags += 'D' if host.flapping: host.host_flags += 'F' if host.passiveonly: host.host_flags += 'P' for service in host.services.values(): # add service name for sorting service.service = service.get_service_name() # Some generic filtering if service.acknowledged is True and conf.filter_acknowledged_hosts_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: ACKNOWLEDGED ' + str(host.name) + ';' + str(service.name)) service.visible = False if service.notifications_disabled is True and\ conf.filter_hosts_services_disabled_notifications is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: NOTIFICATIONS ' + str(host.name) + ';' + str(service.name)) service.visible = False if service.passiveonly is True and conf.filter_hosts_services_disabled_checks is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: PASSIVEONLY ' + str(host.name) + ';' + str(service.name)) service.visible = False if service.scheduled_downtime is True and conf.filter_hosts_services_maintenance is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: DOWNTIME ' + str(host.name) + ';' + str(service.name)) service.visible = False if service.flapping is True and conf.filter_all_flapping_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: FLAPPING SERVICE ' + str(host.name) + ';' + str(service.name)) service.visible = False if host.scheduled_downtime is True and conf.filter_services_on_hosts_in_maintenance is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: Service on host in DOWNTIME ' + str(host.name) + ';' + str( service.name)) service.visible = False if host.acknowledged is True and conf.filter_services_on_acknowledged_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: Service on acknowledged host' + str(host.name) + ';' + str( service.name)) service.visible = False if host.status == 'DOWN' and conf.filter_services_on_down_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: Service on host in DOWN ' + str(host.name) + ';' + str(service.name)) service.visible = False if host.status == 'UNREACHABLE' and conf.filter_services_on_unreachable_hosts is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: Service on host in UNREACHABLE ' + str(host.name) + ';' + str( service.name)) service.visible = False if conf.filter_all_unreachable_services is True and service.unreachable is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: UNREACHABLE ' + str(host.name) + ';' + str( service.name)) service.visible = False # Checkmk and OP5 do not show the status_type so their host.status_type will be empty if service.status_type != '': if conf.filter_services_in_soft_state is True and service.status_type == 'soft': if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: SOFT STATE ' + str(host.name) + ';' + str(service.name)) service.visible = False # fix for https://github.com/HenriWahl/Nagstamon/issues/654 elif not self.TYPE.startswith("Zabbix"): if len(service.attempt) < 3: service.visible = True elif len(service.attempt) == 3: # fixing a bug introduced in 038fa34 for zabbix service name in attempt if service.attempt.find("/") == -1: service.visible = True else: # the old, actually wrong, behaviour real_attempt, max_attempt = service.attempt.split('/') if real_attempt != max_attempt and conf.filter_services_in_soft_state is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: SOFT STATE ' + str(host.name) + ';' + str(service.name)) service.visible = False if host_is_filtered_out_by_re(host.name, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False if service_is_filtered_out_by_re(service.get_name(), conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False if status_information_is_filtered_out_by_re(service.status_information, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False if duration_is_filtered_out_by_re(service.duration, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False if attempt_is_filtered_out_by_re(service.attempt, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False if groups_is_filtered_out_by_re(service.groups, conf) is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) service.visible = False # The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. if self.type == 'Centreon': if criticality_is_filtered_out_by_re(service.criticality, conf): if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: REGEXP Criticality %s;%s %s' % ( (str(host.name), str(service.name), str(service.criticality)))) service.visible = False # Finegrain for the specific state if service.visible: if service.status == 'DISASTER': if conf.filter_all_disaster_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: DISASTER ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['DISASTER'].append(service) self.disaster += 1 if service.status == 'CRITICAL': if conf.filter_all_critical_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: CRITICAL ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['CRITICAL'].append(service) self.critical += 1 if service.status == 'HIGH': if conf.filter_all_high_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: HIGH ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['HIGH'].append(service) self.high += 1 if service.status == 'AVERAGE': if conf.filter_all_average_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: AVERAGE ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['AVERAGE'].append(service) self.average += 1 if service.status == 'WARNING': if conf.filter_all_warning_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: WARNING ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['WARNING'].append(service) self.warning += 1 if service.status == 'INFORMATION': if conf.filter_all_information_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: INFORMATION ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['INFORMATION'].append(service) self.information += 1 if service.status == 'UNKNOWN': if conf.filter_all_unknown_services is True: if conf.debug_mode: self.debug(server=self.get_name(), debug='Filter: UNKNOWN ' + str(host.name) + ';' + str(service.name)) service.visible = False else: self.nagitems_filtered['services']['UNKNOWN'].append(service) self.unknown += 1 # Add service flags for status icons in treeview if service.acknowledged: service.service_flags += 'A' if service.scheduled_downtime: service.service_flags += 'D' if service.flapping: service.service_flags += 'F' if service.passiveonly: service.service_flags += 'P' # Add host of service flags for status icons in treeview if host.acknowledged: service.host_flags += 'A' if host.scheduled_downtime: service.host_flags += 'D' if host.flapping: service.host_flags += 'F' if host.passiveonly: service.host_flags += 'P' # find out if there has been some status change to notify user # compare sorted lists of filtered nagios items new_nagitems_filtered_list = [] for i in self.nagitems_filtered['hosts'].values(): for h in i: new_nagitems_filtered_list.append((h.name, h.status)) for i in self.nagitems_filtered['services'].values(): for s in i: new_nagitems_filtered_list.append((s.host, s.name, s.status)) # sort for better comparison new_nagitems_filtered_list.sort() # in the following lines worst_status_diff only changes from UP to another value if there was some change in the # worst status - if it is the same as before it will just keep UP # if both lists are identical, there was no status change if (self.nagitems_filtered_list == new_nagitems_filtered_list): self.worst_status_diff = 'UP' else: # if the new list is shorter than the first and there are no different hosts # there one host/service must have been recovered, which is not worth a notification diff = [] for i in new_nagitems_filtered_list: if i not in self.nagitems_filtered_list: # collect differences diff.append(i) if len(diff) == 0: self.worst_status_diff = 'UP' else: # if there are different hosts/services in the list of new hosts there must be a notification # get list of states for comparison diff_states = [] for d in diff: diff_states.append(d[-1]) # temporary worst state index worst = 0 for d in diff_states: # only check the worst state if it is valid if d in STATES: if STATES.index(d) > worst: worst = STATES.index(d) # final worst state is one of the predefined states self.worst_status_diff = STATES[worst] del diff_states # get the current worst state, needed at least for systraystatusicon self.worst_status_last = self.worst_status_current self.worst_status_current = 'UP' if self.down > 0: self.worst_status_current = 'DOWN' elif self.unreachable > 0: self.worst_status_current = 'UNREACHABLE' elif self.disaster > 0: self.worst_status_current = 'DISASTER' elif self.critical > 0: self.worst_status_current = 'CRITICAL' elif self.high > 0: self.worst_status_current = 'HIGH' elif self.average > 0: self.worst_status_current = 'AVERAGE' elif self.warning > 0: self.worst_status_current = 'WARNING' elif self.information > 0: self.worst_status_current = 'INFORMATION' elif self.unknown > 0: self.worst_status_current = 'UNKNOWN' # when everything is OK set this flag for GUI to evaluate if self.down == 0 and\ self.unreachable == 0 and\ self.disaster == 0 and\ self.unknown == 0 and\ self.critical == 0 and\ self.high == 0 and\ self.average == 0 and\ self.warning == 0 and\ self.information == 0: self.all_ok = True else: self.all_ok = False # copy of listed nagitems for next comparison self.nagitems_filtered_list = copy.deepcopy(new_nagitems_filtered_list) del new_nagitems_filtered_list # put new informations into respective dictionaries self.hosts = copy.deepcopy(self.new_hosts) self.new_hosts.clear() # taken from GUI.RefreshDisplay() - get event history for notification # first clear current events self.events_current.clear() # get all nagitems for host in self.hosts.values(): if not host.status == 'UP': # only if host is not filtered out add it to current events # the boolean is meaningless for current events if host.visible: self.events_current[host.get_hash()] = True for service in host.services.values(): # same for services of host if service.visible: self.events_current[service.get_hash()] = True # check if some cached event still is relevant - kick it out if not for event in list(self.events_history.keys()): if event not in self.events_current.keys(): self.events_history.pop(event) self.events_notification.pop(event) # if some current event is not yet in event cache add it and mark it as fresh (=True) for event in list(self.events_current.keys()): if event not in self.events_history.keys() and conf.highlight_new_events: self.events_history[event] = True self.events_notification[event] = True # after all checks are done unset checking flag self.isChecking = False # return True if all worked well return Result() def fetch_url(self, url, giveback='obj', cgi_data=None, no_auth=False, multipart=False, headers=None): ''' get content of given url, cgi_data only used if present 'obj' fetch_url() gives back a dict full of miserable hosts/services, 'xml' giving back as objectified xml 'raw' it gives back pure HTML - useful for finding out IP or new version 'json' gives back JSON data existence of cgi_data forces urllib to use POST instead of GET requests NEW: gives back a list containing result and, if necessary, a more clear error description ''' # assume TLS is OK when connecting self.tls_error = False # run this method which checks itself if there is some action to take for initializing connection # if no_auth is true do not use Auth headers, used by check for new version try: try: # debug if conf.debug_mode is True: # unpasswordify CGI data if cgi_data is not None and not isinstance(cgi_data, str): cgi_data_log = copy.copy(cgi_data) for key in cgi_data_log.keys(): if 'pass' in key: cgi_data_log[key] = '***************' else: cgi_data_log = cgi_data self.debug(server=self.get_name(), debug='fetch_url: ' + url + ' CGI Data: ' + str(cgi_data_log)) if OS == OS_MACOS and self.cacert_path and not self.cacert_path.is_file(): # pyinstaller temp folder seems to be emptied completely after a while # so the directories containing the resources have to be recreated too self.cacert_path.parent.mkdir(exist_ok=True) # write cached content of cacert.pem file back onto disk with self.cacert_path.open(mode='wb') as file: file.write(self.cacert_content) # in case we know the server's encoding use it if self.encoding: if cgi_data is not None: try: for k in cgi_data: cgi_data[k] = cgi_data[k].encode(self.encoding) except: # set to false to mark it as invalid self.encoding = False # use session only for connections to monitor servers, other requests like looking for updates # should go out without credentials if no_auth is False and not self.refresh_authentication: # check if there is really a session if not self.session: self.reset_HTTP() self.init_HTTP() # most requests come without multipart/form-data if multipart is False: if cgi_data is None: response = self.session.get(url, timeout=self.timeout, headers=headers) else: response = self.session.post(url, data=cgi_data, timeout=self.timeout, headers=headers) else: # Checkmk and Opsview need multipart/form-data encoding # http://stackoverflow.com/questions/23120974/python-requests-post-multipart-form-data-without-filename-in-http-request#23131823 form_data = dict() for key in cgi_data: form_data[key] = (None, cgi_data[key]) # get response with cgi_data encodes as files response = self.session.post(url, files=form_data, timeout=self.timeout) else: # send request without authentication data temporary_session = requests.Session() temporary_session.headers['User-Agent'] = self.USER_AGENT # default to check TLS validity if self.ignore_cert: temporary_session.verify = False elif self.custom_cert_use: temporary_session.verify = self.custom_cert_ca_file else: temporary_session.verify = True # add proxy information if necessary self.proxify(temporary_session) # most requests come without multipart/form-data if multipart is False: if cgi_data is None: #response = temporary_session.get(url, timeout=self.timeout, verify=not self.ignore_cert) response = temporary_session.get(url, timeout=self.timeout, verify=False, headers=headers) else: #response = temporary_session.post(url, data=cgi_data, timeout=self.timeout, verify=not self.ignore_cert) response = temporary_session.post(url, data=cgi_data, timeout=self.timeout, verify=False, headers=headers) else: # Checkmk and Opsview need multipart/form-data encoding # http://stackoverflow.com/questions/23120974/python-requests-post-multipart-form-data-without-filename-in-http-request#23131823 form_data = dict() for key in cgi_data: form_data[key] = (None, cgi_data[key]) # get response with cgi_data encodes as files response = temporary_session.post(url, files=form_data, timeout=self.timeout, verify=False) # cleanup del temporary_session except: if conf.debug_mode: self.error(sys.exc_info()) result, error = self.error(sys.exc_info()) if error.startswith('requests.exceptions.SSLError:'): self.tls_error = True else: self.tls_error = False return Result(result=result, error=error, status_code=-1) # store encoding in case it is not the server side encoding if self.encoding != response.encoding: self.encoding = response.encoding # give back pure HTML or XML in case giveback is 'raw' if giveback == 'raw': # .text gives content in unicode return Result(result=response.text, status_code=response.status_code) # objectified HTML if giveback == 'obj': yummysoup = BeautifulSoup(response.text, self.PARSER) return Result(result=yummysoup, status_code=response.status_code) # objectified generic XML, valid at least for Opsview and Centreon elif giveback == 'xml': xmlobj = BeautifulSoup(response.text, self.PARSER) return Result(result=xmlobj, status_code=response.status_code) # give back JSON giveback is 'raw' if giveback == 'json': # .text gives content in unicode return Result(result=json.loads(response.text), status_code=response.status_code) except: self.error(sys.exc_info()) result, error = self.error(sys.exc_info()) return Result(result=result, error=error, status_code=response.status_code) result, error = self.error(sys.exc_info()) return Result(result=result, error=error, status_code=response.status_code) def get_host(self, host): ''' find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios ''' # the fasted method is taking hostname as used in monitor if conf.connect_by_host is True or host == '': return Result(result=host) # initialize ip string ip = '' # glue nagios cgi url and hostinfo cgiurl_host = self.monitor_cgi_url + '/extinfo.cgi?type=1&host=' + host # get host info result = self.fetch_url(cgiurl_host, giveback='obj') htobj = result.result try: # take ip from html soup ip = htobj.findAll(name='div', attrs={'class': 'data'})[-1].text # workaround for URL-ified IP as described in SF bug 2967416 # https://sourceforge.net/tracker/?func=detail&aid=2967416&group_id=236865&atid=1101370 if '://' in ip: ip = ip.split('://')[1] # last-minute-workaround for https://github.com/HenriWahl/Nagstamon/issues/48 if ',' in ip: ip = ip.split(',')[0] # print IP in debug mode if conf.debug_mode is True: self.debug(server=self.get_name(), host=host, debug='IP of %s:' % (host) + ' ' + ip) # when connection by DNS is not configured do it by IP if conf.connect_by_dns is True: # try to get DNS name for ip, if not available use ip try: address = socket.gethostbyaddr(ip)[0] except socket.error: address = ip else: address = ip except Exception: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del htobj # give back host or ip return Result(result=address) def get_items_generator(self): ''' Generator for plain listing of all filtered items, used in qui for tableview ''' # reset number of filtered items self.nagitems_filtered_count = 0 for state in self.nagitems_filtered['hosts'].values(): for host in state: # increase number of items for use in table self.nagitems_filtered_count += 1 yield (host) for state in self.nagitems_filtered['services'].values(): for service in state: # increase number of items for use in table self.nagitems_filtered_count += 1 yield (service) def hook(self): ''' allows to add some extra actions for a monitor server to be executed in RefreshLoop inspired by Centreon and its seemingly Alzheimer disease regarding session ID/Cookie/whatever ''' pass def error(self, error): ''' Handle errors somehow - print them or later log them into not yet existing log file ''' if conf.debug_mode: debug = '' for line in traceback.format_exception(error[0], error[1], error[2], 5): debug += line self.debug(server=self.get_name(), debug=debug, head='ERROR') return ['ERROR', traceback.format_exception_only(error[0], error[1])[0]] def debug(self, server='', host='', service='', debug='', head='DEBUG'): ''' centralized debugging ''' # initialize items in line to be logged log_line = [head + ':', str(datetime.datetime.now())] if server != '': log_line.append(server) if host != '': log_line.append(host) if service != '': log_line.append(service) if debug != '': log_line.append(debug) # put debug info into debug queue debug_queue.append(' '.join(log_line)) def get_events_history_count(self): """ return number of unseen events - those which are set True as unseen """ return len(list((e for e in self.events_history if self.events_history[e] is True))) @staticmethod def check_for_error(result, error, status_code) -> Optional[Result]: """ check if any error occured - if so, return error """ if error != '' or status_code > 400: return Result(result=copy.deepcopy(result), error=copy.deepcopy(error), status_code=copy.deepcopy(status_code)) else: return None def get_worst_status_current(self): """ hand over the current worst status for get_worst_status() """ # get the current worst state, needed at least for systraystatusicon self.worst_status_current = 'UP' if self.down > 0: self.worst_status_current = 'DOWN' elif self.unreachable > 0: self.worst_status_current = 'UNREACHABLE' elif self.disaster > 0: self.worst_status_current = 'DISASTER' elif self.critical > 0: self.worst_status_current = 'CRITICAL' elif self.high > 0: self.worst_status_current = 'HIGH' elif self.average > 0: self.worst_status_current = 'AVERAGE' elif self.warning > 0: self.worst_status_current = 'WARNING' elif self.information > 0: self.worst_status_current = 'INFORMATION' elif self.unknown > 0: self.worst_status_current = 'UNKNOWN' return self.worst_status_current def get_worst_status_diff(self): """ hand over the current worst status difference for qui """ return self.worst_status_diff Nagstamon-master/Nagstamon/Servers/Icinga.py000066400000000000000000001060761505160700500214330ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import urllib.request, urllib.parse, urllib.error import sys import copy import json from bs4 import BeautifulSoup from collections import OrderedDict from Nagstamon.Servers.Generic import GenericServer from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.helpers import not_empty class IcingaServer(GenericServer): """ object of Incinga server """ TYPE = 'Icinga' # flag to handle JSON or HTML correctly - checked by get_server_version() json = None def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None def init_HTTP(self): """ Icinga 1.11 needs extra Referer header for actions """ GenericServer.init_HTTP(self) if not 'Referer' in self.session.headers: # to execute actions since Icinga 1.11 a Referer Header is necessary self.session.headers['Referer'] = self.monitor_cgi_url + '/cmd.cgi' def get_server_version(self): """ Try to get Icinga version for different URLs and JSON capabilities """ result = self.fetch_url('%s/tac.cgi?jsonoutput' % (self.monitor_cgi_url), giveback='raw') if result.error != '': return result else: tacraw = result.result if result.status_code < 400: if tacraw.startswith('<'): self.json = False tacsoup = BeautifulSoup(tacraw, 'html.parser') self.version = tacsoup.find('a', { 'class': 'homepageURL' }) # only extract version if HTML seemed to be OK if 'contents' in self.version.__dict__: self.version = self.version.contents[0].split('Icinga ')[1] elif tacraw.startswith('{'): # there seem to be problems with Icinga < 1.6 # in case JSON parsing crashes fall back to HTML try: jsondict = json.loads(tacraw) self.version = jsondict['cgi_json_version'] self.json = True except: self.version = '1.6' self.json = False else: self.refresh_authentication = True def looseversion(self, version): """ Replacement for distutils.version.LooseVersion which got lost in newer Python Fix for https://github.com/HenriWahl/Nagstamon/issues/787 """ # gives back a list with split version components - makes 2 method calls comparable return [int(x) for x in version.split('.')] def _get_status(self): """ Get status from Icinga Server, prefer JSON if possible """ try: if self.json == None: # we need to get the server version and its JSONability result = self.get_server_version() if self.version != '': # define CGI URLs for hosts and services depending on JSON-capable server version if self.cgiurl_hosts == self.cgiurl_services == None: if self.looseversion(self.version) < self.looseversion('1.7'): # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # services (unknown, warning or critical?) as dictionary, sorted by hard and soft state type self.cgiurl_services = {'hard': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=262144', \ 'soft': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=524288'} # hosts (up or down or unreachable) self.cgiurl_hosts = {'hard': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=262144', \ 'soft': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=524288'} else: # services (unknown, warning or critical?) self.cgiurl_services = {'hard': self.monitor_cgi_url + '/status.cgi?style=servicedetail&servicestatustypes=253&serviceprops=262144', \ 'soft': self.monitor_cgi_url + '/status.cgi?style=servicedetail&servicestatustypes=253&serviceprops=524288'} # hosts (up or down or unreachable) self.cgiurl_hosts = {'hard': self.monitor_cgi_url + '/status.cgi?style=hostdetail&hoststatustypes=12&hostprops=262144', \ 'soft': self.monitor_cgi_url + '/status.cgi?style=hostdetail&hoststatustypes=12&hostprops=524288'} if self.json: for status_type in 'hard', 'soft': self.cgiurl_services[status_type] += '&jsonoutput' self.cgiurl_hosts[status_type] += '&jsonoutput' # get status depending on JSONablility if self.json: return self._get_status_JSON() else: return self._get_status_HTML() else: # error result in case version still was '' return result except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # dummy return in case all is OK return Result() def _get_status_JSON(self): """ Get status from Icinga Server - the JSON way """ # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # now using JSON output from Icinga try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_hosts[status_type], giveback='raw') # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured errors_occured = self.check_for_error(jsonraw, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured jsondict = json.loads(jsonraw) hosts = copy.deepcopy(jsondict['status']['host_status']) for host in hosts: # make dict of tuples for better reading h = dict(host.items()) # host if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if 'host_name' in h: host_name = h['host_name'] elif 'host' in h: host_name = h['host'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = h['host_display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status = h['status'] self.new_hosts[host_name].last_check = h['last_check'] self.new_hosts[host_name].duration = h['duration'] self.new_hosts[host_name].attempt = h['attempts'] self.new_hosts[host_name].status_information = h['status_information'].replace('\n', ' ').strip() self.new_hosts[host_name].passiveonly = not(h['active_checks_enabled']) self.new_hosts[host_name].notifications_disabled = not(h['notifications_enabled']) self.new_hosts[host_name].flapping = h['is_flapping'] self.new_hosts[host_name].acknowledged = h['has_been_acknowledged'] self.new_hosts[host_name].scheduled_downtime = h['in_scheduled_downtime'] self.new_hosts[host_name].status_type = status_type # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = h['host_name'] del h, host_name except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_services[status_type], giveback='raw') # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured errors_occured = self.check_for_error(jsonraw, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured jsondict = json.loads(jsonraw) services = copy.deepcopy(jsondict['status']['service_status']) for service in services: # make dict of tuples for better reading s = dict(service.items()) if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if 'host_name' in s: host_name = s['host_name'] elif 'host' in s: host_name = s['host'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = s['host_display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].status = 'UP' # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name if 'host_name' in s: self.new_hosts[host_name].real_name = s['host_name'] elif 'host' in s: self.new_hosts[host_name].real_name = s['host'] if not self.use_display_name_host: # legacy Icinga adjustments if 'service_description' in s: service_name = s['service_description'] elif 'description' in s: service_name = s['description'] elif 'service' in s: service_name = s['service'] else: service_name = s['service_display_name'] # if a service does not exist create its object if not service_name in self.new_hosts[host_name].services: self.new_hosts[host_name].services[service_name] = GenericService() self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status = s['status'] self.new_hosts[host_name].services[service_name].last_check = s['last_check'] self.new_hosts[host_name].services[service_name].duration = s['duration'] self.new_hosts[host_name].services[service_name].attempt = s['attempts'] if s['status_information']: self.new_hosts[host_name].services[service_name].status_information = s['status_information'].replace('\n', ' ').strip() self.new_hosts[host_name].services[service_name].passiveonly = not(s['active_checks_enabled']) self.new_hosts[host_name].services[service_name].notifications_disabled = not(s['notifications_enabled']) self.new_hosts[host_name].services[service_name].flapping = s['is_flapping'] self.new_hosts[host_name].services[service_name].acknowledged = s['has_been_acknowledged'] self.new_hosts[host_name].services[service_name].scheduled_downtime = s['in_scheduled_downtime'] self.new_hosts[host_name].services[service_name].status_type = status_type # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs service_description and no display name self.new_hosts[host_name].services[service_name].real_name = s.get('service_description', s.get('service_display_name')) del s, host_name, service_name except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del jsonraw, jsondict, error, hosts, services # dummy return in case all is OK return Result() def _get_status_HTML(self): """ Get status from Nagios Server - the oldschool CGI HTML way """ # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily # ##global icons nagitems = {'services':[], 'hosts':[]} # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_hosts[status_type]) htobj, error, status_code = result.result,\ result.error,\ result.status_code # check if any error occured errors_occured = self.check_for_error(htobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # put a copy of a part of htobj into table to be able to delete htobj table = htobj('table', {'class': 'status'})[0] # do some cleanup del result, error # access table rows # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # kick out table heads trs.pop(0) for tr in trs: try: # ignore empty rows if len(tr('td', recursive=False)) > 1: n = {} # get tds in one tr tds = tr('td', recursive=False) # host try: n['host'] = str(tds[0].table.tr.td.table.tr.td.a.string) except: n['host'] = str(nagitems[len(nagitems) - 1]['host']) # status n['status'] = str(tds[1].string) # last_check n['last_check'] = str(tds[2].string) # duration n['duration'] = str(tds[3].string) # division between Nagios and Icinga in real life... where # Nagios has only 5 columns there are 7 in Icinga 1.3... # ... and 6 in Icinga 1.2 :-) if len(tds) < 7: # the old Nagios table # status_information if len(tds[4](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[4].string) # attempts are not shown in case of hosts so it defaults to 'N/A' n['attempt'] = 'N/A' else: # attempts are shown for hosts # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n['attempt'] = str(tds[4].string).strip() # status_information if len(tds[5](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[5].string) # status flags n['passiveonly'] = False n['notifications_disabled'] = False n['flapping'] = False n['acknowledged'] = False n['scheduled_downtime'] = False # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this host item to nagitems nagitems['hosts'].append(n) # after collection data in nagitems create objects from its informations # host objects contain service objects if not 'host' in self.new_hosts: new_host = n['host'] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n['host'] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n['status'] self.new_hosts[new_host].last_check = n['last_check'] self.new_hosts[new_host].duration = n['duration'] self.new_hosts[new_host].attempt = n['attempt'] self.new_hosts[new_host].status_information = n['status_information'].replace('\n', ' ').strip() self.new_hosts[new_host].passiveonly = n['passiveonly'] self.new_hosts[new_host].notifications_disabled = n['notifications_disabled'] self.new_hosts[new_host].flapping = n['flapping'] self.new_hosts[new_host].acknowledged = n['acknowledged'] self.new_hosts[new_host].scheduled_downtime = n['scheduled_downtime'] self.new_hosts[new_host].status_type = status_type # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_name and no display name self.new_hosts[new_host].real_name = n['host'] # some cleanup del tds, n except: self.error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_services[status_type]) htobj, error, status_code = result.result,\ result.error,\ result.status_code # check if any error occured errors_occured = self.check_for_error(htobj, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured table = htobj('table', {'class': 'status'})[0] # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # do some cleanup del result, error # kick out table heads trs.pop(0) for tr in trs: try: # ignore empty rows - there are a lot of them - a Nagios bug? tds = tr('td', recursive=False) if len(tds) > 1: n = {} # host # the resulting table of Nagios status.cgi table omits the # hostname of a failing service if there are more than one # so if the hostname is empty the nagios status item should get # its hostname from the previuos item - one reason to keep 'nagitems' try: n['host'] = str(tds[0](text=not_empty)[0]) except: n['host'] = str(nagitems['services'][len(nagitems['services']) - 1]['host']) # service n['service'] = str(tds[1](text=not_empty)[0]) # status n['status'] = str(tds[2](text=not_empty)[0]) # last_check n['last_check'] = str(tds[3](text=not_empty)[0]) # duration n['duration'] = str(tds[4](text=not_empty)[0]) # attempt # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n['attempt'] = str(tds[5](text=not_empty)[0]).strip() # status_information if len(tds[6](text=not_empty)) == 0: n['status_information'] = '' else: n['status_information'] = str(tds[6](text=not_empty)[0]) # status flags n['passiveonly'] = False n['notifications_disabled'] = False n['flapping'] = False n['acknowledged'] = False n['scheduled_downtime'] = False # map status icons to status flags icons = tds[1].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this service item to nagitems - only if service nagitems['services'].append(n) # after collection data in nagitems create objects of its informations # host objects contain service objects if not n['host'] in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = 'UP' # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[n['host']].real_name = n['host'] # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 # if host is not down but in downtime or any other flag this should be evaluated too # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i['src'].split('/')[-1] if icon in self.STATUS_MAPPING: self.new_hosts[n['host']].__dict__[self.STATUS_MAPPING[icon]] = True # cleaning del icons # if a service does not exist create its object if not n['service'] in self.new_hosts[n['host']].services: new_service = n['service'] self.new_hosts[n['host']].services[new_service] = GenericService() self.new_hosts[n['host']].services[new_service].host = n['host'] self.new_hosts[n['host']].services[new_service].server = self.name self.new_hosts[n['host']].services[new_service].name = n['service'] self.new_hosts[n['host']].services[new_service].status = n['status'] self.new_hosts[n['host']].services[new_service].last_check = n['last_check'] self.new_hosts[n['host']].services[new_service].duration = n['duration'] self.new_hosts[n['host']].services[new_service].attempt = n['attempt'] self.new_hosts[n['host']].services[new_service].status_information = n['status_information'].replace('\n', ' ').strip() self.new_hosts[n['host']].services[new_service].passiveonly = n['passiveonly'] self.new_hosts[n['host']].services[new_service].notifications_disabled = n['notifications_disabled'] self.new_hosts[n['host']].services[new_service].flapping = n['flapping'] self.new_hosts[n['host']].services[new_service].acknowledged = n['acknowledged'] self.new_hosts[n['host']].services[new_service].scheduled_downtime = n['scheduled_downtime'] # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs service_description and no display name self.new_hosts[n['host']].services[new_service].real_name = n['service_description'] # some cleanup del tds, n except: self.error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del nagitems # dummy return in case all is OK return Result() def _set_recheck(self, host, service): """ to solve https://sourceforge.net/p/nagstamon/feature-requests/74/ there is a comment parameter added to cgi request """ if service != '': if self.hosts[host].services[service].is_passive_only(): # Do not check passive only checks return # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios result = self.fetch_url(self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '96', 'host':host})) self.start_time = dict(result.result.find(attrs={'name':'start_time'}).attrs)['value'] # decision about host or service - they have different URLs if service == '': # host cmd_typ = '96' else: # service @ host cmd_typ = '7' # ignore empty service in case of rechecking a host cgi_data = urllib.parse.urlencode([('cmd_typ', cmd_typ), \ ('cmd_mod', '2'), \ ('host', host), \ ('service', service), \ ('start_time', self.start_time), \ ('force_check', 'on'), \ ('com_data', 'Recheck by %s' % self.username), \ ('btnSubmit', 'Commit')]) # execute POST request self.fetch_url(self.monitor_cgi_url + '/cmd.cgi', giveback='raw', cgi_data=cgi_data) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): ''' send acknowledge to monitor server extra _method necessary due to https://github.com/HenriWahl/Nagstamon/issues/192 ''' url = self.monitor_cgi_url + '/cmd.cgi' # the following flags apply to hosts and services # # according to sf.net bug #3304098 (https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3304098&group_id=236865) # the send_notification-flag must not exist if it is set to 'off', otherwise # the Nagios core interpretes it as set, regardless its real value # # for whatever silly reason Icinga depends on the correct order of submitted form items... # see sf.net bug 3428844 # # Thanks to Icinga ORDER OF ARGUMENTS IS IMPORTANT HERE! # cgi_data = OrderedDict() if service == '': cgi_data['cmd_typ'] = '33' else: cgi_data['cmd_typ'] = '34' cgi_data['cmd_mod'] = '2' # better to use host_name instead of display_name cgi_data['host'] = self.hosts[host].real_name if service != '': # better to use service_description instead of display_name # this is an extra Icinga property cgi_data['service'] = self.hosts[host].services[service].real_name cgi_data['com_author'] = author cgi_data['com_data'] = comment cgi_data['btnSubmit'] = 'Commit' if notify: cgi_data['send_notification'] = '1' if persistent: cgi_data['persistent'] = 'on' if sticky: cgi_data['sticky_ack'] = '1' self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # acknowledge all services on a host if all_services: for s in all_services: cgi_data['cmd_typ'] = '34' cgi_data['service'] = s self.fetch_url(url, giveback='raw', cgi_data=cgi_data) Nagstamon-master/Nagstamon/Servers/Icinga2API.py000066400000000000000000000351451505160700500220450ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import arrow import copy import datetime import json import logging import sys import dateutil.parser import urllib.parse from Nagstamon.config import conf from Nagstamon.Servers.Generic import GenericServer from Nagstamon.objects import (GenericHost, GenericService, Result) log = logging.getLogger(__name__) class Icinga2APIServer(GenericServer): """ object of Icinga2 server API """ TYPE = 'Icinga2API' # ICINGA2API does not provide a web interface for humans MENU_ACTIONS = ['Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] STATES_MAPPING = {'hosts' : {0 : 'UP', 1 : 'DOWN', 2 : 'UNREACHABLE'}, \ 'services' : {0 : 'OK', 1 : 'WARNING', 2 : 'CRITICAL', 3 : 'UNKNOWN'}} STATES_MAPPING_REV = {'hosts' : { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2}, \ 'services' : {'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3}} BROWSER_URLS = {} def __init__(self, **kwds): """ Prepare all urls needed by nagstamon and icinga """ GenericServer.__init__(self, **kwds) self.url = conf.servers[self.get_name()].monitor_url self.username = conf.servers[self.get_name()].username self.password = conf.servers[self.get_name()].password def _insert_service_to_hosts(self, service: GenericService): """ We want to create hosts for faulty services as GenericService requires that logic. """ service_host = service.get_host_name() if service_host not in self.new_hosts: self.new_hosts[service_host] = GenericHost() self.new_hosts[service_host].name = service_host self.new_hosts[service_host].site = service.site self.new_hosts[service_host].services[service.name] = service def _get_status(self): """ Get status from Icinga Server and translate it into Nagstamon magic generic array """ # new_hosts dictionary self.new_hosts = dict() # hosts - the down ones try: # We ask icinga for hosts which are not doing well hosts = self._get_host_events() assert isinstance(hosts, list), "Fail to list hosts" for host in hosts: host_name = host['attrs']['name'] if host_name not in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].site = self.name try: self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'].get(host['attrs']['state']) except KeyError: self.new_hosts[host_name].status = 'UNKNOWN' if int(host['attrs']['state_type']) > 0: # if state is not SOFT, icinga does not report attempts properly self.new_hosts[host_name].attempt = "{}/{}".format( int(host['attrs']['max_check_attempts']), int(host['attrs']['max_check_attempts'])) else: self.new_hosts[host_name].attempt = "{}/{}".format( int(host['attrs']['check_attempt']), int(host['attrs']['max_check_attempts'])) self.new_hosts[host_name].last_check = arrow.get(host['attrs']['last_check']).humanize() self.new_hosts[host_name].duration = arrow.get(host['attrs']['previous_state_change']).humanize() self.new_hosts[host_name].status_information = host['attrs']['last_check_result']['output'] self.new_hosts[host_name].passiveonly = not(host['attrs']['enable_active_checks']) self.new_hosts[host_name].notifications_disabled = not(host['attrs']['enable_notifications']) self.new_hosts[host_name].flapping = host['attrs']['flapping'] self.new_hosts[host_name].acknowledged = host['attrs']['acknowledgement'] self.new_hosts[host_name].scheduled_downtime = bool(host['attrs']['downtime_depth']) self.new_hosts[host_name].status_type = {0: "soft", 1: "hard"}[host['attrs']['state_type']] del host_name del hosts except Exception as e: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) log.exception(e) return Result(result=result, error=error) # services try: services = self._get_service_events() for service in services: new_service = GenericService() new_service.host = service['attrs']['host_name'] new_service.name = service['attrs']['name'] try: new_service.status = self.STATES_MAPPING['services'].get(service['attrs']['state']) except KeyError: new_service.status = 'UNKNOWN' if int(service['attrs']['state_type']) > 0: # if state is not SOFT, icinga does not report attempts properly new_service.attempt = "{}/{}".format( int(service['attrs']['max_check_attempts']), int(service['attrs']['max_check_attempts'])) else: new_service.attempt = "{}/{}".format( int(service['attrs']['check_attempt']), int(service['attrs']['max_check_attempts'])) if service['attrs']['last_check_result'] is None: new_service.status_information = 'UNKNOWN' else: new_service.status_information = service['attrs']['last_check_result']['output'] new_service.last_check = arrow.get(service['attrs']['last_check']).humanize() new_service.duration = arrow.get(service['attrs']['previous_state_change']).humanize() new_service.passiveonly = not(service['attrs']['enable_active_checks']) new_service.notifications_disabled = not(service['attrs']['enable_notifications']) new_service.flapping = service['attrs']['flapping'] new_service.acknowledged = service['attrs']['acknowledgement'] new_service.scheduled_downtime = bool(service['attrs']['downtime_depth']) new_service.status_type = {0: "soft", 1: "hard"}[service['attrs']['state_type']] self._insert_service_to_hosts(new_service) del services except Exception as e: log.exception(e) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # dummy return in case all is OK return Result() def _list_objects(self, object_type, filter): """List objects""" result = self.fetch_url( f'{self.url}/objects/{object_type}?{urllib.parse.urlencode({"filter": filter})}', giveback='raw' ) # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured errors_occured = self.check_for_error(jsonraw, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured jsondict = json.loads(jsonraw) return jsondict.get('results', []) def _get_service_events(self): """ Suck faulty service events from API """ return self._list_objects('services', 'service.state!=ServiceOK') def _get_host_events(self): """ Suck faulty hosts from API """ return self._list_objects('hosts', 'host.state!=0') def _trigger_action(self, action, **data): """Trigger on action using Icinga2 API""" action_data = {k: v for k, v in data.items() if v is not None} self.debug(server=self.get_name(), debug=f"Trigger action {action} with data={action_data}") try: response = self.session.post( f'{self.url}/actions/{action}', headers={'Accept': 'application/json'}, json=action_data, ) self.debug( server=self.get_name(), debug=f"API return on triggering action {action} (status={response.status_code}): " f"{response.text}" ) if 200 <= response.status_code <= 299: return True self.error(f"Fail to trigger action {action}: {response.json().get('status', 'Unknown error')}") except IOError as err: log.exception("Fail to trigger action %s with data %s", action, data) self.error(f"Fail to trigger action {action}: {err}") def _set_recheck(self, host, service): """ Please check again Icinga! """ self._trigger_action( "reschedule-check", type="Service" if service else "Host", filter=( 'host.name == host_name && service.name == service_name' if service else 'host.name == host_name' ), filter_vars=( {'host_name': host, 'service_name': service} if service else {'host_name': host} ), ) # Overwrite function from generic server to add expire_time value def set_acknowledge(self, info_dict): ''' different monitors might have different implementations of _set_acknowledge ''' if info_dict['acknowledge_all_services'] is True: all_services = info_dict['all_services'] else: all_services = [] # Make sure expire_time is set #if not info_dict['expire_time']: # info_dict['expire_time'] = None self._set_acknowledge(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['sticky'], info_dict['notify'], info_dict['persistent'], all_services, info_dict['expire_time']) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None, expire_time=None): ''' Send acknowledge to monitor server ''' self._trigger_action( "acknowledge-problem", type="Service" if service else "Host", filter=( 'host.name == host_name && service.name == service_name' if service else 'host.name == host_name' ), filter_vars=( {'host_name': host, 'service_name': service} if service else {'host_name': host} ), author=author, comment=comment, sticky=sticky, notify=notify, expiry=( dateutil.parser.parse(expire_time).timestamp() if expire_time else None ), persistent=persistent, ) if len(all_services) > 0: for s in all_services: # cheap, recursive solution... self._set_acknowledge(host, s, author, comment, sticky, notify, persistent, [], expire_time) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): ''' Submit check results ''' self._trigger_action( "process-check-result", type="Service" if service else "Host", filter=( 'host.name == host_name && service.name == service_name' if service else 'host.name == host_name' ), filter_vars=( {'host_name': host, 'service_name': service} if service else {'host_name': host} ), exit_status=self.STATES_MAPPING_REV['services' if service else 'hosts'][state.upper()], plugin_output=check_output, performance_data=performance_data, ) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ Submit downtime """ self._trigger_action( "schedule-downtime", type="Service" if service else "Host", filter=( 'host.name == host_name && service.name == service_name' if service else 'host.name == host_name' ), filter_vars=( {'host_name': host, 'service_name': service} if service else {'host_name': host} ), author=author, comment=comment, start_time=( datetime.datetime.now().timestamp() if start_time == '' or start_time == 'n/a' else dateutil.parser.parse(start_time).timestamp() ), end_time=( (datetime.datetime.now() + datetime.timedelta(hours=hours, minutes=minutes)).timestamp() if end_time == '' or end_time == 'n/a' else dateutil.parser.parse(end_time).timestamp() ), fixed=fixed, duration=( (hours * 3600 + minutes * 60) if not fixed else None ), ) Nagstamon-master/Nagstamon/Servers/IcingaDBWeb.py000066400000000000000000001235261505160700500222760ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Marcus MÃļnnig # # This Server class connects against IcingaWeb2. The monitor URL in the setup should be # something like http://icinga2/icingaweb2 # # Status/TODOs: # # * The IcingaWeb2 API is not implemented yet, so currently this implementation uses # two HTTP requests per action. The first fetches the HTML, then the form data is extracted and # then a second HTTP POST request is made which actually executed the action. # Once IcingaWeb2 has an API, it's probably the better choice. from Nagstamon.Servers.Generic import GenericServer import urllib.parse import sys import copy import json import datetime from datetime import timezone import socket from bs4 import BeautifulSoup from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.config import conf from Nagstamon.helpers import webbrowser_open def strfdelta(tdelta, fmt): d = {'days': tdelta.days} d['hours'], rem = divmod(tdelta.seconds, 3600) d['minutes'], d['seconds'] = divmod(rem, 60) return fmt.format(**d) class IcingaDBWebServer(GenericServer): """ object of Icinga server """ TYPE = 'IcingaDBWeb' MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] STATES_MAPPING = {'hosts' : {0 : 'UP', 1 : 'DOWN', 2 : 'UNREACHABLE'}, 'services' : {0 : 'OK', 1 : 'WARNING', 2 : 'CRITICAL', 3 : 'UNKNOWN'}} STATES_MAPPING_REV = {'hosts' : { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2}, 'services' : {'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3}} BROWSER_URLS = { 'monitor': '$MONITOR-CGI$/dashboard', 'hosts': '$MONITOR-CGI$/icingadb/hosts', 'services': '$MONITOR-CGI$/icingadb/services', 'history': '$MONITOR-CGI$/icingadb/history'} def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None self.cgiurl_monitoring_health = None # https://github.com/HenriWahl/Nagstamon/issues/400 # The displayed name for host and service is the Icinga2 "internal" name and not the display_name from host/service configuration # This name is stored in host/service dict under key 'name' but is also used as dict key for dict containing all hosts/services # The "internal" name must still be used to query IcingaWeb2 and is in dict under key 'real_name' since https://github.com/HenriWahl/Nagstamon/issues/192 self.use_display_name_host = True self.use_display_name_service = True def init_HTTP(self): """ initializing of session object """ GenericServer.init_HTTP(self) if self.session and not 'Referer' in self.session.headers: self.session.headers['Referer'] = self.monitor_cgi_url # normally cookie auth will be used if not self.no_cookie_auth: if 'cookies' not in dir(self.session) or len(self.session.cookies) == 0: # get login page, thus automatically a cookie login = self.fetch_url('{0}/authentication/login'.format(self.monitor_url)) if login.error == '' and login.status_code == 200: form = login.result.find('form') form_inputs = {} for form_input in ('redirect', 'formUID', 'CSRFToken', 'btn_submit'): if form is not None and not form.find('input', {'name': form_input}) is None: form_inputs[form_input] = form.find('input', {'name': form_input})['value'] else: form_inputs[form_input] = '' form_inputs['username'] = self.username form_inputs['password'] = self.password # fire up login button with all needed data self.fetch_url('{0}/authentication/login'.format(self.monitor_url), cgi_data=form_inputs) def _get_status(self): """ Get status from Icinga Server - only JSON """ # define CGI URLs for hosts and services if self.cgiurl_hosts is None and self.cgiurl_services is None and self.cgiurl_monitoring_health is None: # services (unknown, warning or critical?) self.cgiurl_services = {'hard': self.monitor_cgi_url + '/icingadb/services?service.state.is_problem=y&service.state.state_type=hard&columns=service.state.last_update,service.state.is_reachable&format=json', \ 'soft': self.monitor_cgi_url + '/icingadb/services?service.state.is_problem=y&service.state.state_type=soft&columns=service.state.last_update,service.state.is_reachable&format=json'} # hosts (up or down or unreachable) self.cgiurl_hosts = {'hard': self.monitor_cgi_url + '/icingadb/hosts?host.state.is_problem=y&host.state.state_type=hard&columns=host.state.last_update&format=json', \ 'soft': self.monitor_cgi_url + '/icingadb/hosts?host.state.is_problem=y&host.state.state_type=soft&columns=host.state.last_update&format=json'} # monitoring health self.cgiurl_monitoring_health = self.monitor_cgi_url + '/health?format=json' # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # now using JSON output from Icinga try: for status_type in 'hard', 'soft': # first attempt result = self.fetch_url(self.cgiurl_hosts[status_type], giveback='raw') # authentication errors get a status code 200 too back because its # HTML works fine :-( if result.status_code < 400 and\ result.result.startswith('<'): # in case of auth error reset HTTP session and try again self.reset_HTTP() result = self.fetch_url(self.cgiurl_hosts[status_type], giveback='raw') # if it does not work again tell GUI there is a problem if result.status_code < 400 and\ result.result.startswith('<'): self.refresh_authentication = True return Result(result=result.result, error='Authentication error', status_code=result.status_code) # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured potential_error = self.check_for_error(jsonraw, error, status_code) if potential_error is not None: return potential_error # Check if the backend is running # If it isn't running the last values stored in the database are returned/shown # Unfortunately we need to make a extra request for this and only, if monitoring health is possible if self.cgiurl_monitoring_health: pass # TODO: Health checks for IcingaDB and icinga-redis # try: # result = self.fetch_url(self.cgiurl_monitoring_health, giveback='raw') # monitoring_health = json.loads(result.result)[0] # if (monitoring_health['is_currently_running'] == '0'): # return Result(result=monitoring_health, # error='Icinga2 backend not running') # except json.decoder.JSONDecodeError: # # https://github.com/HenriWahl/Nagstamon/issues/619 # # Icinga2 monitoring health status query does not seem to work (on older version?) # self.cgiurl_monitoring_health = None hosts = json.loads(jsonraw) for host in hosts: # make dict of tuples for better reading h = dict(host.items()) # host if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be name instead of display_name host_name = h['name'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = h['display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status_type = status_type if (status_type == 'hard'): self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][int(h['state']['hard_state'])] else: self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][int(h['state']['soft_state'])] if h['state']['last_update'].replace(".", "").isnumeric(): # new version of icingadb doesnt return unix timestamp #self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp(int(float(h['state']['last_update']))) utc_time = datetime.datetime.fromtimestamp(int(float(h['state']['last_update'])), tz=timezone.utc) else: #self.new_hosts[host_name].last_check = datetime.datetime.fromisoformat(h['state']['last_update']) utc_time = datetime.datetime.fromisoformat(h['state']['last_update']) local_time = utc_time.astimezone() self.new_hosts[host_name].last_check = local_time.strftime("%Y-%m-%d %H:%M:%S") # format without microseconds and tz self.new_hosts[host_name].attempt = "{}/{}".format(h['state']['check_attempt'],h['max_check_attempts']) self.new_hosts[host_name].status_information = BeautifulSoup(str(h['state']['output']).replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].passiveonly = not int(h.get('active_checks_enabled') or '0') self.new_hosts[host_name].notifications_disabled = not int(h.get('notifications_enabled') or '0') self.new_hosts[host_name].flapping = bool(int(h['state']['is_flapping'] or 0)) #s['state']['is_acknowledged'] can be null, 0, 1, or 'sticky' self.new_hosts[host_name].acknowledged = h['state']['is_acknowledged'] if isinstance(h['state']['is_acknowledged'], bool) else bool(int(h['state']['is_acknowledged'].replace('sticky', '1') or 0)) self.new_hosts[host_name].scheduled_downtime = bool(int(h['state']['in_downtime'] or 0)) # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = h['name'] # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attemt is set to 1/x. if (status_type == 'hard'): try: self.new_hosts[host_name].attempt = "{0}/{0}".format(h['max_check_attempts']) except Exception: self.new_hosts[host_name].attempt = "HARD" # extra duration needed for calculation self.new_hosts[host_name].duration = 'n/a' if h['state']['last_state_change'] is not None: if h['state']['last_state_change'].replace(".", "").isnumeric(): # new version of icingadb doesnt return unix timestamp duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(float(h['state']['last_state_change']))) else: last_state_change = datetime.datetime.fromisoformat(h['state']['last_state_change']) duration = datetime.datetime.now(timezone.utc).astimezone() - last_state_change if duration.total_seconds() > 0: self.new_hosts[host_name].duration = strfdelta(duration,'{days}d {hours}h {minutes}m {seconds}s') del h, host_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_services[status_type], giveback='raw') # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) # check if any error occured self.check_for_error(jsonraw, error, status_code) services = copy.deepcopy(json.loads(jsonraw)) for service in services: # make dict of tuples for better reading s = dict(service.items()) if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be name instead of display_name host_name = s['host']['name'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = s['host']['display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].status = 'UP' # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = s['host']['name'] service_name = s['display_name'] # if a service does not exist create its object if not service_name in self.new_hosts[host_name].services: self.new_hosts[host_name].services[service_name] = GenericService() self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status_type = status_type if (status_type == 'hard'): self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][int(s['state']['hard_state'])] else: self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][int(s['state']['soft_state'])] if s['state']['last_update'].replace(".", "").isnumeric(): # new version of icingadb doesnt return unix timestamp #self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp(int(float(s['state']['last_update']))) utc_time = datetime.datetime.fromtimestamp(int(float(s['state']['last_update'])), tz=timezone.utc) else: #self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromisoformat(s['state']['last_update']) utc_time = datetime.datetime.fromisoformat(s['state']['last_update']) local_time = utc_time.astimezone() self.new_hosts[host_name].services[service_name].last_check = local_time.strftime("%Y-%m-%d %H:%M:%S") # format without microseconds and tz self.new_hosts[host_name].services[service_name].attempt = "{}/{}".format(s['state']['check_attempt'],s['max_check_attempts']) self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(str(s['state']['output']).replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].services[service_name].passiveonly = not int(s.get('active_checks_enabled') or '0') self.new_hosts[host_name].services[service_name].notifications_disabled = not int(s.get('notifications_enabled') or '0') self.new_hosts[host_name].services[service_name].flapping = bool(int(s['state']['is_flapping'] or 0)) #s['state']['is_acknowledged'] can be null, 0, 1, or 'sticky' self.new_hosts[host_name].services[service_name].acknowledged = s['state']['is_acknowledged'] if isinstance(s['state']['is_acknowledged'], bool) else bool(int(s['state']['is_acknowledged'].replace('sticky', '1') or 0)) self.new_hosts[host_name].services[service_name].scheduled_downtime = bool(int(s['state']['in_downtime'] or 0)) self.new_hosts[host_name].services[service_name].unreachable = not bool(int(s['state']['is_reachable'] or 0)) if self.new_hosts[host_name].services[service_name].unreachable: self.new_hosts[host_name].services[service_name].status_information += " (SERVICE UNREACHABLE)" # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs service_description and no display name self.new_hosts[host_name].services[service_name].real_name = s['name'] # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attemt is set to 1/x. if (status_type == 'hard'): try: self.new_hosts[host_name].services[service_name].attempt = "{0}/{0}".format(s['max_check_attempts']) except Exception: self.new_hosts[host_name].services[service_name].attempt = "HARD" # extra duration needed for calculation self.new_hosts[host_name].services[service_name].duration = 'n/a' if s['state']['last_state_change'] is not None: if s['state']['last_update'].replace(".", "").isnumeric(): # new version of icingadb doesnt return unix timestamp duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(float(s['state']['last_state_change']))) else: last_state_change = datetime.datetime.fromisoformat(s['state']['last_state_change']) duration = datetime.datetime.now(timezone.utc).astimezone() - last_state_change if duration.total_seconds() > 0: self.new_hosts[host_name].services[service_name].duration = strfdelta(duration, '{days}d {hours}h {minutes}m {seconds}s') del s, host_name, service_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del jsonraw, error, hosts, services # dummy return in case all is OK return Result() def _set_recheck(self, host, service): # Set correct headers self.session.headers['X-Requested-With'] = 'XMLHttpRequest' self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) # First retrieve the info page for this host/service if service == '': url = '{0}/icingadb/host?name={1}'.format(self.monitor_cgi_url, self.hosts[host].real_name) else: url = '{0}/icingadb/service?name={1}&host.name={2}'.format(self.monitor_cgi_url, self.hosts[host].services[service].real_name, self.hosts[host].real_name ) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') print(pagesoup.prettify()) # Extract the relevant form element values formtag = pagesoup.select_one('form[action*="check-now"]') if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='[Recheck] Retrieve html form from {0}: \n{1}'.format(url,formtag.prettify())) btn_submit = formtag.findNext('button', {'name':'btn_submit'})['value'] CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] # Pass these values to the check-now URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['btn_submit'] = btn_submit if service == '': url = '{0}/icingadb/host/check-now?name={1}'.format(self.monitor_cgi_url, self.hosts[host].real_name) else: url = '{0}/icingadb/service/check-now?name={1}&host.name={2}'.format(self.monitor_cgi_url, self.hosts[host].services[service].real_name, self.hosts[host].real_name ) response = self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # Some debug data data = response.result error = response.error status_code = response.status_code if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Recheck response') self.debug(server=self.get_name(), host=host, service=service, debug="- response: {0}".format(response)) self.debug(server=self.get_name(), host=host, service=service, debug="- data: {0}".format(data)) self.debug(server=self.get_name(), host=host, service=service, debug="- error: {0}".format(error)) self.debug(server=self.get_name(), host=host, service=service, debug="- status_code: {0}".format(status_code)) # Overwrite function from generic server to add expire_time value def set_acknowledge(self, info_dict): ''' different monitors might have different implementations of _set_acknowledge ''' if info_dict['acknowledge_all_services'] is True: all_services = info_dict['all_services'] else: all_services = [] # Make sure expire_time is set #if not info_dict['expire_time']: # info_dict['expire_time'] = None try: self._set_acknowledge(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['sticky'], info_dict['notify'], info_dict['persistent'], all_services, info_dict['expire_time']) except: import traceback traceback.print_exc(file=sys.stdout) result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None, expire_time=None): # Set correct headers self.session.headers['X-Requested-With'] = 'XMLHttpRequest' self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) # First retrieve the info page for this host/service if service == '': url = '{0}/icingadb/host/acknowledge?name={1}&showCompact=1'.format(self.monitor_cgi_url, self.hosts[host].real_name) else: url = '{0}/icingadb/service/acknowledge?name={1}&host.name={2}&showCompact=1'.format(self.monitor_cgi_url, self.hosts[host].services[service].real_name, self.hosts[host].real_name ) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values formtag = pagesoup.select_one('form[action*="acknowledge"]') #print('-----------------') #print(formtag.prettify()) #print('-----------------') if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='[Acknowledge] Retrieve html form from {0}: \n{1}'.format(url,formtag.prettify())) btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['btn_submit'] = btn_submit cgi_data['comment'] = comment cgi_data['persistent'] = str(persistent).replace('True', 'y').replace('False', 'n') cgi_data['sticky'] = str(sticky).replace('True', 'y').replace('False', 'n') cgi_data['notify'] = str(notify).replace('True', 'y').replace('False', 'n') if expire_time: cgi_data['expire'] = 'y' cgi_data['expire_time'] = expire_time else: cgi_data['expire'] = 'n' response = self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # Some debug data data = response.result error = response.error status_code = response.status_code if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Achnowledgement response') self.debug(server=self.get_name(), host=host, service=service, debug="- response: {0}".format(response)) self.debug(server=self.get_name(), host=host, service=service, debug="- data: {0}".format(data)) self.debug(server=self.get_name(), host=host, service=service, debug="- error: {0}".format(error)) self.debug(server=self.get_name(), host=host, service=service, debug="- status_code: {0}".format(status_code)) if len(all_services) > 0: for s in all_services: # cheap, recursive solution... self._set_acknowledge(host, s, author, comment, sticky, notify, persistent, [], expire_time) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): # Set correct headers self.session.headers['X-Requested-With'] = 'XMLHttpRequest' self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) # First retrieve the info page for this host/service if service == '': url = '{0}/icingadb/host/process-checkresult?name='.format(self.monitor_cgi_url, self.hosts[host].real_name) status = self.STATES_MAPPING_REV['hosts'][state.upper()] else: url = '{0}/icingadb/service/process-checkresult?name={1}&host.name={2}'.format(self.monitor_cgi_url, self.hosts[host].services[service].real_name, self.hosts[host].real_name ) status = self.STATES_MAPPING_REV['services'][state.upper()] result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values formtag = pagesoup.select_one('form[action*="process-checkresult"]') #print('-----------------') #print(formtag.prettify()) #print('-----------------') if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='[Submit check result] Retrieve html form from {0}: \n{1}'.format(url,formtag.prettify())) btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['btn_submit'] = btn_submit cgi_data['status'] = status cgi_data['output'] = check_output cgi_data['perfdata'] = performance_data response = self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # Some debug data data = response.result error = response.error status_code = response.status_code if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Achnowledgement response') self.debug(server=self.get_name(), host=host, service=service, debug="- response: {0}".format(response)) self.debug(server=self.get_name(), host=host, service=service, debug="- data: {0}".format(data)) self.debug(server=self.get_name(), host=host, service=service, debug="- error: {0}".format(error)) self.debug(server=self.get_name(), host=host, service=service, debug="- status_code: {0}".format(status_code)) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): # Set correct headers self.session.headers['X-Requested-With'] = 'XMLHttpRequest' self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) # First retrieve the info page for this host/service if service == '': url = '{0}/icingadb/host/schedule-downtime?name={1}'.format(self.monitor_cgi_url, self.hosts[host].real_name) else: url = '{0}/icingadb/service/schedule-downtime?name={1}&host.name={2}&showCompact=1'.format(self.monitor_cgi_url, self.hosts[host].services[service].real_name, self.hosts[host].real_name ) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values formtag = pagesoup.select_one('form[action*="schedule-downtime"]') #print('-----------------') #print(formtag.prettify()) #print('-----------------') if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='[Set downtime] Retrieve html form from {0}: \n{1}'.format(url,formtag.prettify())) btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['btn_submit'] = btn_submit cgi_data['comment'] = comment if fixed: cgi_data['flexible'] = 'n' else: cgi_data['flexible'] = 'y' cgi_data['hours'] = hours cgi_data['minutes'] = minutes if start_time == '' or start_time == 'n/a': start = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') else: start = start_time if end_time == '' or end_time == 'n/a': end = (datetime.datetime.now() + datetime.timedelta(hours=hours, minutes=minutes)).strftime('%Y-%m-%dT%H:%M:%S') else: end = end_time cgi_data['start'] = start cgi_data['end'] = end response = self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # Some debug data data = response.result error = response.error status_code = response.status_code if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Achnowledgement response') self.debug(server=self.get_name(), host=host, service=service, debug="- response: {0}".format(response)) self.debug(server=self.get_name(), host=host, service=service, debug="- data: {0}".format(data)) self.debug(server=self.get_name(), host=host, service=service, debug="- error: {0}".format(error)) self.debug(server=self.get_name(), host=host, service=service, debug="- status_code: {0}".format(status_code)) def get_start_end(self, host): ''' for GUI to get actual downtime start and end from server - they may vary so it's better to get directly from web interface ''' # Set correct headers self.session.headers['X-Requested-With'] = 'XMLHttpRequest' self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) try: url = '{0}/icingadb/host/schedule-downtime?name={1}'.format(self.monitor_cgi_url, self.hosts[host].real_name) downtime = self.fetch_url(url, giveback='raw') if downtime.error != '': return 'n/a', 'n/a' else: pageraw = downtime.result pagesoup = BeautifulSoup(pageraw, 'html.parser') #print('-----------------') #print(pagesoup.prettify()) #print('-----------------') # Super debug if conf.debug_mode: self.debug(server=self.get_name(), host=host, service='', debug='[Get downtime start/end] Retrieve html from {0}: {1}'.format(url,pagesoup.prettify())) start = pagesoup.find('input', {'name': 'start'})['value'] end = pagesoup.find('input', {'name': 'end'})['value'] # give values back as tuple return start, end except: self.error(sys.exc_info()) return 'n/a', 'n/a' def open_monitor(self, host, service=''): """Open monitor from tablewidget context menu.""" if host not in self.hosts: print("Cannot find {}. Skipping!".format(host)) return if service and service not in self.hosts[host].services: print("Cannot find {}::{}. Skipping!".format(host, service)) return # Generate the base path for the URL base_path = urllib.parse.urlparse(self.monitor_url).path # Handle URL for host monitoring if service == '': url = '{0}/icingadb/hosts?host.state.is_problem=y&sort=host.state.severity#!{1}/icingadb/host?{2}'.format( self.monitor_url, base_path, urllib.parse.urlencode({'name': self.hosts[host].real_name}, quote_via=urllib.parse.quote) ) else: # Handle URL for service monitoring url = '{0}/icingadb/services?service.state.is_problem=y&sort=service.state.severity%20desc#!{1}/icingadb/service?{2}'.format( self.monitor_url, base_path, urllib.parse.urlencode({ 'name': self.hosts[host].services[service].real_name, 'host.name': self.hosts[host].real_name }, quote_via=urllib.parse.quote) ) if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='[Open monitor] Open host/service monitor web page {0}'.format(url)) webbrowser_open(url) def get_host(self, host): ''' find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Icinga ''' # Host is the display name as in the GUI # but we need the FQDN not the display name host = self.hosts[host].real_name # the fasted method is taking hostname as used in monitor if conf.connect_by_host is True or host == '': return Result(result=host) # initialize ip string ip = '' address = '' # glue nagios cgi url and hostinfo cgiurl_host = self.monitor_cgi_url + '/icingadb/hosts?name={0}&columns=host.address&format=json'.format(host) # get host info hostobj = self.fetch_url(cgiurl_host, giveback='raw') jsonhost = hostobj.result try: # take ip from json output result = json.loads(jsonhost)[0] ip = result["address"] # print IP in debug mode if conf.debug_mode is True: self.debug(server=self.get_name(), host=host, debug='IP of %s:' % (host) + ' ' + ip) # when connection by DNS is not configured do it by IP if conf.connect_by_dns is True: # try to get DNS name for ip, if not available use ip try: address = socket.gethostbyaddr(ip)[0] except socket.error: address = ip else: address = ip except Exception: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del hostobj # give back host or ip return Result(result=address) Nagstamon-master/Nagstamon/Servers/IcingaDBWebNotifications.py000066400000000000000000000327741505160700500250340ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Marcus MÃļnnig # # This Server class connects against IcingaWeb2. The monitor URL in the setup should be # something like http://icinga2/icingaweb2 # # Status/TODOs: # # * The IcingaWeb2 API is not implemented yet, so currently this implementation uses # two HTTP requests per action. The first fetches the HTML, then the form data is extracted and # then a second HTTP POST request is made which actually executed the action. # Once IcingaWeb2 has an API, it's probably the better choice. from Nagstamon.Servers.Generic import GenericServer import urllib.parse import sys import json import datetime import socket from bs4 import BeautifulSoup from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.config import (conf, AppInfo) from Nagstamon.helpers import webbrowser_open from Nagstamon.Servers.IcingaDBWeb import IcingaDBWebServer def strfdelta(tdelta, fmt): d = {'days': tdelta.days} d['hours'], rem = divmod(tdelta.seconds, 3600) d['minutes'], d['seconds'] = divmod(rem, 60) return fmt.format(**d) class IcingaDBWebNotificationsServer(IcingaDBWebServer): """Read data from IcingaDB in IcingaWeb via the Notification endpoint.""" TYPE = 'IcingaDBWebNotifications' def _get_status(self) -> Result: """Update internal status variables. This method updates self.new_host. It will not return any status. It will return an empty Result object on success or a Result with an error on error. """ try: return self._update_new_host_content() except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def _update_new_host_content(self) -> Result: """Update self.new_host based on icinga notifications.""" notification_url = "{}/icingadb/notifications?{}&history.event_time>{} ago&format=json".format( self.monitor_cgi_url, self.notification_filter, self.notification_lookback) health_url = '{}/health?format=json'.format(self.monitor_cgi_url) result = self.fetch_url(notification_url, giveback='raw') # check if any error occurred potential_error = self.check_for_error(result.result, result.error, result.status_code) if potential_error is not None: return potential_error # HEALTH CHECK health_result = self.fetch_url(health_url, giveback='raw') if health_result.status_code == 200: # we already got check results so icinga is unlikely down. do not break it without need. monitoring_health_results = json.loads(health_result.result) if monitoring_health_results["status"] != "success": errors = [e["message"] for e in monitoring_health_results["data"] if e["state"] != 0] return Result(result="UNKNOWN", error='Icinga2 not healthy: {}'.format("; ".join(errors))) self.new_hosts = {} notifications = json.loads(result.result) for notification in reversed(notifications): if notification["object_type"] == "host": # host if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be name instead of display_name host_name = notification['host']['name'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = notification['host']['display_name'] status_type = notification['host']["state_type"] if status_type == 'hard': status_numeric = int(notification['host']['state']['hard_state']) else: status_numeric = int(notification['host']['state']['soft_state']) if status_numeric not in (1, 2): try: del self.new_hosts[host_name] except KeyError: pass continue self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status_type = status_type self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][status_numeric] self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp(int(float(notification['host']['state']['last_update']))) self.new_hosts[host_name].attempt = "{}/{}".format(notification['host']['state']['check_attempt'],notification['host']['max_check_attempts']) self.new_hosts[host_name].status_information = BeautifulSoup(notification['host']['state']['output'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].passiveonly = not int(notification['host'].get('active_checks_enabled') or '0') self.new_hosts[host_name].notifications_disabled = not int(notification['host'].get('notifications_enabled') or '0') self.new_hosts[host_name].flapping = bool(int(notification['host']['state']['is_flapping'] or 0)) #s['state']['is_acknowledged'] can be null, 0, 1, or 'sticky' self.new_hosts[host_name].acknowledged = bool(int(notification['host']['state']['is_acknowledged'].replace('sticky', '1') or 0)) self.new_hosts[host_name].scheduled_downtime = bool(int(notification['host']['state']['in_downtime'] or 0)) # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = notification['host']['name'] # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attemt is set to 1/x. if status_type == 'hard': try: self.new_hosts[host_name].attempt = "{0}/{0}".format(notification['host']['max_check_attempts']) except Exception: self.new_hosts[host_name].attempt = "HARD" # extra duration needed for calculation if notification['host']['state']['last_state_change'] is not None and notification['host']['state']['last_state_change'] != 0: duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(float(notification['host']['state']['last_state_change']))) self.new_hosts[host_name].duration = strfdelta(duration,'{days}d {hours}h {minutes}m {seconds}s') else: self.new_hosts[host_name].duration = 'n/a' elif notification["object_type"] == "service": if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be name instead of display_name host_name = notification['host']['name'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = notification['host']['display_name'] status_type = notification['service']["state"]["state_type"] service_name = notification['service']['display_name'] if status_type == 'hard': status_numeric = int(notification['service']['state']['hard_state']) else: status_numeric = int(notification['service']['state']['soft_state']) if status_numeric not in (1, 2, 3): try: del self.new_hosts[host_name].services[service_name] if not self.new_hosts[host_name].services: del self.new_hosts[host_name] except KeyError: pass continue # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].status = 'UP' # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = notification['host']['name'] # if a service does not exist create its object self.new_hosts[host_name].services[service_name] = GenericService() self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status_type = status_type self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][status_numeric] self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp(int(float(notification['service']['state']['last_update']))) self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(notification['service']['state']['output'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].services[service_name].passiveonly = not int(notification['service'].get('active_checks_enabled') or '0') self.new_hosts[host_name].services[service_name].notifications_disabled = not int(notification['service'].get('notifications_enabled') or '0') self.new_hosts[host_name].services[service_name].flapping = bool(int(notification['service']['state']['is_flapping'] or 0)) #s['state']['is_acknowledged'] can be null, 0, 1, or 'sticky' self.new_hosts[host_name].services[service_name].acknowledged = bool(int(notification['service']['state']['is_acknowledged'].replace('sticky', '1') or 0)) self.new_hosts[host_name].services[service_name].scheduled_downtime = bool(int(notification['service']['state']['in_downtime'] or 0)) self.new_hosts[host_name].services[service_name].unreachable = not bool(int(notification['service']['state']['is_reachable'] or 0)) if self.new_hosts[host_name].services[service_name].unreachable: self.new_hosts[host_name].services[service_name].status_information += " (SERVICE UNREACHABLE)" # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs service_description and no display name self.new_hosts[host_name].services[service_name].real_name = notification['service']['name'] if status_type == 'hard': # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attempt is set to 1/x. self.new_hosts[host_name].services[service_name].attempt = "{0}/{0}".format( notification['service']['max_check_attempts']) else: self.new_hosts[host_name].services[service_name].attempt = "{}/{}".format( notification['service']['state']['check_attempt'], notification['service']['max_check_attempts']) # extra duration needed for calculation if notification['service']['state']['last_state_change'] is not None and notification['service']['state']['last_state_change'] != 0: duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(float(notification['service']['state']['last_state_change']))) self.new_hosts[host_name].services[service_name].duration = strfdelta(duration, '{days}d {hours}h {minutes}m {seconds}s') else: self.new_hosts[host_name].services[service_name].duration = 'n/a' # return success return Result()Nagstamon-master/Nagstamon/Servers/IcingaWeb2.py000066400000000000000000001037721505160700500221530ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Marcus MÃļnnig # # This Server class connects against IcingaWeb2. The monitor URL in the setup should be # something like http://icinga2/icingaweb2 # # Status/TODOs: # # * The IcingaWeb2 API is not implemented yet, so currently this implementation uses # two HTTP requests per action. The first fetches the HTML, then the form data is extracted and # then a second HTTP POST request is made which actually executed the action. # Once IcingaWeb2 has an API, it's probably the better choice. from Nagstamon.Servers.Generic import GenericServer import urllib.parse import sys import copy import json import datetime import socket from bs4 import BeautifulSoup from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.config import (conf, AppInfo) from Nagstamon.helpers import webbrowser_open def strfdelta(tdelta, fmt): d = {'days': tdelta.days} d['hours'], rem = divmod(tdelta.seconds, 3600) d['minutes'], d['seconds'] = divmod(rem, 60) return fmt.format(**d) class IcingaWeb2Server(GenericServer): """ object of Incinga server """ TYPE = 'IcingaWeb2' MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] STATES_MAPPING = {'hosts' : {0 : 'UP', 1 : 'DOWN', 2 : 'UNREACHABLE'}, \ 'services' : {0 : 'OK', 1 : 'WARNING', 2 : 'CRITICAL', 3 : 'UNKNOWN'}} STATES_MAPPING_REV = {'hosts' : { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2}, \ 'services' : {'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3}} BROWSER_URLS = { 'monitor': '$MONITOR-CGI$/dashboard', \ 'hosts': '$MONITOR-CGI$/monitoring/list/hosts', \ 'services': '$MONITOR-CGI$/monitoring/list/services', \ 'history': '$MONITOR-CGI$/monitoring/list/eventhistory?timestamp>=-7 days'} def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None self.cgiurl_monitoring_health = None # https://github.com/HenriWahl/Nagstamon/issues/400 # The displayed name for host and service is the Icinga2 "internal" name and not the display_name from host/service configuration # This name is stored in host/service dict under key 'name' but is also used as dict key for dict containing all hosts/services # The "internal" name must still be used to query IcingaWeb2 and is in dict under key 'real_name' since https://github.com/HenriWahl/Nagstamon/issues/192 self.use_display_name_host = True self.use_display_name_service = True def init_HTTP(self): """ initializing of session object """ GenericServer.init_HTTP(self) if self.session and not 'Referer' in self.session.headers: self.session.headers['Referer'] = self.monitor_cgi_url + '/icingaweb2/monitoring' # normally cookie auth will be used if not self.no_cookie_auth: if 'cookies' not in dir(self.session) or len(self.session.cookies) == 0: # get login page, thus automatically a cookie login = self.fetch_url('{0}/authentication/login'.format(self.monitor_url)) if login.error == '' and login.status_code == 200: form = login.result.find('form') form_inputs = {} for form_input in ('redirect', 'formUID', 'CSRFToken', 'btn_submit'): if form is not None and not form.find('input', {'name': form_input}) is None: form_inputs[form_input] = form.find('input', {'name': form_input})['value'] else: form_inputs[form_input] = '' form_inputs['username'] = self.username form_inputs['password'] = self.password # fire up login button with all needed data self.fetch_url('{0}/authentication/login'.format(self.monitor_url), cgi_data=form_inputs) def _get_status(self): """ Get status from Icinga Server - only JSON """ # define CGI URLs for hosts and services if self.cgiurl_hosts == self.cgiurl_services == self.cgiurl_monitoring_health == None: # services (unknown, warning or critical?) self.cgiurl_services = {'hard': self.monitor_cgi_url + '/monitoring/list/services?service_state>=0&service_state<=3&service_state_type=1&addColumns=service_last_check,service_is_reachable&format=json', \ 'soft': self.monitor_cgi_url + '/monitoring/list/services?service_state>=0&service_state<=3&service_state_type=0&addColumns=service_last_check,service_is_reachable&format=json'} # hosts (up or down or unreachable) self.cgiurl_hosts = {'hard': self.monitor_cgi_url + '/monitoring/list/hosts?host_state>=0&host_state<=2&host_state_type=1&addColumns=host_last_check&format=json', \ 'soft': self.monitor_cgi_url + '/monitoring/list/hosts?host_state>=0&host_state<=2&host_state_type=0&addColumns=host_last_check&format=json'} # monitoring health self.cgiurl_monitoring_health = self.monitor_cgi_url + '/monitoring/health/info?format=json' # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # now using JSON output from Icinga try: for status_type in 'hard', 'soft': # first attempt result = self.fetch_url(self.cgiurl_hosts[status_type], giveback='raw') # authentication errors get a status code 200 too back because its # HTML works fine :-( if result.status_code < 400 and\ result.result.startswith('<'): # in case of auth error reset HTTP session and try again self.reset_HTTP() result = self.fetch_url(self.cgiurl_hosts[status_type], giveback='raw') # if it does not work again tell GUI there is a problem if result.status_code < 400 and\ result.result.startswith('<'): self.refresh_authentication = True return Result(result=result.result, error='Authentication error', status_code=result.status_code) # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) # check if any error occured self.check_for_error(jsonraw, error, status_code) # Check if the backend is running # If it isn't running the last values stored in the database are returned/shown # Unfortunately we need to make a extra request for this and only, if monitoring health is possible if self.cgiurl_monitoring_health: try: result = self.fetch_url(self.cgiurl_monitoring_health, giveback='raw') monitoring_health = json.loads(result.result)[0] if (monitoring_health['is_currently_running'] == '0'): return Result(result=monitoring_health, error='Icinga2 backend not running') except json.decoder.JSONDecodeError: # https://github.com/HenriWahl/Nagstamon/issues/619 # Icinga2 monitoring health status query does not seem to work (on older version?) self.cgiurl_monitoring_health = None hosts = json.loads(jsonraw) for host in hosts: # make dict of tuples for better reading h = dict(host.items()) # host if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if 'host_name' in h: host_name = h['host_name'] elif 'host' in h: host_name = h['host'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = h['host_display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][int(h['host_state'])] self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp(int(h['host_last_check'])) self.new_hosts[host_name].attempt = h['host_attempt'] self.new_hosts[host_name].status_information = BeautifulSoup(h['host_output'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].passiveonly = not(int(h['host_active_checks_enabled'])) self.new_hosts[host_name].notifications_disabled = not(int(h['host_notifications_enabled'])) self.new_hosts[host_name].flapping = bool(int(h['host_is_flapping'])) self.new_hosts[host_name].acknowledged = bool(int(h['host_acknowledged'])) self.new_hosts[host_name].scheduled_downtime = bool(int(h['host_in_downtime'])) self.new_hosts[host_name].status_type = status_type # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = h['host_name'] # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attemt is set to 1/x. if (status_type == 'hard'): try: maxAttempts = h['host_attempt'].split('/')[1] self.new_hosts[host_name].attempt = "{0}/{0}".format(maxAttempts) except Exception: self.new_hosts[host_name].attempt = "HARD" # extra duration needed for calculation if h['host_last_state_change'] is not None: last_change = h['host_last_state_change'] if h['host_last_state_change'] is not None else 0 duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(last_change)) self.new_hosts[host_name].duration = strfdelta(duration,'{days}d {hours}h {minutes}m {seconds}s') else: self.new_hosts[host_name].duration = 'n/a' del h, host_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in 'hard', 'soft': result = self.fetch_url(self.cgiurl_services[status_type], giveback='raw') # purify JSON result of unnecessary control sequence \n jsonraw, error, status_code = copy.deepcopy(result.result.replace('\n', '')),\ copy.deepcopy(result.error),\ result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) # check if any error occured self.check_for_error(jsonraw, error, status_code) services = copy.deepcopy(json.loads(jsonraw)) for service in services: # make dict of tuples for better reading s = dict(service.items()) if not self.use_display_name_host: # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if 'host_name' in s: host_name = s['host_name'] elif 'host' in s: host_name = s['host'] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = s['host_display_name'] # host objects contain service objects if not host_name in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].status = 'UP' # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs host_description and no display name self.new_hosts[host_name].real_name = s['host_name'] service_name = s['service_display_name'] # if a service does not exist create its object if not service_name in self.new_hosts[host_name].services: self.new_hosts[host_name].services[service_name] = GenericService() self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][int(s['service_state'])] self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp(int(s['service_last_check'])) self.new_hosts[host_name].services[service_name].attempt = s['service_attempt'] self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(s['service_output'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].services[service_name].passiveonly = not(int(s['service_active_checks_enabled'])) self.new_hosts[host_name].services[service_name].notifications_disabled = not(int(s['service_notifications_enabled'])) self.new_hosts[host_name].services[service_name].flapping = bool(int(s['service_is_flapping'])) self.new_hosts[host_name].services[service_name].acknowledged = bool(int(s['service_acknowledged'])) self.new_hosts[host_name].services[service_name].scheduled_downtime = bool(int(s['service_in_downtime'])) self.new_hosts[host_name].services[service_name].status_type = status_type self.new_hosts[host_name].services[service_name].unreachable = s['service_is_reachable'] == '0' if self.new_hosts[host_name].services[service_name].unreachable: self.new_hosts[host_name].services[service_name].status_information += " (SERVICE UNREACHABLE)" # extra Icinga properties to solve https://github.com/HenriWahl/Nagstamon/issues/192 # acknowledge needs service_description and no display name self.new_hosts[host_name].services[service_name].real_name = s['service_description'] # Icinga only updates the attempts for soft states. When hard state is reached, a flag is set and # attemt is set to 1/x. if (status_type == 'hard'): try: maxAttempts = s['service_attempt'].split('/')[1] self.new_hosts[host_name].services[service_name].attempt = "{0}/{0}".format(maxAttempts) except Exception: self.new_hosts[host_name].services[service_name].attempt = "HARD" # extra duration needed for calculation if s['service_last_state_change'] is not None: last_change = s['service_last_state_change'] if s['service_last_state_change'] is not None else 0 duration = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(last_change)) self.new_hosts[host_name].services[service_name].duration = strfdelta(duration, '{days}d {hours}h {minutes}m {seconds}s') else: self.new_hosts[host_name].services[service_name].duration = 'n/a' del s, host_name, service_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del jsonraw, error, hosts, services # dummy return in case all is OK return Result() def _set_recheck(self, host, service): # First retrieve the info page for this host/service if service == '': url = self.monitor_cgi_url + '/monitoring/host/show?host=' + self.hosts[host].real_name else: # to make the request working even with %-characters in service name it has to be quoted url = self.monitor_cgi_url + \ '/monitoring/service/show?host=' + self.hosts[host].real_name + \ '&service=' + urllib.parse.quote(self.hosts[host].services[service].real_name) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values # try-except needed in case the CSRFToken will not be found try: formtag = pagesoup.find('form', {'name':'IcingaModuleMonitoringFormsCommandObjectCheckNowCommandForm'}) CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] formUID = formtag.findNext('input', {'name':'formUID'})['value'] btn_submit = formtag.findNext('button', {'name':'btn_submit'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['formUID'] = formUID cgi_data['btn_submit'] = btn_submit self.fetch_url(url, giveback='raw', cgi_data=cgi_data) except AttributeError: if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='No valid CSRFToken available') # Overwrite function from generic server to add expire_time value def set_acknowledge(self, info_dict): ''' different monitors might have different implementations of _set_acknowledge ''' if info_dict['acknowledge_all_services'] is True: all_services = info_dict['all_services'] else: all_services = [] # Make sure expire_time is set #if not info_dict['expire_time']: # info_dict['expire_time'] = None self._set_acknowledge(info_dict['host'], info_dict['service'], info_dict['author'], info_dict['comment'], info_dict['sticky'], info_dict['notify'], info_dict['persistent'], all_services, info_dict['expire_time']) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None, expire_time=None): # First retrieve the info page for this host/service if service == '': url = '{0}/monitoring/host/acknowledge-problem?host={1}'.format(self.monitor_cgi_url, self.hosts[host].real_name) else: # to make the request working even with %-characters in service name it has to be quoted url = '{0}/monitoring/service/acknowledge-problem?host={1}&service={2}'.format(self.monitor_cgi_url, self.hosts[host].real_name, urllib.parse.quote(self.hosts[host].services[service].real_name)) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values # try-except needed in case the CSRFToken will not be found try: formtag = pagesoup.find('form', {'name':'IcingaModuleMonitoringFormsCommandObjectAcknowledgeProblemCommandForm'}) CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] formUID = formtag.findNext('input', {'name':'formUID'})['value'] btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['formUID'] = formUID cgi_data['btn_submit'] = btn_submit cgi_data['comment'] = comment cgi_data['persistent'] = int(persistent) cgi_data['sticky'] = int(sticky) cgi_data['notify'] = int(notify) cgi_data['comment'] = comment if expire_time: cgi_data['expire'] = 1 cgi_data['expire_time'] = expire_time self.fetch_url(url, giveback='raw', cgi_data=cgi_data) except AttributeError: if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='No valid CSRFToken available') if len(all_services) > 0: for s in all_services: # cheap, recursive solution... self._set_acknowledge(host, s, author, comment, sticky, notify, persistent, [], expire_time) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): # First retrieve the info page for this host/service if service == '': url = self.monitor_cgi_url + '/monitoring/host/process-check-result?host=' + self.hosts[host].real_name status = self.STATES_MAPPING_REV['hosts'][state.upper()] else: # to make the request working even with %-characters in service name it has to be quoted url = self.monitor_cgi_url + \ '/monitoring/service/process-check-result?host=' + self.hosts[host].real_name + \ '&service=' + urllib.parse.quote(self.hosts[host].services[service].real_name) status = self.STATES_MAPPING_REV['services'][state.upper()] result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values # try-except needed in case the CSRFToken will not be found try: formtag = pagesoup.find('form', {'name':'IcingaModuleMonitoringFormsCommandObjectProcessCheckResultCommandForm'}) CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] formUID = formtag.findNext('input', {'name':'formUID'})['value'] btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['formUID'] = formUID cgi_data['btn_submit'] = btn_submit cgi_data['status'] = status cgi_data['output'] = check_output cgi_data['perfdata'] = performance_data self.fetch_url(url, giveback='raw', cgi_data=cgi_data) except AttributeError: if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='No valid CSRFToken available') def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): # First retrieve the info page for this host/service if service == '': url = self.monitor_cgi_url + '/monitoring/host/schedule-downtime?host=' + self.hosts[host].real_name else: url = self.monitor_cgi_url + \ '/monitoring/service/schedule-downtime?host=' + self.hosts[host].real_name + \ '&service=' + urllib.parse.quote(self.hosts[host].services[service].real_name) result = self.fetch_url(url, giveback='raw') if result.error != '': return result else: pageraw = result.result pagesoup = BeautifulSoup(pageraw, 'html.parser') # Extract the relevant form element values # try-except needed in case the CSRFToken will not be found try: if service == '': formtag = pagesoup.find('form', {'name':'IcingaModuleMonitoringFormsCommandObjectScheduleHostDowntimeCommandForm'}) else: formtag = pagesoup.find('form', {'name':'IcingaModuleMonitoringFormsCommandObjectScheduleServiceDowntimeCommandForm'}) CSRFToken = formtag.findNext('input', {'name':'CSRFToken'})['value'] formUID = formtag.findNext('input', {'name':'formUID'})['value'] btn_submit = formtag.findNext('input', {'name':'btn_submit'})['value'] # Pass these values to the same URL as cgi_data cgi_data = {} cgi_data['CSRFToken'] = CSRFToken cgi_data['formUID'] = formUID cgi_data['btn_submit'] = btn_submit cgi_data['comment'] = comment if fixed: cgi_data['type'] = 'fixed' else: cgi_data['type'] = 'flexible' cgi_data['hours'] = hours cgi_data['minutes'] = minutes if start_time == '' or start_time == 'n/a': start = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') else: start = start_time if end_time == '' or end_time == 'n/a': end = (datetime.datetime.now() + datetime.timedelta(hours=hours, minutes=minutes)).strftime('%Y-%m-%dT%H:%M:%S') else: end = end_time cgi_data['start'] = start cgi_data['end'] = end self.fetch_url(url, giveback='raw', cgi_data=cgi_data) except AttributeError: if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='No valid CSRFToken available') def get_start_end(self, host): ''' for GUI to get actual downtime start and end from server - they may vary so it's better to get directly from web interface ''' try: downtime = self.fetch_url(self.monitor_cgi_url + '/monitoring/host/schedule-downtime?host=' + self.hosts[host].real_name) start = downtime.result.find('input', {'name': 'start'})['value'] end = downtime.result.find('input', {'name': 'end'})['value'] # give values back as tuple return start, end except: self.error(sys.exc_info()) return 'n/a', 'n/a' def open_monitor(self, host, service=''): ''' open monitor from tablewidget context menu ''' # only type is important so do not care of service '' in case of host monitor if service == '': url = '{0}/monitoring/list/hosts?host_problem=1&sort=host_severity#!{1}/monitoring/host/show?{2}'.format(self.monitor_url, (urllib.parse.urlparse(self.monitor_url).path), urllib.parse.urlencode( {'host': self.hosts[host].real_name}).replace('+', ' ')) else: url = '{0}/monitoring/list/services?service_problem=1&sort=service_severity&dir=desc#!{1}/monitoring/service/show?{2}'.format(self.monitor_url, (urllib.parse.urlparse(self.monitor_url).path), urllib.parse.urlencode( {'host': self.hosts[host].real_name, 'service': self.hosts[host].services[service].real_name}).replace('+', ' ')) if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Open host/service monitor web page {0}'.format(url)) webbrowser_open(url) def get_host(self, host): ''' find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Icinga ''' # Host is the display name as in the GUI # but we need the FQDN not the display name host = self.hosts[host].real_name # the fasted method is taking hostname as used in monitor if conf.connect_by_host is True or host == '': return Result(result=host) # initialize ip string ip = '' address = '' # glue nagios cgi url and hostinfo cgiurl_host = self.monitor_cgi_url + '/monitoring/list/hosts?host={0}&addColumns=host_address&format=json'.format(host) # get host info hostobj = self.fetch_url(cgiurl_host, giveback='raw') jsonhost = hostobj.result try: # take ip from json output result = json.loads(jsonhost)[0] ip = result["address"] # print IP in debug mode if conf.debug_mode is True: self.debug(server=self.get_name(), host=host, debug='IP of %s:' % (host) + ' ' + ip) # when connection by DNS is not configured do it by IP if conf.connect_by_dns is True: # try to get DNS name for ip, if not available use ip try: address = socket.gethostbyaddr(ip)[0] except socket.error: address = ip else: address = ip except Exception: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del hostobj # give back host or ip return Result(result=address) Nagstamon-master/Nagstamon/Servers/Livestatus.py000066400000000000000000000230301505160700500223700ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.objects import Result from Nagstamon.objects import GenericHost from Nagstamon.objects import GenericService from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf import logging log = logging.getLogger('Livestatus') import re import json import socket import time def format_timestamp(timestamp): """format unix timestamp""" ts_tuple = time.localtime(timestamp) return time.strftime('%Y-%m-%d %H:%M:%S', ts_tuple) def duration(timestamp): """human representation of a duration""" factors = (60 * 60 * 24, 60 * 60, 60, 1) result = [] diff = time.time() - timestamp for f in factors: x = int(diff / f) result.append(x) diff = diff - x * f return '%02dd %02dh %02dm %02ds' % tuple(result) def service_to_host(data): """create the host data blob from the implicit join data of a service""" result = {} for key in data.keys(): if key.startswith('host_'): result[key[5:]] = data[key] return result class LivestatusServer(GenericServer): """A server running MK Livestatus plugin. Tested with icinga2""" TYPE = 'Livestatus' def init_config(self): log.info(self.monitor_url) # we abuse the monitor_url for the connection information self.address = ('localhost', 6558) m = re.match(r'.*?://([^:/]+?)(?::(\d+))?(?:/|$)', self.monitor_url) if m: host, port = m.groups() if not port: port = 6558 else: port = int(port) self.address = (host, port) else: log.error('unable to parse monitor_url %s', self.monitor_url) self.enable = False def init_HTTP(self): pass def communicate(self, data, response=True): buffersize = 2**20 data.append('') data.append('') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log.debug('connecting') s.connect(self.address) s.send('\n'.join(data).encode('utf8')) if not response: log.debug('no response required, disconnect') s.close() return '' result = bytes() line = s.recv(buffersize) while len(line) > 0: result += line line = s.recv(buffersize) log.debug('disconnect') s.close() log.debug('received %d bytes', len(result)) result = result.decode('utf8') return result def get(self, table, raw=[], headers={}): """send data to livestatus socket, receive result, format as json""" data = ['GET %s' % table, ] headers['OutputFormat'] = 'json' headers['ColumnHeaders'] = 'on' for k, v in headers.items(): data.append('%s: %s' % (k, v)) for line in raw: data.append(line) result = self.communicate(data) if result: return json.loads(result) return result def command(self, *cmd): """execute nagios command via livestatus socket. For commands see https://old.nagios.org/developerinfo/externalcommands/commandlist.php """ data = [] ts = str(int(time.time()) + 5) # current epoch timestamp + 5 seconds for line in cmd: line = 'COMMAND [TIMESTAMP] ' + line data.append(line.replace('TIMESTAMP', ts)) self.communicate(data, response=False) def table(self, data): """take a livestatus answer and format it as a table, list of dictionaries [ {host: 'foo1', service: 'bar1'}, {host: 'foo2', service: 'bar2'} ] """ try: header = data[0] except IndexError: raise StopIteration for line in data[1:]: yield(dict(zip(header, line))) def _get_status(self): """fetch any host/service not in OK state store the information in self.new_hosts applies basic filtering. All additional filtering and merging new_hosts to hosts is left to nagstamon """ log.debug('_get_status') self.new_hosts = dict() filters = [] filters.append('Filter: state != 0') # ignore OK state if conf.filter_acknowledged_hosts_services: filters.append('Filter: acknowledged != 1') # hosts data = self.get("hosts", raw=filters) for h in self.table(data): host = self._create_host(h) self.new_hosts[host.name] = host log.info("host %s is %s", host.name, host.status) # services data = self.get("services", raw=filters) for s in self.table(data): # service are attached to host objects if s['host_name'] in self.new_hosts: host = self.new_hosts[s['host_name']] else: # need to create the host # icinga2 adds all host information to the server # prefixed with HOST_ xdata = service_to_host(s) # any field starting with HOST_ host = self._create_host(xdata) self.new_hosts[host.name] = host service = self._create_service(s) service.host = host.name host.services[service.name] = service return Result() def _update_object(self, obj, data): """populate the generic fields of obj (GenericHost or GenericService) from data.""" result = obj result.server = self.name result.last_check = format_timestamp(data['last_check']) result.duration = duration(data['last_state_change']) result.attempt = data['current_attempt'] result.status_information = data['plugin_output'] result.passiveonly = False result.notifications_disabled = data['notifications_enabled'] != 1 result.flapping = data['is_flapping'] == 1 result.acknowledged = data['acknowledged'] == 1 result.scheduled_downtime = data['scheduled_downtime_depth'] == 1 if data['state'] == data['last_hard_state']: result.status_type = 'hard' else: result.status_type = 'soft' return result def _create_host(self, data): """create GenericHost from json data""" result = self._update_object(GenericHost(), data) result.name = data['name'] host_states = {0: 'UP', 1: 'DOWN', 2: 'UNKNOWN'} result.status = host_states[data['state']] return result def _create_service(self, data): """create GenericService from json data""" result = self._update_object(GenericService(), data) result.name = data['display_name'] service_states = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} result.status = service_states[data['state']] return result def set_recheck(self, info_dict): """schedule a forced recheck of a service or host""" service = info_dict['service'] host = info_dict['host'] if service: if self.hosts[host].services[service].is_passive_only(): return cmd = ['SCHEDULE_FORCED_SVC_CHECK', host, service, 'TIMESTAMP'] else: cmd = ['SCHEDULE_FORCED_HOST_CHECK', host, 'TIMESTAMP'] self.command(';'.join(cmd)) def set_acknowledge(self, info_dict): """acknowledge a service or host""" host = info_dict['host'] service = info_dict['service'] if service: cmd = ['ACKNOWLEDGE_SVC_PROBLEM', host, service] else: cmd = ['ACKNOWLEDGE_HOST_PROBLEM', host] cmd.extend([ '2' if info_dict['sticky'] else '1', '1' if info_dict['notify'] else '0', '1' if info_dict['persistent'] else '0', info_dict['author'], info_dict['comment'], ]) self.command(';'.join(cmd)) def set_downtime(self, info_dict): log.info('set_downtime not implemented') def set_submit_check_result(self, info_dict): log.info('set_submit_check_result not implemented') def get_start_end(self, host): log.info('get_start_end not implemented') return 'n/a', 'n/a' def open_monitor(self, host, service=''): log.info('open_monitor not implemented') # TODO figure out how to add more config options like socket and weburl def open_monitor_webpage(self): log.info('open_monitor_webpage not implemented') # TODO # config dialog fields # config Nagstamon-master/Nagstamon/Servers/Monitos3.py000066400000000000000000000374541505160700500217570ustar00rootroot00000000000000# encoding: utf-8 from Nagstamon.helpers import webbrowser_open # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Nagstamon plugin for Monitos3 from Freicon GmbH & Co. KG # Based on the Livestatus plugin from MK # 2017_06_27 # studo from Nagstamon.objects import Result from Nagstamon.objects import GenericHost from Nagstamon.objects import GenericService from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf import logging # logging.basicConfig( level=logging.INFO ) # logging.basicConfig(filename='nagstamon.log',level=logging.INFO) log = logging.getLogger('Monitos3') log.setLevel(logging.INFO) import re import json import socket import time def format_timestamp(timestamp): """format unix timestamp""" ts_tuple = time.localtime(timestamp) return time.strftime('%Y-%m-%d %H:%M:%S', ts_tuple) def duration(timestamp): """human representation of a duration""" factors = (60 * 60 * 24, 60 * 60, 60, 1) result = [] diff = time.time() - timestamp for f in factors: x = int(diff / f) result.append(x) diff = diff - x * f return '%02dd %02dh %02dm %02ds' % tuple(result) def service_to_host(data): """create the host data blob from the implicit join data of a service""" result = {} for key in data.keys(): if key.startswith('host_'): result[key[5:]] = data[key] return result class Monitos3Server(GenericServer): """A server running Monitos3 with the MK Livestatus NEB. Tested with Monitos3.7.17""" TYPE = 'Monitos3' MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] # STATES_MAPPING = {'hosts' : {0 : 'UP', 1 : 'DOWN', 2 : 'UNREACHABLE'}, \ # 'services' : {0 : 'OK', 1 : 'WARNING', 2 : 'CRITICAL', 3 : 'UNKNOWN'}} # STATES_MAPPING_REV = {'hosts' : { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2}, \ # 'services' : {'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3}} BROWSER_URLS = { 'monitor': '$MONITOR$/dashboard', \ 'hosts': '$MONITOR$/monitoring/list/hosts', \ 'services': '$MONITOR$/monitoring/list/services', \ 'history': '$MONITOR$/monitoring/list/eventhistory?timestamp>=-7 days'} def init_config(self): log.info( time.strftime('%a %H:%M:%S') ) log.info(self.monitor_url) # we abuse the monitor_url for the connection information self.address = ('localhost', 6558) m = re.match(r'.*?://([^:/]+?)(?::(\d+))?(?:/|$)', self.monitor_url) if m: host, port = m.groups() if not port: port = 6558 else: port = int(port) self.address = (host, port) else: log.error('unable to parse monitor_url %s', self.monitor_url) self.enable = False def init_HTTP(self): pass def communicate(self, data, response=True): buffersize = 2**20 data.append('') data.append('') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log.debug('connecting') s.connect(self.address) s.send('\n'.join(data).encode('utf8')) if not response: log.debug('no response required, disconnect') s.close() return '' result = bytes() line = s.recv(buffersize) while len(line) > 0: result += line line = s.recv(buffersize) log.debug('disconnect') s.close() log.debug('received %d bytes', len(result)) result = result.decode('utf8') return result def get(self, table, raw=[], headers={}): """send data to livestatus socket, receive result, format as json""" data = ['GET %s' % table, ] headers['OutputFormat'] = 'json' headers['ColumnHeaders'] = 'on' for k, v in headers.items(): data.append('%s: %s' % (k, v)) for line in raw: data.append(line) result = self.communicate(data) if result: return json.loads(result) return result def command(self, *cmd): """execute nagios command via livestatus socket. For commands see https://old.nagios.org/developerinfo/externalcommands/commandlist.php """ data = [] ts = str(int(time.time()) + 5) # current epoch timestamp + 5 seconds for line in cmd: line = 'COMMAND [TIMESTAMP] ' + line data.append(line.replace('TIMESTAMP', ts)) self.communicate(data, response=False) def table(self, data): """take a livestatus answer and format it as a table, list of dictionaries [ {host: 'foo1', service: 'bar1'}, {host: 'foo2', service: 'bar2'} ] """ try: header = data[0] except IndexError: raise StopIteration for line in data[1:]: yield(dict(zip(header, line))) def _get_status(self): """fetch any host/service not in OK state store the information in self.new_hosts applies basic filtering. All additional filtering and merging new_hosts to hosts is left to nagstamon """ log.debug('in def _get_status') self.new_hosts = dict() filters = [] filters.append('Filter: state != 0') # ignore OK state if conf.filter_acknowledged_hosts_services: filters.append('Filter: acknowledged != 1') # hosts data = self.get("hosts", raw=filters) for h in self.table(data): host = self._create_host(h) self.new_hosts[host.name] = host # from icinga2 # self.new_hosts[host_name].real_name = h['host_name'] # 2017_06_02 log.info("monitos3_host %s with svid %s, alias %s, dn %s, addr %s is %s", host.name, host.svid, host.alias, host.display_name, host.address, host.status) # log.debug("host %s is %s", host.name, host.status) # services data = self.get("services", raw=filters) for s in self.table(data): # service are attached to host objects """ 2017_07_12_22_29_43 """ if s['custom_variables']['_HOST_NAME'] in self.new_hosts: # if s['host_name'] in self.new_hosts: host = self.new_hosts[s['custom_variables']['_HOST_NAME']] log.info("In new_hosts services host %s with svid %s, svc %s, svid %s is %s", host.name, host.svid, service.name, service.svid, service.status) else: # need to create the host # icinga2 adds all host information to the server # prefixed with HOST_ xdata = service_to_host(s) # any field starting with HOST_ host = self._create_host(xdata) self.new_hosts[host.name] = host log.info("In else services host %s with svid %s", host.name, host.svid ) service = self._create_service(s) service.host = host.name host.services[service.name] = service log.debug("All services: host %s with svid %s, svc %s, svid %s is %s", host.name, host.svid, service.name, service.svid, service.status) log.debug("monitos3_svcs host.services are: %s", service ) return Result() def _update_object(self, obj, data): """populate the generic fields of obj (GenericHost or GenericService) from data.""" result = obj result.server = self.name result.last_check = format_timestamp(data['last_check']) result.duration = duration(data['last_state_change']) result.attempt = data['current_attempt'] result.status_information = data['plugin_output'] result.passiveonly = False result.notifications_disabled = data['notifications_enabled'] != 1 result.flapping = data['is_flapping'] == 1 result.acknowledged = data['acknowledged'] == 1 result.scheduled_downtime = data['scheduled_downtime_depth'] == 1 if data['state'] == data['last_hard_state']: result.status_type = 'hard' else: result.status_type = 'soft' return result def _create_host(self, data): """create GenericHost from json data""" result = self._update_object(GenericHost(), data) result.name = data['custom_variables']['_HOST_NAME'] result.svid = data['name'] result.display_name = data['custom_variables']['_HOST_NAME'] result.alias = data['alias'] result.address = data['address'] result.custom_variables = data['custom_variables'] result.show_name = data['custom_variables']['_HOST_NAME'] # 2017_06_02 log.debug('data for obj %s is: %s', result.name, data) log.debug("monitos3 host name is %s", result.name) log.debug("monitos3 host custom_variables are %s", result.custom_variables) log.debug("monitos3 host address is %s", result.address) # TODO: fix other host states host_states = { 0: 'UP', 1: 'DOWN', 2: 'UNREACHABLE' } # host_states = { 0: 'UP', 1: 'DOWN', 2: 'UNREACHABLE', 3: 'OTHER' } host_state_list = [ 0, 1, 2 ] if data['state'] in host_state_list: result.status = host_states[data['state']] else: result.status = 'OTHER' return result def _create_service(self, data): """create GenericService from json data""" result = self._update_object(GenericService(), data) result.name = data['custom_variables']['_SERVICE_NAME'] result.svid = data['display_name'] result.custom_variables = data['custom_variables'] # result.name = data['display_name'] service_states = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} service_state_list = [ 0, 1, 2, 3 ] if data['state'] in service_state_list: result.status = service_states[data['state']] else: result.status = 'OTHER' return result def set_recheck(self, info_dict): host = info_dict['host'] log.info('host is: %s', host) host_svid = self.hosts[host].svid log.info('host_svid is: %s', host_svid) service = info_dict['service'] if service: svc_svid = self.hosts[host].services[service].svid log.info('svc_svid is: %s', svc_svid) log.info('service is: %s', service) if self.hosts[host].services[service].is_passive_only(): return cmd = ['SCHEDULE_FORCED_SVC_CHECK', host_svid, svc_svid, 'TIMESTAMP'] log.info('cmd is: %s', cmd ) else: cmd = ['SCHEDULE_FORCED_HOST_CHECK', host_svid, 'TIMESTAMP'] log.info('cmd is: %s', cmd ) self.command(';'.join(cmd)) # log.debug('recheck cmd is: %s', self.command ) def set_acknowledge(self, info_dict): """ acknowledge a service or host {'author': 'username', 'all_services': ['PING', 'DUMMY'], 'host': 'fritz', 'service': '', 'server': , 'acknowledge_all_services': True, 'sticky': True, 'notify': True, 'comment': 'acknowledged', 'persistent': True} """ log.info('called def set_acknowledge') log.info('info_dict is: %s', info_dict ) host = info_dict['host'] log.info('host is: %s', host) host_svid = self.hosts[host].svid log.info('host_svid is: %s', host_svid) service = info_dict['service'] if service: svc_svid = self.hosts[host].services[service].svid log.info('svc_svid is: %s', svc_svid) cmd = ['ACKNOWLEDGE_SVC_PROBLEM', host_svid, svc_svid] else: cmd = ['ACKNOWLEDGE_HOST_PROBLEM', host_svid] cmd.extend([ '2' if info_dict['sticky'] else '1', '1' if info_dict['notify'] else '0', '1' if info_dict['persistent'] else '0', info_dict['author'], info_dict['comment'], ]) log.info('ack cmd is: %s', ';'.join(cmd) ) self.command(';'.join(cmd)) def set_downtime(self, info_dict): log.info('set_downtime not implemented') def set_submit_check_result(self, info_dict): # INFO:Monitos3:info_dict is: { # 'host': 'BPMon', 'performance_data': 'ggggg', 'service': 'BP_test cw', 'state': 'critical', # 'comment': 'check result submitted', 'server': , # 'check_output': 'tesstestus'} log.debug( json.dumps( str( info_dict ), sort_keys=True, indent=4) ) host = info_dict['host'] log.debug('host is: %s', host) host_svid = self.hosts[host].svid log.debug('host_svid is: %s', host_svid) plugin_output = info_dict['check_output'] service = info_dict['service'] if service: # PROCESS_SERVICE_CHECK_RESULT;;;; log.debug('service is: %s', service) svc_svid = self.hosts[host].services[service].svid log.debug('svc_svid is: %s', svc_svid) rev_service_states = { 'ok': 0, 'warning': 1, 'critical': 2, 'unknown': 3} return_code = rev_service_states[ info_dict[ 'state' ] ] cmd = ['PROCESS_SERVICE_CHECK_RESULT', host_svid, svc_svid, str( return_code ), plugin_output] log.debug('cmd is: %s', cmd ) else: # PROCESS_HOST_CHECK_RESULT;;; log.debug('host is: %s', host) rev_host_states = { 'up': 0, 'down': 1, 'unreachable': 2, 'unknown': 3 } return_code = rev_host_states[ info_dict[ 'state' ] ] cmd = ['PROCESS_HOST_CHECK_RESULT', host_svid, str( return_code ), plugin_output] log.debug('cmd is: %s', cmd ) self.command(';'.join(cmd)) def get_start_end(self, host): log.info('get_start_end not implemented') return 'n/a', 'n/a' def open_monitor(self, host, service=''): log.info('open_monitor not implemented. host is %s', host) # TODO figure out how to add more config options like socket and weburl # line 77 def open_monitor_webpage(self): ''' open monitor from systray/toparea context menu ''' log.info('trying to implement def open_monitor_webpage') log.info('address is: %s', self.address) # self.address = ('http://localhost') monitos3_url = 'http://172.16.10.102' log.info('monitos3_url is: %s', monitos3_url ) webbrowser_open( monitos3_url ) # TODO # config dialog fields # config Nagstamon-master/Nagstamon/Servers/Monitos4x.py000066400000000000000000000643061505160700500221440ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Marcus MÃļnnig # # This Server class connects against monitos 4. # # Status/TODOs: # import copy import datetime import json import logging import sys import time import requests from bs4 import BeautifulSoup from Nagstamon.helpers import webbrowser_open from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf log = logging.getLogger('monitos4x') log.setLevel('INFO') def strfdelta(tdelta, fmt): d = {'days': tdelta.days} d['hours'], rem = divmod(tdelta.seconds, 3600) d['minutes'], d['seconds'] = divmod(rem, 60) return fmt.format(**d) class Monitos4xServer(GenericServer): """ object of monitos 4x server from Freicon GmbH & Co. KG """ TYPE = 'monitos4x' MENU_ACTIONS = [ 'Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime' ] STATES_MAPPING = { 'hosts': { 0: 'UP', 1: 'DOWN', 2: 'UNREACHABLE', 4: 'PENDING' }, 'services': { 0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN', 4: 'PENDING' } } STATES_MAPPING_REV = { 'hosts': { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2, 'PENDING': 4 }, 'services': { 'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3, 'PENDING': 4 } } BROWSER_URLS = { 'monitor': '$MONITOR$', 'hosts': '$MONITOR$/#/host-problems', 'services': '$MONITOR$/#/service-problems', 'history': '$MONITOR$/#/alert/ticker' } def init_config(self): """ Set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None def init_HTTP(self): """ Initializing of session object """ GenericServer.init_HTTP(self) self.session.auth = NoAuth() if self.use_autologin is False: if len(self.session.cookies) == 0: form_inputs = dict() if '@' in self.username: user = self.username.split('@') form_inputs['module'] = 'ldap' form_inputs['_username'] = user[0] else: form_inputs['module'] = 'sv' form_inputs['_username'] = self.username form_inputs['urm:login:client'] = '' form_inputs['_password'] = self.password # call login page to get temporary cookie self.fetch_url('{0}/security/login'.format(self.monitor_url)) # submit login form to retrieve authentication cookie self.fetch_url( '{0}/security/login_check'.format(self.monitor_url), cgi_data=form_inputs, multipart=True ) def _get_status(self): """ Get status from monitos 4 Server - only JSON """ # define CGI URLs for hosts and services if self.use_autologin is True: if self.cgiurl_hosts is None: # hosts (up, down, unreachable) self.cgiurl_hosts = self.monitor_cgi_url + '/api/host?include=status,configuration&limit=100&filter[states]=0,1,2&filter[onlysyncenabled]' + '&authtoken=' + self.autologin_key if self.cgiurl_services is None: # services (warning, critical, unknown) self.cgiurl_services = self.monitor_cgi_url + \ '/api/serviceinstance?include=status,configuration&limit=100&filter[states]=1,2,3&filter[onlysyncenabled]' + '&authtoken=' + self.autologin_key else: if self.cgiurl_hosts is None: # hosts (up, down, unreachable) self.cgiurl_hosts = self.monitor_cgi_url + '/api/host?include=status,configuration&limit=100&filter[states]=0,1,2&filter[onlysyncenabled]' if self.cgiurl_services is None: # services (warning, critical, unknown) self.cgiurl_services = self.monitor_cgi_url + '/api/serviceinstance?include=status,configuration&limit=100&filter[states]=1,2,3&filter[onlysyncenabled]' self.new_hosts = dict() # hosts try: page = 1 # loop trough all api pages while True: cgiurl_hosts_page = self.cgiurl_hosts + '&page=' + str(page) result = self.fetch_url( cgiurl_hosts_page, giveback='raw', cgi_data=None) # authentication errors get a status code 200 too if result.status_code < 400 and \ result.result.startswith('<'): # in case of auth error reset HTTP session and try again self.reset_HTTP() result = self.fetch_url( cgiurl_hosts_page, giveback='raw', cgi_data=None) if result.status_code < 400 and \ result.result.startswith('<'): self.refresh_authentication = True return Result(result=result.result, error='Authentication error', status_code=result.status_code) # purify JSON result jsonraw = copy.deepcopy(result.result.replace('\n', '')) error = copy.deepcopy(result.error) status_code = result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) self.check_for_error(jsonraw, error, status_code) hosts = json.loads(jsonraw) if not hosts['data']: break page+=1 for host in hosts['data']: h = dict(host) # Skip if host is disabled if h['syncEnabled'] is not None: if not int(h['syncEnabled']): continue # host host_name = h['name'] if conf.debug_mode: self.debug(server=self.get_name(), debug=time.strftime('%a %H:%M:%S') + ' host_name is: ' + host_name) # If a host does not exist, create its object if host_name not in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].uuid = h['uuid'] self.new_hosts[host_name].server = 'monitos' try: self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][int( h['status']['currentState'])] except: pass try: self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp( int(h['status']['lastCheck'])) except: pass self.new_hosts[host_name].attempt = h['configuration']['maxCheckAttempts'] try: self.new_hosts[host_name].status_information = BeautifulSoup(h['status']['output'].replace('\n', ' ').strip(), 'html.parser').text except: self.new_hosts[host_name].services[service_name].status_information = 'Cant parse output' self.new_hosts[host_name].passiveonly = not ( int(h['status']['checksEnabled'])) try: self.new_hosts[host_name].notifications_disabled = not (int(s['status']['notificationsEnabled'])) except: self.new_hosts[host_name].notifications_disabled = False try: self.new_hosts[host_name].flapping = (int(h['status']['isFlapping'])) except: self.new_hosts[host_name].flapping = False if h['status']['acknowleged'] is None: self.new_hosts[host_name].acknowledged = False else: if h['status']['acknowleged'] != 0: self.new_hosts[host_name].acknowledged = True try: if int(h['status']['scheduledDowntimeDepth']) != 0: self.new_hosts[host_name].scheduled_downtime = True except: self.new_hosts[host_name].scheduled_downtime = False try: self.new_hosts[host_name].status_type = 'soft' if int(h['status']['stateType']) == 0 else 'hard' except: self.new_hosts[host_name].status_type = 'hard' # extra duration needed for calculation if h['status']['lastStateChange'] is None: self.debug(server=self.get_name(), debug=time.strftime('%a %H:%M:%S') + 'Host has wrong lastStateChange - host_name is: ' + host_name) else: duration = datetime.datetime.now( ) - datetime.datetime.fromtimestamp(int(h['status']['lastStateChange'])) self.new_hosts[host_name].duration = strfdelta( duration, '{days}d {hours}h {minutes}m {seconds}s') del h, host_name del hosts except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: page = 1 # loop trough all api pages while True: cgiurl_services_page = self.cgiurl_services + '&page=' + str(page) result = self.fetch_url(cgiurl_services_page, giveback='raw', cgi_data=None) # purify JSON result jsonraw = copy.deepcopy(result.result.replace('\n', '')) error = copy.deepcopy(result.error) status_code = result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) self.check_for_error(jsonraw, error, status_code) services = json.loads(jsonraw) if not services['data']: break page+=1 for service in services['data']: s = dict(service) # Skip if host is disabled if s['syncEnabled'] is not None: if not int(s['syncEnabled']): continue # host and service host_name = s['configuration']['hostName'] service_name = s['configuration']['serviceDescription'] if conf.debug_mode: self.debug(server=self.get_name(), debug=time.strftime('%a %H:%M:%S') + ' host_name is: ' + host_name + ' service_name is: ' + service_name) # If host not in problem list, create it if host_name not in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].uuid = s['configuration']['host']['uuid'] self.new_hosts[host_name].status = self.STATES_MAPPING['services'][0] # If a service does not exist, create its object if service_name not in self.new_hosts[host_name].services: self.new_hosts[host_name].services[service_name] = GenericService( ) self.new_hosts[host_name].services[service_name].host = s['configuration']['hostName'] self.new_hosts[host_name].services[service_name].uuid = s['uuid'] self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = 'monitos' try: self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][int( s['status']['currentState'])] except: pass try: self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp( int(s['status']['lastCheck'])) except: pass self.new_hosts[host_name].services[service_name].attempt = s['configuration']['maxCheckAttempts'] try: self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(s['status']['output'].replace('\n', ' ').strip(), 'html.parser').text except: self.new_hosts[host_name].services[service_name].status_information = 'Cant parse output' self.new_hosts[host_name].services[service_name].passiveonly = not (int(s['status']['checksEnabled'])) try: self.new_hosts[host_name].services[service_name].notifications_disabled = not ( int(s['status']['notificationsEnabled'])) except: self.new_hosts[host_name].services[service_name].notifications_disabled = False try: self.new_hosts[host_name].services[service_name].flapping = (int(s['status']['isFlapping'])) except: self.new_hosts[host_name].services[service_name].flapping = False if s['status']['acknowleged'] is None: self.new_hosts[host_name].services[service_name].acknowledged = False else: if s['status']['acknowleged'] != 0: self.new_hosts[host_name].services[service_name].acknowledged = True try: if int(s['status']['scheduledDowntimeDepth']) != 0: self.new_hosts[host_name].services[service_name].scheduled_downtime = True except: self.new_hosts[host_name].services[service_name].scheduled_downtime = False try: self.new_hosts[host_name].services[service_name].status_type = 'soft' if int(s['status']['stateType']) == 0 else 'hard' except: self.new_hosts[host_name].services[service_name].status_type = 'hard' # extra duration needed for calculation if s['status']['lastStateChange'] is None: self.debug(server=self.get_name(), debug=time.strftime('%a %H:%M:%S') + 'Service has wrong lastStateChange - host_name is ' + host_name + ' service_name is: ' + service_name) else: duration = datetime.datetime.now( ) - datetime.datetime.fromtimestamp(int(s['status']['lastStateChange'])) self.new_hosts[host_name].services[service_name].duration = strfdelta( duration, '{days}d {hours}h {minutes}m {seconds}s') del s, host_name, service_name del services except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) del jsonraw, error, hosts # dummy return in case all is OK return Result() def _set_recheck(self, host, service): """ Do a POST-Request to recheck the given host or service in monitos 4 :param host: String - Host name :param service: String - Service name """ type_ = 'host' if service == '': uuid = self.hosts[host].uuid else: type_ = 'serviceinstance' uuid = self.hosts[host].services[service].uuid if self.use_autologin is True: self.session.post('{0}/api/{1}/{2}/reschedule?authtoken={3}'.format(self.monitor_url, type_, uuid, self.autologin_key)) else: self.session.post('{0}/api/{1}/{2}/reschedule'.format(self.monitor_url, type_, uuid)) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): """ Do a POST-Request to set an acknowledgement for a host, service or host with all services in monitos 4 :param host: String - Host name :param service: String - Service name :param author: String - Author name (username) :param comment: String - Additional comment :param sticky: Bool - Sticky Acknowledgement :param notify: Bool - Send Notifications :param persistent: Bool - Persistent comment :param all_services: Optional[Array] - List of all services (filled only if 'Acknowledge all services on host' is set) """ type_ = 'host' if all_services: # Host & all Services uuid = self.hosts[host].uuid form_data = json.dumps( {'comment': comment, 'notify': int(notify), 'persistent': int(persistent), 'sticky': int(sticky), 'includeServices': 1}) elif service == '': # Host uuid = self.hosts[host].uuid form_data = json.dumps( {'comment': comment, 'notify': int(notify), 'persistent': int(persistent), 'sticky': int(sticky), 'includeServices': 0}) else: # Service uuid = self.hosts[host].services[service].uuid type_ = 'serviceinstance' form_data = json.dumps( {'comment': comment, 'notify': int(notify), 'persistent': int(persistent), 'sticky': int(sticky)}) if self.use_autologin is True: self.session.post('{0}/api/{1}/{2}/acknowledge?authtoken={3}'.format(self.monitor_url, type_ ,uuid, self.autologin_key), data=form_data) else: self.session.post('{0}/api/{1}/{2}/acknowledge'.format(self.monitor_url, type_,uuid), data=form_data) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): """ Do a POST-Request to submit a check result to monitos 4 :param host: String - Host name :param service: String - Service name :param state: String - Selected state :param comment: NOT IN USE - String - Additional comment :param check_output: String - Check output :param performance_data: String - Performance data """ state = state.upper() type_ = 'host' if service == '': # Host uuid = self.hosts[host].uuid if state == 'OK' or state == 'UNKNOWN': log.info('Setting OK or UNKNOWN to UP') state = 'UP' state_number = self.STATES_MAPPING_REV['hosts'][state] if performance_data == '': form_data = json.dumps( {'exit_status': state_number, 'plugin_output': check_output}) else: form_data = json.dumps({'exit_status': state_number, 'plugin_output': check_output, 'performance_data': performance_data}) else: # Service if state == 'UP': log.info('Setting UP or OK') state = 'OK' if state == 'UNREACHABLE': log.info('Setting UNREACHABLE to CRITICAL') state = 'CRITICAL' type_ = 'serviceinstance' uuid = self.hosts[host].services[service].uuid state_number = self.STATES_MAPPING_REV['services'][state] if performance_data == '': form_data = json.dumps( {'exit_status': state_number, 'plugin_output': check_output}) else: form_data = json.dumps( {'exit_status': state_number, 'plugin_output': check_output, 'performance_data': performance_data}) if self.use_autologin is True: self.session.post('{0}/api/{1}/{2}/checkresult?authtoken={3}'.format(self.monitor_url, type_ ,uuid, self.autologin_key), data=form_data) else: self.session.post('{0}/api/{1}/{2}/checkresult'.format(self.monitor_url, type_ ,uuid), data=form_data) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ Do a PUT-Request to create a downtime for a host or service in monitos 4 :param host: String - Host name :param service: String - Service name :param author: String - Author name (username) :param comment: String - Additional comment :param fixed: Bool - Fixed Downtime :param start_time: String - Date in Y-m-d H:M:S format - Start of Downtime :param end_time: String - Date in Y-m-d H:M:S format - End of Downtime :param hours: Integer - Flexible Downtime :param minutes: Integer - Flexible Downtime """ if service == '': type_ = 'sv_host' uuid = self.hosts[host].uuid else: type_ = 'sv_service_status' uuid = self.hosts[host].services[service].uuid # Format start_time and end_time from user-friendly format to timestamp start_time = time.mktime(datetime.datetime.strptime( start_time, "%Y-%m-%d %H:%M:%S").timetuple()) start_time = str(start_time).split('.')[0] end_time = time.mktime(datetime.datetime.strptime( end_time, "%Y-%m-%d %H:%M:%S").timetuple()) end_time = str(end_time).split('.')[0] duration = (hours * 60 * 60) + (minutes * 60) if fixed: form_data = json.dumps({'start': start_time, 'end': end_time, 'comment': comment, 'is_recurring': 'FALSE', 'includeServices': 'TRUE', 'includeChildren': 'FALSE', 'schedule_now': 'FALSE', 'id': uuid, 'type': type_}) else: form_data = json.dumps({'start': start_time, 'end': end_time, 'comment': comment, 'is_recurring': 'FALSE', 'includeServices': 'TRUE', 'includeChildren': 'FALSE', 'schedule_now': 'FALSE', 'id': uuid, 'duration': duration, 'type': type_}) if self.use_autologin is True: self.session.post('{0}/api/downtime?authtoken={1}'.format(self.monitor_url, self.autologin_key), data=form_data) else: self.session.post('{0}/api/downtime'.format(self.monitor_url), data=form_data) def get_start_end(self, host): """ Set default of start time to "now" and end time is "now + 24 hours" :param host: String - Host name """ start = datetime.datetime.now() end = datetime.datetime.now() + datetime.timedelta(hours=24) return str(start.strftime("%Y-%m-%d %H:%M:%S")), str(end.strftime("%Y-%m-%d %H:%M:%S")) def open_monitor(self, host, service=''): """ Open specific Host or Service in monitos 4 browser window :param host: String - Host name :param service: String - Service name """ if service == '': url = '{0}/#/object/details/{1}'.format( self.monitor_url, self.hosts[host].uuid) else: url = '{0}/#/object/details/{1}'.format( self.monitor_url, self.hosts[host].services[service].uuid) webbrowser_open(url) class NoAuth(requests.auth.AuthBase): """ Override to avoid auth headers Needed for LDAP login """ def __call__(self, r): return r Nagstamon-master/Nagstamon/Servers/Multisite.py000066400000000000000000000723431505160700500222170ustar00rootroot00000000000000# encoding: utf-8 import json # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # The initial implementation was contributed to the Nagstamon project # by tribe29 GmbH. import sys import urllib.request, urllib.parse, urllib.error import time import copy import html import tzlocal from datetime import datetime from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.helpers import webbrowser_open from Nagstamon.config import conf class MultisiteError(Exception): def __init__(self, terminate, result): self.terminate = terminate self.result = result class MultisiteServer(GenericServer): """ special treatment for Checkmk Multisite JSON API """ TYPE = 'Checkmk Multisite' # URLs for browser shortlinks/buttons on popup window BROWSER_URLS= { 'monitor': '$MONITOR$', 'hosts': '$MONITOR$/index.py?start_url=view.py?view_name=hostproblems', 'services': '$MONITOR$/index.py?start_url=view.py?view_name=svcproblems', 'history': '$MONITOR$/index.py?start_url=view.py?view_name=events'} def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon - self.urls = {} self.statemap = {} # Entries for monitor default actions in context menu self.MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Downtime'] # flag for newer cookie authentication self.CookieAuth = False def init_HTTP(self): # general initialization if not self.session: GenericServer.init_HTTP(self) # Fix eventually missing tailing '/' in url if self.monitor_url.endswith('/'): self.monitor_url.rstrip('/') # Add /check_mk if not already existent - makes setting URL simpler if not self.monitor_url.endswith('/check_mk'): self.monitor_url += '/check_mk' # Prepare all urls needed by nagstamon if not yet done if len(self.urls) == len(self.statemap): self.urls = { 'api_services': self.monitor_url + '/view.py?view_name={0}&output_format=python&lang=&limit=hard'.\ format(self.checkmk_view_services), 'human_services': self.monitor_url + '/index.py?%s' % \ urllib.parse.urlencode({'start_url': 'view.py?view_name={0}'.\ format(self.checkmk_view_services)}), 'human_service': self.monitor_url + '/index.py?%s' % urllib.parse.urlencode({'start_url': 'view.py?view_name=service'}), 'api_hosts': self.monitor_url + '/view.py?view_name={0}&output_format=python&lang=&limit=hard'.\ format(self.checkmk_view_hosts), 'human_hosts': self.monitor_url + '/index.py?%s' % urllib.parse.urlencode({'start_url': 'view.py?view_name={0}'.\ format(self.checkmk_view_services)}), 'human_host': self.monitor_url + '/index.py?%s' % urllib.parse.urlencode({'start_url': 'view.py?view_name=hoststatus'}), # URLs do not need pythonic output because since werk #0766 API does not work with transid=-1 anymore # thus access to normal webinterface is used 'api_host_act': self.monitor_url + '/view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=hoststatus&filled_in=actions&lang=', 'api_service_act': self.monitor_url + '/view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=service&filled_in=actions&lang=', 'api_svcprob_act': self.monitor_url + '/view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=svcproblems&filled_in=actions&lang=', 'human_events': self.monitor_url + '/index.py?%s' % urllib.parse.urlencode({'start_url': 'view.py?view_name=events'}), 'omd_host_downtime': self.monitor_url + '/api/1.0/domain-types/downtime/collections/host', 'omd_svc_downtime': self.monitor_url + '/api/1.0/domain-types/downtime/collections/service', 'recheck': self.monitor_url + '/ajax_reschedule.py?_ajaxid=0', 'omd_version': self.monitor_url + '/api/1.0/version', 'transid': self.monitor_url + '/view.py?actions=yes&filled_in=actions&host=$HOST$&service=$SERVICE$&view_name=service' } self.statemap = { 'UNREACH': 'UNREACHABLE', 'CRIT': 'CRITICAL', 'WARN': 'WARNING', 'UNKN': 'UNKNOWN', 'PEND': 'PENDING', } # Function overrides for Checkmk 2.3+ version = self._omd_get_version() if version >= [2, 3]: self._set_downtime = self._omd_set_downtime self._set_recheck = self._omd_set_recheck if self.CookieAuth and not self.refresh_authentication: # get cookie to access Checkmk web interface if 'cookies' in dir(self.session): if len(self.session.cookies) == 0: # if no cookie yet login self._get_cookie_login() elif self.session == None: # if no cookie yet login self._get_cookie_login() elif self.CookieAuth and self.refresh_authentication: #if self.session is None: self.session = self.create_session() # force re-auth self._get_cookie_login() def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def _is_auth_in_cookies(self): """ check if there is any valid auth session in cookies which has the name 'auth_' """ if self.session: for cookie in self.session.cookies: if cookie.name.startswith('auth_'): return True return False def _get_url(self, url): result = self.fetch_url(url, 'raw') content, error, status_code = result.result, result.error, result.status_code if error != '' or status_code >= 400: raise MultisiteError(True, Result(result=content, error=error, status_code=status_code)) if content.startswith('WARNING:'): c = content.split('\n') # Print non ERRORS to the log in debug mode self.debug(server=self.get_name(), debug=c[0]) raise MultisiteError(False, Result(result='\n'.join(c[1:]), error=c[0], status_code=status_code)) elif content.startswith('ERROR:'): raise MultisiteError(True, Result(result=content, error=content, status_code=status_code)) # in case of auth problem enable GUI auth part in popup #if self.CookieAuth and self.session is not None: # if not self._is_auth_in_cookies(): # self.refresh_authentication = True # return '' # looks like cookieauth elif content.startswith('<') or\ '' in content: self.CookieAuth = True # if first attempt login and then try to get data again if not self._is_auth_in_cookies(): self._get_cookie_login() result = self.fetch_url(url, 'raw') content, error = result.result, result.error if content.startswith('<') or\ '' in content: return '' # if finally still some is sent this looks like a new login due to password change if content.startswith('<') or\ '' in content: self.refresh_authentication = True return '' return eval(content) def _get_cookie_login(self): """ login on cookie monitor site """ # put all necessary data into url string login_data = {'_username': self.get_username(), '_password': self.get_password(), '_login': '1', '_origtarget' : '', 'filled_in' :' login'} # get cookie from login page via url retrieving as with other urls try: # login and get cookie self.fetch_url(self.monitor_url + '/login.py', cgi_data=login_data, multipart=True) except: self.error(sys.exc_info()) def _get_status(self): """ Get status from Checkmk Server """ ret = Result() # Create URLs for the configured filters url_params = '' if self.force_authuser: url_params += "&force_authuser=1" url_params += '&is_host_acknowledged=-1&is_service_acknowledged=-1' url_params += '&is_host_notifications_enabled=-1&is_service_notifications_enabled=-1' url_params += '&is_host_active_checks_enabled=-1&is_service_active_checks_enabled=-1' url_params += '&host_scheduled_downtime_depth=-1&is_in_downtime=-1' try: response = [] try: response = self._get_url(self.urls['api_hosts'] + url_params) except MultisiteError as e: if e.terminate: return e.result if response == '': return Result(result='', error='Login failed', status_code=401) for row in response[1:]: host= dict(list(zip(copy.deepcopy(response[0]), copy.deepcopy(row)))) n = { 'host': host['host'], 'status': self.statemap.get(host['host_state'], host['host_state']), 'last_check': host['host_check_age'], 'duration': host['host_state_age'], 'status_information': html.unescape(host['host_plugin_output'].replace('\n', ' ')), 'attempt': host['host_attempt'], 'site': host['sitename_plain'], 'address': host['host_address'] } # host objects contain service objects if n['host'] not in self.new_hosts: new_host = n['host'] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n['host'] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n['status'] self.new_hosts[new_host].last_check = n['last_check'] self.new_hosts[new_host].duration = n['duration'] self.new_hosts[new_host].attempt = n['attempt'] self.new_hosts[new_host].status_information= html.unescape(n['status_information'].replace('\n', ' ')) self.new_hosts[new_host].site = n['site'] self.new_hosts[new_host].address = n['address'] # transisition to Checkmk 1.1.10p2 if 'host_in_downtime' in host: if host['host_in_downtime'] == 'yes': self.new_hosts[new_host].scheduled_downtime = True if 'host_acknowledged' in host: if host['host_acknowledged'] == 'yes': self.new_hosts[new_host].acknowledged = True if 'host_notifications_enabled' in host: if host['host_notifications_enabled'] == 'no': self.new_hosts[new_host].notifications_disabled = True # hard/soft state for later filter evaluation real_attempt, max_attempt = self.new_hosts[new_host].attempt.split('/') if real_attempt != max_attempt: self.new_hosts[new_host].status_type = 'soft' else: self.new_hosts[new_host].status_type = 'hard' del response except: import traceback traceback.print_exc(file=sys.stdout) self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # Add filters to the url which should only be applied to the service request if conf.filter_services_on_unreachable_hosts: # thanks to https://github.com/HenriWahl/Nagstamon/issues/510 url_params += '&hst0=On&hst1=On' # services try: response = [] try: response = self._get_url(self.urls['api_services'] + url_params) except MultisiteError as e: if e.terminate: return e.result else: response = copy.deepcopy(e.result.content) ret = copy.deepcopy(e.result) for row in response[1:]: service = dict(list(zip(copy.deepcopy(response[0]), copy.deepcopy(row)))) n = { 'host': service['host'], 'service': service['service_description'], 'status': self.statemap.get(service['service_state'], service['service_state']), 'last_check': service['svc_check_age'], 'duration': service['svc_state_age'], 'attempt': service['svc_attempt'], 'status_information': html.unescape(service['svc_plugin_output'].replace('\n', ' ')), # Checkmk passive services can be re-scheduled by using the Checkmk service 'passiveonly': service['svc_is_active'] == 'no' and not service['svc_check_command'].startswith('check_mk'), 'flapping': service['svc_flapping'] == 'yes', 'site': service['sitename_plain'], 'address': service['host_address'], 'command': service['svc_check_command'], } # host objects contain service objects if n['host'] not in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = 'UP' self.new_hosts[n['host']].site = n['site'] self.new_hosts[n['host']].address = n['address'] # if a service does not exist create its object if n['service'] not in self.new_hosts[n['host']].services: new_service = n['service'] self.new_hosts[n['host']].services[new_service] = GenericService() self.new_hosts[n['host']].services[new_service].host = n['host'] self.new_hosts[n['host']].services[new_service].server = self.name self.new_hosts[n['host']].services[new_service].name = n['service'] self.new_hosts[n['host']].services[new_service].status = n['status'] self.new_hosts[n['host']].services[new_service].last_check = n['last_check'] self.new_hosts[n['host']].services[new_service].duration = n['duration'] self.new_hosts[n['host']].services[new_service].attempt = n['attempt'] self.new_hosts[n['host']].services[new_service].status_information = n['status_information'].strip() self.new_hosts[n['host']].services[new_service].passiveonly = n['passiveonly'] self.new_hosts[n['host']].services[new_service].flapping = n['flapping'] self.new_hosts[n['host']].services[new_service].site = n['site'] self.new_hosts[n['host']].services[new_service].address = n['address'] self.new_hosts[n['host']].services[new_service].command = n['command'] # transition to Checkmk 1.1.10p2 if 'svc_in_downtime' in service: if service['svc_in_downtime'] == 'yes': self.new_hosts[n['host']].services[new_service].scheduled_downtime = True if 'svc_acknowledged' in service: if service['svc_acknowledged'] == 'yes': self.new_hosts[n['host']].services[new_service].acknowledged = True if 'svc_flapping' in service: if service['svc_flapping'] == 'yes': self.new_hosts[n['host']].services[new_service].flapping = True if 'svc_notifications_enabled' in service: if service['svc_notifications_enabled'] == 'no': self.new_hosts[n['host']].services[new_service].notifications_disabled = True if 'host_in_downtime' in service: if service['host_in_downtime'] == 'yes': self.new_hosts[n['host']].scheduled_downtime = True # hard/soft state for later filter evaluation real_attempt, max_attempt = self.new_hosts[n['host']].services[new_service].attempt.split('/') if real_attempt != max_attempt: self.new_hosts[n['host']].services[new_service].status_type = 'soft' else: self.new_hosts[n['host']].services[new_service].status_type = 'hard' del response except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=copy.deepcopy(result), error=copy.deepcopy(error)) del url_params return ret def open_monitor(self, host, service=''): """ open monitor from treeview context menu """ if service == '': url = self.urls['human_host'] + urllib.parse.urlencode({'x': 'site='+self.hosts[host].site+'&host='+host}).replace('x=', '%26') else: url = self.urls['human_service'] + urllib.parse.urlencode({'x': 'site='+self.hosts[host].site+'&host='+host+'&service='+service}).replace('x=', '%26') if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Open host/service monitor web page ' + url) webbrowser_open(url) def get_host(self, host): """ find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios """ # the fastest method is taking hostname as used in monitor if conf.connect_by_host == True or host == '': return Result(result=host) ip = '' try: if host in self.hosts: ip = self.hosts[host].address if conf.debug_mode: self.debug(server=self.get_name(), host=host, debug ='IP of %s:' % (host) + ' ' + ip) if conf.connect_by_dns: try: address = socket.gethostbyaddr(ip)[0] except: address = ip else: address = ip except: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) return Result(result=address) def get_start_end(self, host): return time.strftime('%Y-%m-%d %H:%M'), time.strftime('%Y-%m-%d %H:%M', time.localtime(time.time() + 7200)) def _action(self, site, host, service, specific_params): params = { 'site': self.hosts[host].site, 'host': host, } params.update(specific_params) # decide about service or host url if service != '': url = self.urls['api_service_act'] else: url = self.urls['api_host_act'] # set service params['service'] = service # get current transid transid = self._get_transid(host, service) url = url.replace('?_transid=-1&', '?_transid=%s&' % (transid)) if conf.debug_mode: self.debug(server=self.get_name(), host=host, debug ='Submitting action: ' + url + '&' + urllib.parse.urlencode(params)) # apply action self.fetch_url(url + '&' + urllib.parse.urlencode(params)) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): try: # might be more sophisticated, especially if there is a localized Checkmk web interface from_date, from_time = start_time.split(' ') from_year, from_month, from_day = from_date.split('-') from_hour, from_min = from_time.split(':') to_date, to_time = end_time.split(' ') to_year, to_month, to_day = to_date.split('-') to_hour, to_min = to_time.split(':') # let's try to push downtime info in all variants to server - somewhat holzhammery but well... params = { '_down_comment': author == self.username and comment or '%s: %s' % (author, comment), '_down_flexible': fixed == 0 and 'on' or '', '_down_custom': 'Custom+time+range', '_down_from_date': from_date, '_down_from_time': from_time, '_down_to_date': to_date, '_down_to_time': to_time, '_down_duration': '%s:%s' % (hours, minutes), '_down_from_year': from_year, '_down_from_month': from_month, '_down_from_day': from_day, '_down_from_hour': from_hour, '_down_from_min': from_min, '_down_from_sec': '00', '_down_to_year': to_year, '_down_to_month': to_month, '_down_to_day': to_day, '_down_to_hour': to_hour, '_down_to_min': to_min, '_down_to_sec': '00', 'actions': 'yes' } # service needs extra parameter if service: params['_do_confirm_service_downtime'] = 'Schedule+downtime+for+1+service' self._action(self.hosts[host].site, host, service, params) except: if conf.debug_mode: self.debug(server=self.get_name(), host=host, debug='Invalid start/end date/time given') def _omd_set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ _set_downtime function for Checkmk version 2.3+ """ try: # Headers required for Checkmk API headers = { "Authorization": f"Bearer {self.username} {self.password}", "Content-Type": "application/json", "Accept": "application/json", } # Only timezone aware dates are allowed iso_start_time = datetime.strptime(start_time, "%Y-%m-%d %H:%M").replace(tzinfo=tzlocal.get_localzone()).isoformat() iso_end_time = datetime.strptime(end_time, "%Y-%m-%d %H:%M").replace(tzinfo=tzlocal.get_localzone()).isoformat() # Set parameters for host downtimes url = self.urls["omd_host_downtime"] params = { "start_time": iso_start_time, "end_time": iso_end_time, "comment": author == self.username and comment or "%s: %s" % (author, comment), "downtime_type": "host", "host_name": host, } # Downtime type is "flexible" if "duration" is set if fixed == 0: params['duration'] = hours * 60 + minutes # Parameter overrides for service downtimes if service: url = self.urls['omd_svc_downtime'] params['downtime_type'] = 'service' params['service_descriptions'] = [service] self.fetch_url(url, headers=headers, cgi_data=json.dumps(params)) except Exception as error: if conf.debug_mode: self.debug(server=self.get_name(), host=host, debug='Invalid start/end date/time given') def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): p = { '_acknowledge': 'Acknowledge', '_ack_sticky': sticky == 1 and 'on' or '', '_ack_notify': notify == 1 and 'on' or '', '_ack_persistent': persistent == 1 and 'on' or '', '_ack_comment': author == self.username and comment or '%s: %s' % (author, comment) } self._action(self.hosts[host].site, host, service, p) # acknowledge all services on a host when told to do so if all_services: for s in all_services: self._action(self.hosts[host].site, host, s, p) def _set_recheck(self, host, service): p = { '_resched_checks': 'Reschedule active checks', '_resched_pread': '0' } self._action(self.hosts[host].site, host, service, p) def _omd_set_recheck(self, host, service): """ _set_recheck function for Checkmk version 2.3+ """ csrf_token = self._get_csrf_token(host, service) data = { "site": self.hosts[host].site, "host": host, "service": service, "wait_svc": service, "csrf_token": csrf_token, } self.fetch_url(self.urls["recheck"], cgi_data=data) def recheck_all(self): """ special method for Checkmk as there is one URL for rescheduling all problems to be checked """ params = dict() params['_resched_checks'] = 'Reschedule active checks' url = self.urls['api_svcprob_act'] if conf.debug_mode: self.debug(server=self.get_name(), debug ='Rechecking all action: ' + url + '&' + urllib.parse.urlencode(params)) result = self.fetch_url(url + '&' + urllib.parse.urlencode(params), giveback ='raw') def _get_transid(self, host, service): """ get transid for an action """ # since Checkmk 2.0 it seems to be a problem if service is empty so fill it with a definitively existing one if not service: service = 'PING' transid = self.fetch_url(self.urls['transid'].replace('$HOST$', host).replace('$SERVICE$', service.replace(' ', '+')), 'obj').result.find(attrs={'name' : '_transid'})['value'] return transid def _get_csrf_token(self, host, service): """ get csrf token for the session """ # since Checkmk 2.0 it seems to be a problem if service is empty so fill it with a definitively existing one if not service: service = "PING" csrf_token = self.fetch_url(self.urls["transid"].replace("$HOST$", host).replace("$SERVICE$", service.replace(" ", "+")), "obj").result.find(attrs={"name": ["csrf_token", "_csrf_token"]})["value"] return csrf_token def _omd_get_version(self): """ get version of OMD Checkmk as [major_version, minor_version] """ try: version = [int(x) for x in self.fetch_url(self.urls['omd_version'], giveback='json', no_auth=True).result['versions']['checkmk'].split('.')[:2]] # If /version api is not supported, return the lowest non-negative pair except: version = [0, 0] return version Nagstamon-master/Nagstamon/Servers/Nagios.py000066400000000000000000000022171505160700500214510ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.Servers.Generic import GenericServer class NagiosServer(GenericServer): """ object of Nagios server - when nagstamon will be able to poll various servers this will be useful As Nagios is the default server type all its methods are in GenericServer """ TYPE = u'Nagios'Nagstamon-master/Nagstamon/Servers/Opsview.py000066400000000000000000000333551505160700500216740ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # Based on https://github.com/duncs/Nagstamon by @duncs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import urllib.request, urllib.parse, urllib.error import copy import pprint import re import json from datetime import datetime, timedelta from ast import literal_eval from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.helpers import (human_readable_duration_from_seconds, webbrowser_open) class OpsviewService(GenericService): """ add Opsview specific service property to generic service class """ service_object_id = "" class OpsviewServer(GenericServer): """ special treatment for Opsview XML based API """ TYPE = 'Opsview' # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ["comment"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS= {'monitor': '$MONITOR$/monitoring', 'hosts': '$MONITOR$/monitoring/#!/allproblems', 'services': '$MONITOR$/monitoring/#!/allproblems', 'history': '$MONITOR$/monitoring/#!/events'} def init_HTTP(self): """ things to do if HTTP is not initialized """ GenericServer.init_HTTP(self) # prepare for JSON self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) # get cookie to access Opsview web interface to access Opsviews Nagios part if len(self.session.cookies) == 0: if conf.debug_mode: self.debug(server=self.get_name(), debug="Fetching Login token") logindata = json.dumps({'username': self.get_username(), 'password': self.get_password()}) # the following is necessary for Opsview servers # get cookie from login page via url retrieving as with other urls try: # login and get cookie resp = literal_eval(self.fetch_url(self.monitor_url + "/rest/login", giveback='raw', cgi_data=logindata).result) if conf.debug_mode: self.debug(server=self.get_name(), debug="Login Token: " + resp.get('token')) self.session.headers.update({'X-Opsview-Username': self.get_username(), 'X-Opsview-Token':resp.get('token')}) except: self.error(sys.exc_info()) def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def get_start_end(self, host): """ Set a default of starttime of "now" and endtime is "now + 24 hours" directly from web interface """ start = datetime.now() end = datetime.now() + timedelta(hours=24) return str(start.strftime("%Y-%m-%d %H:%M:%S")), str(end.strftime("%Y-%m-%d %H:%M:%S")) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): url = self.monitor_url + "/rest/downtime?" data = dict(); data["comment"]=str(comment) data["starttime"]=start_time data["endtime"]=end_time if service == "": data["hst.hostname"]=str(host) if service != "": data["svc.hostname"]=str(host) data["svc.servicename"]=str(service) cgi_data = urllib.parse.urlencode(data) self.debug(server=self.get_name(), debug="Downtime url: " + url) self.fetch_url(url + cgi_data, giveback="raw", cgi_data=({ })) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): """ worker for submitting check result """ url = self.monitor_url + "/rest/status?" data = dict(); data["comment"]=str(comment) data["new_state"]=({"ok":0,"warning":1,"critical":2,"unknown":3})[state] if service == "": data["hst.hostname"]=str(host) if service != "": data["svc.hostname"]=str(host) data["svc.servicename"]=str(service) cgi_data = urllib.parse.urlencode(data) self.debug(server=self.get_name(), debug="Submit result url: " + url) self.fetch_url(url + cgi_data, giveback="raw", cgi_data=({ })) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): """ Sumit acknowledgement for host or service """ url = self.monitor_url + "/rest/acknowledge?" data=dict(); data["notify"]=str(notify) data["sticky"]=str(sticky) data["comment"]=str(comment) data["host"]=str(host) if service != "": data["servicecheck"]=str(service) cgi_data = urllib.parse.urlencode(data) self.debug(server=self.get_name(), debug="ACK url: " + url) self.fetch_url(url + cgi_data, giveback="raw", cgi_data=({ })) def _set_recheck(self, host, service): """ Sumit recheck request for host or service """ url = self.monitor_url + "/rest/recheck?" data=dict(); data["host"]=str(host) if service != "": data["servicecheck"]=str(service) cgi_data = urllib.parse.urlencode(data) self.debug(server=self.get_name(), debug="Recheck url: " + url) self.fetch_url(url + cgi_data, giveback="raw", cgi_data=({ })) def _get_status(self): """ Get status from Opsview Server """ if self.can_change_only: if conf.debug_mode: self.debug(server=self.get_name(), debug="Showing only objects that the user can change or put in downtime") can_change = '&can_change=true' else: can_change = '' if self.hashtag_filter != '': if conf.debug_mode: self.debug(server=self.get_name(), debug="Raw hashtag filter string: " + self.hashtag_filter) trimmed_hashtags = re.sub(r'[#\s]', '', self.hashtag_filter).split(",") list_of_non_empty_hashtags = [i for i in trimmed_hashtags if i] if conf.debug_mode: self.debug(server=self.get_name(), debug="List of trimmed hashtags" + pprint.pformat(list_of_non_empty_hashtags)) keywords = "&keyword=" + "&keyword=".join(list_of_non_empty_hashtags) if conf.debug_mode: self.debug(server=self.get_name(), debug="Keyword string" + pprint.pformat(keywords)) else: keywords = '' # following XXXX to get ALL services in ALL states except OK # because we filter them out later # the REST API gets all host and service info in one call try: result = self.fetch_url(self.monitor_url + "/rest/status/service?state=1&state=2&state=3" + can_change + keywords, giveback="raw") data, error, status_code = json.loads(result.result), result.error, result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured if conf.debug_mode: self.debug(server=self.get_name(), debug="Fetched JSON: " + pprint.pformat(data)) for host in data["list"]: self.new_hosts[host["name"]] = GenericHost() self.new_hosts[host["name"]].name = str(host["name"]) self.new_hosts[host["name"]].server = self.name # states come in lower case from Opsview self.new_hosts[host["name"]].status = str(host["state"].upper()) self.new_hosts[host["name"]].status_type = str(host["state_type"]) self.new_hosts[host["name"]].last_check = datetime.fromtimestamp(int(host["last_check"])).strftime("%Y-%m-%d %H:%M:%S %z") self.new_hosts[host["name"]].duration = human_readable_duration_from_seconds(host["state_duration"]) self.new_hosts[host["name"]].attempt = host["current_check_attempt"]+ "/" + host["max_check_attempts"] self.new_hosts[host["name"]].status_information = host["output"].replace("\n", " ") # if host is in downtime add it to known maintained hosts if host['downtime'] != "0": self.new_hosts[host["name"]].scheduled_downtime = True #if host.has_key("acknowledged"): if 'acknowledged' in host: self.new_hosts[host["name"]].acknowledged = True #if host.has_key("flapping"): if 'flapping' in host: self.new_hosts[host["name"]].flapping = True #services for service in host["services"]: self.new_hosts[host["name"]].services[service["name"]] = OpsviewService() self.new_hosts[host["name"]].services[service["name"]].host = str(host["name"]) self.new_hosts[host["name"]].services[service["name"]].name = service["name"] self.new_hosts[host["name"]].services[service["name"]].server = self.name # states come in lower case from Opsview self.new_hosts[host["name"]].services[service["name"]].status = service["state"].upper() self.new_hosts[host["name"]].services[service["name"]].status_type = service["state_type"] self.new_hosts[host["name"]].services[service["name"]].last_check = datetime.fromtimestamp(int(service["last_check"])).strftime("%Y-%m-%d %H:%M:%S %z") self.new_hosts[host["name"]].services[service["name"]].duration = human_readable_duration_from_seconds(service["state_duration"]) self.new_hosts[host["name"]].services[service["name"]].attempt = service["current_check_attempt"]+ "/" + service["max_check_attempts"] self.new_hosts[host["name"]].services[service["name"]].status_information= service["output"].replace("\n", " ") if service['downtime'] != '0': self.new_hosts[host["name"]].services[service["name"]].scheduled_downtime = True #if service.has_key("acknowledged"): if 'acknowledged' in service: self.new_hosts[host["name"]].services[service["name"]].acknowledged = True #f service.has_key("flapping"): if 'flapping' in service: self.new_hosts[host["name"]].services[service["name"]].flapping = True # extra opsview id for service, needed for submitting check results self.new_hosts[host["name"]].services[service["name"]].service_object_id = service["service_object_id"] except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) #dummy return in case all is OK return Result() def open_monitor(self, host, service=''): ''' open monitor from tablewidget context menu ''' base_url = self.monitor_url + '/monitoring/#!?' host_url = base_url + urllib.parse.urlencode({'autoSelectHost': host}) service_url = base_url + urllib.parse.urlencode({'autoSelectHost': host, 'autoSelectService': service}, quote_via=urllib.parse.quote) if service == '': if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Open host monitor web page ' + host_url) webbrowser_open(host_url) else: self.debug(server=self.get_name(), host=host, service=service, debug='Open service monitor web page ' + service_url) webbrowser_open(service_url) def open_monitor_webpage(self): webbrowser_open('%s/monitoring/' % (self.monitor_url)) Nagstamon-master/Nagstamon/Servers/Prometheus.py000066400000000000000000000177311505160700500223730ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Stephan Schwarz (@stearz) # # This Server class connects against Prometheus. # The monitor URL in the setup should be something like # http://prometheus.example.com:9090 # # Release Notes: # # [1.1.0] - 2020-06-12: # * fixed: # Some more errors with unset fields from Prometheus # * added: # Feature for configuring which labels get mapped to servicename and hostname... # ...and which annotations get mapped to status_information # # [1.0.2] - 2020-06-07: # * fixed: # Missing message field in alert stopped integration from working # Alerts with an unknown severity were not shown # # [1.0.1] - 2020-05-13: # * fixed: # Nagstamon crashes due to missing url handling # # [1.0.0] - 2020-04-20: # * added: # Inital version # import sys import urllib.request import urllib.parse import urllib.error import pprint import json from datetime import datetime, timedelta, timezone import dateutil.parser from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.helpers import webbrowser_open class PrometheusService(GenericService): """ add Prometheus specific service property to generic service class """ service_object_id = "" labels = {} class PrometheusServer(GenericServer): """ special treatment for Prometheus API """ TYPE = 'Prometheus' # Prometheus actions are limited to visiting the monitor for now MENU_ACTIONS = ['Monitor'] BROWSER_URLS = { 'monitor': '$MONITOR$/alerts', 'hosts': '$MONITOR$/targets', 'services': '$MONITOR$/service-discovery', 'history': '$MONITOR$/graph' } API_PATH_ALERTS = "/api/v1/alerts" def init_HTTP(self): """ things to do if HTTP is not initialized """ GenericServer.init_HTTP(self) # prepare for JSON self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json'}) def init_config(self): """ dummy init_config, called at thread start """ pass def get_start_end(self, host): """ Set a default of starttime of "now" and endtime is "now + 24 hours" directly from web interface """ start = datetime.now() end = datetime.now() + timedelta(hours=24) return (str(start.strftime("%Y-%m-%d %H:%M:%S")), str(end.strftime("%Y-%m-%d %H:%M:%S"))) def _get_duration(self, timestring): """ calculates the duration (delta) from Prometheus' activeAt (ISO8601 format) until now an returns a human friendly string """ time_object = dateutil.parser.parse(timestring) duration = datetime.now(timezone.utc) - time_object h = int(duration.seconds / 3600) m = int(duration.seconds % 3600 / 60) s = int(duration.seconds % 60) if duration.days > 0: return "%sd %sh %02dm %02ds" % (duration.days, h, m, s) elif h > 0: return "%sh %02dm %02ds" % (h, m, s) elif m > 0: return "%02dm %02ds" % (m, s) else: return "%02ds" % (s) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ to be implemented in a future release """ pass def _get_status(self): """ Get status from Prometheus Server """ # get all alerts from the API server try: result = self.fetch_url(self.monitor_url + self.API_PATH_ALERTS, giveback="raw") data = json.loads(result.result) error = result.error status_code = result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) if errors_occured is not None: return errors_occured if conf.debug_mode: self.debug(server=self.get_name(), debug="Fetched JSON: " + pprint.pformat(data)) for alert in data["data"]["alerts"]: if conf.debug_mode: self.debug( server=self.get_name(), debug="Processing Alert: " + pprint.pformat(alert) ) labels = alert.get("labels", {}) # skip alerts with none severity severity = labels.get("severity", "UNKNOWN").upper() if severity == "NONE": continue hostname = "unknown" for host_label in self.map_to_hostname.split(','): if host_label in labels: hostname = labels.get(host_label) break servicename = "unknown" for service_label in self.map_to_servicename.split(','): if service_label in labels: servicename = labels.get(service_label) break service = PrometheusService() service.host = str(hostname) service.name = servicename service.server = self.name service.status = severity service.last_check = "n/a" service.attempt = alert.get("state", "firirng") service.duration = str(self._get_duration(alert["activeAt"])) annotations = alert.get("annotations", {}) status_information = "" for status_information_label in self.map_to_status_information.split(','): if status_information_label in annotations: status_information = annotations.get(status_information_label) break service.status_information = status_information if hostname not in self.new_hosts: self.new_hosts[hostname] = GenericHost() self.new_hosts[hostname].name = str(hostname) self.new_hosts[hostname].server = self.name self.new_hosts[hostname].services[servicename] = service except: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # dummy return in case all is OK return Result() def open_monitor_webpage(self, host, service): """ open monitor from tablewidget context menu """ webbrowser_open('%s' % (self.monitor_url)) def open_monitor(self, host, service=''): """ open monitor for alert """ url = '%s/graph?g0.range_input=1h&g0.expr=%s' url = url % (self.monitor_url, urllib.parse.quote('ALERTS{alertname="%s"}' % service)) webbrowser_open(url) Nagstamon-master/Nagstamon/Servers/Sensu.py000066400000000000000000000237361505160700500213370ustar00rootroot00000000000000# adapted from the Zenoss.py code # Copyright (C) 2017 Vyron Tsingaras interworks.cloud # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from datetime import timezone, datetime import sys import traceback from requests.structures import CaseInsensitiveDict from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.thirdparty.sensu_api import SensuAPI, SensuAPIException from Nagstamon.helpers import human_readable_duration_from_timestamp, webbrowser_open class SensuServer(GenericServer): TYPE = 'Sensu' SEVERITY_CODE_TEXT_MAP = dict() SEVERITY_STATUS_TEXT_MAP = CaseInsensitiveDict() MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] sensu_api = None authentication = 'basic' api_url = '' uchiwa_url = '' uchiwa_datacenter = '' username = '' password = '' def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon self.urls = {} self.statemap = {} self.api_url = conf.servers[self.get_name()].monitor_cgi_url self.uchiwa_url = conf.servers[self.get_name()].monitor_url self.uchiwa_datacenter = conf.servers[self.get_name()].monitor_site self.username = conf.servers[self.get_name()].username self.password = conf.servers[self.get_name()].password self.ignore_cert = conf.servers[self.get_name()].ignore_cert self.custom_cert_use = conf.servers[self.get_name()].custom_cert_use self.custom_cert_ca_file = conf.servers[self.get_name()].custom_cert_ca_file self.BROWSER_URLS = { 'monitor': '$MONITOR$', 'hosts': '$MONITOR$/#/clients', 'services': '$MONITOR$/#/checks', 'history': '$MONITOR$/#/clients' } self.SEVERITY_CODE_TEXT_MAP = { 0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN' } # SEVERITY_STATUS_TEXT_MAP is a Case-Insensitive dict self.SEVERITY_STATUS_TEXT_MAP['OK'] = 0 self.SEVERITY_STATUS_TEXT_MAP['WARNING'] = 1 self.SEVERITY_STATUS_TEXT_MAP['CRITICAL'] = 2 self.SEVERITY_STATUS_TEXT_MAP['UNKNOWN'] = 3 def init_HTTP(self): """ things to do if HTTP is not initialized """ GenericServer.init_HTTP(self) if self.custom_cert_use: verify = self.custom_cert_ca_file else: verify = not self.ignore_cert try: self.sensu_api = SensuAPI( self.api_url, username=self.username, password=self.password, verify=verify ) except SensuAPIException: self.error(sys.exc_info()) def _insert_service_to_hosts(self, service: GenericService): service_host = service.get_host_name() if service_host not in self.new_hosts: self.new_hosts[service_host] = GenericHost() self.new_hosts[service_host].name = service_host self.new_hosts[service_host].site = service.site self.new_hosts[service_host].services[service.name] = service @staticmethod def _aslocaltimestr(utc_dt): local_dt = utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) return local_dt.strftime('%Y-%m-%d %H:%M:%S') def _get_status(self): self.new_hosts = dict() try: events = self._get_all_events() for event in events: event_check = event['check'] event_client = event['client'] new_service = GenericService() new_service.event_id = event['id'] new_service.host = event_client['name'] new_service.name = event_check['name'] # Uchiwa needs the 'dc' for re_check; Sensu does not if 'dc' in event: new_service.site = event['dc'] else: new_service.site = '' new_service.status = '' try: new_service.status = self.SEVERITY_CODE_TEXT_MAP[event_check['status']] except KeyError: new_service.status = 'UNKNOWN' last_check_time = datetime.utcfromtimestamp(int(event['timestamp'])) new_service.last_check = self._aslocaltimestr(last_check_time) new_service.duration = human_readable_duration_from_timestamp(int(event['last_state_change'])) new_service.status_information = event_check['output'] # needs a / with a number on either side to work new_service.attempt = str(event['occurrences']) + '/1' new_service.passiveonly = False new_service.notifications_disabled = event['silenced'] new_service.flapping = False new_service.acknowledged = event['silenced'] new_service.scheduled_downtime = False self._insert_service_to_hosts(new_service) except: self.isChecking = False result, error = self.error(sys.exc_info()) print(traceback.format_exc()) return Result(result=result, error=error) return Result(error="") def get_username(self): return str(self.username) def get_password(self): return str(self.password) def set_acknowledge(self, info_dict): subscription = self._format_client_subscription(info_dict['host']) try: silenece_args = { 'check': info_dict['service'], 'subscription': subscription, 'reason': info_dict['comment'], 'creator': info_dict['author'], 'expire_on_resolve': True } self.sensu_api.post_silence_request(silenece_args) except SensuAPIException as e: pass @staticmethod def _format_client_subscription(client: str): return 'client:' + client def _acknowledge_client_check(self, client: str, check: str, comment: str = None, author: str = None): subscription = self._format_client_subscription(client) try: silenece_args = { 'check': check, 'subscription': subscription, 'reason': comment, 'creator': author, 'expire_on_resolve': True } self.sensu_api.post_silence_request(silenece_args) except SensuAPIException as e: pass def _get_all_events(self): events = self.sensu_api.get_events() return events def _get_event_duration_string(self, start_seconds: int, end_seconds: int): sec = end_seconds - start_seconds days, rem = divmod(sec, 60 * 60 * 24) hours, rem = divmod(rem, 60 * 60) mins, sec = divmod(rem, 60) if days == 0 and hours == 0 and mins == 0 and sec == 0: return None return '%sd %sh %sm %ss' % (days, hours, mins, sec) def set_recheck(self, info_dict): if info_dict['service'] == 'keepalive': if conf.debug_mode: self.debug(server=self.name, debug='Keepalive results must come from the client running on host {0}, unable to recheck'.format(info_dict['host'])) else: standalone = self.sensu_api.get_event(info_dict['host'], info_dict['service'])['check']['standalone'] if standalone: if conf.debug_mode: self.debug(server=self.name, debug='Service {0} on host {1} is a standalone service, will not recheck'.format(info_dict['service'], info_dict['host'])) else: self.sensu_api.post_check_request( info_dict['service'], self._format_client_subscription(info_dict['host']), self.hosts[info_dict['host']].site ) def set_downtime(self, info_dict): subscription = self._format_client_subscription(info_dict['host']) silence_args = { 'check': info_dict['service'], 'subscription': subscription, 'reason': info_dict['comment'], 'creator': info_dict['author'], 'expire': int(info_dict['hours']) * 3600 + int(info_dict['minutes']) * 60, 'expire_on_resolve': False } self.sensu_api.post_silence_request(silence_args) def set_submit_check_result(self, info_dict): sensu_status = None try: sensu_status = self.SEVERITY_STATUS_TEXT_MAP[info_dict['state']] except KeyError: sensu_status = 3 # 3 stands for UNKNOWN, anything other than 0,1,2 is UNKNOWN to Sensu self.sensu_api.post_result_data(info_dict['host'], info_dict['service'], info_dict['check_output'], sensu_status) def get_start_end(self, host): return 'Not Supported!', 'Not Supported!' def open_monitor(self, host, service=''): ''' open monitor from tablewidget context menu ''' detail_url = self.monitor_url + '/#/client/' + self.uchiwa_datacenter + '/' + host if service != '': detail_url += '?check=' + service webbrowser_open(detail_url) Nagstamon-master/Nagstamon/Servers/SensuGo.py000066400000000000000000000103251505160700500216130ustar00rootroot00000000000000import sys import traceback from Nagstamon.Servers.Generic import GenericServer from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.config import conf from Nagstamon.thirdparty.sensugo_api import SensuGoAPI, SensuGoAPIException from Nagstamon.helpers import human_readable_duration_from_timestamp from time import time from datetime import datetime NAMESPACE_SEPARATOR = ' ||| ' class SensuGoServer(GenericServer): TYPE = 'SensuGo' MENU_ACTIONS = ['Acknowledge'] _authentication = 'basic' _api_url = '' _sensugo_api = None def __init__(self, **kwds): GenericServer.__init__(self, **kwds) self._api_url = conf.servers[self.get_name()].monitor_cgi_url self.reset_HTTP() def init_HTTP(self): GenericServer.init_HTTP(self) self._setup_sensugo_api() def reset_HTTP(self): self._sensugo_api = SensuGoAPI(self._api_url) def _setup_sensugo_api(self): if not self._sensugo_api.has_acquired_token(): if self.custom_cert_use: verify = self.custom_cert_ca_file else: verify = not self.ignore_cert try: self._sensugo_api.auth(self.username, self.password, verify) except Exception: self.error(sys.exc_info()) def _get_status(self): try: response_code, events = self._sensugo_api.get_all_events() self._create_services(events) except Exception: result, error = self.error(sys.exc_info()) print(traceback.format_exc()) return Result(result=result, error=error) return Result() def _create_services(self, events): for event in events: service = self._parse_event_to_service(event) self._insert_service_to_hosts(service) def _parse_event_to_service(self, event): service = GenericService() namespace_host = event['entity']['metadata']['namespace'] + NAMESPACE_SEPARATOR + event['entity']['metadata']['name'] service.hostid = namespace_host service.host = namespace_host service.name = event['check']['metadata']['name'] service.status = SensuGoAPI.parse_check_status(event['check']['status']) service.last_check = datetime.fromtimestamp(int(event['timestamp'])).strftime('%Y-%m-%d %H:%M:%S') service.duration = self._duration_since(event['check']['last_ok']) service.status_information = event['check']['output'] service.acknowledged = event['check']['is_silenced'] service.notifications_disabled = event['check']['is_silenced'] service.attempt = str(event['check']['occurrences']) + '/1' service.passiveonly = event['check']['publish'] service.flapping = False service.scheduled_downtime = False return service def _insert_service_to_hosts(self, service: GenericService): service_host = service.get_host_name() if service_host not in self.new_hosts: self.new_hosts[service_host] = GenericHost() self.new_hosts[service_host].name = service_host self.new_hosts[service_host].site = service.site self.new_hosts[service_host].services[service.name] = service def _duration_since(self, timestamp): if (timestamp == 0) or (timestamp > time()): duration_text = 'n/a' else: duration_text = human_readable_duration_from_timestamp(timestamp) return duration_text def set_acknowledge(self, info_dict): namespace = self._extract_namespace(info_dict['host']) silenece_args = { 'metadata': { 'name': info_dict['service'], 'namespace': namespace }, 'expire': -1, 'expire_on_resolve': True, 'creator': info_dict['author'], 'reason': info_dict['comment'], 'check': info_dict['service'] } self._sensugo_api.create_or_update_silence(silenece_args) def _extract_namespace(self, host_column: str): return host_column.split(NAMESPACE_SEPARATOR)[0] Nagstamon-master/Nagstamon/Servers/SnagView3.py000066400000000000000000000543561505160700500220520ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Initial implementation by Marcus MÃļnnig # # This Server class connects against SNAG-View 3. # # Status/TODOs: # import copy import datetime import json import logging import sys import time import requests from bs4 import BeautifulSoup from Nagstamon.helpers import webbrowser_open from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer log = logging.getLogger('SNAG-View 3') log.setLevel('INFO') def strfdelta(tdelta, fmt): d = {'days': tdelta.days} d['hours'], rem = divmod(tdelta.seconds, 3600) d['minutes'], d['seconds'] = divmod(rem, 60) return fmt.format(**d) class SnagViewServer(GenericServer): """ object of SNAG-View 3 server """ TYPE = 'SNAG-View 3' MENU_ACTIONS = [ 'Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime' ] STATES_MAPPING = { 'hosts': { 0: 'UP', 1: 'DOWN', 2: 'UNREACHABLE', 4: 'PENDING' }, 'services': { 0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN', 4: 'PENDING' } } STATES_MAPPING_REV = { 'hosts': { 'UP': 0, 'DOWN': 1, 'UNREACHABLE': 2, 'PENDING': 4 }, 'services': { 'OK': 0, 'WARNING': 1, 'CRITICAL': 2, 'UNKNOWN': 3, 'PENDING': 4 } } BROWSER_URLS = { 'monitor': '$MONITOR$', 'hosts': '$MONITOR$', 'services': '$MONITOR$', 'history': '$MONITOR$/#/alert/ticker' } def init_config(self): """ Set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None def init_HTTP(self): """ Initializing of session object """ GenericServer.init_HTTP(self) self.session.auth = NoAuth() if len(self.session.cookies) == 0: form_inputs = dict() if self.username.startswith('ldap:'): form_inputs['module'] = 'ldap' form_inputs['_username'] = self.username[5:] else: form_inputs['module'] = 'sv' form_inputs['_username'] = self.username form_inputs['urm:login:client'] = '' form_inputs['_password'] = self.password # call login page to get temporary cookie self.fetch_url('{0}/security/login'.format(self.monitor_url)) # submit login form to retrieve authentication cookie self.fetch_url( '{0}/security/login_check'.format(self.monitor_url), cgi_data=form_inputs, multipart=True ) def _get_status(self): """ Get status from SNAG-View 3 Server - only JSON """ # define CGI URLs for hosts and services if self.cgiurl_hosts == None: # hosts (up, down, unreachable or pending) self.cgiurl_hosts = self.monitor_cgi_url + '/rest/private/nagios/host' if self.cgiurl_services == None: # services (warning, critical, unknown or pending) self.cgiurl_services = self.monitor_cgi_url + \ '/rest/private/nagios/service_status/browser' self.new_hosts = dict() # hosts try: form_data = dict() form_data['acknowledged'] = 1 form_data['downtime'] = 1 form_data['inactiveHosts'] = 0 form_data['disabledNotification'] = 1 form_data['limit_start'] = 0 # Get all hosts form_data['limit_length'] = 99999 result = self.fetch_url( self.cgiurl_hosts, giveback='raw', cgi_data=form_data) # authentication errors get a status code 200 too if result.status_code < 400 and \ result.result.startswith('<'): # in case of auth error reset HTTP session and try again self.reset_HTTP() result = self.fetch_url( self.cgiurl_hosts, giveback='raw', cgi_data=form_data) if result.status_code < 400 and \ result.result.startswith('<'): self.refresh_authentication = True return Result(result=result.result, error='Authentication error', status_code=result.status_code) # purify JSON result jsonraw = copy.deepcopy(result.result.replace('\n', '')) error = copy.deepcopy(result.error) status_code = result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) self.check_for_error(jsonraw, error, status_code) hosts = json.loads(jsonraw) for host in hosts['data']: h = dict(host) # Skip if Host is 'Pending' if int(h['sv_host__nagios_status__current_state']) == 4: continue # host host_name = h['sv_host__nagios__host_name'] # If a host does not exist, create its object if host_name not in self.new_hosts: self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].svid = h['sv_host__svobjects____SVID'] self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][int( h['sv_host__nagios_status__current_state'] or 4)] self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp( int(h['sv_host__nagios_status__last_check'])) self.new_hosts[host_name].attempt = h['sv_host__nagios__max_check_attempts'] self.new_hosts[host_name].status_information = h['sv_host__nagios_status__plugin_output'] self.new_hosts[host_name].passiveonly = not ( bool(h['sv_host__nagios_status__checks_enabled'] or False)) self.new_hosts[host_name].notifications_disabled = not ( bool(h['sv_host__nagios_status__notifications_enabled'] or False)) self.new_hosts[host_name].flapping = bool( h['sv_host__nagios_status__is_flapping'] or False) self.new_hosts[host_name].acknowledged = bool( h['sv_host__nagios_status__problem_has_been_acknowledged'] or False) self.new_hosts[host_name].scheduled_downtime = bool( h['sv_host__nagios_status__scheduled_downtime_depth'] or False) self.new_hosts[host_name].status_type = 'soft' if int( h['sv_host__nagios_status__state_type'] or 0) == 0 else 'hard' # extra duration needed for calculation duration = datetime.datetime.now( ) - datetime.datetime.fromtimestamp(int(h['sv_host__nagios_status__last_state_change'])) self.new_hosts[host_name].duration = strfdelta( duration, '{days}d {hours}h {minutes}m {seconds}s') del h, host_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: form_data = dict() form_data['acknowledged'] = 1 form_data['downtime'] = 1 form_data['inactiveHosts'] = 0 form_data['disabledNotification'] = 1 form_data['softstate'] = 1 form_data['limit_start'] = 0 # Get all services form_data['limit_length'] = 99999 result = self.fetch_url(self.cgiurl_services, giveback='raw', cgi_data=form_data) # purify JSON result jsonraw = copy.deepcopy(result.result.replace('\n', '')) error = copy.deepcopy(result.error) status_code = result.status_code if error != '' or status_code >= 400: return Result(result=jsonraw, error=error, status_code=status_code) self.check_for_error(jsonraw, error, status_code) services = json.loads(jsonraw) for service in services['data']: s = dict(service) # Skip if Host or Service is 'Pending' if int(s['sv_service_status__nagios_status__current_state']) == 4 or int( s['sv_host__nagios_status__current_state']) == 4: continue # host and service host_name = s['sv_host__nagios__host_name'] service_name = s['sv_service_status__svobjects__rendered_label'] # If a service does not exist, create its object if service_name not in self.new_hosts[host_name].services: self.new_hosts[host_name].services[service_name] = GenericService( ) self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].svid = s[ 'sv_service_status__svobjects____SVID'] self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][int( s['sv_service_status__nagios_status__current_state'] or 4)] self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp( int(s['sv_service_status__nagios_status__last_check'])) self.new_hosts[host_name].services[service_name].attempt = s[ 'sv_service_status__nagios__max_check_attempts'] self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup( s['sv_service_status__nagios_status__plugin_output'].replace( '\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].services[service_name].passiveonly = not ( bool(s['sv_service_status__nagios_status__checks_enabled'] or False)) self.new_hosts[host_name].services[service_name].notifications_disabled = not ( bool(s['sv_service_status__nagios_status__notifications_enabled'] or False)) self.new_hosts[host_name].services[service_name].flapping = bool( s['sv_service_status__nagios_status__is_flapping'] or False) self.new_hosts[host_name].services[service_name].acknowledged = bool( s['sv_service_status__nagios_status__problem_has_been_acknowledged'] or False) self.new_hosts[host_name].services[service_name].scheduled_downtime = bool( s['sv_service_status__nagios_status__scheduled_downtime_depth'] or False) self.new_hosts[host_name].services[service_name].status_type = 'soft' if int( s['sv_service_status__nagios_status__state_type'] or 0) == 0 else 'hard' # acknowledge needs service_description and no display name self.new_hosts[host_name].services[service_name].real_name = s[ 'sv_service_status__nagios__service_description'] # extra duration needed for calculation duration = datetime.datetime.now( ) - datetime.datetime.fromtimestamp( int(s['sv_service_status__nagios_status__last_state_change'])) self.new_hosts[host_name].services[service_name].duration = strfdelta( duration, '{days}d {hours}h {minutes}m {seconds}s') del s, host_name, service_name except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) del jsonraw, error, hosts # dummy return in case all is OK return Result() def _set_recheck(self, host, service): """ Do a POST-Request to recheck the given host or service in SNAG-View 3 :param host: String - Host name :param service: String - Service name """ form_data = dict() form_data['commandName'] = 'check-now' if service == '': form_data['params'] = json.dumps({'__SVID': self.hosts[host].svid}) form_data['commandType'] = 'sv_host' else: form_data['params'] = json.dumps( {'__SVID': self.hosts[host].services[service].svid}) form_data['commandType'] = 'sv_service_status' self.session.post( '{0}/rest/private/nagios/command/execute'.format(self.monitor_url), data=form_data) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): """ Do a POST-Request to set an acknowledgement for a host, service or host with all services in SNAG-View 3 :param host: String - Host name :param service: String - Service name :param author: String - Author name (username) :param comment: String - Additional comment :param sticky: Bool - Sticky Acknowledgement :param notify: Bool - Send Notifications :param persistent: Bool - Persistent comment :param all_services: Optional[Array] - List of all services (filled only if 'Acknowledge all services on host' is set) """ form_data = dict() if all_services: # Host & all Services form_data['commandType'] = 'sv_host' form_data['commandName'] = 'acknowledge-host-service-problems' form_data['params'] = json.dumps( {'__SVID': self.hosts[host].svid, 'comment': comment, 'notify': notify, 'persistent': persistent, 'sticky': sticky}) elif service == '': # Host form_data['commandType'] = 'sv_host' form_data['commandName'] = 'acknowledge-problem' form_data['params'] = json.dumps( {'__SVID': self.hosts[host].svid, 'comment': comment, 'notify': notify, 'persistent': persistent, 'sticky': sticky}) else: # Service form_data['commandType'] = 'sv_service_status' form_data['commandName'] = 'acknowledge-problem' form_data['params'] = json.dumps( {'__SVID': self.hosts[host].services[service].svid, 'comment': comment, 'notify': notify, 'persistent': persistent, 'sticky': sticky}) self.session.post( '{0}/rest/private/nagios/command/execute'.format(self.monitor_url), data=form_data) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): """ Do a POST-Request to submit a check result to SNAG-View 3 :param host: String - Host name :param service: String - Service name :param state: String - Selected state :param comment: NOT IN USE - String - Additional comment :param check_output: String - Check output :param performance_data: String - Performance data """ state = state.upper() form_data = dict() form_data['commandName'] = 'process-check-result' # TODO 'state' contains wrong information # Variable 'state' can contain any standard state # ('up','down','unreachable', 'ok', 'warning', 'critical' or 'unknown') # Selecting something else for example 'information' or 'disaster' puts 'ok' into the variable state # This makes it impossible to log errors for unsupported states because you can't differentiate # between selecting 'ok' and 'information' because in both cases the variable contains 'ok' log.info( 'Selecting an unsupported check result submits \'UP\' for hosts and \'OK\' for services!') if service == '': # Host form_data['commandType'] = 'sv_host' if state == 'OK' or state == 'UNKNOWN': log.info('Setting OK or UNKNOWN to UP') state = 'UP' state_number = self.STATES_MAPPING_REV['hosts'][state] if performance_data == '': form_data['params'] = json.dumps( {'__SVID': self.hosts[host].svid, 'status_code': state_number, 'plugin_output': check_output}) else: form_data['params'] = json.dumps({'__SVID': self.hosts[host].svid, 'status_code': state_number, 'plugin_output': check_output + ' | ' + performance_data}) else: # Service form_data['commandType'] = 'sv_service_status' state_number = self.STATES_MAPPING_REV['services'][state] if performance_data == '': form_data['params'] = json.dumps( {'__SVID': self.hosts[host].services[service].svid, 'status_code': state_number, 'plugin_output': check_output}) else: form_data['params'] = json.dumps( {'__SVID': self.hosts[host].services[service].svid, 'status_code': state_number, 'plugin_output': check_output + ' | ' + performance_data}) self.session.post( '{0}/rest/private/nagios/command/execute'.format(self.monitor_url), data=form_data) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ Do a PUT-Request to create a downtime for a host or service in SNAG-View 3 :param host: String - Host name :param service: String - Service name :param author: String - Author name (username) :param comment: String - Additional comment :param fixed: Bool - Fixed Downtime :param start_time: String - Date in Y-m-d H:M:S format - Start of Downtime :param end_time: String - Date in Y-m-d H:M:S format - End of Downtime :param hours: NOT SUPPORTED - Integer - Flexible Downtime :param minutes: NOT SUPPORTED - Integer - Flexible Downtime """ form_data = dict() if service == '': form_data['type'] = 'sv_host' form_data['host_effects'] = 'hostOnly' form_data['svid'] = self.hosts[host].svid else: form_data['type'] = 'sv_service_status' form_data['svid'] = self.hosts[host].services[service].svid # Format start_time and end_time from user-friendly format to timestamp start_time = time.mktime(datetime.datetime.strptime( start_time, "%Y-%m-%d %H:%M:%S").timetuple()) start_time = str(start_time).split('.')[0] end_time = time.mktime(datetime.datetime.strptime( end_time, "%Y-%m-%d %H:%M:%S").timetuple()) end_time = str(end_time).split('.')[0] form_data['start'] = start_time form_data['end'] = end_time form_data['comment'] = comment self.session.put( '{0}/rest/private/nagios/downtime'.format(self.monitor_url), data=form_data) def get_start_end(self, host): """ Set default of start time to "now" and end time is "now + 24 hours" :param host: String - Host name """ log.info("Flexible Downtimes are not supported by SNAG-View 3") start = datetime.datetime.now() end = datetime.datetime.now() + datetime.timedelta(hours=24) return str(start.strftime("%Y-%m-%d %H:%M:%S")), str(end.strftime("%Y-%m-%d %H:%M:%S")) def open_monitor(self, host, service=''): """ Open specific Host or Service in SNAG-View 3 browser window :param host: String - Host name :param service: String - Service name """ if service == '': url = '{0}/#/object/details/{1}'.format( self.monitor_url, self.hosts[host].svid) else: url = '{0}/#/object/details/{1}'.format( self.monitor_url, self.hosts[host].services[service].svid) webbrowser_open(url) class NoAuth(requests.auth.AuthBase): """ Override to avoid auth headers Needed for LDAP login """ def __call__(self, r): return r Nagstamon-master/Nagstamon/Servers/Thruk.py000066400000000000000000000471761505160700500213430ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # Thruk additions copyright by dcec@Github # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from collections import OrderedDict from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import conf import sys import json import datetime import copy import urllib.parse from Nagstamon.helpers import human_readable_duration_from_timestamp from Nagstamon.helpers import webbrowser_open from Nagstamon.objects import (GenericHost, GenericService, Result) class ThrukServer(GenericServer): """ Thruk is derived from generic (Nagios) server """ TYPE = 'Thruk' # dictionary to translate status bitmaps on webinterface into status flags # this are defaults from Nagios # "disabled.gif" is in Nagios for hosts the same as "passiveonly.gif" for services STATUS_MAPPING = { "ack.gif" : "acknowledged", \ "passiveonly.gif" : "passiveonly", \ "disabled.gif" : "passiveonly", \ "ndisabled.gif" : "notifications_disabled", \ "downtime.gif" : "scheduled_downtime", \ "flapping.gif" : "flapping"} # Entries for monitor default actions in context menu MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Submit check result", "Downtime"] # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ["check_output", "performance_data"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = { "monitor": "$MONITOR$", \ "hosts": "$MONITOR-CGI$/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&page=1&entries=all", \ "services": "$MONITOR-CGI$/status.cgi?dfl_s0_value_sel=5&dfl_s0_servicestatustypes=29&dfl_s0_op=%3D&style=detail&dfl_s0_type=host&dfl_s0_serviceprops=0&dfl_s0_servicestatustype=4&dfl_s0_servicestatustype=8&dfl_s0_servicestatustype=16&dfl_s0_servicestatustype=1&hidetop=&dfl_s0_hoststatustypes=15&dfl_s0_val_pre=&hidesearch=2&dfl_s0_value=all&dfl_s0_hostprops=0&nav=&page=1&entries=all", \ "history": "$MONITOR-CGI$/history.cgi?host=all&page=1&entries=all"} STATES_MAPPING = {"hosts" : {0 : "OK", 1 : "DOWN", 2 : "UNREACHABLE"}, \ "services" : {0 : "OK", 1 : "WARNING", 2 : "CRITICAL", 3 : "UNKNOWN"}} def __init__(self, **kwds): GenericServer.__init__(self, **kwds) def init_HTTP(self): """ partly not constantly working Basic Authorization requires extra Autorization headers, different between various server types """ if self.session is None: GenericServer.init_HTTP(self) # get cookie from login page via url retrieving as with other urls try: # login and get cookie if self.session is None or self.session.cookies.get('thruk_auth') is None: self.login() except: self.error(sys.exc_info()) def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # create filters like described in # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # Thruk allows requesting only needed information to reduce traffic self.cgiurl_services = self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=28&view_mode=json&"\ "entries=all&columns=host_name,description,state,last_check,"\ "last_state_change,plugin_output,current_attempt,"\ "max_check_attempts,active_checks_enabled,is_flapping,"\ "notifications_enabled,acknowledged,state_type,"\ "scheduled_downtime_depth,host_display_name,display_name" # hosts (up or down or unreachable) self.cgiurl_hosts = self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&"\ "dfl_s0_hoststatustypes=12&dfl_s1_hostprops=1&dfl_s2_hostprops=4&dfl_s3_hostprops=524288&&dfl_s4_hostprops=4096&dfl_s5_hostprop=16&"\ "view_mode=json&entries=all&"\ "columns=name,state,last_check,last_state_change,"\ "plugin_output,current_attempt,max_check_attempts,"\ "active_checks_enabled,notifications_enabled,is_flapping,"\ "acknowledged,scheduled_downtime_depth,state_type,host_display_name,display_name" def login(self): """ use pure session instead of fetch_url to get Thruk session """ if self.session is None: self.refresh_authentication = False GenericServer.init_HTTP(self) if self.use_autologin is True: req = self.session.post(self.monitor_cgi_url + '/user.cgi?', data={}, headers={'X-Thruk-Auth-Key':self.autologin_key.strip()}) if conf.debug_mode: self.debug(server=self.get_name(), debug='Auto Login status: ' + req.url + ' http code : ' + str(req.status_code)) if req.status_code != 200: self.refresh_authentication = True return Result(result=None, error="Login failed") else: # set thruk test cookie to in order to directly login self.session.cookies.set('thruk_test', '***') req = self.session.post(self.monitor_cgi_url + '/login.cgi?', data={'login': self.get_username(), 'password': self.get_password(), 'submit': 'Login'}) if conf.debug_mode: self.debug(server=self.get_name(), debug='Login status: ' + req.url + ' http code : ' + str(req.status_code)) if req.status_code != 200: self.refresh_authentication = True return Result(result=None, error="Login failed") if self.disabled_backends is not None: self.session.cookies.set('thruk_backends', '&'.join((f"{v}=2" for v in self.disabled_backends.split(',')))) print(self.session.cookies.get('thruk_backends')) def open_monitor(self, host, service=''): ''' open monitor from tablewidget context menu ''' # only type is important so do not care of service '' in case of host monitor if service == '': url = self.monitor_cgi_url + '/extinfo.cgi?type=1&' + urllib.parse.urlencode( { 'host': host }) else: url = self.monitor_cgi_url + '/extinfo.cgi?type=2&' + urllib.parse.urlencode( { 'host': host, 'service': self.hosts[host].services[ service ].real_name }) if conf.debug_mode: self.debug(server=self.get_name(), host=host, service=service, debug='Open host/service monitor web page {0}'.format(url)) webbrowser_open(url) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): ''' send acknowledge to monitor server - might be different on every monitor type ''' url = self.monitor_cgi_url + '/cmd.cgi' # the following flags apply to hosts and services # # according to sf.net bug #3304098 (https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3304098&group_id=236865) # the send_notification-flag must not exist if it is set to 'off', otherwise # the Nagios core interpretes it as set, regardless its real value # # for whatever silly reason Icinga depends on the correct order of submitted form items... # see sf.net bug 3428844 # # Thanks to Icinga ORDER OF ARGUMENTS IS IMPORTANT HERE! # cgi_data = OrderedDict() if service == '': cgi_data['cmd_typ'] = '33' else: cgi_data['cmd_typ'] = '34' cgi_data['cmd_mod'] = '2' cgi_data['host'] = host if service != '': cgi_data['service'] = self.hosts[host].services[ service ].real_name cgi_data['com_author'] = author cgi_data['com_data'] = comment cgi_data['btnSubmit'] = 'Commit' if notify is True: cgi_data['send_notification'] = 'on' if persistent is True: cgi_data['persistent'] = 'on' if sticky is True: cgi_data['sticky_ack'] = 'on' self.fetch_url(url, giveback='raw', cgi_data=cgi_data) # acknowledge all services on a host if all_services: for s in all_services: cgi_data['cmd_typ'] = '34' cgi_data['service'] = self.hosts[host].services[ s ].real_name self.fetch_url(url, giveback='raw', cgi_data=cgi_data) def _set_recheck(self, host, service): self.session.headers.update({'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}) if service != '': if self.hosts[host].services[ service ].is_passive_only(): # Do not check passive only checks return try: # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios result = self.fetch_url( self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '96', 'host': host})) self.start_time = dict(result.result.find(attrs={'name': 'start_time'}).attrs)['value'] # decision about host or service - they have different URLs if service == '': # host cmd_typ = '96' service_name = '' else: # service @ host cmd_typ = '7' service_name = self.hosts[host].services[ service ].real_name # ignore empty service in case of rechecking a host cgi_data = urllib.parse.urlencode([('cmd_typ', cmd_typ), ('cmd_mod', '2'), ('host', host), ('service', service_name), ('start_time', self.start_time), ('force_check', 'on'), ('btnSubmit', 'Commit')]) # execute POST request self.fetch_url(self.monitor_cgi_url + '/cmd.cgi', giveback='raw', cgi_data=cgi_data) except: traceback.print_exc(file=sys.stdout) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): ''' finally send downtime command to monitor server ''' url = self.monitor_cgi_url + '/cmd.cgi' # for some reason Icinga is very fastidiuos about the order of CGI arguments, so please # here we go... it took DAYS :-( cgi_data = OrderedDict() if service == '': cgi_data['cmd_typ'] = '55' else: cgi_data['cmd_typ'] = '56' cgi_data['cmd_mod'] = '2' cgi_data['trigger'] = '0' cgi_data['host'] = host if service != '': cgi_data['service'] = self.hosts[host].services[ service ].real_name cgi_data['com_author'] = author cgi_data['com_data'] = comment cgi_data['fixed'] = fixed cgi_data['start_time'] = start_time cgi_data['end_time'] = end_time cgi_data['hours'] = hours cgi_data['minutes'] = minutes cgi_data['btnSubmit'] = 'Commit' # running remote cgi command self.fetch_url(url, giveback='raw', cgi_data=cgi_data) def _get_status(self): """ Get status from Thruk Server """ # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: # JSON experiments result = self.fetch_url(self.cgiurl_hosts, giveback='raw') jsonraw, error, status_code = copy.deepcopy(result.result),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured errors_occured = self.check_for_error(jsonraw, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # in case basic auth did not work try form login cookie based login if jsonraw.startswith("<"): self.refresh_authentication = True return Result(result=None, error="Login failed") # in case JSON is not empty evaluate it elif not jsonraw == "[]": hosts = json.loads(jsonraw) for h in hosts: if h["name"] not in self.new_hosts: self.new_hosts[h["name"]] = GenericHost() self.new_hosts[h["name"]].name = h["name"] self.new_hosts[h["name"]].server = self.name self.new_hosts[h["name"]].status = self.STATES_MAPPING["hosts"][h["state"]] self.new_hosts[h["name"]].last_check = datetime.datetime.fromtimestamp(int(h["last_check"])).isoformat(" ") self.new_hosts[h["name"]].duration = human_readable_duration_from_timestamp(h["last_state_change"]) self.new_hosts[h["name"]].attempt = "%s/%s" % (h["current_attempt"], h["max_check_attempts"]) self.new_hosts[h["name"]].status_information = h["plugin_output"].replace("\n", " ").strip() self.new_hosts[h["name"]].passiveonly = not(bool(int(h["active_checks_enabled"]))) self.new_hosts[h["name"]].notifications_disabled = not(bool(int(h["notifications_enabled"]))) self.new_hosts[h["name"]].flapping = bool(int(h["is_flapping"])) self.new_hosts[h["name"]].acknowledged = bool(int(h["acknowledged"])) self.new_hosts[h["name"]].scheduled_downtime = bool(int(h["scheduled_downtime_depth"])) self.new_hosts[h["name"]].status_type = {0: "soft", 1: "hard"}[h["state_type"]] del h except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # services try: # JSON experiments result = self.fetch_url(self.cgiurl_services, giveback="raw") jsonraw, error, status_code = copy.deepcopy(result.result),\ copy.deepcopy(result.error),\ result.status_code # check if any error occured errors_occured = self.check_for_error(jsonraw, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured # in case basic auth did not work try form login cookie based login if jsonraw.startswith("<"): self.refresh_authentication = True return Result(result=None, error="Login failed") # in case JSON is not empty evaluate it elif not jsonraw == "[]": services = json.loads(jsonraw) for s in services: # host objects contain service objects if s["host_name"] not in self.new_hosts: self.new_hosts[s["host_name"]] = GenericHost() self.new_hosts[s["host_name"]].name = s["host_name"] self.new_hosts[s["host_name"]].server = self.name self.new_hosts[s["host_name"]].status = "UP" if self.use_display_name_service: entry = s["display_name"] else: entry = s["description"] # if a service does not exist create its object if entry not in self.new_hosts[s["host_name"]].services: self.new_hosts[s["host_name"]].services[ entry ] = GenericService() self.new_hosts[s["host_name"]].services[ entry ].host = s["host_name"] self.new_hosts[s["host_name"]].services[ entry ].name = entry self.new_hosts[s["host_name"]].services[ entry ].real_name = s["description"] self.new_hosts[s["host_name"]].services[ entry ].server = self.name self.new_hosts[s["host_name"]].services[ entry ].status = self.STATES_MAPPING["services"][s["state"]] self.new_hosts[s["host_name"]].services[ entry ].last_check = datetime.datetime.fromtimestamp(int(s["last_check"])).isoformat(" ") self.new_hosts[s["host_name"]].services[ entry ].duration = human_readable_duration_from_timestamp(s["last_state_change"]) self.new_hosts[s["host_name"]].services[ entry ].attempt = "%s/%s" % (s["current_attempt"], s["max_check_attempts"]) self.new_hosts[s["host_name"]].services[ entry ].status_information = s["plugin_output"].replace("\n", " ").strip() self.new_hosts[s["host_name"]].services[ entry ].passiveonly = not(bool(int(s["active_checks_enabled"]))) self.new_hosts[s["host_name"]].services[ entry ].notifications_disabled = not(bool(int(s["notifications_enabled"]))) self.new_hosts[s["host_name"]].services[ entry ].flapping = not(bool(int(s["notifications_enabled"]))) self.new_hosts[s["host_name"]].services[ entry ] .acknowledged = bool(int(s["acknowledged"])) self.new_hosts[s["host_name"]].services[ entry ].scheduled_downtime = bool(int(s["scheduled_downtime_depth"])) self.new_hosts[s["host_name"]].services[ entry ].status_type = {0: "soft", 1: "hard"}[s["state_type"]] del s except: import traceback traceback.print_exc(file=sys.stdout) # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) # dummy return in case all is OK return Result() Nagstamon-master/Nagstamon/Servers/Zabbix.py000066400000000000000000000506311505160700500214530ustar00rootroot00000000000000# -*- encoding: utf-8; py-indent-offset: 4 -*- # # Zabbix.py based on Checkmk Multisite.py import base64 import json import sys import time import datetime import socket from packaging import version from Nagstamon.helpers import (human_readable_duration_from_timestamp, webbrowser_open) from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer, BearerAuth class ZabbixError(Exception): def __init__(self, terminate, result): self.terminate = terminate self.result = result class ZabbixServer(GenericServer): """ special treatment for Zabbix, taken from Check_MK Multisite JSON API """ TYPE = 'Zabbix' def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon - self.authentication = conf.servers[self.get_name()].authentication self.urls = {} # self.statemap = {} self.statemap = { 'UNREACH': 'UNREACHABLE', 'CRIT': 'CRITICAL', 'WARN': 'WARNING', 'UNKN': 'UNKNOWN', 'PEND': 'PENDING', '0': 'OK', '1': 'INFORMATION', '2': 'WARNING', '3': 'AVERAGE', '4': 'HIGH', '5': 'DISASTER'} # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Acknowledge", "Downtime"] # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$', 'hosts': '$MONITOR-CGI$/hosts.php?ddreset=1', 'services': '$MONITOR-CGI$/zabbix.php?action=problem.view&fullscreen=0&page=1&filter_show=3&filter_set=1', 'history': '$MONITOR-CGI$/zabbix.php?action=problem.view&fullscreen=0&page=1&filter_show=2&filter_set=1'} self.username = conf.servers[self.get_name()].username self.password = conf.servers[self.get_name()].password self.timeout = conf.servers[self.get_name()].timeout self.ignore_cert = conf.servers[self.get_name()].ignore_cert self.use_description_name_service = conf.servers[self.get_name()].use_description_name_service self.api_version = '' self.auth_token = '' self.monitor_path = '/api_jsonrpc.php' def init_HTTP(self): """ things to do if HTTP is not initialized """ GenericServer.init_HTTP(self) # prepare for JSON self.session.headers.update({'Accept': 'application/json', 'Content-Type': 'application/json-rpc'}) try: self.set_zabbix_version() self.check_authentication() if self.refresh_authentication: self.login() except Exception: self.error(sys.exc_info()) return def api_request(self, cgi_data, no_auth=False): """ Make a request to the Zabbix API Returns the response as a dictionary """ url = self.monitor_url if self.monitor_url.endswith(self.monitor_path) else f"{self.monitor_url}{self.monitor_path}" result = self.fetch_url(url, headers=self.session.headers, cgi_data=cgi_data, giveback='json', no_auth=no_auth) # Check if the result is a valid JSON response data = result.result error = result.error status_code = result.status_code if error: raise ZabbixError(terminate=True, result=Result(result=False, error=error, status_code=status_code)) return data def login(self): if conf.servers[self.get_name()].authentication == 'bearer': return # check version to use the correct keyword for username which changed since 6.4 if version.parse(self.api_version) < version.parse("6.4"): username_keyword = 'user' else: username_keyword = 'username' obj = self.generate_cgi_data('user.login', {username_keyword: self.username, 'password': self.password}, no_auth=True) result = self.api_request(obj) self.auth_token = result['result'] # Store the auth token for later use # Use bearer authentication for Zabbix versions 6.4 and above self.authentication = "bearer" if version.parse(self.api_version) >= version.parse("6.4") else "basic" self.session.auth = BearerAuth(self.auth_token) self.refresh_authentication = False # Reset the flag after successful login def check_authentication(self): try: if conf.servers[self.get_name()].authentication == 'bearer': obj = self.generate_cgi_data('user.checkAuthentication', {'token': self.auth_token}, no_auth=True) else: obj = self.generate_cgi_data('user.checkAuthentication', {'sessionid': self.auth_token}, no_auth=True) result = self.api_request(obj, no_auth=True) if result['error']: self.refresh_authentication = True except Exception as e: raise RuntimeError(f"Authentication check failed: {str(e)}") def generate_cgi_data(self, method, params=None, no_auth=False): """ Generate data for Zabbix API requests """ if params is None: params = {} data = { 'jsonrpc': '2.0', 'method': method, 'params': params, 'id': 1 } # Only include auth parameter for Zabbix versions before 6.4 if not no_auth and self.auth_token and version.parse(self.api_version) < version.parse("6.4"): data['auth'] = self.auth_token return json.dumps(data) def set_zabbix_version(self): """ Set the Zabbix API version and other related attributes """ try: obj = self.generate_cgi_data('apiinfo.version', no_auth=True) result = self.api_request(obj, no_auth=True) self.api_version = result['result'] except Exception as e: raise RuntimeError(f"Failed to set Zabbix version: {str(e)}") def _get_status(self): """ Get status from Zabbix Server """ ret = Result() # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily # ========================================= # Service # ========================================= try: # Get a list of all issues (AKA tripped triggers) # add Pagination chunk_size = 200 # services_ids will contain all trigger ids of active services results = self.api_request( self.generate_cgi_data('trigger.get', { 'only_true': True, 'skipDependent': True, 'monitored': True, 'active': True, 'output': ['triggerid'] }) ) services_ids = results['result'] services = [] for i in range(0, len(services_ids), chunk_size): results = self.api_request( self.generate_cgi_data('trigger.get', { 'only_true': True, 'skipDependent': True, 'monitored': True, 'active': True, 'output': ['triggerid', 'description', 'lastchange', 'manual_close'], # 'expandDescription': True, # 'expandComment': True, 'triggerids': [trigger['triggerid'] for trigger in services_ids[i:i + chunk_size]], 'selectLastEvent': ['eventid', 'name', 'ns', 'clock', 'acknowledged', 'value', 'severity'], 'selectHosts': ["hostid", "host", "name", "status", "available", "active_available", "maintenance_status", "maintenance_from"], 'selectItems': ['name', 'lastvalue', 'state', 'lastclock'] })) services.extend(results['result']) for service in services: status_information = ", ".join( [f"{item['name']}: {item['lastvalue']}" for item in service['items']]) # Add opdata to status information if available (from problem API) if 'opdata' in service['lastEvent']: if service['lastEvent']['opdata'] != "": status_information = service['lastEvent']['name'] + " (" + service['lastEvent'][ 'opdata'] + ")" service_obj = GenericService() service_obj.name = service['lastEvent']['name'] service_obj.status = self.statemap.get(service['lastEvent']['severity'], service['lastEvent']['severity']) service_obj.last_check = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(max(int(item['lastclock']) for item in service['items']))) service_obj.duration = human_readable_duration_from_timestamp(service['lastEvent']['clock']) service_obj.status_information = status_information service_obj.acknowledged = False if service['lastEvent']['acknowledged'] == '0' else True #service_obj.address = '' # Todo: check if address is available service_obj.triggerid = service['triggerid'] service_obj.eventid = service['lastEvent']['eventid'] service_obj.allow_manual_close = False if str(service['manual_close']) == '0' else True if service['hosts']: # Get the first host only, because we only support one host per service for host in service['hosts']: self.new_hosts[host['name']] = GenericHost() self.new_hosts[host['name']].name = host['name'] self.new_hosts[host['name']].server = self.name self.new_hosts[host['name']].status = 'UP' self.new_hosts[host['name']].scheduled_downtime = True if host["maintenance_status"] == '1' else False # Map Stuff from Service to Host self.new_hosts[host['name']].services[service["triggerid"]] = service_obj self.new_hosts[host['name']].services[service["triggerid"]].host = host['name'] self.new_hosts[host['name']].services[service["triggerid"]].hostid = host['hostid'] except ZabbixError as e: print(f"ZabbixError: {e.result}") return Result(result=e.result, error=e.result.content) except Exception: self.isChecking = False result, error = self.error(sys.exc_info()) print(sys.exc_info()) return Result(result=result, error=error) return ret def _open_browser(self, url): webbrowser_open(url) if conf.debug_mode is True: self.debug(server=self.get_name(), debug="Open web page " + url) def open_services(self): self._open_browser(self.urls['human_services']) def open_hosts(self): self._open_browser(self.urls['human_hosts']) def open_monitor(self, host, service=""): """ open monitor from treeview context menu """ host_id = self.hosts[host].hostid url = f"{self.monitor_url}/zabbix.php?action=problem.view&hostids%5B%5D={host_id}&filter_set=1&show_suppressed=1" if conf.debug_mode is True: self.debug(server=self.get_name(), host=host, service=service, debug="Open host/service monitor web page " + url) webbrowser_open(url) # Disable set_recheck (nosense in Zabbix) def set_recheck(self, info_dict): pass def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): if conf.debug_mode is True: self.debug(server=self.get_name(), debug="Set Acknowledge Host: " + host + " Service: " + service + " Sticky: " + str( sticky) + " persistent:" + str(persistent) + " All services: " + str(all_services)) eventids = set() unclosable_events = set() if all_services is None: all_services = [] all_services.append(service) get_host = self.hosts[host] # Through all Services for s in all_services: # find Trigger ID for host_service in get_host.services: host_service = get_host.services[host_service] if host_service.name == s: eventid = host_service.eventid # https://github.com/HenriWahl/Nagstamon/issues/826 we may have set eventid = -1 earlier if there was no associated event if eventid == -1: continue eventids.add(eventid) if not host_service.allow_manual_close: unclosable_events.add(eventid) # If events pending of acknowledge, execute ack if len(eventids) > 0: # actions is a bitmask with values: # 1 - close problem # 2 - acknowledge event # 4 - add message # 8 - change severity # 16 - unacknowledge event # 32 - suppress event; # 64 - unsuppress event; # 128 - change event rank to cause; # 256 - change event rank to symptom. # sticky = close problem # TODO: make visible in GUI actions = 2 if comment: actions |= 4 if conf.debug_mode: self.debug(server=self.get_name(), debug="Events to acknowledge: " + str(eventids) + " Close: " + str(actions)) # If some events are not closable, we need to make 2 requests, 1 for the closable and one for the not closable if sticky and unclosable_events: closable_actions = actions | 1 closable_events = set(e for e in eventids if e not in unclosable_events) self.api_request( self.generate_cgi_data('event.acknowledge', { 'eventids': list(closable_events), 'message': comment, 'action': closable_actions }), ) self.api_request( self.generate_cgi_data('event.acknowledge', { 'eventids': list(unclosable_events), 'message': comment, 'action': actions }), ) else: if sticky: actions |= 1 try: self.api_request( self.generate_cgi_data('event.acknowledge', { 'eventids': list(eventids), 'message': comment, 'action': actions }), ) except RuntimeError as e: if "Incorrect user name or password or account is temporarily blocked" in str(e): self.error(str(e)) return else: raise e def _set_downtime(self, hostname, service, author, comment, fixed, start_time, end_time, hours, minutes): # Check if there is an associated Application tag with this trigger/item triggerid = None for host_service in self.hosts[hostname].services: if self.hosts[hostname].services[host_service].name == service: triggerid = self.hosts[hostname].services[host_service].triggerid break if self.hosts[hostname].hostid is None: self.error("Host ID is None for " + hostname) return hostids = [self.hosts[hostname].hostid] if fixed == 1: start_date = datetime.datetime.strptime(start_time, "%Y-%m-%d %H:%M") end_date = datetime.datetime.strptime(end_time, "%Y-%m-%d %H:%M") else: start_date = datetime.datetime.now() end_date = start_date + datetime.timedelta(hours=hours, minutes=minutes) stime = int(time.mktime(start_date.timetuple())) etime = int(time.mktime(end_date.timetuple())) if conf.debug_mode is True: self.debug(server=self.get_name(), debug="Downtime for " + hostname + "[" + str(hostids) + "] stime:" + str( stime) + " etime:" + str(etime)) # print("Downtime for " + hostname + "[" + str(hostids) + "] stime:" + str(stime) + " etime:" + str(etime)) body = {'hostids': hostids, 'name': comment, 'description': author, 'active_since': stime, 'active_till': etime, 'maintenance_type': 0, "timeperiods": [ {"timeperiod_type": 0, "start_date": stime, "period": etime - stime} ] } if triggerid: body['tags'] = [{'tag': 'triggerid', 'operator': 0, 'value': triggerid}] body['description'] = body['description'] + '(Nagstamon): ' + comment body['name'] = f'{hostname}: {service}' try: self.api_request( self.generate_cgi_data('maintenance.create', body) ) except ValueError as e: if "already exists" in str(e).lower(): self.debug(server=self.get_name(), debug=f"Maintanence with name {body['name']} already exists") else: raise e def get_start_end(self, host): return time.strftime("%Y-%m-%d %H:%M"), time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() + 7200)) def get_host(self, host): """ find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios """ # the fasted method is taking hostname as used in monitor if conf.connect_by_host is True: return Result(result=host) ip = "" address = host try: if host in self.hosts: ip = self.hosts[host].address if conf.debug_mode is True: self.debug(server=self.get_name(), host=host, debug="IP of %s:" % host + " " + ip) if conf.connect_by_dns is True: try: address = socket.gethostbyaddr(ip)[0] except socket.herror: address = ip else: address = ip except ZabbixError: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) return Result(result=address) def nagiosify_service(self, service): """ next dirty workaround to get Zabbix events to look Nagios-esque """ if (" on " or " is ") in service: for separator in [" on ", " is "]: service = service.split(separator)[0] return service Nagstamon-master/Nagstamon/Servers/ZabbixProblemBased.py000066400000000000000000000311461505160700500237330ustar00rootroot00000000000000# encoding: utf-8 import sys import time import logging import requests from packaging import version from Nagstamon.helpers import human_readable_duration_from_timestamp from Nagstamon.config import conf from Nagstamon.objects import GenericHost, GenericService, Result from Nagstamon.Servers.Generic import GenericServer class ZabbixLightApi(): logger = None server_name = "Zabbix" monitor_url = "http://127.0.0.1/api_jsonrpc.php" validate_certs = False r_session = None zbx_auth = None zbx_req_id = 0 def __init__(self, server_name, monitor_url, validate_certs): self.server_name = server_name self.monitor_url = monitor_url + "/api_jsonrpc.php" self.validate_certs = validate_certs #persistent connections self.r_session = requests.Session() #configure logging self.logger = logging.getLogger('ZabbixLightApi_'+server_name) console_logger = logging.StreamHandler() console_logger_formatter = logging.Formatter('[%(name)s] %(levelname)s - %(message)s') console_logger.setFormatter(console_logger_formatter) self.logger.addHandler(console_logger) if conf.debug_mode is True: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(logging.INFO) self.logger.debug("ZabbixLightApi START!") self.logger.debug("monitor_url = " + self.monitor_url) def do_request(self, method, params={}, no_auth=False): zabbix_rpc_obj = { "jsonrpc": "2.0", "method": method, "params": params, "id": self.zbx_req_id } if not no_auth: zabbix_rpc_obj["auth"] = self.zbx_auth self.zbx_req_id += 1 self.logger.debug("ZBX: > " + str(zabbix_rpc_obj)) try: response = self.r_session.post(self.monitor_url, json=zabbix_rpc_obj, verify=self.validate_certs) #we didnt get HTTP code 200 if response.status_code != 200: raise ZabbixLightApiException("Got return code - " + str(response.status_code)) #parse response json response_json = response.json() self.logger.debug("ZBX: < " + str(response_json)) #there was some kind of error during processing our request if "error" in response_json.keys(): raise ZabbixLightApiException("ZBX: < " + response_json["error"]["data"]) #zabbix returned garbage if "result" not in response_json.keys(): raise ZabbixLightApiException("ZBX: < no result object in response") #all other network related errors except Exception as e: raise ZabbixLightApiException(e) return response_json['result'] def api_version(self, **options): obj = self.do_request('apiinfo.version', options, no_auth=True) return obj def logged_in(self): if self.zbx_auth is None: return False else: is_auth=self.do_request("user.checkAuthentication", {"sessionid": self.zbx_auth}, no_auth=True) if is_auth: return True else: self.zbx_auth = None return False def login(self, username, password): self.logger.debug("Login in as " + username) # see issue https://github.com/HenriWahl/Nagstamon/issues/1018 if self.api_version() < '6.4': self.zbx_auth = self.do_request('user.login', {'user': username, 'password': password}) else: self.zbx_auth = self.do_request('user.login', {'username': username, 'password': password}) class ZabbixLightApiException(Exception): pass class ZabbixProblemBasedServer(GenericServer): TYPE = 'ZabbixProblemBased' zlapi = None zbx_version = "" def __init__(self, **kwds): GenericServer.__init__(self, **kwds) self.statemap = { '0': 'OK', '1': 'INFORMATION', '2': 'WARNING', '3': 'AVERAGE', '4': 'HIGH', '5': 'DISASTER'} # Entries for monitor default actions in context menu self.MENU_ACTIONS = [] # URLs for browser shortlinks/buttons on popup window self.BROWSER_URLS = {'monitor': '$MONITOR$', 'hosts': '$MONITOR-CGI$/hosts.php?ddreset=1', 'services': '$MONITOR-CGI$/zabbix.php?action=problem.view&fullscreen=0&page=1&filter_show=3&filter_set=1', 'history': '$MONITOR-CGI$/zabbix.php?action=problem.view&fullscreen=0&page=1&filter_show=2&filter_set=1'} self.username = conf.servers[self.get_name()].username self.password = conf.servers[self.get_name()].password self.validate_certs = not conf.servers[self.get_name()].ignore_cert def _get_status(self): """ Get status from Zabbix Server """ # ========================================= # problems # ========================================= problems = [] try: #Are we logged in? if self.zlapi is None: self.zlapi = ZabbixLightApi(server_name=self.name, monitor_url=self.monitor_url, validate_certs=self.validate_certs) #zabbix could get an upgrade between checks, we need to check version each time self.zbx_version = self.zlapi.do_request("apiinfo.version", {}, no_auth=True) #check are we still logged in, if not, relogin if not self.zlapi.logged_in(): self.zlapi.login(self.username, self.password) #Get all current problems (trigger based), no need to check acknowledged problems if they are filtered out (load reduce) if conf.filter_acknowledged_hosts_services: # old versions doesnt support suppressed problems if version.parse(self.zbx_version) < version.parse("6.2.0"): problems = self.zlapi.do_request("problem.get", {'recent': False, 'acknowledged': False}) else: problems = self.zlapi.do_request("problem.get", {'recent': False, 'acknowledged': False, 'suppressed': False}) else: problems = self.zlapi.do_request("problem.get", {'recent': False}) for problem in problems: #get trigger which rose current problem trigger = self.zlapi.do_request("trigger.get", {'triggerids': problem['objectid'], 'monitored': True, 'active': True, 'skipDependent': True, 'selectHosts': ['hostid', 'name', 'maintenance_status', 'available', 'error', 'errors_from', 'ipmi_available', 'ipmi_error', 'ipmi_errors_from', 'jmx_available', 'jmx_error', 'jmx_errors_from', 'snmp_available', 'snmp_error', 'snmp_errors_from'], 'selectItems': ['key_', 'lastclock']}) #problems on disabled/maintenance/deleted hosts don't have triggers #have to do that because of how zabbix housekeeping service work #API reports past problems for hosts that no longer exist if not trigger: continue service_id = problem['eventid'] host_id = trigger[0]['hosts'][0]['hostid'] #new host to report, we only need to do that at first problem for that host if host_id not in self.new_hosts: self.new_hosts[host_id] = GenericHost() self.new_hosts[host_id].name = trigger[0]['hosts'][0]['name'] #host has active maintenance period if trigger[0]['hosts'][0]['maintenance_status'] == "1": self.new_hosts[host_id].scheduled_downtime = True #old api shows host interfaces status in host object if version.parse(self.zbx_version) < version.parse("5.4.0"): #host not available via agent if trigger[0]['hosts'][0].get('available', '0') == "2": self.new_hosts[host_id].status = "DOWN" self.new_hosts[host_id].status_information = trigger[0]['hosts'][0]['error'] self.new_hosts[host_id].duration = human_readable_duration_from_timestamp(trigger[0]['hosts'][0]['errors_from']) #host not available via ipmi if trigger[0]['hosts'][0].get('ipmi_available', '0') == "2": self.new_hosts[host_id].status = "DOWN" self.new_hosts[host_id].status_information = trigger[0]['hosts'][0]['ipmi_error'] self.new_hosts[host_id].duration = human_readable_duration_from_timestamp(trigger[0]['hosts'][0]['ipmi_errors_from']) #host not available via jmx if trigger[0]['hosts'][0].get('jmx_available', '0') == "2": self.new_hosts[host_id].status = "DOWN" self.new_hosts[host_id].status_information = trigger[0]['hosts'][0]['jmx_error'] self.new_hosts[host_id].duration = human_readable_duration_from_timestamp(trigger[0]['hosts'][0]['jmx_errors_from']) #host not available via snmp if trigger[0]['hosts'][0].get('snmp_available', '0') == "2": self.new_hosts[host_id].status = "DOWN" self.new_hosts[host_id].status_information = trigger[0]['hosts'][0]['snmp_error'] self.new_hosts[host_id].duration = human_readable_duration_from_timestamp(trigger[0]['hosts'][0]['snmp_errors_from']) #new api shows host interfaces status in hostinterfaces object else: #get all host interfaces hostinterfaces = self.zlapi.do_request("hostinterface.get", {"hostids": host_id}) #check them all and mark host as DOWN on first not available interface for hostinterface in hostinterfaces: if hostinterface.get('available', '0') == "2": self.new_hosts[host_id].status = "DOWN" self.new_hosts[host_id].status_information = hostinterface['error'] self.new_hosts[host_id].duration = human_readable_duration_from_timestamp(hostinterface['errors_from']) #we stop checking rest of interfaces break #service to report self.new_hosts[host_id].services[service_id] = GenericService() self.new_hosts[host_id].services[service_id].host = trigger[0]['hosts'][0]['name'] self.new_hosts[host_id].services[service_id].status = self.statemap.get(problem['severity'], problem['severity']) self.new_hosts[host_id].services[service_id].duration = human_readable_duration_from_timestamp(problem['clock']) self.new_hosts[host_id].services[service_id].name = trigger[0]['items'][0]['key_'] self.new_hosts[host_id].services[service_id].last_check = time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(int(trigger[0]['items'][0]['lastclock']))) #we add opdata to status information just like in zabbix GUI if problem["opdata"] != "": self.new_hosts[host_id].services[service_id].status_information = problem['name'] + " (" + problem["opdata"] + ")" else: self.new_hosts[host_id].services[service_id].status_information = problem['name'] #service is acknowledged if problem['acknowledged'] == "1": self.new_hosts[host_id].services[service_id].acknowledged = True except ZabbixLightApiException: # set checking flag back to False self.isChecking = False result, error = self.error(sys.exc_info()) return Result(result=result, error=error) return Result() # Disable set_recheck (nosense in Zabbix) def set_recheck(self, info_dict): pass Nagstamon-master/Nagstamon/Servers/Zenoss.py000066400000000000000000000177701505160700500215240ustar00rootroot00000000000000īģŋ#!/usr/bin/python # adapted from the zabbix.py code # Copyright (C) 2016 Jake Murphy Far Edge Technology # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import urllib import base64 import time import copy import datetime import traceback from datetime import datetime from Nagstamon.config import conf from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.thirdparty.zenoss_api import ZenossAPI class ZenossServer(GenericServer): TYPE = 'Zenoss' zapi = None SEVERITY_MAP = {0: 'OK', 1: 'UNKNOWN', 2: 'UNKNOWN', 3: 'WARNING', 4: 'WARNING', 5: 'CRITICAL'} MENU_ACTIONS = ['Monitor', 'Acknowledge'] def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon self.urls = {} self.statemap = {} self.server = Server() if ":" in conf.servers[self.get_name()].monitor_url: self.server.server_url, self.server.server_port = conf.servers[self.get_name()].monitor_url.split(':') else: self.server.server_url = conf.servers[self.get_name()].monitor_url self.server.server_port = 8080 #the default is 8080 self.server.username = conf.servers[self.get_name()].username self.server.password = conf.servers[self.get_name()].password # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Acknowledge"] def _zlogin(self): try: self.zapi = ZenossAPI(Server=self.server) except Exception: result, error = self.error(sys.exc_info()) return Result(result=result, error=error) def _get_status(self): nagitems = {"services":[], "hosts":[]} self.new_hosts = dict() try: hosts = self._get_all_events() if 'events' in hosts: hosts = hosts['events'] for host in hosts: n = dict() n['evid'] = host['evid'] n['host'] = host['device']['text'] n['service'] = host['eventClass']['text'] n['status'] = self.SEVERITY_MAP.get(host['severity']) n['last_check'] = host['lastTime'] duration = self._calc_duration(host['firstTime'], host['lastTime']) if (duration == None): continue #Zenoss needs a length to cause an error n['duration'] = duration n["status_information"] = host['message'] n["attempt"] = str(host['count'])+"/1" # needs a / with a number on either side to work n["passiveonly"] = False n["notifications_disabled"] = False n["flapping"] = False n["acknowledged"] = (host['eventState'] == 'Acknowledged') n["scheduled_downtime"] = False nagitems["hosts"].append(n) new_host = n["host"] if not new_host in self.new_hosts: self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = new_host if not new_host in self.new_hosts[new_host].services: new_service = new_host self.new_hosts[new_host].services[new_service] = GenericService() self.new_hosts[new_host].services[new_service].host = new_host self.new_hosts[new_host].services[new_service].evid = n['evid'] self.new_hosts[new_host].services[new_service].name = n["service"] self.new_hosts[new_host].services[new_service].server = self.name self.new_hosts[new_host].services[new_service].status = n["status"] self.new_hosts[new_host].services[new_service].last_check = n["last_check"] self.new_hosts[new_host].services[new_service].duration = n["duration"] self.new_hosts[new_host].services[new_service].status_information= n["status_information"].encode("utf-8") self.new_hosts[new_host].services[new_service].attempt = n["attempt"] self.new_hosts[new_host].services[new_service].passiveonly = n["passiveonly"] self.new_hosts[new_host].services[new_service].notifications_disabled = n["notifications_disabled"] self.new_hosts[new_host].services[new_service].flapping = n["flapping"] self.new_hosts[new_host].services[new_service].acknowledged = n["acknowledged"] self.new_hosts[new_host].services[new_service].scheduled_downtime = n["scheduled_downtime"] del n except: self.isChecking = False result, error = self.error(sys.exc_info()) print(traceback.format_exc()) return Result(result=result, error=error) del nagitems return Result(error="") def get_username(self): return str(self.server.username) def get_password(self): return str(self.server.password) def set_acknowledge(self, info_dict): if info_dict['host'] in self.hosts: evid = self.hosts[info_dict['host']].services[info_dict['host']].evid self.zapi.set_event_ack(evid) def _open_browser(self, url): webbrowser.open(self.monitor_url) def _get_all_events(self): if self.zapi is None: self._zlogin() events = self.zapi.get_event() return events ##http://stackoverflow.com/questions/538666/python-format-timedelta-to-string def _calc_duration(self, startStr, endStr): # like: '2016-10-2213: 53: 43' (that day/hour gap..) start = datetime.strptime(startStr, '%Y-%m-%d %H:%M:%S') end = datetime.strptime(endStr, '%Y-%m-%d %H:%M:%S') sec = (int)((end - start).total_seconds()) days, rem = divmod(sec, 60*60*24) hours, rem = divmod(rem, 60*60) mins, sec = divmod(rem, 60) if (days == 0 and hours == 0 and mins == 0 and sec == 0): return None return '%sd %sh %sm %ss' % (days,hours,mins,sec) #Note these methods are invalid for the zenoss api that this uses def set_recheck(self, info_dict): pass def set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): pass def set_submit_check_result(self, info_dict): pass def get_start_end(self, host): pass class Server: #server object for api configuration connections def __init__(self): self.server_url = "" self.server_port = "" self.username = "" self.password = "" Nagstamon-master/Nagstamon/Servers/__init__.py000066400000000000000000000226601505160700500217740ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module Servers""" import urllib.request import urllib.error import urllib.parse from collections import OrderedDict # load all existing server types from Nagstamon.Servers.Nagios import NagiosServer from Nagstamon.Servers.Centreon import CentreonServer from Nagstamon.Servers.Icinga import IcingaServer from Nagstamon.Servers.IcingaWeb2 import IcingaWeb2Server from Nagstamon.Servers.IcingaDBWeb import IcingaDBWebServer from Nagstamon.Servers.IcingaDBWebNotifications import IcingaDBWebNotificationsServer from Nagstamon.Servers.Icinga2API import Icinga2APIServer from Nagstamon.Servers.Multisite import MultisiteServer from Nagstamon.Servers.op5Monitor import Op5MonitorServer from Nagstamon.Servers.Opsview import OpsviewServer from Nagstamon.Servers.Thruk import ThrukServer from Nagstamon.Servers.Zabbix import ZabbixServer from Nagstamon.Servers.ZabbixProblemBased import ZabbixProblemBasedServer from Nagstamon.Servers.Livestatus import LivestatusServer from Nagstamon.Servers.Zenoss import ZenossServer from Nagstamon.Servers.Monitos3 import Monitos3Server from Nagstamon.Servers.Monitos4x import Monitos4xServer from Nagstamon.Servers.SnagView3 import SnagViewServer from Nagstamon.Servers.Sensu import SensuServer from Nagstamon.Servers.SensuGo import SensuGoServer from Nagstamon.Servers.Prometheus import PrometheusServer from Nagstamon.Servers.Alertmanager import AlertmanagerServer from Nagstamon.config import conf from Nagstamon.helpers import STATES # dictionary for servers servers = OrderedDict() # contains dict with available server classes # key is type of server, value is server class # used for automatic config generation # and holding this information in one place SERVER_TYPES = OrderedDict() def register_server(server): """ Once new server class is created, should be registered with this function for being visible in config and accessible in application. """ if server.TYPE not in SERVER_TYPES: SERVER_TYPES[server.TYPE] = server def get_enabled_servers(): """ list of enabled servers which connections outside should be used to check """ return [x for x in servers.values() if x.enabled is True] def get_worst_status(): """ get worst status of all servers """ worst_status = 'UP' for server in get_enabled_servers(): worst_status_current = server.get_worst_status_current() if STATES.index(worst_status_current) > STATES.index(worst_status): worst_status = worst_status_current del worst_status_current return worst_status def get_status_count(): """ get all states of all servers and count them """ state_count = {'UNKNOWN': 0, 'INFORMATION': 0, 'WARNING': 0, 'AVERAGE': 0, 'HIGH': 0, 'CRITICAL': 0, 'DISASTER': 0, 'UNREACHABLE': 0, 'DOWN': 0} for server in get_enabled_servers(): state_count['UNKNOWN'] += server.unknown state_count['INFORMATION'] += server.information state_count['WARNING'] += server.warning state_count['AVERAGE'] += server.average state_count['HIGH'] += server.high state_count['CRITICAL'] += server.critical state_count['DISASTER'] += server.disaster state_count['UNREACHABLE'] += server.unreachable state_count['DOWN'] += server.down return state_count def get_errors(): """ find out if any server has any error, used by statusbar error label """ for server in get_enabled_servers(): if server.has_error: return True break # return default value return False def create_server(server=None): # create Server from config if server.type not in SERVER_TYPES: print(('Server type not supported: %s' % server.type)) return # give argument servername so CentreonServer could use it for initializing MD5 cache new_server = SERVER_TYPES[server.type](name=server.name) # apparently somewhat hacky but at the end works - might be used for others than Centreon as well if hasattr(new_server, 'ClassServerReal'): new_server = new_server.ClassServerReal(name=server.name) new_server.type = server.type new_server.enabled = server.enabled new_server.monitor_url = server.monitor_url new_server.monitor_cgi_url = server.monitor_cgi_url new_server.username = server.username new_server.password = server.password new_server.use_proxy = server.use_proxy new_server.use_proxy_from_os = server.use_proxy_from_os new_server.proxy_address = server.proxy_address new_server.proxy_username = server.proxy_username new_server.proxy_password = server.proxy_password new_server.authentication = server.authentication new_server.timeout = server.timeout # SSL/TLS new_server.ignore_cert = server.ignore_cert new_server.custom_cert_use = server.custom_cert_use new_server.custom_cert_ca_file = server.custom_cert_ca_file # ECP authentication new_server.idp_ecp_endpoint = server.idp_ecp_endpoint # if password is not to be saved ask for it at startup if (server.enabled is True and server.save_password is False and server.use_autologin is False): new_server.refresh_authentication = True # Special FX # Centreon new_server.use_autologin = server.use_autologin new_server.autologin_key = server.autologin_key # Icinga new_server.use_display_name_host = server.use_display_name_host new_server.use_display_name_service = server.use_display_name_service # IcingaWeb2 new_server.no_cookie_auth = server.no_cookie_auth # IcingaDBWebNotifications new_server.notification_filter = server.notification_filter new_server.notification_lookback = server.notification_lookback # Checkmk Multisite new_server.force_authuser = server.force_authuser new_server.checkmk_view_hosts = server.checkmk_view_hosts new_server.checkmk_view_services = server.checkmk_view_services # OP5 api filters new_server.host_filter = server.host_filter new_server.service_filter = server.service_filter # Opsview hashtag filter and can_change_only option new_server.hashtag_filter = server.hashtag_filter new_server.can_change_only = server.can_change_only # Zabbix new_server.use_description_name_service = server.use_description_name_service # Prometheus & Alertmanager new_server.alertmanager_filter = server.alertmanager_filter new_server.map_to_hostname = server.map_to_hostname new_server.map_to_servicename = server.map_to_servicename new_server.map_to_status_information = server.map_to_status_information new_server.map_to_ok = server.map_to_ok new_server.map_to_unknown = server.map_to_unknown new_server.map_to_warning = server.map_to_warning new_server.map_to_critical = server.map_to_critical new_server.map_to_down = server.map_to_down # Thruk new_server.disabled_backends = server.disabled_backends # server's individual preparations for HTTP connections (for example cookie creation) # is done in GetStatus() method of monitor if server.enabled is True: new_server.enabled = True # start with high thread counter so server update thread does not have to wait new_server.thread_counter = conf.update_interval_seconds # debug if conf.debug_mode is True: new_server.debug(server=server.name, debug="Created server.") return new_server # moved registration process here because of circular dependencies servers_list = [AlertmanagerServer, CentreonServer, IcingaServer, IcingaDBWebServer, IcingaDBWebNotificationsServer, IcingaWeb2Server, Icinga2APIServer, LivestatusServer, Monitos3Server, Monitos4xServer, MultisiteServer, NagiosServer, Op5MonitorServer, OpsviewServer, PrometheusServer, SensuGoServer, SensuServer, SnagViewServer, ThrukServer, ZabbixProblemBasedServer, ZabbixServer, ZenossServer] for server in servers_list: register_server(server) # create servers for server in conf.servers.values(): created_server = create_server(server) if created_server is not None: servers[server.name] = created_server # for the next time no auth needed servers[server.name].refresh_authentication = False Nagstamon-master/Nagstamon/Servers/op5Monitor.py000066400000000000000000000402411505160700500223030ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import json import time from datetime import datetime from Nagstamon.helpers import webbrowser_open from Nagstamon.objects import (GenericHost, GenericService, Result) from Nagstamon.Servers.Generic import GenericServer from Nagstamon.config import BOOLPOOL def human_duration(start): """ transform timestamp to human readable some changes necessary due to https://github.com/HenriWahl/Nagstamon/issues/93 - move definition of stop out of def() statement because it kept static """ stop = time.time() if stop <= start: return "n/a" else: ret = '' first = True secs = stop - start units = 'wdhms' divisors = {'w': 86400 * 7, 'd': 86400, 'h': 3600, 'm': 60, 's': 1} for unit in units: divisor = divisors[unit] if secs < divisor: continue amount = int(secs / divisor) secs %= divisor if not first: ret += ' ' ret += "%d%c" % (amount, unit) first = False return ret class Op5MonitorServer(GenericServer): """ object of Nagios server - when nagstamon will be able to poll various servers this will be useful As Nagios is the default server type all its methods are in GenericServer """ TYPE = 'op5Monitor' api_count='/api/filter/count/?query=' api_query='/api/filter/query/?query=' api_cmd='/api/command' api_svc_col = [] api_host_col = [] api_host_col.append('acknowledged') api_host_col.append('active_checks_enabled') api_host_col.append('alias') api_host_col.append('current_attempt') api_host_col.append('is_flapping') api_host_col.append('last_check') api_host_col.append('last_state_change') api_host_col.append('max_check_attempts') api_host_col.append('name') api_host_col.append('notifications_enabled') api_host_col.append('plugin_output') api_host_col.append('scheduled_downtime_depth') api_host_col.append('state') api_host_col.append('groups') api_svc_col.append('acknowledged') api_svc_col.append('active_checks_enabled') api_svc_col.append('current_attempt') api_svc_col.append('description') api_svc_col.append('host.name') api_svc_col.append('host.state') api_svc_col.append('host.active_checks_enabled') api_svc_col.append('host.scheduled_downtime_depth') api_svc_col.append('is_flapping') api_svc_col.append('last_check') api_svc_col.append('last_state_change') api_svc_col.append('max_check_attempts') api_svc_col.append('notifications_enabled') api_svc_col.append('plugin_output') api_svc_col.append('scheduled_downtime_depth') api_svc_col.append('state') api_svc_col.append('host.groups') # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = { "monitor": "$MONITOR$/monitor",\ "hosts": "$MONITOR$/monitor/index.php/listview?q=%s" % '[hosts] all and state != 0'.replace(" ", "%20"),\ "services": "$MONITOR$/monitor/index.php/listview?q=%s" % '[services] all and state != 0'.replace(" ", "%20"),\ "history": "$MONITOR$/monitor/index.php/alert_history/generate"} def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Downtime"] self.STATUS_SVC_MAPPING = {'0':'OK', '1':'WARNING', '2':'CRITICAL', '3':'UNKNOWN'} self.STATUS_HOST_MAPPING = {'0':'UP', '1':'DOWN', '2':'UNREACHABLE'} # Op5Monitor gives a 500 when auth is wrong self.STATUS_CODES_NO_AUTH.append(500) def _get_status(self): """ Get status from op5 Monitor Server """ # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily nagitems = {"hosts":[], "services":[]} # new_hosts dictionary self.new_hosts = dict() # Fetch api listview with filters try: # Fetch Host info api_default_host_query='[hosts] %s ' % self.host_filter api_default_host_query+='&columns=%s' % (','.join(self.api_host_col)) api_default_host_query+='&format=json' api_default_host_query = api_default_host_query.replace(" ", "%20") result = self.fetch_url(self.monitor_url + self.api_count + api_default_host_query, giveback="raw") data, error, status_code = json.loads(result.result),\ result.error,\ result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured if data['count']: count = data['count'] api_default_host_query='[hosts] %s ' % self.host_filter api_default_host_query+='&columns=%s' % (','.join(self.api_host_col)) api_default_host_query+='&format=json' api_default_host_query = api_default_host_query.replace(" ", "%20") result = self.fetch_url(self.monitor_url + self.api_query + api_default_host_query + '&limit=' + str(count), giveback="raw") data = json.loads(result.result) n = dict() for api in data: n['host'] = api['name'] n["acknowledged"] = BOOLPOOL[api['acknowledged']] n["flapping"] = BOOLPOOL[api['is_flapping']] n["notifications_disabled"] = False if api['notifications_enabled'] else True n["passiveonly"] = False if api['active_checks_enabled'] else True n["scheduled_downtime"] = True if api['scheduled_downtime_depth'] else False n['attempt'] = "%s/%s" % (str(api['current_attempt']), str(api['max_check_attempts'])) n['duration'] = human_duration(api['last_state_change']) n['last_check'] = datetime.fromtimestamp(int(api['last_check'])).strftime('%Y-%m-%d %H:%M:%S') n['status'] = self.STATUS_HOST_MAPPING[str(api['state'])] n['status_information'] = api['plugin_output'] n['status_type'] = api['state'] n['groups'] = str(api['groups']) if not n['host'] in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].acknowledged = n["acknowledged"] self.new_hosts[n['host']].attempt = n['attempt'] self.new_hosts[n['host']].duration = n['duration'] self.new_hosts[n['host']].flapping = n["flapping"] self.new_hosts[n['host']].last_check = n['last_check'] self.new_hosts[n['host']].notifications_disabled = n["notifications_disabled"] self.new_hosts[n['host']].passiveonly = n["passiveonly"] self.new_hosts[n['host']].scheduled_downtime = n["scheduled_downtime"] self.new_hosts[n['host']].status = n['status'] self.new_hosts[n['host']].status_information = n['status_information'].replace("\n", " ").strip() self.new_hosts[n['host']].status_type = n['status_type'] self.new_hosts[n['host']].groups = n['groups'] nagitems['hosts'].append(n) del n # Fetch services info api_default_svc_query='[services] %s ' % self.service_filter api_default_svc_query+='&columns=%s' % (','.join(self.api_svc_col)) api_default_svc_query+='&format=json' api_default_svc_query = api_default_svc_query.replace(" ", "%20") result = self.fetch_url(self.monitor_url + self.api_count + api_default_svc_query, giveback="raw") data, error, status_code = json.loads(result.result),\ result.error,\ result.status_code # check if any error occured errors_occured = self.check_for_error(data, error, status_code) # if there are errors return them if errors_occured is not None: return errors_occured if data['count']: count = data['count'] api_default_svc_query='[services] %s ' % self.service_filter api_default_svc_query+='&columns=%s' % (','.join(self.api_svc_col)) api_default_svc_query+='&format=json' api_default_svc_query = api_default_svc_query.replace(" ", "%20") result = self.fetch_url(self.monitor_url + self.api_query + api_default_svc_query + '&limit=' + str(count), giveback="raw") data = json.loads(result.result) for api in data: n = dict() n['host'] = api['host']['name'] n['status'] = self.STATUS_HOST_MAPPING[str(api['host']['state'])] n["passiveonly"] = False if api['host']['active_checks_enabled'] else True if not n['host'] in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = n['status'] self.new_hosts[n['host']].passiveonly = n["passiveonly"] n['service'] = api['description'] n["acknowledged"] = BOOLPOOL[api['acknowledged']] n["flapping"] = BOOLPOOL[api['is_flapping']] n["notifications_disabled"] = False if api['notifications_enabled'] else True n["passiveonly"] = False if api['active_checks_enabled'] else True n["scheduled_downtime"] = True if api['scheduled_downtime_depth'] or api['host']['scheduled_downtime_depth'] else False n['attempt'] = "%s/%s" % (str(api['current_attempt']), str(api['max_check_attempts'])) n['duration'] = human_duration(api['last_state_change']) n['last_check'] = datetime.fromtimestamp(int(api['last_check'])).strftime('%Y-%m-%d %H:%M:%S') n['status_information'] = api['plugin_output'] n['groups'] = str(api['host']['groups']) if not n['host'] in self.new_hosts: self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = n['status'] if not n['service'] in self.new_hosts[n['host']].services: n['status'] = self.STATUS_SVC_MAPPING[str(api['state'])] self.new_hosts[n['host']].services[n['service']] = GenericService() self.new_hosts[n['host']].services[n['service']].acknowledged = n['acknowledged'] self.new_hosts[n['host']].services[n['service']].attempt = n['attempt'] self.new_hosts[n['host']].services[n['service']].duration = n['duration'] self.new_hosts[n['host']].services[n['service']].flapping = n['flapping'] self.new_hosts[n['host']].services[n['service']].host = n['host'] self.new_hosts[n['host']].services[n['service']].last_check = n['last_check'] self.new_hosts[n['host']].services[n['service']].name = n['service'] self.new_hosts[n['host']].services[n['service']].notifications_disabled = n["notifications_disabled"] self.new_hosts[n['host']].services[n['service']].passiveonly = n['passiveonly'] self.new_hosts[n['host']].services[n['service']].scheduled_downtime = n['duration'] self.new_hosts[n['host']].services[n['service']].scheduled_downtime = n['scheduled_downtime'] self.new_hosts[n['host']].services[n['service']].status = n['status'] self.new_hosts[n['host']].services[n['service']].status_information = n['status_information'].replace("\n", " ").strip() self.new_hosts[n['host']].services[n['service']].groups = n['groups'] nagitems['services'].append(n) return Result() except: self.isChecking = False # store status_code for returning result to tell GUI to reauthenticate status_code = result.status_code result, error = self.error(sys.exc_info()) return Result(result=result, error=error, status_code=status_code) return Result() def open_monitor(self, host, service): if not service: url = "%s/monitor/index.php/extinfo/details?host=%s" % (self.monitor_url, host) else: url = "%s/monitor/index.php/extinfo/details?host=%s&service=%s" % (self.monitor_url, host, service) webbrowser_open(url) def get_start_end(self, host): return time.strftime("%Y-%m-%d %H:%M"), time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() + 7200)) def send_command(self, command, params=False): url = self.monitor_url + self.api_cmd + '/' + command self.fetch_url(url, cgi_data=params, giveback='raw') def _set_recheck(self, host, service): params = {'host_name': host, 'check_time': int(time.time())} if not service: command = 'SCHEDULE_HOST_CHECK' else: if self.hosts[host].services[service].is_passive_only(): return command = 'SCHEDULE_SVC_CHECK' params['service_description'] = service self.send_command(command, params) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=None): params = {'host_name': host, 'sticky': int(sticky), 'notify': int(notify), 'persistent': int(persistent), 'comment': comment} if not service: command = 'ACKNOWLEDGE_HOST_PROBLEM' else: params['service_description'] = service command = 'ACKNOWLEDGE_SVC_PROBLEM' self.send_command(command, params) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): start_time = int(time.mktime(time.strptime(start_time, "%Y-%m-%d %H:%M"))) end_time = int(time.mktime(time.strptime(end_time, "%Y-%m-%d %H:%M"))) duration = end_time - start_time params = {'host_name': host, 'comment': comment, 'fixed': fixed, 'trigger_id': '0', 'start_time': start_time, 'end_time': end_time, 'duration': duration} if not service: command = 'SCHEDULE_HOST_DOWNTIME' else: command = 'SCHEDULE_SVC_DOWNTIME' params['service_description'] = service self.send_command(command, params) Nagstamon-master/Nagstamon/__init__.py000066400000000000000000000015441505160700500203410ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module Nagstamon""" Nagstamon-master/Nagstamon/config.py000066400000000000000000001733721505160700500200600ustar00rootroot00000000000000#!/usr/bin/env python3 # encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from argparse import ArgumentParser from importlib import resources import os import platform import sys import configparser import base64 import zlib import datetime from urllib.parse import quote from collections import OrderedDict from pathlib import Path # older Kubuntu has trouble with keyring # see https://github.com/HenriWahl/Nagstamon/issues/447 # necessary to avoid any import because might result in a segmentation fault KEYRING = True if platform.system() == 'Linux': if 'ubuntu' in platform.platform().lower(): if 'XDG_SESSION_DESKTOP' in os.environ: if os.environ['XDG_SESSION_DESKTOP'].lower() == 'kde': KEYRING = False if KEYRING: import keyring # instead of calling platform.system() every now and then just do it once here OS = platform.system() # needed when OS-specific decisions have to be made, mostly Linux/non-Linux OS_MACOS = 'Darwin' OS_WINDOWS = 'Windows' OS_NON_LINUX = (OS_MACOS, OS_WINDOWS) # simple Wayland detection if 'WAYLAND_DISPLAY' in os.environ or \ 'XDG_SESSION_TYPE' in os.environ and os.environ['XDG_SESSION_TYPE'] == 'wayland': # dirty workaround to activate X11 support in Wayland environment - just a test if not os.environ.get('QT_QPA_PLATFORM'): os.environ['QT_QPA_PLATFORM'] = 'xcb' DESKTOP_WAYLAND = True else: DESKTOP_WAYLAND = False # detection of somehow quirky desktop enviroments which might need a fix QUIRKY_DESKTOPS = ('cinnamon', 'gnome-flashback-metacity') if os.environ.get('CINNAMON_VERSION') or \ os.environ.get('DESKTOP_SESSION') in QUIRKY_DESKTOPS or \ os.environ.get('XDG_SESSION_DESKTOP') in QUIRKY_DESKTOPS: DESKTOP_NEEDS_FIX = True else: DESKTOP_NEEDS_FIX = False # use QT platform plugins if not set otherwise on GNOME desktop # based on QGnomePlatform if not os.environ.get('QT_QPA_PLATFORMTHEME'): if os.environ.get('XDG_SESSION_DESKTOP'): if os.environ['XDG_SESSION_DESKTOP'].lower() == 'gnome': os.environ['QT_QPA_PLATFORMTHEME'] = 'gnome' # queue.Queue() needs threading module which might be not such a good idea to be used # because QThread is already in use # maybe not the most logical place here to be defined but at least all # modules access config.py so it can be distributed from here debug_queue = list() # temporary dict for string-to-bool-conversion # the bool:bool relations are thought to make things easier in Dialog_Settings.ok() BOOLPOOL = {'False': False, 'True': True, False: False, True: True} # config settings which should always be strings, never converted to integer or bool CONFIG_STRINGS = ['custom_browser', 'debug_file', 'notification_custom_sound_warning', 'notification_custom_sound_critical', 'notification_custom_sound_down', 'notification_action_warning_string', 'notification_action_critical_string', 'notification_action_down_string', 'notification_action_ok_string', 'notification_custom_action_string', 'notification_custom_action_separator', 're_host_pattern', 're_service_pattern', 're_status_information_pattern', 're_duration_pattern', 're_attempt_pattern', 're_groups_pattern', 're_criticality_pattern', 'font', 'defaults_acknowledge_comment', 'defaults_submit_check_result_comment', 'defaults_downtime_comment', 'name', 'monitor_url', 'monitor_cgi_url', 'username', 'password', 'proxy_address', 'proxy_username', 'proxy_password', 'autologin_key', 'custom_cert_ca_file', 'idp_ecp_endpoint', 'monitor_site' ] class AppInfo: """ contains app information previously located in GUI.py """ NAME = 'Nagstamon' VERSION = '3.17-20250821' WEBSITE = 'https://nagstamon.de' COPYRIGHT = 'Š2008-2025 Henri Wahl et al.' COMMENTS = 'Nagios status monitor for your desktop' # dict of servers to offer for downloads if an update is available DOWNLOAD_SERVERS = {'nagstamon.de': 'https://github.com/HenriWahl/Nagstamon/releases'} # version URL depends on version string if 'alpha' in VERSION.lower() or \ 'beta' in VERSION.lower() or \ 'rc' in VERSION.lower() or \ '-' in VERSION.lower(): VERSION_URL = WEBSITE + '/version/unstable' VERSION_PATH = '/version/unstable' else: VERSION_URL = WEBSITE + '/version/stable' VERSION_PATH = '/version/stable' class Config: """ the place for central configuration. """ def __init__(self): """ read config file and set the appropriate attributes supposed to be sensible defaults """ # move from minute interval to seconds self.update_interval_seconds = 60 self.short_display = False self.long_display = True self.show_tooltips = True self.show_grid = True self.grid_use_custom_intensity = False self.grid_alternation_intensity = 10 self.highlight_new_events = True self.default_sort_field = 'status' self.default_sort_order = 'descending' self.filter_all_down_hosts = False self.filter_all_unreachable_hosts = False self.filter_all_unreachable_services = False self.filter_all_flapping_hosts = False self.filter_all_unknown_services = False self.filter_all_information_services = False self.filter_all_warning_services = False self.filter_all_average_services = False self.filter_all_high_services = False self.filter_all_critical_services = False self.filter_all_disaster_services = False self.filter_all_flapping_services = False self.filter_acknowledged_hosts_services = False self.filter_hosts_services_disabled_notifications = False self.filter_hosts_services_disabled_checks = False self.filter_hosts_services_maintenance = False self.filter_services_on_acknowledged_hosts = False self.filter_services_on_down_hosts = False self.filter_services_on_hosts_in_maintenance = False self.filter_services_on_unreachable_hosts = False self.filter_hosts_in_soft_state = False self.filter_services_in_soft_state = False self.position_x = 30 self.position_y = 30 self.position_width = 640 self.position_height = 480 self.popup_details_hover = True self.popup_details_clicking = False self.close_details_hover = True self.close_details_clicking = False self.close_details_clicking_somewhere = False self.connect_by_host = True self.connect_by_dns = False self.connect_by_ip = False self.use_default_browser = True self.use_custom_browser = False self.custom_browser = '' self.debug_mode = False self.debug_to_file = False self.debug_file = os.path.expanduser('~') + os.sep + "nagstamon.log" self.check_for_new_version = True self.notification = True self.notification_flashing = True self.notification_desktop = False self.notification_actions = False self.notification_sound = True self.notification_sound_repeat = False self.notification_default_sound = True self.notification_custom_sound = False self.notification_custom_sound_warning = '' self.notification_custom_sound_critical = '' self.notification_custom_sound_down = '' self.notification_action_warning = False self.notification_action_warning_string = '' self.notification_action_critical = False self.notification_action_critical_string = '' self.notification_action_down = False self.notification_action_down_string = '' self.notification_action_ok = False self.notification_action_ok_string = '' self.notification_custom_action = False self.notification_custom_action_string = '' self.notification_custom_action_separator = '|' self.notification_custom_action_single = True self.notify_if_up = False self.notify_if_information = True self.notify_if_warning = True self.notify_if_average = True self.notify_if_high = True self.notify_if_critical = True self.notify_if_disaster = True self.notify_if_unknown = True self.notify_if_unreachable = True self.notify_if_down = True # Regular expression filters self.re_host_enabled = False self.re_host_pattern = '' self.re_host_reverse = False self.re_service_enabled = False self.re_service_pattern = '' self.re_service_reverse = False self.re_status_information_enabled = False self.re_status_information_pattern = '' self.re_status_information_reverse = False self.re_duration_enabled = False self.re_duration_pattern = '' self.re_duration_reverse = False self.re_attempt_enabled = False self.re_attempt_pattern = '' self.re_attempt_reverse = False self.re_groups_enabled = False self.re_groups_pattern = '' self.re_groups_reverse = False self.color_ok_text = self.default_color_ok_text = '#FFFFFF' self.color_ok_background = self.default_color_ok_background = '#006400' self.color_information_text = self.default_color_information_text = "#000000" self.color_information_background = self.default_color_information_background = "#7499FF" self.color_warning_text = self.default_color_warning_text = "#000000" self.color_warning_background = self.default_color_warning_background = '#FFFF00' self.color_average_text = self.default_color_average_text = "#000000" self.color_average_background = self.default_color_average_background = "#FFA059" self.color_high_text = self.default_color_high_text = "#000000" self.color_high_background = self.default_color_high_background = "#E97659" self.color_unknown_text = self.default_color_unknown_text = '#000000' self.color_unknown_background = self.default_color_unknown_background = '#FFA500' self.color_critical_text = self.default_color_critical_text = '#FFFFFF' self.color_critical_background = self.default_color_critical_background = '#FF0000' self.color_disaster_text = self.default_color_disaster_text = "#FFFFFF" self.color_disaster_background = self.default_color_disaster_background = "#660000" self.color_unreachable_text = self.default_color_unreachable_text = '#FFFFFF' self.color_unreachable_background = self.default_color_unreachable_background = '#8B0000' self.color_down_text = self.default_color_down_text = '#FFFFFF' self.color_down_background = self.default_color_down_background = '#000000' self.color_error_text = self.default_color_error_text = '#000000' self.color_error_background = self.default_color_error_background = '#D3D3D3' self.statusbar_floating = True self.icon_in_systray = False self.windowed = False self.fullscreen = False self.fullscreen_display = 0 self.systray_offset_use = False self.systray_offset = 10 self.hide_macos_dock_icon = False # as default enable on Linux Desktops like Cinnamon and Gnome Flashback if DESKTOP_NEEDS_FIX: self.enable_position_fix = True else: self.enable_position_fix = False self.font = '' self.defaults_acknowledge_sticky = False self.defaults_acknowledge_send_notification = False self.defaults_acknowledge_persistent_comment = False self.defaults_acknowledge_all_services = False self.defaults_acknowledge_expire = False self.defaults_acknowledge_expire_duration_hours = 2 self.defaults_acknowledge_expire_duration_minutes = 0 self.defaults_acknowledge_comment = 'acknowledged' self.defaults_submit_check_result_comment = 'check result submitted' self.defaults_downtime_duration_hours = 2 self.defaults_downtime_duration_minutes = 0 self.defaults_downtime_comment = 'scheduled downtime' self.defaults_downtime_type_fixed = True self.defaults_downtime_type_flexible = False # internal flag to determine if keyring is available at all - defaults to False # use_system_keyring is checked and defined some lines later after config file was read self.keyring_available = False # setting for keyring usage - might cause trouble on Linux so disable it there as default to avoid crash at start if OS in OS_NON_LINUX: self.use_system_keyring = True else: self.use_system_keyring = False # Special FX # Centreon self.re_criticality_enabled = False self.re_criticality_pattern = '' self.re_criticality_reverse = False # the app is unconfigured by default and will stay so if it # would not find a config file self.unconfigured = True # get CLI arguments parser = ArgumentParser(prog='nagstamon') # mitigate session restore problem https://github.com/HenriWahl/Nagstamon/issues/878 if len(sys.argv) == 2 or len(sys.argv) >= 6: # only add configdir if it might be included at all parser.add_argument('configdir', default=None) # -session and -name are used by X11 session management and could be # safely ignored because nagstamon keeps its own session info parser.add_argument('-session', default=None, required=False) parser.add_argument('-name', default=None, required=False) arguments, unknown_arguments = parser.parse_known_args() if 'configdir' in arguments: self.configdir = arguments.configdir # otherwise if there exists a configdir in current working directory it should be used elif os.path.exists(os.getcwd() + os.sep + 'nagstamon.config'): self.configdir = os.getcwd() + os.sep + 'nagstamon.config' else: # ~/.nagstamon/nagstamon.conf is the user conf file # os.path.expanduser('~') finds out the user HOME dir where # nagstamon expects its conf file to be self.configdir = os.path.expanduser('~') + os.sep + '.nagstamon' self.configfile = self.configdir + os.sep + 'nagstamon.conf' # make path fit for actual os, normcase for letters and normpath for path self.configfile = os.path.normpath(os.path.normcase(self.configfile)) # because the name of the configdir is also stored in the configfile # there may be situations where the name gets overwritten by a # wrong name so it will be stored here temporarily configdir_temp = self.configdir # default settings dicts self.servers = dict() self.actions = dict() if os.path.exists(self.configfile): # instantiate a configparser to parse the conf file # SF.net bug #3304423 could be fixed with allow_no_value argument which # is only available since Python 2.7 # since Python 3 '%' will be interpolated by default which crashes # with some URLs config = configparser.ConfigParser(allow_no_value=True, interpolation=None) config.read(self.configfile) # go through all sections of the conf file for section in config.sections(): # go through all items of each sections (in fact there is only on # section which has to be there to comply to the .INI file standard for i in config.items(section): # omit config file info as it makes no sense to store its path if not i[0] in ('configfile', 'configdir'): # create a key of every config item with its appropriate value # check first if it is a bool value and convert string if it is if i[1] in BOOLPOOL: object.__setattr__(self, i[0], BOOLPOOL[i[1]]) # in case there are numbers intify them to avoid later conversions # treat negative value specially as .isdecimal() will not detect it elif i[1].isdecimal() or \ (i[1].startswith('-') and i[1].split('-')[1].isdecimal()): object.__setattr__(self, i[0], int(i[1])) else: object.__setattr__(self, i[0], i[1]) # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property # and all the thousands 1.0 installations do not know it yet it will be more comfortable # for most of the Windows users if it is only defined as False after it was checked # from config file if 'use_system_keyring' not in self.__dict__.keys(): if self.unconfigured is True: # an unconfigured system should start with no keyring to prevent crashes self.use_system_keyring = False else: # a configured system seemed to be able to run and thus use system keyring if OS in OS_NON_LINUX: self.use_system_keyring = True else: self.use_system_keyring = self.KeyringAvailable() # reset self.configdir to temporarily saved value in case it differs from # the one read from configfile and so it would fail to save next time self.configdir = configdir_temp # Servers configuration... self.servers = self._load_servers_multiple_config() # ... and actions self.actions = self.load_multiple_config("actions", "action", "Action") # seems like there is a config file so the app is not unconfigured anymore self.unconfigured = False # Load actions if Nagstamon is not unconfigured, otherwise load defaults if self.unconfigured is True: self.actions = self._default_actions() # do some conversion stuff needed because of config changes and code cleanup self._legacy_adjustments() def _load_servers_multiple_config(self): """ Loads the servers configuration with special handling for obfuscated passwords. This method loads server configuration entries from files, handling deobfuscation of usernames and passwords as needed. It supports migration from legacy formats, integration with the system keyring for secure password storage, and conversion of server types and settings for compatibility with newer versions. Returns: dict: A dictionary mapping server names to their configuration objects. Error Handling: Any exceptions during loading or deobfuscation are caught, a stack trace is printed, and loading continues for other entries. Special Considerations: - Usernames and passwords are deobfuscated or retrieved from the system keyring if enabled. - Legacy server types and settings are migrated to current formats. - Handles both monitor and proxy credentials, as well as optional autologin keys. - Ensures server names are always strings for consistency. """ self.keyring_available = self.check_keyring_availability() servers = self.load_multiple_config('servers', 'server', 'Server') # deobfuscate username + password inside a try-except loop # if entries have not been obfuscated yet this action should raise an error # and old values (from nagstamon < 0.9.0) stay and will be converted when next # time saving config try: for server in servers: # server name needs to be a string at every cost # see issue https://github.com/HenriWahl/Nagstamon/issues/1010 servers[server].name = str(servers[server].name) # usernames for monitor server and proxy servers[server].username = self.deobfuscate(servers[server].username) servers[server].proxy_username = self.deobfuscate(servers[server].proxy_username) # passwords for monitor server and proxy if servers[server].save_password == 'False': servers[server].password = "" elif self.keyring_available and self.use_system_keyring: password = keyring.get_password('Nagstamon', '@'.join((servers[server].username, servers[server].monitor_url))) or "" if password == "": if servers[server].password != "": servers[server].password = self.deobfuscate(servers[server].password) else: servers[server].password = password elif servers[server].password != "": servers[server].password = self.deobfuscate(servers[server].password) # proxy password if self.keyring_available and self.use_system_keyring: proxy_password = keyring.get_password('Nagstamon', '@'.join(('proxy', servers[server].proxy_username, servers[server].proxy_address))) or "" if proxy_password == "": if servers[server].proxy_password != "": servers[server].proxy_password = self.deobfuscate(servers[server].proxy_password) else: servers[server].proxy_password = proxy_password elif servers[server].proxy_password != "": servers[server].proxy_password = self.deobfuscate(servers[server].proxy_password) # do only deobfuscating if any autologin_key is set - will be only Centreon/Thruk if 'autologin_key' in servers[server].__dict__.keys(): if len(servers[server].__dict__['autologin_key']) > 0: servers[server].autologin_key = self.deobfuscate(servers[server].autologin_key) # only needed for those who used Icinga2 before it became IcingaWeb2 if servers[server].type == 'Icinga2': servers[server].type = 'IcingaWeb2' # Check_MK is now Checkmk - same renaming as with IcingaWeb2 if servers[server].type == 'Check_MK Multisite': servers[server].type = 'Checkmk Multisite' if 'check_mk_view_hosts' in servers[server].__dict__.keys(): servers[server].checkmk_view_hosts = servers[server].check_mk_view_hosts servers[server].__dict__.pop('check_mk_view_hosts') if 'check_mk_view_services' in servers[server].__dict__.keys(): servers[server].checkmk_view_services = servers[server].check_mk_view_services servers[server].__dict__.pop('check_mk_view_services') except Exception: import traceback traceback.print_exc(file=sys.stdout) return servers def load_multiple_config(self, settingsdir, setting, configobj): """ Loads configuration entries from files in a given settings directory and returns them as a dictionary. This method scans the specified directory for configuration files matching the expected naming pattern, parses each file, and creates an object for each configuration entry. It converts string values to booleans or integers where appropriate and handles legacy file cleanup if necessary. Args: settingsdir (str): Name of the subdirectory containing the configuration files (e.g., 'servers' or 'actions'). setting (str): Prefix used in the configuration file names (e.g., 'server' or 'action'). configobj (str): Name of the class to instantiate for each configuration entry. Returns: OrderedDict: A dictionary mapping configuration entry names to their corresponding objects. Error Handling: Any exceptions during loading are caught, a stack trace is printed, and loading continues for other files. Special Considerations: - Only files matching the expected naming pattern are processed. - Legacy or duplicate files are deleted if found. - String values are converted to bool or int where possible for consistency. """ # defaults as empty dict in case settings dir/files could not be found settings = OrderedDict() try: if os.path.exists(self.configdir + os.sep + settingsdir): for settingsfile in sorted(os.listdir(self.configdir + os.sep + settingsdir)): if settingsfile.startswith(setting + '_') and settingsfile.endswith('.conf'): config = configparser.ConfigParser(allow_no_value=True, interpolation=None) config.read(self.configdir + os.sep + settingsdir + os.sep + settingsfile) # create object for every setting name = config.get(config.sections()[0], 'name') settings[name] = globals()[configobj]() # go through all items of the server for i in config.items(config.sections()[0]): # create a key of every config item with its appropriate value if i[1] in BOOLPOOL: value = BOOLPOOL[i[1]] # in case there are numbers intify them to avoid later conversions # treat negative value specially as .isdecimal() will not detect it elif i[1].isdecimal() or \ (i[1].startswith('-') and i[1].split('-')[1].isdecimal()): value = int(i[1]) else: value = i[1] settings[name].__setattr__(i[0], value) # if filename is still one of the non-URL-ones delete duplicate file if settingsfile != '{0}_{1}.conf'.format(setting, quote(name, safe='')): self.delete_file(settingsdir, settingsfile) # set flag to store the settings via legacy adjustments self.save_config_after_urlencode = True except Exception: import traceback traceback.print_exc(file=sys.stdout) return settings def save_config(self): """ Saves the current configuration to the configuration file. This method writes all current settings of the Config object to the main configuration file, as well as to separate files for server and action configurations. Before saving, it ensures that the configuration directory exists. Passwords and sensitive data are encrypted or stored in the system keyring, depending on the platform and settings. Error Handling: Exceptions during saving are caught, a stack trace is printed, and debug information is logged if debug mode is enabled. Special Considerations: - The configuration directory is created if it does not exist. - Server and action configurations are saved in their own subdirectories. - Use of the system keyring is considered based on platform and configuration. - Debug information is logged when debug mode is active. """ try: # Make sure .nagstamon is created if not os.path.exists(self.configdir): os.makedirs(self.configdir) # save config file with configparser config = configparser.ConfigParser(allow_no_value=True, interpolation=None) # general section for Nagstamon config.add_section('Nagstamon') for option in self.__dict__: if option not in ['servers', 'actions', 'configfile', 'configdir', 'cli_args']: config.set('Nagstamon', option, str(self.__dict__[option])) # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property # and all the thousands 1.0 installations do not know it yet it will be more comfortable # for most of the Windows users if it is only defined as False after it was checked # from config file if 'use_system_keyring' not in self.__dict__.keys(): if self.unconfigured is True: # an unconfigured system should start with no keyring to prevent crashes self.use_system_keyring = False else: # a configured system seemed to be able to run and thus use system keyring if OS in OS_NON_LINUX: self.use_system_keyring = True else: self.use_system_keyring = self.check_keyring_availability() # save servers dict self.save_multiple_config('servers', 'server') # save actions dict self.save_multiple_config('actions', 'action') # open, save and close config file with open(os.path.normpath(self.configfile), 'w') as file: config.write(file) # debug if self.debug_mode: debug_queue.append('DEBUG: {0} Saving configuration to file {1}'.format(str(datetime.datetime.now()), self.configfile)) except Exception as err: import traceback traceback.print_exc(file=sys.stdout) # debug if self.debug_mode: debug_queue.append( 'ERROR: {0} {1} while saving configuration to file {2}'.format(str(datetime.datetime.now()), err, self.configfile)) def save_multiple_config(self, settingsdir, setting): """ Saves configuration files for settings such as actions or servers in dedicated subdirectories. This method iterates over the specified settings dictionary (e.g., servers or actions) and writes each configuration entry to its own file within a subdirectory. Sensitive data such as passwords are obfuscated or stored in the system keyring if enabled. The method ensures that the target directory exists before saving and handles platform-specific keyring integration. Args: settingsdir (str): Name of the subdirectory where configuration files will be saved (e.g., 'servers' or 'actions'). setting (str): Prefix used for the configuration file names (e.g., 'server' or 'action'). Error Handling: Exceptions during saving are caught and printed to the standard output. If password saving to the keyring fails, the application will exit to prevent insecure storage. Special Considerations: - Each configuration entry is saved in a separate file for modularity. - Passwords and sensitive fields are obfuscated or stored securely. - The method creates the target directory if it does not exist. - Old or duplicate configuration files may be cleaned up as needed. """ # only import keyring lib if configured to do so - to avoid Windows crashes # like https://github.com/HenriWahl/Nagstamon/issues/97 if self.use_system_keyring is True: self.keyring_available = self.check_keyring_availability() # one section for each setting for s in self.__dict__[settingsdir]: config = configparser.ConfigParser(allow_no_value=True, interpolation=None) config.add_section(setting + '_' + s) for option in self.__dict__[settingsdir][s].__dict__: # obfuscate certain entries in config file - special arrangement for servers if settingsdir == 'servers': if option in ['username', 'password', 'proxy_username', 'proxy_password', 'autologin_key']: value = self.obfuscate(self.__dict__[settingsdir][s].__dict__[option]) if option == 'password': if self.__dict__[settingsdir][s].save_password is False: value = '' elif self.keyring_available and self.use_system_keyring: if self.__dict__[settingsdir][s].password != '': # provoke crash if password saving does not work - this is the case # on newer Ubuntu releases try: keyring.set_password('Nagstamon', '@'.join((self.__dict__[settingsdir][s].username, self.__dict__[settingsdir][s].monitor_url)), self.__dict__[settingsdir][s].password) except Exception: import traceback traceback.print_exc(file=sys.stdout) sys.exit(1) value = '' if option == 'proxy_password': if self.keyring_available and self.use_system_keyring: if self.__dict__[settingsdir][s].proxy_password != '': # provoke crash if password saving does not work - this is the case # on newer Ubuntu releases try: keyring.set_password('Nagstamon', '@'.join(('proxy', self.__dict__[settingsdir][s].proxy_username, self.__dict__[settingsdir][s].proxy_address)), self.__dict__[settingsdir][s].proxy_password) except Exception: import traceback traceback.print_exc(file=sys.stdout) sys.exit(1) value = '' config.set(setting + '_' + s, option, str(value)) else: config.set(setting + '_' + s, option, str(self.__dict__[settingsdir][s].__dict__[option])) else: config.set(setting + '_' + s, option, str(self.__dict__[settingsdir][s].__dict__[option])) # open, save and close config_server file if not os.path.exists(self.configdir + os.sep + settingsdir): os.makedirs(self.configdir + os.sep + settingsdir) # quote strings and thus avoid invalid characters with open( os.path.normpath('{0}{1}{2}{1}{3}_{4}.conf'.format(self.configdir, os.sep, settingsdir, setting, quote(s, safe=''))), 'w') as file: config.write(file) # ### clean up old deleted/renamed config files # ##if os.path.exists(self.configdir + os.sep + settingsdir): # ## for f in os.listdir(self.configdir + os.sep + settingsdir): # ## if not f.split(setting + "_")[1].split(".conf")[0] in self.__dict__[settingsdir]: # ## os.unlink(self.configdir + os.sep + settingsdir + os.sep + f) def check_keyring_availability(self): """ Determines if the keyring module and a suitable backend are available for secure password storage. This method checks whether the keyring library is installed and whether a supported keyring backend is accessible on the current platform. It handles platform-specific logic to avoid known issues, such as crashes on certain Linux distributions. If keyring is available and functional, the method returns True; otherwise, it returns False. Returns: bool: True if a working keyring backend is available, False otherwise. Error Handling: Any exceptions during the check are caught, a stack trace is printed, and the method returns False. Special Considerations: - On Linux, keyring is only used if it is provided by the distribution to avoid compatibility issues. - The method avoids importing keyring unless necessary to prevent crashes on some platforms. - Designed to be robust against missing dependencies or misconfigured environments. """ try: # Linux systems should use keyring only if it comes with the distro, otherwise chances are small # that keyring works at all if OS in OS_NON_LINUX: # safety first - if not yet available disable it if 'use_system_keyring' not in self.__dict__.keys(): self.use_system_keyring = False # only import keyring lib if configured to do so # necessary to avoid Windows crashes like https://github.com/HenriWahl/Nagstamon/issues/97 if self.use_system_keyring is True: # hint for packaging: nagstamon.spec always have to match module path # keyring has to be bound to object to be used later import keyring return not (keyring.get_keyring() is None) else: return False elif KEYRING: # keyring and secretstorage have to be importable import keyring # import secretstorage module as dependency of keyring - # if not available keyring won't work import secretstorage if keyring.get_keyring(): return True else: return False else: # apparently an Ubuntu KDE session which has problems with keyring return False except Exception: import traceback traceback.print_exc(file=sys.stdout) return False def obfuscate(self, string, count=5): """ Obfuscates a given string to securely store sensitive information such as passwords. Args: string (str): The input string to be obfuscated (e.g., a password). count (int, optional): The number of obfuscation rounds to apply. Default is 5. Returns: str: The obfuscated string, encoded as a base64 Unicode string. Usage: Use this method to obfuscate passwords or other sensitive fields before saving them to disk. To retrieve the original value, use the corresponding DeObfuscate method. """ string = string.encode() for i in range(count): string = base64.b64encode(string).decode() string = list(string) string.reverse() string = "".join(string) string = string.encode() string = zlib.compress(string) # make unicode of bytes string string = base64.b64encode(string).decode() return string def deobfuscate(self, string, count=5): """ Deobfuscates a previously obfuscated string to retrieve the original sensitive information. Args: string (str): The obfuscated string to be deobfuscated. count (int, optional): The number of deobfuscation rounds to apply. Default is 5. Returns: str: The original, deobfuscated string. Usage: Use this method to retrieve sensitive information (e.g., passwords) that was previously obfuscated before storage. The obfuscation and deobfuscation routines must use the same number of rounds to ensure correct results. """ string = base64.b64decode(string) for i in range(count): string = zlib.decompress(string) string = string.decode() string = list(string) string.reverse() string = "".join(string) string = base64.b64decode(string) # make unicode of bytes coming from base64 operations string = string.decode() return string def _default_actions(self): """ Creates a set of default actions such as SSH, RDP, VNC, and others for use within the application. Returns: dict: A dictionary mapping action names to their corresponding Action objects. Usage: Called during initial configuration or when resetting to defaults to ensure a baseline set of actions is always available for the user. """ if OS == OS_WINDOWS: defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="C:/windows/system32/mstsc.exe /v:$ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="C:/Program Files/TightVNC/vncviewer.exe $ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="C:/Windows/System32\telnet.exe root@$ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="C:/Program Files/PuTTY/putty.exe -l root $ADDRESS$")} elif OS == OS_MACOS: defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="open rdp://$ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="open vnc://$ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="open ssh://root@$ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="open telnet://root@$ADDRESS$")} else: # the Linux settings defaultactions = {"RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="/usr/bin/rdesktop -g 1024x768 $ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="/usr/bin/vncviewer $ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="/usr/bin/gnome-terminal -x ssh root@$ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="/usr/bin/gnome-terminal -x telnet root@$ADDRESS$"), "Update-Linux": Action(name="Update-Linux", description="Run remote update script.", type="command", string="/usr/bin/terminator -x ssh root@$HOST$ update.sh", enabled=False)} # OS agnostic actions as examples defaultactions["Nagios-1-Click-Acknowledge-Host"] = Action(name="Nagios-1-Click-Acknowledge-Host", type="url", description="Acknowledges a host with one click.", filter_target_service=False, enabled=False, string="$MONITOR-CGI$/cmd.cgi?cmd_typ=33&cmd_mod=2&host=$HOST$\ &com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit") defaultactions["Nagios-1-Click-Acknowledge-Service"] = Action(name="Nagios-1-Click-Acknowledge-Service", type="url", description="Acknowledges a service with one click.", filter_target_host=False, enabled=False, string="$MONITOR-CGI$/cmd.cgi?cmd_typ=34&cmd_mod=2&host=$HOST$\ &service=$SERVICE$&com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit") defaultactions["Opsview-Graph-Service"] = Action(name="Opsview-Graph-Service", type="browser", description="Show graph in browser.", filter_target_host=False, string="$MONITOR$/graph?service=$SERVICE$&host=$HOST$", enabled=False) defaultactions["Opsview-History-Host"] = Action(name="Opsview-Host-Service", type="browser", description="Show host in browser.", filter_target_host=True, string="$MONITOR$/event?host=$HOST$", enabled=False) defaultactions["Opsview-History-Service"] = Action(name="Opsview-History-Service", type="browser", description="Show history in browser.", filter_target_host=True, string="$MONITOR$/event?host=$HOST$&service=$SERVICE$", enabled=False) defaultactions["Checkmk-1-Click-Acknowledge-Host"] = Action(name="Checkmk-1-Click-Acknowledge-Host", type="url", description="Acknowledges a host with one click.", filter_target_service=False, enabled=False, string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=hoststatus&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge") defaultactions["Checkmk-1-Click-Acknowledge-Service"] = Action(name="Checkmk-1-Click-Acknowledge-Service", type="url", description="Acknowledges a host with one click.", filter_target_host=False, enabled=False, string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=service&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge&service=$SERVICE$") defaultactions["Checkmk Edit host in WATO"] = Action(name="Checkmk Edit host in WATO", enabled=False, monitor_type="Checkmk Multisite", description="Edit host in WATO.", string="$MONITOR$/wato.py?host=$HOST$&mode=edit_host") defaultactions["Email"] = Action(name="Email", enabled=False, description="Send email to someone.", type="browser", string="mailto:servicedesk@my.org?subject=Monitor alert: $HOST$ - $SERVICE$ - $STATUS-INFO$&body=Please help!.%0d%0aBest regards from Nagstamon") return defaultactions def _legacy_adjustments(self): """ Performs legacy adjustments to configuration settings for improved clarity and future compatibility. This section updates configuration keys and values to reflect naming conventions and structural changes introduced in newer versions of the application. Specifically, it replaces any configuration keys or references containing 'nagios' with 'monitor' to standardize terminology. This is primarily a cosmetic change but helps maintain consistency and prepares the codebase for future enhancements. Special Considerations: - Ensures backward compatibility by migrating legacy configuration keys. - Only affects settings where 'nagios' is present, leaving other settings unchanged. - Intended to be clear and maintainable for future additions or refactoring. """ for server in self.servers.values(): if 'nagios_url' in server.__dict__.keys(): server.monitor_url = server.nagios_url if 'nagios_cgi_url' in server.__dict__.keys(): server.monitor_cgi_url = server.nagios_cgi_url # to reduce complexity in Centreon there is also only one URL necessary if server.type == "Centreon": server.monitor_url = server.monitor_cgi_url # switch to update interval in seconds not minutes if 'update_interval' in self.__dict__.keys(): self.update_interval_seconds = int(self.update_interval) * 60 self.__dict__.pop('update_interval') # remove support for GNOME2-trayicon-egg-stuff if 'statusbar_systray' in self.__dict__.keys(): if self.statusbar_systray is True: self.icon_in_systray = True self.__dict__.pop('statusbar_systray') # some legacy action settings might need a little fix for action in self.actions.values(): if not action.type.lower() in ('browser', 'command', 'url'): # set browser as default to make user notice something is wrong action.type = 'browser' # might be there only once after starting Nagstamon 3.4 the first time if 'save_config_after_urlencode' in self.__dict__.keys(): self.save_config() def get_number_of_enabled_monitors(self): """ Returns the number of enabled monitors. This method iterates through all configured servers and counts how many are currently enabled. If all monitors are disabled, there is no need to display the popup window in the application. Returns: int: The number of enabled monitor servers. Usage: Used to determine whether the popup window should be shown based on active monitors. """ # to be returned number = 0 for server in self.servers.values(): # ##if str(server.enabled) == "True": if server.enabled is True: number += 1 return number def delete_file(self, settings_dir, settings_file): """ delete specified .conf file if setting is deleted in GUI """ # clean up old deleted/renamed config file file = os.path.abspath('{1}{0}{2}{0}{3}'.format(os.sep, self.configdir, settings_dir, settings_file)) if os.path.exists(file) and (os.path.isfile(file) or os.path.islink(file)): try: os.unlink(file) except Exception: import traceback traceback.print_exc(file=sys.stdout) class Server: """ one Server realized as object for config info """ def __init__(self): self.enabled = True self.type = 'Nagios' self.name = 'Monitor server' self.monitor_url = 'https://monitor-server' self.monitor_cgi_url = 'https://monitor-server/monitor/cgi-bin' self.username = 'username' self.password = 'password' self.save_password = False self.use_proxy = False self.use_proxy_from_os = False self.proxy_address = 'http://proxyserver:port/' self.proxy_username = 'proxyusername' self.proxy_password = 'proxypassword' # defaults to 'basic', other possible values are 'digest' and 'kerberos' self.authentication = 'basic' self.timeout = 10 # just GUI-wise deciding if more options are shown in server dialog self.show_options = False # SSL/TLS certificate verification self.ignore_cert = False self.custom_cert_use = False self.custom_cert_ca_file = '' self.idp_ecp_endpoint = 'https://idp/idp/profile/SAML2/SOAP/ECP' # special FX # Centreon autologin self.use_autologin = False self.autologin_key = '' # Icinga "host_display_name" instead of "host" self.use_display_name_host = False self.use_display_name_service = False # IcingaWeb2 might authenticate without cookies too - default is WITH cookies self.no_cookie_auth = False # Checkmk Multisite # Force Checkmk livestatus code to set AuthUser header for users who # are permitted to see all objects. self.force_authuser = False self.checkmk_view_hosts = 'nagstamon_hosts' self.checkmk_view_services = 'nagstamon_svc' # OP5 api filters self.host_filter = 'state !=0' self.service_filter = 'state !=0 or host.state != 0' # For more information about he Opsview options below, see this link: # https://knowledge.opsview.com/reference/api-status-filtering-service-objects # The Opsview hashtag filter will filter out any services NOT having the # listed hashtags (previously known as keywords). self.hashtag_filter = '' # The Opsview can_change_only option allows a user to show only # services for which the user has permissions to make changes OR set # downtimes. self.can_change_only = False # Sensu/Uchiwa/??? Datacenter/Site config self.monitor_site = 'Site 1' # Zabbix "Item Description" as "Service Name" self.use_description_name_service = False # Prometheus/Alertmanager mappings self.map_to_hostname = "pod_name,namespace,instance" self.map_to_servicename = "alertname" self.map_to_status_information = "message,summary,description" # Alertmanager mappings self.alertmanager_filter = '' self.map_to_critical = 'critical,error' self.map_to_warning = 'warning,warn' self.map_to_down = 'down' self.map_to_unknown = 'unknown' self.map_to_ok = 'ok' # IcingaDBWebNotificationsServer self.notification_filter = "user.name=*" self.notification_lookback = "30 minutes" # Thruk self.disabled_backends = "" class Action: """ class for custom actions, which whill be thrown into one config dictionary like the servers """ def __init__(self, **kwds): # to be or not to be enabled... self.enabled = True # monitor type self.monitor_type = "" # one of those: browser, url or command self.type = "browser" # thy name is... self.name = "Custom action" # OS of host where Nagstamon runs - especially commands are mostly not platform agnostic self.os = "" # description self.description = "Starts a custom action." # might be URL in case of type browser/url and a commandline for commands self.string = "" # version - maybe in future this might be more sophisticated self.version = "1" # kind of Nagios item this action is targeted to - maybe also usable for states self.filter_target_host = True self.filter_target_service = True # action applies only to certain hosts or services self.re_host_enabled = False self.re_host_pattern = "" self.re_host_reverse = False self.re_service_enabled = False self.re_service_pattern = "" self.re_service_reverse = False self.re_status_information_enabled = False self.re_status_information_pattern = "" self.re_status_information_reverse = False self.re_duration_enabled = False self.re_duration_pattern = "" self.re_duration_reverse = False self.re_attempt_enabled = False self.re_attempt_pattern = "" self.re_attempt_reverse = False self.re_groups_enabled = False self.re_groups_pattern = "" self.re_groups_reverse = False # close powin or not, depends on personal preference self.close_popwin = True self.leave_popwin_open = False # do an immediate recheck after action was applied self.recheck = False # special FX # Centreon criticality and autologin self.re_criticality_enabled = False self.re_criticality_pattern = "" self.re_criticality_reverse = False # add and/or all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # Initialize configuration to be accessed globally conf = Config() # try to get resources path if nagstamon got be installed by setup.py RESOURCES = "" try: # first try to find local resources directory in case Nagstamon was frozen with cx-Freeze for OSX or Windows executable_dir = os.path.join(os.sep.join(sys.executable.split(os.sep)[:-1])) if os.path.exists(os.path.normcase(os.sep.join((executable_dir, "resources")))): RESOURCES = os.path.normcase(os.sep.join((executable_dir, "resources"))) else: RESOURCES = str(resources.files("Nagstamon").joinpath("resources")) except Exception as err: # get resources directory from current directory - only if not being set before # try-excepts necessary for platforms like Windows .EXE paths_to_check = [os.path.normcase(os.path.join(os.getcwd(), "Nagstamon", "resources")), os.path.normcase(os.path.join(os.getcwd(), "resources"))] try: # if resources dir is not available in CWD, try the # libs dir (site-packages) for the current Python from distutils.sysconfig import get_python_lib paths_to_check.append(os.path.normcase(os.path.join(get_python_lib(), "Nagstamon", "resources"))) except Exception: pass # if we're still out of luck, maybe this was a user scheme install try: import site site.getusersitepackages() # make sure USER_SITE is set paths_to_check.append(os.path.normcase(os.path.join(site.USER_SITE, "Nagstamon", "resources"))) except Exception: pass # add directory nagstamon.py where nagstamon.py resides for cases like 0install without installed pkg-resources paths_to_check.append(os.sep.join(sys.argv[0].split(os.sep)[:-1] + ["Nagstamon", "resources"])) for path in paths_to_check: if os.path.exists(path): RESOURCES = path print('resources 3: ' + RESOURCES, paths_to_check) break else: RESOURCES = str(Path(__file__).parent.absolute().joinpath('resources')) # try to fix missing resources path for Windows if OS == OS_WINDOWS: if r'\_internal\Nagstamon\resources' in RESOURCES: RESOURCES = RESOURCES.replace(r'\_internal\Nagstamon\resources', r'\_internal\resources') Nagstamon-master/Nagstamon/helpers.py000066400000000000000000000436471505160700500202560ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import datetime import getpass from glob import glob import os from urllib.parse import quote import psutil from pathlib import Path import platform import re import sys import traceback import webbrowser # import md5 for centreon url autologin encoding from hashlib import md5 from Nagstamon.config import (conf, OS, OS_MACOS, RESOURCES) # states needed for gravity comparison for notification and Generic.py STATES = ['UP', 'UNKNOWN', 'INFORMATION', 'WARNING', 'AVERAGE', 'HIGH', 'CRITICAL', 'DISASTER', 'UNREACHABLE', 'DOWN'] # sound at the moment is only available for these states STATES_SOUND = ['WARNING', 'CRITICAL', 'DOWN'] # store default sounds as buffers to avoid https://github.com/HenriWahl/Nagstamon/issues/578 # meanwhile used as backup copy in case they had been deleted by macOS # https://github.com/HenriWahl/Nagstamon/issues/578 class ResourceFiles(dict): """ Care about vanished resource files in macOS pyinstaller temp folder """ def check(self, resource_file): if not os.path.exists(resource_file): # pyinstaller temp folder seems to be emptied completely after a while # so the directories containing the resources have to be recreated too os.makedirs(os.path.dirname(resource_file), exist_ok=True) # write cached content of resource file back onto disk with open(resource_file, mode='wb') as file: file.write(self[resource_file]) # store default sounds as buffers to avoid https://github.com/HenriWahl/Nagstamon/issues/578 # meanwhile used as backup copy in case they had been deleted by macOS # https://github.com/HenriWahl/Nagstamon/issues/578 class FilesDict(dict): """ Care about vanished files in macOS pyinstaller temp folder """ def __init__(self, files_path): # after becoming a dict load given media files from given path super().__init__() self.read_dir(files_path) def read_dir(self, files_path): """ Read media files into contained dict :param files_path: :return: """ for suffix in ('*.wav', '*.svg'): for filename in glob('{0}{1}{2}'.format(files_path, os.sep, suffix)): with open(filename, mode='rb') as file: # macOS sometines cleans up the /var/folders-something-path used by the onefile created by pyinstaller # this backup dict allows to recreate the needed default files # madness at least... but works self.update({filename: file.read()}) def __getitem__(self, key): """ Overwrite commont __getitem__ method of dict to restore files on macOS :param key: :return: """ if OS == OS_MACOS: if not os.path.exists(key): # pyinstaller temp folder seems to be emptied completely after a while # so the directories containing the resources have to be recreated too os.makedirs(os.path.dirname(key), exist_ok=True) # write cached content of resource file back onto disk with open(key, mode='wb') as file: file.write(dict.__getitem__(self, key)) # looks strange but the filename is all the caller expects here return key def not_empty(x): ''' tiny helper function for BeautifulSoup in server Generic.py to filter text elements ''' return bool(x.replace(' ', '').strip()) def is_found_by_re(string, pattern, reverse): """ helper for context menu actions in context menu - hosts and services might be filtered out also useful for services and hosts and status information """ pattern = re.compile(pattern) if len(pattern.findall(string)) > 0: if str(reverse) == "True": return False else: return True else: if str(reverse) == "True": return True else: return False def host_is_filtered_out_by_re(host, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_host_enabled is True: return is_found_by_re(host, conf.re_host_pattern, conf.re_host_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def service_is_filtered_out_by_re(service, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_service_enabled is True: return is_found_by_re(service, conf.re_service_pattern, conf.re_service_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def status_information_is_filtered_out_by_re(status_information, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_status_information_enabled is True: return is_found_by_re(status_information, conf.re_status_information_pattern, conf.re_status_information_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def duration_is_filtered_out_by_re(duration, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_duration_enabled is True: return is_found_by_re(duration, conf.re_duration_pattern, conf.re_duration_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def attempt_is_filtered_out_by_re(attempt, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_attempt_enabled is True: return is_found_by_re(attempt, conf.re_attempt_pattern, conf.re_attempt_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def groups_is_filtered_out_by_re(groups, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_groups_enabled is True: return is_found_by_re(groups, conf.re_groups_pattern, conf.re_groups_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def criticality_is_filtered_out_by_re(criticality, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if conf.re_criticality_enabled is True: return is_found_by_re(criticality, conf.re_criticality_pattern, conf.re_criticality_reverse) # if RE are disabled return True because host is not filtered return False except Exception: traceback.print_exc(file=sys.stdout) def human_readable_duration_from_seconds(seconds): """ convert seconds given by Opsview to the form Nagios gives them like 70d 3h 34m 34s """ timedelta = str(datetime.timedelta(seconds=int(seconds))) try: if timedelta.find("day") == -1: hms = timedelta.split(":") if len(hms) == 1: return "%02sh" % (hms[0]) elif len(hms) == 2: return "%02sm %02ss" % (hms[1], hms[2]) else: return "%sh %02sm %02ss" % (hms[0], hms[1], hms[2]) else: # waste is waste - does anyone need it? days, waste, hms = str(timedelta).split(" ") hms = hms.split(":") return "%sd %sh %02sm %02ss" % (days, hms[0], hms[1], hms[2]) except Exception: traceback.print_exc(file=sys.stdout) # in case of any error return seconds we got return seconds def human_readable_duration_from_timestamp(timestamp): """ Thruk server supplies timestamp of latest state change which has to be subtracted from .now() """ try: td = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(timestamp)) h = int(td.seconds / 3600) m = int(td.seconds % 3600 / 60) s = int(td.seconds % 60) if td.days > 0: return "%sd %sh %02dm %02ds" % (td.days, h, m, s) elif h > 0: return "%sh %02dm %02ds" % (h, m, s) elif m > 0: return "%02dm %02ds" % (m, s) else: return "%02ds" % (s) except Exception: traceback.print_exc(file=sys.stdout) # unified machine-readable date might go back to module Actions def machine_sortable_date(raw): """ Try to compute machine-readable date for all types of monitor servers """ # dictionary for duration date string components d = {'M': 0, 'w': 0, 'd': 0, 'h': 0, 'm': 0, 's': 0} # if for some reason the value is empty/none make it compatible: 0s if raw is None: raw = '0s' # Checkmk style - added new variants in 1.4.x, based on abbreviations with spaces :-( if ('-' in raw and ':' in raw) or\ ('sec' in raw or 'min' in raw or 'hrs' in raw or 'days' in raw or\ ' s' in raw or ' m' in raw or ' h' in raw or ' d' in raw): # check_mk has different formats - if duration takes too long it changes its scheme if '-' in raw and ':' in raw: datepart, timepart = raw.split(' ') # need to convert years into months for later comparison Y, M, D = datepart.split('-') d['M'] = int(Y) * 12 + int(M) d['d'] = int(D) # time does not need to be changed h, m, s = timepart.split(':') d['h'], d['m'], d['s'] = int(h), int(m), int(s) del datepart, timepart, Y, M, D, h, m, s else: # recalculate a timedelta of the given value if 'sec' in raw or ' s' in raw: d['s'] = raw.split(' ')[0].split('.')[0] delta = datetime.datetime.now() - datetime.timedelta(seconds=int(d['s'])) elif 'min' in raw or ' m' in raw: d['m'] = raw.split(' ')[0].split('.')[0] delta = datetime.datetime.now() - datetime.timedelta(minutes=int(d['m'])) elif 'hrs' in raw or ' h' in raw: d['h'] = raw.split(' ')[0] delta = datetime.datetime.now() - datetime.timedelta(hours=int(d['h'])) elif 'days' in raw or ' d' in raw: d['d'] = raw.split(' ')[0] delta = datetime.datetime.now() - datetime.timedelta(days=int(d['d'])) else: delta = datetime.datetime.now() Y, M, d['d'], d['h'], d['m'], d['s'] = delta.strftime('%Y %m %d %H %M %S').split(' ') # need to convert years into months for later comparison d['M'] = int(Y) * 12 + int(M) # int-ify d for i in d: # workaround to make values negative to fix Checkmk's different order d[i] = -int(d[i]) else: # strip and replace necessary for Nagios duration values, # split components of duration into dictionary for c in raw.strip().replace(' ', ' ').split(' '): number, period = c[0:-1], c[-1] # attempt to be more robust in case of https://github.com/HenriWahl/Nagstamon/issues/405 try: d[period] = int(number) except: d[period] = 0 del number, period # convert collected duration data components into seconds for being comparable return 16934400 * d['M'] + 604800 * d['w'] + 86400 * d['d'] + 3600 * d['h'] + 60 * d['m'] + d['s'] def md5ify(string): """ makes something md5y of a given username or password for Centreon web interface access """ return md5(string).hexdigest() def lock_config_folder(folder): ''' Locks the config folder by writing a PID file into it. The lock is relative to user name and system's boot time. Returns True on success, False when lock failed Return True too if there is any locking error - if no locking ins possible it might run as well This is also the case if some setup uses the nagstamon.config directory which most probably will be read-only ''' pid_file_path = os.path.join(folder, 'nagstamon.pid') try: # Open the file for rw or create a new one if missing if os.path.exists(pid_file_path): mode = 'r+t' else: mode = 'wt' with open(pid_file_path, mode, newline=None) as pid_file: currrent_pid = os.getpid() current_boot_time = int(psutil.boot_time()) current_user_name = getpass.getuser().replace('@', '_').strip() pid = None boot_time = None user_name = None if mode.startswith('r'): try: process_info = pid_file.readline().strip().split('@') pid = int(process_info[0]) boot_time = int(process_info[1]) user_name = process_info[2].strip() except(ValueError, IndexError): pass if pid is not None and boot_time is not None and user_name is not None: # Found a pid stored in the pid file, check if its still running if boot_time == current_boot_time and user_name == current_user_name and psutil.pid_exists(pid): return False pid_file.seek(0) pid_file.truncate() pid_file.write('{}@{}@{}'.format(currrent_pid, current_boot_time, current_user_name)) except Exception as error: print(error) return True # the following functions are used for sorted() in sort_data_array() def compare_host(item): return item.lower() def compare_service(item): return item.lower() def compare_status(item): return STATES.index(item) def compare_last_check(item): return machine_sortable_date(item) def compare_duration(item): return machine_sortable_date(item) def compare_attempt(item): return item def compare_status_information(item): return item.lower() def webbrowser_open(url): """ decide if default or custom browser is used for various tasks used by almost all """ if conf.use_default_browser: webbrowser.open(url) else: webbrowser.get('{0} %s &'.format(conf.custom_browser)).open(url) def get_distro(): """ replacement for platform.dist() which is deprecated and not available anymore since Python 3.8 read content of /etc/os-release and return it - all relevant distros should deliver this file on older Python platform.dist still can be used and even should be on Debian 8 with Python 3.4 :return: """ if sys.version_info > (3,7): os_release_file = Path('/etc/os-release') if os_release_file.exists() and (os_release_file.is_file() or os_release_file.is_symlink()): os_release_dict = {} for line in os_release_file.read_text().splitlines(): if not line.startswith('#'): try: key, value = line.split('=', 1) except ValueError: continue else: os_release_dict[key] = value.strip('"').strip("'") # Since CentOS Linux got retired by Red Hat, there are various RHEL derivatives/clones; flow is: # CentOS Stream -> Red Hat Enterprise Linux -> (AlmaLinux, EuroLinux, Oracle Linux, Rocky Linux) # Goal of this hack is to rule them all as Red Hat Enterprise Linux, the baseline distribution. if re.search(r'^platform:el\d+$', os_release_dict.get('PLATFORM_ID', 'unknown')): os_release_dict['ID'] = 'rhel' os_release_dict['VERSION_ID'] = os_release_dict.get('VERSION_ID', 'unknown').split('.', 1)[0] os_release_dict['NAME'] = 'Red Hat Enterprise Linux' return (os_release_dict.get('ID').lower(), os_release_dict.get('VERSION_ID', 'unknown').lower(), os_release_dict.get('NAME').lower()) else: return '', '', '' else: # fix for non-working build on Debian<10 dist_name, dist_version, dist_id = platform.dist() return dist_name.lower(), dist_version, dist_id def urlify(self, string): """ return a string that fulfills requirements for URLs exclude several chars """ return quote(string, ":/=?&@+") # depending on column different functions have to be used # 0 + 1 are column "Hosts", 1 + 2 are column "Service" due to extra font flag pictograms SORT_COLUMNS_FUNCTIONS = {0: compare_host, 1: compare_host, 2: compare_service, 3: compare_service, 4: compare_status, 5: compare_last_check, 6: compare_duration, 7: compare_attempt, 8: compare_status_information, 9: compare_status_information} Nagstamon-master/Nagstamon/objects.py000066400000000000000000000113761505160700500202370ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA class GenericObject: """ template for hosts and services """ def __init__(self): self.hostid = '' self.name = '' self.status = '' self.status_information = '' # default state is soft, to be changed by status_type check self.status_type = '' self.last_check = '' self.duration = '' self.attempt = '' self.passiveonly = False self.acknowledged = False self.notifications_disabled = False self.flapping = False self.scheduled_downtime = False # compress all flags like acknowledged and flapping into one string self.host_flags = '' self.service_flags = '' self.visible = True # Checkmk also has site info self.site = '' # server to be added to hash self.server = '' # might help in Qt self.host = '' self.service = '' self.dummy_column = '' # might be used in Op5Monitor and maybe more if groups are used more widely self.groups = '' def is_passive_only(self): return bool(self.passiveonly) def is_flapping(self): return bool(self.flapping) def has_notifications_disabled(self): return bool(self.notifications) def is_acknowledged(self): return bool(self.acknowledged) def is_in_scheduled_downtime(self): return bool(self.scheduled_downtime) def is_visible(self): return bool(self.visible) def get_name(self): """ return stringified name """ return str(self.name) def get_host_name(self): """ Extracts host name from status item. Presentation purpose. """ return '' def get_service_name(self): """ Extracts service name from status item. Presentation purpose. """ return '' def get_hash(self): """ returns hash of event status information - different for host and service thus empty here """ return '' def get_columns(self, columns_wanted): """ Yield host/service status information for treeview table columns """ for c in columns_wanted: yield str(self.__dict__[c]) class GenericHost(GenericObject): """ one host which is monitored by a Nagios server, gets populated with services """ def __init__(self): GenericObject.__init__(self) # take all the faulty services on host self.services = dict() def get_host_name(self): return str(self.name) def is_host(self): """ decides where to put acknowledged/downtime pixbufs in Liststore for Treeview in Popwin """ return True def get_hash(self): """ return hash for event history tracking """ return " ".join((self.server, self.site, self.name, self.status)) class GenericService(GenericObject): """ one service which runs on a host """ def __init__(self): GenericObject.__init__(self) self.unreachable = False def get_host_name(self): return str(self.host) def get_service_name(self): return str(self.name) def is_host(self): """ decides where to put acknowledged/downtime pixbufs in Liststore for Treeview in Popwin """ return False def get_hash(self): """ return hash for event history tracking """ return " ".join((self.server, self.site, self.host, self.name, self.status)) class Result: """ multi purpose result object, used in Servers.Generic.fetch_url() """ result = '' error = '' status_code = 0 def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] Nagstamon-master/Nagstamon/qui/000077500000000000000000000000001505160700500170225ustar00rootroot00000000000000Nagstamon-master/Nagstamon/qui/__init__.py000066400000000000000000000221161505160700500211350ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from os import sep import os.path import sys # it is important that this import is done before importing any other qui module, because # they may need a QApplication instance to be created from Nagstamon.qui.widgets.app import app from Nagstamon.qui.constants import (COLORS, COLOR_STATE_NAMES, COLOR_STATUS_LABEL, HEADERS, HEADERS_HEADERS, HEADERS_HEADERS_COLUMNS, HEADERS_HEADERS_KEYS, HEADERS_KEYS_COLUMNS, HEADERS_KEYS_HEADERS, SORT_ORDER, SORT_COLUMNS_INDEX, SPACE, WINDOW_FLAGS) from Nagstamon.qui.globals import (dbus_connection, font, font_default, font_icons, statuswindow_properties) from Nagstamon.qui.helpers import (check_servers, hide_macos_dock_icon) from Nagstamon.qui.widgets.buttons import (Button, CSS_CLOSE_BUTTON, PushButtonHamburger) from Nagstamon.qui.dialogs import dialogs from Nagstamon.qui.dialogs.check_version import CheckVersion from Nagstamon.qui.qt import (MediaPlayer, Slot) from Nagstamon.qui.widgets.buttons import PushButtonBrowserURL from Nagstamon.qui.widgets.draggables import (DraggableLabel, DraggableWidget) from Nagstamon.qui.widgets.icon import QIconWithFilename from Nagstamon.qui.widgets.labels import (LabelAllOK, ServerStatusLabel) from Nagstamon.qui.widgets.layout import HBoxLayout from Nagstamon.qui.widgets.mediaplayer import mediaplayer from Nagstamon.qui.widgets.menu import (MenuAtCursor, MenuContext, MenuContextSystrayicon) from Nagstamon.qui.widgets.model import Model from Nagstamon.qui.widgets.statuswindow import StatusWindow from Nagstamon.qui.widgets.system_tray_icon import SystemTrayIcon from Nagstamon.qui.widgets.toparea import TopArea from Nagstamon.qui.widgets.combobox_servers import ComboBoxServers from Nagstamon.qui.widgets.treeview import TreeView from Nagstamon.qui.widgets.nagstamon_logo import NagstamonLogo from Nagstamon.qui.widgets.labels import ClosingLabel from Nagstamon.config import (conf, OS_NON_LINUX, OS, OS_MACOS) # make icon status in macOS dock accessible via NSApp, used by set_macos_dock_icon_visible() if OS == OS_MACOS: from AppKit import (NSApp, NSApplicationPresentationDefault, NSApplicationPresentationHideDock) # check for updates check_version = CheckVersion() # system tray icon systrayicon = SystemTrayIcon() # set to none here due to race condition #statusw indow = None #menu = None # combined statusbar/status window statuswindow = StatusWindow(dialogs=dialogs, systrayicon=systrayicon) # context menu for statuswindow etc. menu = MenuContext(parent=statuswindow) # necessary extra menu due to Qt5-Unity-integration if not OS in OS_NON_LINUX: menu_systray = MenuContextSystrayicon(parent=statuswindow) menu_systray.menu_ready.connect(systrayicon.set_menu) # menu has to be set here to solve Qt-5.10-Windows-systray-mess # and non-existence of macOS-systray-context-menu else: systrayicon.set_menu(menu) # to be connected someday elsewhere # server -> statuswindow remove_previous server dialogs.server.edited_remove_previous.connect(statuswindow.remove_previous_server_vbox) dialogs.server.create_server_vbox.connect(statuswindow.create_server_vbox) dialogs.authentication.show_up.connect(statuswindow.hide_window) dialogs.settings.settings_ok.connect(statuswindow.save_position_to_conf) # trigger the statuswindow.worker to check if debug loop is neede and if so, start it dialogs.settings.settings_ok.connect(statuswindow.worker.debug_loop) dialogs.settings.server_deleted.connect(statuswindow.worker.debug_loop) dialogs.settings.changed.connect(check_servers.check) dialogs.settings.changed.connect(statuswindow.label_all_ok.set_color) dialogs.settings.cancelled.connect(check_servers.check) # when there are new settings/colors recreate icons dialogs.settings.changed.connect(systrayicon.create_icons) # when there are new settings/colors refresh labels dialogs.settings.changed.connect(statuswindow.statusbar.reset) # when new setings are applied to adjust font size dialogs.settings.changed.connect(statuswindow.statusbar.adjust_size) # menu dialogs.settings.changed.connect(menu.initialize) # statuswindow statuswindow.toparea.button_filters.clicked.connect(dialogs.settings.show_filters) statuswindow.toparea.button_settings.clicked.connect(dialogs.settings.show) statuswindow.toparea.action_exit.triggered.connect(statuswindow.exit) # hide if settings dialog pops up dialogs.settings.show_dialog.connect(statuswindow.hide_window) # workaround for the timestamp trick to avoid flickering dialogs.settings.show_dialog.connect(statuswindow.decrease_shown_timestamp) # refresh all information after changed settings dialogs.settings.changed.connect(statuswindow.refresh) dialogs.settings.changed.connect(statuswindow.toparea.combobox_servers.fill) # hide status window if version check finished check_version.version_info_retrieved.connect(statuswindow.hide_window) # start debug loop by signal dialogs.settings.start_debug_loop.connect(statuswindow.worker.debug_loop) # clenaup vbox after server deletion dialogs.settings.server_deleted.connect(statuswindow.delete_server_vbox) # reinitialize statuswindow when display mode settings were changed dialogs.settings.changed_display_mode.connect(statuswindow.reinitialize) # connect application exit with server missing dialog dialogs.server_missing.window.button_exit.clicked.connect(statuswindow.exit) # systray connections # show status popup when systray icon was clicked systrayicon.show_popwin.connect(statuswindow.show_window_systrayicon) systrayicon.hide_popwin.connect(statuswindow.hide_window) # flashing statusicon statuswindow.worker_notification.start_flash.connect(systrayicon.flash) statuswindow.worker_notification.stop_flash.connect(systrayicon.reset) # trigger showing and hiding of systray icon depending on display mode statuswindow.systrayicon_enabled.connect(systrayicon.show) statuswindow.systrayicon_disabled.connect(systrayicon.hide) # retrieve systray icon position for statuswindow position calculation statuswindow.request_systrayicon_position.connect(systrayicon.retrieve_icon_position) # connect statuswindow to authentication dialog statuswindow.authenticate.connect(dialogs.authentication.show_auth_dialog) # let statuswindow show message mediaplayer.send_message.connect(statuswindow.show_message) # connect with statuswindow notification worker statuswindow.worker_notification.load_sound.connect(mediaplayer.set_media) statuswindow.worker_notification.play_sound.connect(mediaplayer.play) # necessary extra menu due to Qt5-Unity-integration if not OS in OS_NON_LINUX: # change menu if there are changes in settings/servers dialogs.settings.changed.connect(menu_systray.initialize) menu_systray.action_settings.triggered.connect(statuswindow.hide_window) menu_systray.action_settings.triggered.connect(dialogs.settings.show) menu_systray.action_save_position.triggered.connect(statuswindow.save_position_to_conf) menu_systray.action_about.triggered.connect(statuswindow.hide_window) menu_systray.action_about.triggered.connect(dialogs.about.show) menu_systray.menu_ready.connect(systrayicon.set_menu) menu_systray.menu_ready.emit(menu_systray) # needs to be emitted adter signal/slots are connected, # might be necessary for others too if conf.icon_in_systray: statuswindow.systrayicon_enabled.emit() else: statuswindow.systrayicon_disabled.emit() # tell the widgets that the menu is ready menu.menu_ready.emit(menu) Nagstamon-master/Nagstamon/qui/constants.py000066400000000000000000000117661505160700500214230ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Constants for Qt from collections import OrderedDict from os import sep from Nagstamon.config import RESOURCES from Nagstamon.qui.qt import (QIcon, Qt) # fixed shortened and lowered color names for cells, also used by statusbar label snippets COLORS = OrderedDict([('DOWN', 'color_down_'), ('UNREACHABLE', 'color_unreachable_'), ('DISASTER', 'color_disaster_'), ('CRITICAL', 'color_critical_'), ('HIGH', 'color_high_'), ('AVERAGE', 'color_average_'), ('WARNING', 'color_warning_'), ('INFORMATION', 'color_information_'), ('UNKNOWN', 'color_unknown_')]) # states to be used in statusbar if the long version is used COLOR_STATE_NAMES = {'DOWN': {True: 'DOWN', False: ''}, 'UNREACHABLE': {True: 'UNREACHABLE', False: ''}, 'DISASTER': {True: 'DISASTER', False: ''}, 'CRITICAL': {True: 'CRITICAL', False: ''}, 'HIGH': {True: 'HIGH', False: ''}, 'AVERAGE': {True: 'AVERAGE', False: ''}, 'WARNING': {True: 'WARNING', False: ''}, 'INFORMATION': {True: 'INFORMATION', False: ''}, 'UNKNOWN': {True: 'UNKNOWN', False: ''}} # colors for server status label in ServerVBox COLOR_STATUS_LABEL = {'critical': 'lightsalmon', 'error': 'orange', 'unknown': 'gray'} # headers for tablewidgets HEADERS = OrderedDict([('host', {'header': 'Host', 'column': 0}), ('host_flags', {'header': '', 'column': 0}), ('service', {'header': 'Service', 'column': 2}), ('service_flags', {'header': '', 'column': 2}), ('status', {'header': 'Status', 'column': 4}), ('last_check', {'header': 'Last Check', 'column': 5}), ('duration', {'header': 'Duration', 'column': 6}), ('attempt', {'header': 'Attempt', 'column': 7}), ('status_information', {'header': 'Status Information', 'column': 8}), ('dummy_column', {'header': '', 'column': 8})]) # various headers-key-columns variations needed in different parts HEADERS_HEADERS = list() for item in HEADERS.values(): HEADERS_HEADERS.append(item['header']) HEADERS_HEADERS_COLUMNS = dict() for item in HEADERS.values(): HEADERS_HEADERS_COLUMNS[item['header']] = item['column'] HEADERS_HEADERS_KEYS = dict() for item in HEADERS.keys(): HEADERS_HEADERS_KEYS[HEADERS[item]['header']] = item HEADERS_KEYS_COLUMNS = dict() for item in HEADERS.keys(): HEADERS_KEYS_COLUMNS[item] = HEADERS[item]['column'] HEADERS_KEYS_HEADERS = dict() for item in HEADERS.keys(): HEADERS_KEYS_HEADERS[item] = HEADERS[item]['header'] # sorting order for tablewidgets SORT_ORDER = {'descending': 1, 'ascending': 0, 0: Qt.SortOrder.DescendingOrder, 1: Qt.SortOrder.AscendingOrder} # bend columns 1 and 3 to 0 and 2 to avoid sorting the extra flag icons of hosts and services SORT_COLUMNS_INDEX = {0: 0, 1: 0, 2: 2, 3: 2, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 8} # space used in LayoutBoxes SPACE = 10 # Flags for statusbar - experiment with Qt.ToolTip for Windows because # statusbar permanently seems to vanish at some users desktops # see https://github.com/HenriWahl/Nagstamon/issues/222 WINDOW_FLAGS = Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool # icon for dialogs ICON = QIcon(f'{RESOURCES}{sep}nagstamon.ico')Nagstamon-master/Nagstamon/qui/dbus.py000066400000000000000000000112431505160700500203320ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # DBus connection for Qt from os import sep from random import random from sys import (modules, stdout) from traceback import print_exc from Nagstamon.config import (AppInfo, OS, OS_NON_LINUX, RESOURCES) from Nagstamon.qui.qt import (QObject, Signal) # DBus only interesting for Linux too if OS not in OS_NON_LINUX: # get DBUS availability - still possible it does not work due to missing # .service file on certain distributions try: from dbus import (Interface, SessionBus) # no DBusQtMainLoop available for Qt6 from dbus.mainloop.glib import DBusGMainLoop as DBusMainLoop # flag to check later if DBus is available DBUS_AVAILABLE = True except ImportError as error: print(error) print('No DBus for desktop notification available.') DBUS_AVAILABLE = False class DBus(QObject): """ Create connection to DBus for desktop notification for Linux/Unix """ open_statuswindow = Signal() # random ID needed because otherwise all instances of Nagstamon # will get commands by clicking on notification bubble via DBUS random_id = str(int(random() * 100000)) def __init__(self): QObject.__init__(self) self.id = 0 self.actions = [('open' + self.random_id), 'Open status window'] self.timeout = 0 # use icon from resources in hints, not the package icon - doesn't work either self.icon = '' # use Nagstamon image if icon is not available from the system # see https://developer.gnome.org/notification-spec/#icons-and-images self.hints = {'image-path': f'{RESOURCES}{sep}nagstamon.svg'} if not OS in OS_NON_LINUX and DBUS_AVAILABLE: if 'dbus' in modules: # try/except needed because of partly occuring problems with DBUS # see https://github.com/HenriWahl/Nagstamon/issues/320 try: # import dbus # never used dbus_mainloop = DBusMainLoop(set_as_default=True) dbus_sessionbus = SessionBus(dbus_mainloop) dbus_object = dbus_sessionbus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications') self.dbus_interface = Interface(dbus_object, dbus_interface='org.freedesktop.Notifications') # connect button to action self.dbus_interface.connect_to_signal('ActionInvoked', self.action_callback) self.connected = True except Exception: print_exc(file=stdout) self.connected = False else: self.connected = False def show(self, summary, message): """ simply show a message """ if self.connected: notification_id = self.dbus_interface.Notify(AppInfo.NAME, self.id, self.icon, summary, message, self.actions, self.hints, self.timeout) # reuse ID self.id = int(notification_id) def action_callback(self, dummy, action): """ react to clicked action button in notification bubble """ if action == 'open' + self.random_id: self.open_statuswindow.emit() Nagstamon-master/Nagstamon/qui/dialogs/000077500000000000000000000000001505160700500204445ustar00rootroot00000000000000Nagstamon-master/Nagstamon/qui/dialogs/__init__.py000066400000000000000000000171561505160700500225670ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.qui.dialogs.about import DialogAbout from Nagstamon.qui.dialogs.acknowledge import DialogAcknowledge from Nagstamon.qui.dialogs.action import DialogAction from Nagstamon.qui.dialogs.authentication import DialogAuthentication from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.dialogs.downtime import DialogDowntime from Nagstamon.qui.dialogs.server import DialogServer from Nagstamon.qui.dialogs.server_missing import DialogServerMissing from Nagstamon.qui.dialogs.settings import DialogSettings from Nagstamon.qui.dialogs.submit import DialogSubmit from Nagstamon.qui.helpers import hide_macos_dock_icon from Nagstamon.qui.qt import (QObject, Slot) class Dialogs(QObject): """ class for accessing all dialogs """ windows = list() settings = None server = None action = None acknowledge = None downtime = None submit = None authentication = None server_missing = None about = None def initialize_dialog_settings(self, dialog): """ initialize settings dialog """ self.settings = dialog self.settings.initialize() self.windows.append(self.settings.window) def initialize_dialog_server(self, dialog): """ initialize settings dialog """ self.server = dialog self.server.initialize() self.windows.append(self.server.window) # check if special widgets have to be shown self.server.edited.connect(self.settings.toggle_zabbix_widgets) self.server.edited.connect(self.settings.toggle_op5monitor_widgets) self.server.edited.connect(self.settings.toggle_expire_time_widgets) def initialize_dialog_action(self, dialog): self.action = dialog self.action.initialize() self.windows.append(self.action.window) def initialize_dialog_acknowledge(self, dialog): self.acknowledge = dialog self.acknowledge.initialize() self.windows.append(self.acknowledge.window) self.acknowledge.window.button_change_defaults_acknowledge.clicked.connect(self.settings.show_defaults) self.acknowledge.window.button_change_defaults_acknowledge.clicked.connect(self.acknowledge.window.close) def initialize_dialog_downtime(self, dialog): self.downtime = dialog self.downtime.initialize() self.windows.append(self.downtime.window) self.downtime.window.button_change_defaults_downtime.clicked.connect(self.settings.show_defaults) self.downtime.window.button_change_defaults_downtime.clicked.connect(self.downtime.window.close) def initialize_dialog_submit(self, dialog): self.submit = dialog self.submit.initialize() self.windows.append(self.submit.window) def initialize_dialog_authentication(self, dialog): self.authentication = dialog self.authentication.initialize() self.windows.append(self.authentication.window) def initialize_dialog_server_missing(self, dialog): self.server_missing = dialog self.server_missing.initialize() self.windows.append(self.server_missing.window) # open server creation dialog self.server_missing.window.button_create_server.clicked.connect(self.settings.show_new_server) self.server_missing.window.button_enable_server.clicked.connect(self.settings.show) def initialize_dialog_about(self, dialog): self.about = dialog self.windows.append(self.about.window) def get_shown_dialogs(self): """ get a list of currently show dialog windows - needed for macOS hide dock icon stuff """ return [x for x in self.windows if x.isVisible()] @Slot() def show_macos_dock_icon_if_necessary(self): """ show macOS dock icon again if it is configured to be hidden was only necessary to show up to let dialog get keyboard focus """ if OS == OS_MACOS and \ conf.icon_in_systray and \ conf.hide_macos_dock_icon: # if no window is shown already show dock icon if not len(self.get_shown_dialogs()): hide_macos_dock_icon(False) @Slot() def hide_macos_dock_icon_if_necessary(self): """ hide macOS dock icon again if it is configured to be hidden was only necessary to show up to let the dialog get keyboard focus """ if OS == OS_MACOS and \ conf.icon_in_systray and \ conf.hide_macos_dock_icon: # if no window is shown anymore hide dock icon if not len(self.get_shown_dialogs()): hide_macos_dock_icon(True) dialogs = Dialogs() dialogs.initialize_dialog_settings(DialogSettings()) dialogs.initialize_dialog_about(DialogAbout()) dialogs.initialize_dialog_acknowledge(DialogAcknowledge()) dialogs.initialize_dialog_action(DialogAction()) dialogs.initialize_dialog_downtime(DialogDowntime()) dialogs.initialize_dialog_submit(DialogSubmit()) dialogs.initialize_dialog_authentication(DialogAuthentication()) dialogs.initialize_dialog_server_missing(DialogServerMissing()) dialogs.initialize_dialog_server(DialogServer()) # signals and slots between dialogs # settings -> server dialogs.settings.server_created.connect(dialogs.server.new) dialogs.settings.server_edited.connect(dialogs.server.edit) dialogs.settings.server_copied.connect(dialogs.server.copy) # settings -> action dialogs.settings.action_created.connect(dialogs.action.new) dialogs.settings.action_edited.connect(dialogs.action.edit) dialogs.settings.action_copied.connect(dialogs.action.copy) # action -> settings refresh_list dialogs.action.edited_update_list.connect(dialogs.settings.refresh_list) # server -> settings refresh_list dialogs.server.edited_update_list.connect(dialogs.settings.refresh_list) # servers and actions list update dialogs.settings.update_list.connect(dialogs.settings.refresh_list) # macOS dock icon fix dialogs.action.check_macos_dock_icon_fix_show.connect(dialogs.show_macos_dock_icon_if_necessary) dialogs.action.check_macos_dock_icon_fix_hide.connect(dialogs.hide_macos_dock_icon_if_necessary) dialogs.authentication.check_macos_dock_icon_fix_show.connect(dialogs.show_macos_dock_icon_if_necessary) dialogs.authentication.check_macos_dock_icon_fix_hide.connect(dialogs.hide_macos_dock_icon_if_necessary) dialogs.authentication.check_macos_dock_icon_fix_show.connect(dialogs.show_macos_dock_icon_if_necessary) dialogs.authentication.check_macos_dock_icon_fix_hide.connect(dialogs.hide_macos_dock_icon_if_necessary) dialogs.server.check_macos_dock_icon_fix_show.connect(dialogs.show_macos_dock_icon_if_necessary) dialogs.server.check_macos_dock_icon_fix_hide.connect(dialogs.hide_macos_dock_icon_if_necessary)Nagstamon-master/Nagstamon/qui/dialogs/about.py000066400000000000000000000060211505160700500221270ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from os import sep from platform import python_version from Nagstamon.config import (AppInfo, RESOURCES) from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (QSvgWidget, Qt, QT_VERSION_STR) class DialogAbout(Dialog): """ About information dialog """ def __init__(self): Dialog.__init__(self, 'dialog_about') # first add the logo on top - no idea how to achive in Qt Designer logo = QSvgWidget(f'{RESOURCES}{sep}nagstamon.svg') logo.setFixedSize(100, 100) self.window.vbox_about.insertWidget(1, logo, 0, Qt.AlignmentFlag.AlignHCenter) # update version information self.window.label_nagstamon.setText(f'

{AppInfo.NAME} {AppInfo.VERSION}

') self.window.label_nagstamon_long.setText('

Nagiosš status monitor for your desktop') self.window.label_copyright.setText(AppInfo.COPYRIGHT) self.window.label_website.setText(f'{AppInfo.WEBSITE}') self.window.label_website.setOpenExternalLinks(True) self.window.label_versions.setText(f'Python: {python_version()}, Qt: {QT_VERSION_STR}') self.window.label_contribution.setText( f'Contribution | Donation') self.window.label_footnote.setText('š meanwhile many more monitors...') # fill in license information license_file = open(f'{RESOURCES}{sep}LICENSE', encoding='utf-8') license_file_content = license_file.read() license_file.close() self.window.textedit_license.setPlainText(license_file_content) self.window.textedit_license.setReadOnly(True) # fill in credits information credits_file = open(f'{RESOURCES}{sep}CREDITS', encoding='utf-8') credits_file_content = credits_file.read() credits_file.close() self.window.textedit_credits.setText(credits_file_content) self.window.textedit_credits.setOpenExternalLinks(True) self.window.textedit_credits.setReadOnly(True) self.window.tabs.setCurrentIndex(0) Nagstamon-master/Nagstamon/qui/dialogs/acknowledge.py000066400000000000000000000165651505160700500233160ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (QDateTime, Signal, Slot) from Nagstamon.Servers import SERVER_TYPES class DialogAcknowledge(Dialog): """ dialog for acknowledging host/service problems """ # store host and service to be used for OK button evaluation server = None host_list = service_list = [] # tell worker to acknowledge some troublesome item acknowledge = Signal(dict) def __init__(self,): Dialog.__init__(self, 'dialog_acknowledge') self.TOGGLE_DEPS = { self.window.input_checkbox_use_expire_time: [self.window.input_datetime_expire_time] } # still clumsy but better than negating the other server types PROMETHEUS_OR_ALERTMANAGER = ['Alertmanager', 'Prometheus'] NOT_PROMETHEUS_OR_ALERTMANAGER = [x.TYPE for x in SERVER_TYPES.values() if x.TYPE not in PROMETHEUS_OR_ALERTMANAGER] self.VOLATILE_WIDGETS = { self.window.input_checkbox_use_expire_time: ['IcingaWeb2', 'Icinga2API'], self.window.input_datetime_expire_time: ['IcingaWeb2', 'Icinga2API', 'Alertmanager'], self.window.input_checkbox_sticky_acknowledgement: NOT_PROMETHEUS_OR_ALERTMANAGER, self.window.input_checkbox_send_notification: NOT_PROMETHEUS_OR_ALERTMANAGER, self.window.input_checkbox_persistent_comment: NOT_PROMETHEUS_OR_ALERTMANAGER, self.window.input_checkbox_acknowledge_all_services: NOT_PROMETHEUS_OR_ALERTMANAGER } self.FORCE_DATETIME_EXPIRE_TIME = ['Alertmanager'] @Slot(object, list, list) def initialize(self, server=None, host=[], service=[]): # store server, host and service to be used for OK button evaluation self.server = server self.host_list = host self.service_list = service self.window.setWindowTitle('Acknowledge hosts and services') text = '' for i in range(len(self.host_list)): if self.service_list[i] == "": text += f'Host {self.host_list[i]}
' else: text += f'Service {self.service_list[i]} on host {self.host_list[i]}
' self.window.input_label_description.setText(text) # default flags of monitor acknowledgement self.window.input_checkbox_sticky_acknowledgement.setChecked(conf.defaults_acknowledge_sticky) self.window.input_checkbox_send_notification.setChecked(conf.defaults_acknowledge_send_notification) self.window.input_checkbox_persistent_comment.setChecked(conf.defaults_acknowledge_persistent_comment) self.window.input_checkbox_use_expire_time.setChecked(conf.defaults_acknowledge_expire) if len(self.host_list) == 1: self.window.input_checkbox_acknowledge_all_services.setChecked(conf.defaults_acknowledge_all_services) self.window.input_checkbox_acknowledge_all_services.show() else: self.window.input_checkbox_acknowledge_all_services.setChecked(False) self.window.input_checkbox_acknowledge_all_services.hide() # default author + comment self.window.input_lineedit_comment.setText(conf.defaults_acknowledge_comment) self.window.input_lineedit_comment.setFocus() # set default and minimum value for expiry time qdatetime = QDateTime.currentDateTime() self.window.input_datetime_expire_time.setMinimumDateTime(qdatetime) # set default expiry time from configuration self.window.input_datetime_expire_time.setDateTime(qdatetime.addSecs( conf.defaults_acknowledge_expire_duration_hours * 60 * 60 + conf.defaults_acknowledge_expire_duration_minutes * 60 )) # Show or hide widgets based on server if self.server is not None: for widget, server_types in self.VOLATILE_WIDGETS.items(): if self.server.TYPE in server_types: widget.show() self.toggle_toggles() else: widget.hide() if self.server.TYPE in self.FORCE_DATETIME_EXPIRE_TIME: self.window.input_datetime_expire_time.show() # Adjust to current size if items are hidden in the menu # Otherwise it will get confused and chop off text self.window.options_groupbox.adjustSize() self.window.adjustSize() def ok(self): """ acknowledge miserable host/service """ # create a list of all service of selected host to acknowledge them all all_services = list() acknowledge_all_services = self.window.input_checkbox_acknowledge_all_services.isChecked() if acknowledge_all_services is True: for i in self.server.nagitems_filtered["services"].values(): for s in i: if s.host in self.host_list: all_services.append(s.name) if self.window.input_checkbox_use_expire_time.isChecked() or self.server.TYPE in self.FORCE_DATETIME_EXPIRE_TIME: # Format used in UI # 2019-11-01T18:17:39 expire_datetime = self.window.input_datetime_expire_time.dateTime().toString("yyyy-MM-ddTHH:mm:ss") else: expire_datetime = None for line_number in range(len(self.host_list)): service = self.service_list[line_number] host = self.host_list[line_number] # send signal to tablewidget worker to care about acknowledging with supplied information self.acknowledge.emit({'server': self.server, 'host': host, 'service': service, 'author': self.server.username, 'comment': self.window.input_lineedit_comment.text(), 'sticky': self.window.input_checkbox_sticky_acknowledgement.isChecked(), 'notify': self.window.input_checkbox_send_notification.isChecked(), 'persistent': self.window.input_checkbox_persistent_comment.isChecked(), 'acknowledge_all_services': acknowledge_all_services, 'all_services': all_services, 'expire_time': expire_datetime}) # call close and macOS dock icon treatment from ancestor super().ok() Nagstamon-master/Nagstamon/qui/dialogs/action.py000066400000000000000000000253441505160700500223030ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from copy import deepcopy from functools import wraps from urllib.parse import quote from Nagstamon.Servers import SERVER_TYPES from Nagstamon.config import (Action, conf) from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (QMessageBox, Signal, Slot) class DialogAction(Dialog): """ Dialog used to set up one single action """ # signal to emit when ok button is pressed - used to update the list of actions edited_update_list = Signal(str, str, str) # mapping between action types and combobox content ACTION_TYPES = {'browser': 'Browser', 'command': 'Command', 'url': 'URL'} def __init__(self): Dialog.__init__(self, 'settings_action') # initial values self.action_conf = None self.mode = None self.previous_action_conf = None # define checkbox-to-widgets dependencies which apply at initialization # which widgets have to be hidden because of irrelevance # dictionary holds checkbox/radiobutton as key and relevant widgets in a list self.TOGGLE_DEPS = { self.window.input_checkbox_re_host_enabled: [self.window.input_lineedit_re_host_pattern, self.window.input_checkbox_re_host_reverse], self.window.input_checkbox_re_service_enabled: [self.window.input_lineedit_re_service_pattern, self.window.input_checkbox_re_service_reverse], self.window.input_checkbox_re_status_information_enabled: [ self.window.input_lineedit_re_status_information_pattern, self.window.input_checkbox_re_status_information_reverse], self.window.input_checkbox_re_duration_enabled: [self.window.input_lineedit_re_duration_pattern, self.window.input_checkbox_re_duration_reverse], self.window.input_checkbox_re_attempt_enabled: [self.window.input_lineedit_re_attempt_pattern, self.window.input_checkbox_re_attempt_reverse], self.window.input_checkbox_re_groups_enabled: [self.window.input_lineedit_re_groups_pattern, self.window.input_checkbox_re_groups_reverse]} # fill action types into combobox self.window.input_combobox_type.addItems(sorted(self.ACTION_TYPES.values())) # fill default order fields combobox with monitor server types self.window.input_combobox_monitor_type.addItem("All monitor servers") self.window.input_combobox_monitor_type.addItems(sorted(SERVER_TYPES.keys(), key=str.lower)) # default to Nagios as it is the mostly used monitor server self.window.input_combobox_monitor_type.setCurrentIndex(0) def dialog_decoration(method, *args): """ try with a decorator instead of repeated calls """ # the function which decorates method # wraps is used to keep the original method's name and docstring @wraps(method) def decoration_function(self, *decorated_args): """ self.server_conf has to be set by decorated method """ # previous action conf only useful when editing - defaults to None self.previous_action_conf = None # call decorated method method(self, *decorated_args) # run through all input widgets and apply defaults from config for widget in self.window.__dict__: if widget.startswith('input_'): if widget.startswith('input_checkbox_'): setting = widget.split('input_checkbox_')[1] self.window.__dict__[widget].setChecked(self.action_conf.__dict__[setting]) elif widget.startswith('input_radiobutton_'): setting = widget.split('input_radiobutton_')[1] self.window.__dict__[widget].setChecked(self.action_conf.__dict__[setting]) elif widget.startswith('input_lineedit_'): setting = widget.split('input_lineedit_')[1] self.window.__dict__[widget].setText(self.action_conf.__dict__[setting]) elif widget.startswith('input_textedit_'): setting = widget.split('input_textedit_')[1] self.window.__dict__[widget].setText(self.action_conf.__dict__[setting]) # set comboboxes self.window.input_combobox_type.setCurrentText(self.ACTION_TYPES[self.action_conf.type.lower()]) self.window.input_combobox_monitor_type.setCurrentText(self.action_conf.monitor_type) # apply toggle-dependencies between checkboxes and certain widgets self.toggle_toggles() # important final size adjustment self.window.adjustSize() # if running on macOS with disabled dock icon, the dock icon might have to be made visible # to make Nagstamon accept keyboard input self.check_macos_dock_icon_fix_show.emit() self.window.exec() # en reverse the dock icon might be hidden again after a potential keyboard input self.check_macos_dock_icon_fix_hide.emit() # give back decorated function return decoration_function @Slot() @dialog_decoration def new(self): """ create new server """ self.mode = 'new' # create a new server config object self.action_conf = Action() # window title might be pretty simple self.window.setWindowTitle('New action') @Slot(str) @dialog_decoration def edit(self, name): """ edit existing action """ self.mode = 'edit' # shorter action conf self.action_conf = conf.actions[name] # store action name in case it will be changed self.previous_action_conf = deepcopy(self.action_conf) # set window title self.window.setWindowTitle(f'Edit {self.action_conf.name}') @Slot(str) @dialog_decoration def copy(self, name): """ copy existing action """ self.mode = 'copy' # shorter action conf self.action_conf = deepcopy(conf.actions[name]) # set window title before name change to reflect copy self.window.setWindowTitle(f'Copy {self.action_conf.name}') # indicate copy of other action self.action_conf.name = f'Copy of {self.action_conf.name}' def ok(self): """ evaluate the state of widgets to get new configuration """ # check that no duplicate name exists if self.window.input_lineedit_name.text() in conf.actions and \ (self.mode in ['new', 'copy'] or self.mode == 'edit' and self.action_conf != conf.actions[self.window.input_lineedit_name.text()]): # cry if duplicate name exists QMessageBox.critical(self.window, 'Nagstamon', f'The action name {self.window.input_lineedit_name.text()} is already used.', QMessageBox.StandardButton.Ok) else: # get configuration from UI for widget in self.window.__dict__: if widget.startswith('input_'): if widget.startswith('input_checkbox_'): setting = widget.split('input_checkbox_')[1] self.action_conf.__dict__[setting] = self.window.__dict__[widget].isChecked() if widget.startswith('input_radiobutton_'): setting = widget.split('input_radiobutton_')[1] self.action_conf.__dict__[setting] = self.window.__dict__[widget].isChecked() elif widget.startswith('input_combobox_'): setting = widget.split('input_combobox_')[1] self.action_conf.__dict__[setting] = self.window.__dict__[widget].currentText() elif widget.startswith('input_lineedit_'): setting = widget.split('input_lineedit_')[1] self.action_conf.__dict__[setting] = self.window.__dict__[widget].text() elif widget.startswith('input_textedit_'): setting = widget.split('input_textedit_')[1] self.action_conf.__dict__[setting] = self.window.__dict__[widget].toPlainText() # edited action will be deleted and recreated with new configuration if self.mode == 'edit': # delete previous name conf.actions.pop(self.previous_action_conf.name) # Avoid the wrong monitor type which blocks display of action if self.action_conf.monitor_type not in SERVER_TYPES: self.action_conf.monitor_type = '' # lower type to recognize action type on monitor self.action_conf.type = self.action_conf.type.lower() # add edited or new/copied action conf.actions[self.action_conf.name] = self.action_conf # refresh list of actions, give call the current action name to highlight it self.edited_update_list.emit('list_actions', 'actions', self.action_conf.name) # delete the old action .conf file to reflect name changes # new one will be written soon if self.previous_action_conf is not None: conf.delete_file('actions', 'action_{0}.conf'.format(quote(self.previous_action_conf.name, safe=''))) # store server settings conf.save_multiple_config('actions', 'action') # call close and macOS dock icon treatment from ancestor super().ok() Nagstamon-master/Nagstamon/qui/dialogs/authentication.py000066400000000000000000000136531505160700500240450ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (Signal, Slot) from Nagstamon.Servers import servers class DialogAuthentication(Dialog): """ dialog for authentication """ # store server server = None # signal for telling server_vbox label to update update = Signal(str) # signal to tell the world that the authentication dialog will show up show_up = Signal() def __init__(self): Dialog.__init__(self, 'dialog_authentication') def initialize(self): """ setup dialog fitting to server """ if self.server is not None: self.window.setWindowTitle('Authenticate {0}'.format(self.server.name)) if self.server.type in ['Centreon', 'Thruk']: self.window.input_checkbox_use_autologin.show() self.window.input_lineedit_autologin_key.show() self.window.input_lineedit_autologin_key.show() self.window.label_autologin_key.show() # enable switching autologin key and password self.window.input_checkbox_use_autologin.clicked.connect(self.toggle_autologin) self.window.input_checkbox_use_autologin.setChecked(self.server.use_autologin) self.window.input_lineedit_autologin_key.setText(self.server.autologin_key) # initialize autologin self.toggle_autologin() else: self.window.input_checkbox_use_autologin.hide() self.window.input_lineedit_autologin_key.hide() self.window.label_autologin_key.hide() # set existing values self.window.input_lineedit_username.setText(self.server.username) self.window.input_lineedit_password.setText(self.server.password) self.window.input_checkbox_save_password.setChecked(conf.servers[self.server.name].save_password) @Slot(str) def show_auth_dialog(self, server): """ initialize and show authentication dialog """ self.server = servers[server] self.initialize() self.show_up.emit() self.window.adjustSize() # the dock icon might be needed to be shown for a potential keyboard input self.check_macos_dock_icon_fix_show.emit() self.window.exec() # en reverse the dock icon might be hidden again after a potential keyboard input self.check_macos_dock_icon_fix_hide.emit() def ok(self): """ take username and password """ # close window fist to avoid lagging UI self.window.close() self.server.username = self.window.input_lineedit_username.text() self.server.password = self.window.input_lineedit_password.text() self.server.refresh_authentication = False # store password if it should be saved if self.window.input_checkbox_save_password.isChecked(): conf.servers[self.server.name].username = self.server.username conf.servers[self.server.name].password = self.server.password conf.servers[self.server.name].save_password = self.window.input_checkbox_save_password.isChecked() # store server settings conf.save_multiple_config('servers', 'server') # Centreon if self.server.type in ['Centreon', 'Thruk']: if self.window.input_checkbox_use_autologin: conf.servers[self.server.name].use_autologin = self.window.input_checkbox_use_autologin.isChecked() conf.servers[self.server.name].autologin_key = self.window.input_lineedit_autologin_key.text() # store server settings conf.save_multiple_config('servers', 'server') # reset server connection self.server.reset_HTTP() # force server to recheck right now self.server.thread_counter = conf.update_interval_seconds # update server_vbox label self.update.emit(self.server.name) # call close and macOS dock icon treatment from ancestor super().ok() @Slot() def toggle_autologin(self): """ toggle autologin option for Centreon """ if self.window.input_checkbox_use_autologin.isChecked(): self.window.label_username.hide() self.window.label_password.hide() self.window.input_lineedit_username.hide() self.window.input_lineedit_password.hide() self.window.input_checkbox_save_password.hide() self.window.label_autologin_key.show() self.window.input_lineedit_autologin_key.show() else: self.window.label_username.show() self.window.label_password.show() self.window.input_lineedit_username.show() self.window.input_lineedit_password.show() self.window.input_checkbox_save_password.show() self.window.label_autologin_key.hide() self.window.input_lineedit_autologin_key.hide() # adjust dialog window size after UI changes self.window.adjustSize() Nagstamon-master/Nagstamon/qui/dialogs/check_version.py000066400000000000000000000163441505160700500236500ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import AppInfo from Nagstamon.qui.qt import (QMessageBox, QObject, Qt, QThread, QWidget, Signal, Slot) from Nagstamon.Servers import get_enabled_servers class CheckVersion(QObject): """ checking for updates """ is_checking = False version_info_retrieved = Signal() @Slot(bool, QWidget) def check(self, start_mode=False, parent=None): if not self.is_checking: # lock checking thread self.is_checking = True # list of enabled servers which connections outside should be used to check self.enabled_servers = get_enabled_servers() # set mode to be evaluated by worker self.start_mode = start_mode # store caller of dialog window - not if at start because this will disturb EWMH if start_mode: self.parent = None else: self.parent = parent # thread for worker to avoid self.worker_thread = QThread(parent=self) self.worker = self.Worker(start_mode) # if update check is ready it sends the message to GUI thread self.worker.ready.connect(self.show_message) # stop thread if worker has finished self.worker.finished.connect(self.worker_thread.quit) # reset checking lock if finished self.worker.finished.connect(self.reset_checking) self.worker.moveToThread(self.worker_thread) # run check when thread starts self.worker_thread.started.connect(self.worker.check) self.worker_thread.start(QThread.Priority.LowestPriority) @Slot() def reset_checking(self): """ reset checking the flag to avoid QThread crashes """ self.is_checking = False @Slot(str) def show_message(self, message): """ message dialog must be shown from GUI thread """ self.version_info_retrieved.emit() # attempt to solve https://github.com/HenriWahl/Nagstamon/issues/303 # might be working this time parent = self.parent messagebox = QMessageBox(QMessageBox.Icon.Information, 'Nagstamon version check', message, QMessageBox.StandardButton.Ok, parent, Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint) messagebox.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) messagebox.setWindowModality(Qt.WindowModality.NonModal) messagebox.exec() class Worker(QObject): """ check for a new version in the background """ # send signal if some version information is available ready = Signal(str) finished = Signal() def __init__(self, start_mode=False): QObject.__init__(self) self.start_mode = start_mode def check(self): """ check for update using server connection """ # get servers to be used for checking the version enabled_servers = get_enabled_servers() # default latest version is 'unavailable' and message empty latest_version = 'unavailable' message = '' # find at least one server which allows getting version information for server in enabled_servers: for download_server, download_url in AppInfo.DOWNLOAD_SERVERS.items(): # dummy message just in case version check does not work message = 'Cannot reach version check at {0}.'.format( f'https://{download_server}{AppInfo.VERSION_PATH}') # retrieve VERSION_URL without auth information response = server.fetch_url(f'https://{download_server}{AppInfo.VERSION_PATH}', giveback='raw', no_auth=True) # stop searching the available download URLs if response.error == '' and \ not response.result.startswith('<') and \ not '\n' in response.result and \ 5 < len(response.result) < 20 and \ response.result[0].isdigit(): latest_version = response.result.strip() break # ignore TLS error in case it was caused by requesting the latest version - not important for monitoring server.tls_error = False # stop searching via enabled servers if response.error == '' and not response.result.startswith('<'): latest_version = response.result.strip() break # compose a message according to version information if latest_version != 'unavailable': if latest_version == AppInfo.VERSION: message = f'You are using the latest version Nagstamon {AppInfo.VERSION}.' # avoid GitHub HTML being evaluated as version number -> checking for length elif latest_version > AppInfo.VERSION and not len(latest_version) > 20: message = f'The new version Nagstamon {latest_version} is available.

' \ f'Get it at {AppInfo.WEBSITE}/download.' elif latest_version < AppInfo.VERSION: # for some reason, the local version is newer than that remote one - just ignore message = '' # check if there is anything to tell if message != '': # if run from startup do not cry if any error occurred or nothing new is available if self.start_mode is False or \ (self.start_mode is True and latest_version not in ('unavailable', AppInfo.VERSION)): self.ready.emit(message) # tell thread to finish self.finished.emit() # initialized an object to be used in other modules check_version = CheckVersion() Nagstamon-master/Nagstamon/qui/dialogs/dialog.py000066400000000000000000000170651505160700500222660ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, RESOURCES, OS, OS_MACOS) from Nagstamon.qui.constants import ICON from Nagstamon.qui.globals import statuswindow_properties from Nagstamon.qui.helpers import hide_macos_dock_icon from Nagstamon.qui.qt import (QBrush, QListWidgetItem, QObject, QSignalMapper, QSizePolicy, Qt, Signal, Slot, uic) # make icon status in macOS dock accessible via NSApp, used by set_macos_dock_icon_visible() if OS == OS_MACOS: from AppKit import NSApp class Dialog(QObject): """ one single dialog """ # send a signal, e.g., to the status window if a dialog pops up show_dialog = Signal() # signals for macOS dock icon fix check_macos_dock_icon_fix_show = Signal() check_macos_dock_icon_fix_hide = Signal() # dummy toggle dependencies TOGGLE_DEPS = {} # auxiliary list of checkboxes which HIDE some other widgets if triggered - for example proxy OS settings TOGGLE_DEPS_INVERTED = [] # widgets that might be enabled/disabled depending on a monitor server type VOLATILE_WIDGETS = {} # names of widgets and their defaults WIDGET_NAMES = {} # style stuff used by settings dialog for servers/actions listwidget GRAY = QBrush(Qt.GlobalColor.gray) def __init__(self, dialog): QObject.__init__(self) # load UI file from resources self.window = uic.loadUi(f'{RESOURCES}/qui/{dialog}.ui') # explicitly set window flags to avoid '?' button on Windows self.window.setWindowFlags(Qt.WindowType.WindowCloseButtonHint) # hoping to avoid overly large dialogs self.window.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) # set small titlebar icon self.window.setWindowIcon(ICON) # treat dialog content after pressing OK button if 'button_box' in dir(self.window): self.window.button_box.accepted.connect(self.ok) self.window.button_box.rejected.connect(self.cancel) # QSignalMapper needed to connect all toggle-needing-checkboxes/radiobuttons to one .toggle()-method which # decides which sender to use as key in self.TOGGLE_DEPS self.signalmapper_toggles = QSignalMapper() # try to get and keep focus self.window.setWindowModality(Qt.WindowModality.ApplicationModal) def initialize(self): """ dummy initialize method """ pass @Slot() def show(self, tab=0): """ simple show method, to be enriched """ # if running on macOS with disabled dock icon the dock icon might have to be made visible # to make Nagstamon accept keyboard input self.check_macos_dock_icon_fix_show.emit() # in case dock icon is configured invisible in macOS it has to be shown while dialog is shown # to be able to get keyboard focus if OS == OS_MACOS and \ conf.icon_in_systray and \ conf.hide_macos_dock_icon: hide_macos_dock_icon(False) # tell the world that dialog pops up self.show_dialog.emit() # reset the window if it only needs smaller screen estate self.window.adjustSize() self.window.show() # make sure the dialog window will be the topmost self.window.raise_() # hidden dock icon on macOS needs extra activation if OS == OS_MACOS and \ conf.icon_in_systray and \ conf.hide_macos_dock_icon: NSApp.activateIgnoringOtherApps_(True) def toggle_visibility(self, checkbox, widgets=None): """ state of checkbox toggles visibility of widgets some checkboxes might trigger an inverted behaviour - thus the 'inverted' value """ if widgets is None: widgets = [] if checkbox in self.TOGGLE_DEPS_INVERTED: if checkbox.isChecked(): for widget in widgets: widget.hide() else: for widget in widgets: widget.show() # normal case - click on checkbox activates more options else: if checkbox.isChecked(): for widget in widgets: widget.show() else: for widget in widgets: widget.hide() @Slot(str) def toggle(self, checkbox): """ change state of the dependant widgets, slot for signals from checkboxes in UI """ # Due to older Qt5 in Ubuntu 14.04 signal mapper has to use strings self.toggle_visibility(self.window.__dict__[checkbox], self.TOGGLE_DEPS[self.window.__dict__[checkbox]]) # adjust dialog window size after UI changes self.window.adjustSize() def toggle_toggles(self): # apply toggle-dependencies between checkboxes as certain widgets for checkbox, widgets in self.TOGGLE_DEPS.items(): # toggle visibility self.toggle_visibility(checkbox, widgets) # multiplex slot .toggle() by signal-mapping # Due to older Qt5 in Ubuntu 14.04 signal mapper has to use strings self.signalmapper_toggles.setMapping(checkbox, checkbox.objectName()) checkbox.toggled.connect(self.signalmapper_toggles.map) checkbox.toggled.connect(self.window.adjustSize) # finally, map signals with .sender() - [QWidget] is important! self.signalmapper_toggles.mappedString[str].connect(self.toggle) def fill_list(self, list_widget, config): """ fill list widget with items from config """ for config_item in sorted(config, key=str.lower): list_item = QListWidgetItem(config_item) if config[config_item].enabled is False: list_item.setForeground(self.GRAY) list_widget.addItem(list_item) @Slot() def ok(self): """ as default closes dialog - might be refined, for example, by settings dialog """ self.window.close() # en reverse the dock icon might be hidden again after a potential keyboard input self.check_macos_dock_icon_fix_show.emit() @Slot() def cancel(self): """ as default closes dialog - might be refined, for example by settings dialog """ self.window.close() # en reverse the dock icon might be hidden again after a potential keyboard input self.check_macos_dock_icon_fix_hide.emit() Nagstamon-master/Nagstamon/qui/dialogs/downtime.py000066400000000000000000000132561505160700500226530ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (Signal, Slot) class DialogDowntime(Dialog): """ dialog for putting hosts/services into downtime """ # send signal to get start and end of downtime asynchronously get_start_end = Signal(str, str) # signal to tell worker to commit downtime downtime = Signal(dict) # store host and service to be used for OK button evaluation server = None host_list = service_list = [] def __init__(self): Dialog.__init__(self, 'dialog_downtime') def initialize(self, server=None, host=[], service=[]): # store server, host and service to be used for OK button evaluation self.server = server self.host_list = host self.service_list = service self.window.setWindowTitle('Downtime for host and service') text = '' for i in range(len(self.host_list)): if self.service_list[i] == "": text += f'Host {self.host_list[i]}
' else: text += f'Service {self.service_list[i]} on host {self.host_list[i]}
' self.window.input_label_description.setText(text) # default flags of monitor acknowledgement self.window.input_spinbox_duration_hours.setValue(int(conf.defaults_downtime_duration_hours)) self.window.input_spinbox_duration_minutes.setValue(int(conf.defaults_downtime_duration_minutes)) self.window.input_radiobutton_type_fixed.setChecked(conf.defaults_downtime_type_fixed) self.window.input_radiobutton_type_flexible.setChecked(conf.defaults_downtime_type_flexible) # hide/show downtime settings according to type self.window.input_radiobutton_type_fixed.clicked.connect(self.set_type_fixed) self.window.input_radiobutton_type_flexible.clicked.connect(self.set_type_flexible) # show or hide widgets for time settings if self.window.input_radiobutton_type_fixed.isChecked(): self.set_type_fixed() else: self.set_type_flexible() # empty times at start, will be filled by set_start_end self.window.input_lineedit_start_time.setText('n/a') self.window.input_lineedit_end_time.setText('n/a') # default author + comment self.window.input_lineedit_comment.setText(conf.defaults_downtime_comment) self.window.input_lineedit_comment.setFocus() if self.server is not None: # at first initialization server is still None self.get_start_end.emit(self.server.name, self.host_list[0]) def ok(self): """ schedule downtime for miserable host/service """ # type of downtime - fixed or flexible if self.window.input_radiobutton_type_fixed.isChecked() is True: fixed = 1 else: fixed = 0 for line_number in range(len(self.host_list)): service = self.service_list[line_number] host = self.host_list[line_number] self.downtime.emit({'server': self.server, 'host': host, 'service': service, 'author': self.server.username, 'comment': self.window.input_lineedit_comment.text(), 'fixed': fixed, 'start_time': self.window.input_lineedit_start_time.text(), 'end_time': self.window.input_lineedit_end_time.text(), 'hours': int(self.window.input_spinbox_duration_hours.value()), 'minutes': int(self.window.input_spinbox_duration_minutes.value())}) # call close and macOS dock icon treatment from ancestor super().ok() @Slot(str, str) def set_start_end(self, start, end): """ put values sent by worker into start and end fields """ self.window.input_lineedit_start_time.setText(start) self.window.input_lineedit_end_time.setText(end) @Slot() def set_type_fixed(self): """ enable/disable appropriate widgets if type is "Fixed" """ self.window.label_duration.hide() self.window.label_duration_hours.hide() self.window.label_duration_minutes.hide() self.window.input_spinbox_duration_hours.hide() self.window.input_spinbox_duration_minutes.hide() @Slot() def set_type_flexible(self): """ enable/disable appropriate widgets if type is "Flexible" """ self.window.label_duration.show() self.window.label_duration_hours.show() self.window.label_duration_minutes.show() self.window.input_spinbox_duration_hours.show() self.window.input_spinbox_duration_minutes.show() Nagstamon-master/Nagstamon/qui/dialogs/server.py000066400000000000000000000544471505160700500223420ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from copy import deepcopy from functools import wraps import os from urllib.parse import quote from Nagstamon.config import conf, CONFIG_STRINGS, BOOLPOOL, Server from Nagstamon.qui.globals import (ecp_available, kerberos_available) from Nagstamon.qui.qt import (QFileDialog, QMessageBox, QStyle, Signal, Slot) from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.Servers import (create_server, servers, SERVER_TYPES) class DialogServer(Dialog): """ dialog used to set up one single server """ # tell server has been edited edited = Signal() # signal to emit when ok button is pressed - used to remove previous server edited_remove_previous = Signal(str) # signal to emit when ok button is pressed - used to update the list of servers edited_update_list = Signal(str, str, str) # signal to emit when a new server vbox has to be created create_server_vbox = Signal(str) def __init__(self): Dialog.__init__(self, 'settings_server') # file chooser Dialog self.file_chooser = QFileDialog() # configuration for server self.server_conf = None # define checkbox-to-widgets dependencies which apply at initialization # which widgets have to be hidden because of irrelevance # dictionary holds checkbox/radiobutton as key and relevant widgets in a list self.TOGGLE_DEPS = { self.window.input_checkbox_use_autologin: [self.window.label_autologin_key, self.window.input_lineedit_autologin_key], self.window.input_checkbox_use_proxy: [self.window.groupbox_proxy], self.window.input_checkbox_use_proxy_from_os: [self.window.label_proxy_address, self.window.input_lineedit_proxy_address, self.window.label_proxy_username, self.window.input_lineedit_proxy_username, self.window.label_proxy_password, self.window.input_lineedit_proxy_password], self.window.input_checkbox_show_options: [self.window.groupbox_options], self.window.input_checkbox_custom_cert_use: [self.window.label_custom_ca_file, self.window.input_lineedit_custom_cert_ca_file, self.window.button_choose_custom_cert_ca_file]} self.TOGGLE_DEPS_INVERTED = [self.window.input_checkbox_use_proxy_from_os] # these widgets are shown or hidden depending on server type properties # the servers listed at each widget do need them self.VOLATILE_WIDGETS = { self.window.label_monitor_cgi_url: ['Nagios', 'Icinga', 'Thruk', 'Sensu', 'SensuGo'], self.window.input_lineedit_monitor_cgi_url: ['Nagios', 'Icinga', 'Thruk', 'Sensu', 'SensuGo'], self.window.input_checkbox_use_autologin: ['Centreon', 'monitos4x', 'Thruk'], self.window.input_lineedit_autologin_key: ['Centreon', 'monitos4x', 'Thruk'], self.window.label_autologin_key: ['Centreon', 'monitos4x', 'Thruk'], self.window.input_checkbox_no_cookie_auth: ['IcingaWeb2', 'Sensu'], self.window.input_checkbox_use_display_name_host: ['Icinga', 'IcingaWeb2'], self.window.input_checkbox_use_display_name_service: ['Icinga', 'IcingaWeb2', 'Thruk'], self.window.input_checkbox_use_description_name_service: ['Zabbix'], self.window.input_checkbox_force_authuser: ['Checkmk Multisite'], self.window.groupbox_checkmk_views: ['Checkmk Multisite'], self.window.input_lineedit_host_filter: ['op5Monitor'], self.window.input_lineedit_service_filter: ['op5Monitor'], self.window.label_service_filter: ['op5Monitor'], self.window.label_host_filter: ['op5Monitor'], self.window.input_lineedit_hashtag_filter: ['Opsview'], self.window.label_hashtag_filter: ['Opsview'], self.window.input_checkbox_can_change_only: ['Opsview'], self.window.label_monitor_site: ['Sensu'], self.window.input_lineedit_monitor_site: ['Sensu'], self.window.label_map_to_hostname: ['Prometheus', 'Alertmanager'], self.window.input_lineedit_map_to_hostname: ['Prometheus', 'Alertmanager'], self.window.label_map_to_servicename: ['Prometheus', 'Alertmanager'], self.window.input_lineedit_map_to_servicename: ['Prometheus', 'Alertmanager'], self.window.label_map_to_status_information: ['Prometheus', 'Alertmanager'], self.window.input_lineedit_map_to_status_information: ['Prometheus', 'Alertmanager'], self.window.label_alertmanager_filter: ['Alertmanager'], self.window.input_lineedit_alertmanager_filter: ['Alertmanager'], self.window.label_map_to_ok: ['Alertmanager'], self.window.input_lineedit_map_to_ok: ['Alertmanager'], self.window.label_map_to_unknown: ['Alertmanager'], self.window.input_lineedit_map_to_unknown: ['Alertmanager'], self.window.label_map_to_warning: ['Alertmanager'], self.window.input_lineedit_map_to_warning: ['Alertmanager'], self.window.label_map_to_critical: ['Alertmanager'], self.window.input_lineedit_map_to_critical: ['Alertmanager'], self.window.label_map_to_down: ['Alertmanager'], self.window.input_lineedit_map_to_down: ['Alertmanager'], self.window.input_lineedit_notification_filter: ['IcingaDBWebNotifications'], self.window.label_notification_filter: ['IcingaDBWebNotifications'], self.window.input_lineedit_notification_lookback: ['IcingaDBWebNotifications'], self.window.label_notification_lookback: ['IcingaDBWebNotifications'], self.window.label_disabled_backends: ['Thruk'], self.window.input_lineedit_disabled_backends: ['Thruk'], } # to be used when selecting authentication method Kerberos self.AUTHENTICATION_WIDGETS = [ self.window.label_username, self.window.input_lineedit_username, self.window.label_password, self.window.input_lineedit_password, self.window.input_checkbox_save_password] self.AUTHENTICATION_BEARER_WIDGETS = [ self.window.label_username, self.window.input_lineedit_username] self.AUTHENTICATION_ECP_WIDGETS = [ self.window.label_idp_ecp_endpoint, self.window.input_lineedit_idp_ecp_endpoint] # fill default order fields combobox with monitor server types self.window.input_combobox_type.addItems(sorted(SERVER_TYPES.keys(), key=str.lower)) # default to Nagios as it is the mostly used monitor server self.window.input_combobox_type.setCurrentText('Nagios') # set folder and play symbols to choose and play buttons self.window.button_choose_custom_cert_ca_file.setText('') self.window.button_choose_custom_cert_ca_file.setIcon( self.window.button_choose_custom_cert_ca_file.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)) # connect choose custom cert CA file button with file dialog self.window.button_choose_custom_cert_ca_file.clicked.connect(self.choose_custom_cert_ca_file) # fill authentication combobox self.window.input_combobox_authentication.addItems(['Basic', 'Digest', 'Bearer']) if ecp_available: self.window.input_combobox_authentication.addItems(['ECP']) if kerberos_available: self.window.input_combobox_authentication.addItems(['Kerberos']) # detect change of a server type which leads to certain options shown or hidden self.window.input_combobox_type.activated.connect(self.toggle_type) # when authentication is changed to Kerberos then disable username/password as they are now useless self.window.input_combobox_authentication.activated.connect(self.toggle_authentication) # reset Checkmk views self.window.button_checkmk_view_hosts_reset.clicked.connect(self.checkmk_view_hosts_reset) self.window.button_checkmk_view_services_reset.clicked.connect(self.checkmk_view_services_reset) # mode needed for evaluate dialog after ok button pressed - defaults to 'new' self.mode = 'new' @Slot(int) def toggle_type(self, server_type_index=0): # server_type_index is not needed - we get the server type from .currentText() # check if server type is listed in volatile widgets to decide if it has to be shown or hidden for widget, server_types in self.VOLATILE_WIDGETS.items(): if self.window.input_combobox_type.currentText() in server_types: widget.show() else: widget.hide() @Slot() def toggle_authentication(self): """ when authentication is changed to Kerberos then disable username/password as they are now useless """ if self.window.input_combobox_authentication.currentText() == 'Kerberos': for widget in self.AUTHENTICATION_WIDGETS: widget.hide() else: for widget in self.AUTHENTICATION_WIDGETS: widget.show() if self.window.input_combobox_authentication.currentText() == 'ECP': for widget in self.AUTHENTICATION_ECP_WIDGETS: widget.show() else: for widget in self.AUTHENTICATION_ECP_WIDGETS: widget.hide() # change credential input for bearer auth if self.window.input_combobox_authentication.currentText() == 'Bearer': for widget in self.AUTHENTICATION_BEARER_WIDGETS: widget.hide() self.window.label_password.setText('Token') else: for widget in self.AUTHENTICATION_BEARER_WIDGETS: widget.show() self.window.label_password.setText('Password') # after hiding authentication widgets dialog might shrink self.window.adjustSize() def dialog_decoration(method, *args, **kwargs): """ try with a decorator instead of repeated calls """ # the function which decorates method # wraps is used to keep the original method's name and docstring @wraps(method) def decoration_function(self, *args, **kwargs): """ self.server_conf has to be set by decorated method """ # previous server conf only useful when editing - defaults to None self.previous_server_conf = None # call decorated method method(self, *args, **kwargs) # run through all input widgets and apply defaults from config for widget in self.window.__dict__: if widget.startswith('input_'): if widget.startswith('input_checkbox_'): setting = widget.split('input_checkbox_')[1] self.window.__dict__[widget].setChecked(self.server_conf.__dict__[setting]) elif widget.startswith('input_radiobutton_'): setting = widget.split('input_radiobutton_')[1] self.window.__dict__[widget].setChecked(self.server_conf.__dict__[setting]) elif widget.startswith('input_combobox_'): setting = widget.split('input_combobox_')[1] self.window.__dict__[widget].setCurrentText(self.server_conf.__dict__[setting]) elif widget.startswith('input_lineedit_'): setting = widget.split('input_lineedit_')[1] self.window.__dict__[widget].setText(self.server_conf.__dict__[setting]) elif widget.startswith('input_spinbox_'): setting = widget.split('input_spinbox_')[1] self.window.__dict__[widget].setValue(self.server_conf.__dict__[setting]) # set the current authentication type by using capitalized first letter via .title() self.window.input_combobox_authentication.setCurrentText(self.server_conf.authentication.title()) # initially hide unnecessary widgets self.toggle_type() # disable unneeded authentication widgets if Kerberos is used self.toggle_authentication() # apply toggle-dependencies between checkboxes and certain widgets self.toggle_toggles() # open extra options if wanted, for example, by button_fix_tls_error if 'show_options' in self.__dict__: if self.show_options: self.window.input_checkbox_show_options.setChecked(True) # important final size adjustment self.window.adjustSize() # if running on macOS with disabled dock icon, the dock icon might have to be made visible # to make Nagstamon accept keyboard input self.check_macos_dock_icon_fix_show.emit() self.window.exec() # en reverse the dock icon might be hidden again after a potential keyboard input self.check_macos_dock_icon_fix_hide.emit() # give back decorated function return decoration_function @Slot() @dialog_decoration def new(self): """ create new server, set default values """ self.mode = 'new' # create a new server config object self.server_conf = Server() # window title might be pretty simple self.window.setWindowTitle('New server') @Slot(str) @dialog_decoration def edit(self, name=None, show_options=False): """ edit existing server when called by Edit button in ServerVBox use given server name to get server config """ self.mode = 'edit' # shorter server conf # if name is None: # self.server_conf = conf.servers[dialogs.settings.window.list_servers.currentItem().text()] # else: # self.server_conf = conf.servers[name] self.server_conf = conf.servers[name] # store monitor name in case it will be changed self.previous_server_conf = deepcopy(self.server_conf) # set window title self.window.setWindowTitle('Edit %s' % (self.server_conf.name)) # set self.show_options to give value to decorator self.show_options = show_options @Slot(str) @dialog_decoration def copy(self, name=None): """ copy existing server """ self.mode = 'copy' # shorter server conf self.server_conf = deepcopy(conf.servers[name]) # set window title before name change to reflect copy self.window.setWindowTitle(f'Copy {self.server_conf.name}') # indicate copy of another server self.server_conf.name = f'Copy of {self.server_conf.name}' def ok(self): """ evaluate the state of widgets to get new configuration """ # strip name to avoid whitespace server_name = self.window.input_lineedit_name.text().strip() # check that no duplicate name exists if server_name in conf.servers and \ (self.mode in ['new', 'copy'] or self.mode == 'edit' and self.server_conf != conf.servers[server_name]): # cry if duplicate name exists QMessageBox.critical(self.window, 'Nagstamon', f'The monitor server name {server_name} is already used.', QMessageBox.StandardButton.Ok) else: # get configuration from UI for widget in self.window.__dict__: if widget.startswith('input_'): if widget.startswith('input_checkbox_'): setting = widget.split('input_checkbox_')[1] self.server_conf.__dict__[setting] = self.window.__dict__[widget].isChecked() elif widget.startswith('input_radiobutton_'): setting = widget.split('input_radiobutton_')[1] self.server_conf.__dict__[setting] = self.window.__dict__[widget].isChecked() elif widget.startswith('input_combobox_'): setting = widget.split('input_combobox_')[1] self.server_conf.__dict__[setting] = self.window.__dict__[widget].currentText() elif widget.startswith('input_lineedit_'): setting = widget.split('input_lineedit_')[1] self.server_conf.__dict__[setting] = self.window.__dict__[widget].text() elif widget.startswith('input_spinbox_'): setting = widget.split('input_spinbox_')[1] self.server_conf.__dict__[setting] = self.window.__dict__[widget].value() # URLs should not end with / - clean it self.server_conf.monitor_url = self.server_conf.monitor_url.rstrip('/') self.server_conf.monitor_cgi_url = self.server_conf.monitor_cgi_url.rstrip('/') # convert some strings to integers and bools for item in self.server_conf.__dict__: if type(self.server_conf.__dict__[item]) == str: # when an item is not one of those which always have to be strings, then it might be OK to convert it if not item in CONFIG_STRINGS: if self.server_conf.__dict__[item] in BOOLPOOL: self.server_conf.__dict__[item] = BOOLPOOL[self.server_conf.__dict__[item]] elif self.server_conf.__dict__[item].isdecimal(): self.server_conf.__dict__[item] = int(self.server_conf.__dict__[item]) # store lowered authentication type self.server_conf.authentication = self.server_conf.authentication.lower() # edited servers will be deleted and recreated with new configuration if self.mode == 'edit': # remove old server vbox from the status window if still running self.edited_remove_previous.emit(self.previous_server_conf.name) # delete previous name conf.servers.pop(self.previous_server_conf.name) # delete edited and now not needed server instance - if it exists if self.previous_server_conf.name in servers.keys(): servers.pop(self.previous_server_conf.name) # some monitor servers do not need cgi-url - reuse self.VOLATILE_WIDGETS to find out which one if self.server_conf.type not in self.VOLATILE_WIDGETS[self.window.input_lineedit_monitor_cgi_url]: self.server_conf.monitor_cgi_url = self.server_conf.monitor_url # add new server configuration in every case and use stripped name to avoid spaces self.server_conf.name = server_name conf.servers[server_name] = self.server_conf # add new server instance to global servers dict servers[server_name] = create_server(self.server_conf) if self.server_conf.enabled: servers[server_name].enabled = True # create vbox self.create_server_vbox.emit(server_name) # reorder servers in dict to reflect changes servers_freshly_sorted = sorted(servers.items()) servers.clear() servers.update(servers_freshly_sorted) del servers_freshly_sorted # refresh the list of servers, give call the current server name to highlight it self.edited_update_list.emit('list_servers', 'servers', self.server_conf.name) # tell the main window about changes (Zabbix, Opsview, for example) self.edited.emit() # delete the old server .conf file to reflect name changes # new one will be written soon if self.previous_server_conf is not None: conf.delete_file('servers', f"server_{quote(self.previous_server_conf.name, safe='')}.conf") # store server settings conf.save_multiple_config('servers', 'server') # call close and macOS dock icon treatment from ancestor super().ok() @Slot() def choose_custom_cert_ca_file(self): """ show dialog for selection of non-default browser """ file_filter = 'All files (*)' file = self.file_chooser.getOpenFileName(self.window, directory=os.path.expanduser('~'), filter=file_filter)[0] # only take filename if QFileDialog gave something useful back if file != '': self.window.input_lineedit_custom_cert_ca_file.setText(file) @Slot() def checkmk_view_hosts_reset(self): self.window.input_lineedit_checkmk_view_hosts.setText('nagstamon_hosts') @Slot() def checkmk_view_services_reset(self): self.window.input_lineedit_checkmk_view_services.setText('nagstamon_svc') Nagstamon-master/Nagstamon/qui/dialogs/server_missing.py000066400000000000000000000050251505160700500240570ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.qui.helpers import check_servers from Nagstamon.qui.qt import Slot from Nagstamon.qui.dialogs.dialog import Dialog class DialogServerMissing(Dialog): """ small dialog to ask about disabled ot not configured servers """ def __init__(self): Dialog.__init__(self, 'dialog_server_missing') # hide dialog when server is to be created or enabled self.window.button_create_server.clicked.connect(self.window.hide) self.window.button_enable_server.clicked.connect(self.window.hide) self.window.button_ignore.clicked.connect(self.ok) # simply hide the window if ignore button chosen self.window.button_ignore.clicked.connect(self.window.hide) self.window.button_ignore.clicked.connect(self.cancel) # bye bye if exit button was pressed - exit is connected in qui/__init__.py self.window.button_exit.clicked.connect(self.window.hide) # check if servers are configured or enabled and show dialog if not check_servers.checked.connect(self.show) check_servers.checked.connect(self.initialize) @Slot(str) def initialize(self, mode='no_server'): """ use dialog for missing and not enabled servers, depending on mode """ if mode == 'no_server': self.window.label_no_server_configured.show() self.window.label_no_server_enabled.hide() self.window.button_enable_server.hide() self.window.button_create_server.show() else: self.window.label_no_server_configured.hide() self.window.label_no_server_enabled.show() self.window.button_enable_server.show() self.window.button_create_server.hide()Nagstamon-master/Nagstamon/qui/dialogs/settings.py000066400000000000000000001441471505160700500226710ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from copy import copy from os import environ from urllib.parse import quote from Nagstamon.config import (AppInfo, BOOLPOOL, conf, CONFIG_STRINGS, KEYRING, OS, OS_NON_LINUX, OS_MACOS, OS_WINDOWS) from Nagstamon.qui.constants import (COLORS, HEADERS_HEADERS, HEADERS_KEYS_HEADERS, HEADERS_HEADERS_KEYS) from Nagstamon.qui.globals import (dbus_connection, font, font_icons, font_default) from Nagstamon.qui.qt import (Signal, Slot, QColor, QColorDialog, QFileDialog, QFont, QFontDialog, QMessageBox, QPalette, QSignalMapper, QStyle, Qt, QWidget) from Nagstamon.qui.widgets.app import app from Nagstamon.qui.widgets.mediaplayer import mediaplayer from Nagstamon.qui.dialogs.check_version import check_version from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.Servers import servers class DialogSettings(Dialog): """ class for settings dialog """ # signal to be fired if OK button was clicked, and new settings are applied changed = Signal() # send signal if check for a new version is wanted check_for_new_version = Signal(bool, QWidget) start_debug_loop = Signal() # signal to be fired if a server is newly created server_created = Signal() # signal to be fired if a server is edited server_edited = Signal(str) # signal to be fired if a server is copied server_copied = Signal(str) # signal to be fired if a server is deleted server_deleted = Signal(str) # signal to be fired if an action is newly created action_created = Signal() # signal to be fired if an action is edited action_edited = Signal(str) # signal to be fired if an action is copied action_copied = Signal(str) # signal to be fired when the settings dialog OK button is pressed settings_ok = Signal() # signal to be fired when the settings dialog is cancelled cancelled = Signal() # to be fired when the servers and actions lists have to be changed update_list = Signal(str, str, str) # sent when display mode has changed changed_display_mode = Signal() def __init__(self): Dialog.__init__(self, 'settings_main') # file chooser Dialog self.file_chooser = QFileDialog() # define checkbox-to-widgets dependencies which apply at initialization # which widgets have to be hidden because of irrelevance # dictionary holds checkbox/radiobutton as key and relevant widgets in a list self.TOGGLE_DEPS = { # debug mode self.window.input_checkbox_debug_mode: [self.window.input_checkbox_debug_to_file, self.window.input_lineedit_debug_file], # regular expressions for filtering hosts self.window.input_checkbox_re_host_enabled: [self.window.input_lineedit_re_host_pattern, self.window.input_checkbox_re_host_reverse], # regular expressions for filtering services self.window.input_checkbox_re_service_enabled: [self.window.input_lineedit_re_service_pattern, self.window.input_checkbox_re_service_reverse], # regular expressions for filtering status information self.window.input_checkbox_re_status_information_enabled: [ self.window.input_lineedit_re_status_information_pattern, self.window.input_checkbox_re_status_information_reverse], # regular expressions for filtering duration self.window.input_checkbox_re_duration_enabled: [self.window.input_lineedit_re_duration_pattern, self.window.input_checkbox_re_duration_reverse], # regular expressions for filtering duration self.window.input_checkbox_re_attempt_enabled: [self.window.input_lineedit_re_attempt_pattern, self.window.input_checkbox_re_attempt_reverse], # regular expressions for filtering groups self.window.input_checkbox_re_groups_enabled: [self.window.input_lineedit_re_groups_pattern, self.window.input_checkbox_re_groups_reverse], # offset for statuswindow when using systray self.window.input_radiobutton_icon_in_systray: [self.window.input_checkbox_systray_offset_use], self.window.input_checkbox_systray_offset_use: [self.window.input_spinbox_systray_offset, self.window.label_offset_statuswindow], # display to use in fullscreen mode self.window.input_radiobutton_fullscreen: [self.window.label_fullscreen_display, self.window.input_combobox_fullscreen_display], # notifications in general self.window.input_checkbox_notification: [self.window.notification_groupbox], # sound notifications self.window.input_checkbox_notification_sound: [self.window.notification_sounds_groupbox], # custom sounds self.window.input_radiobutton_notification_custom_sound: [self.window.notification_custom_sounds_groupbox], # notification actions self.window.input_checkbox_notification_actions: [self.window.notification_actions_groupbox], # several notification actions depending on status self.window.input_checkbox_notification_action_warning: [ self.window.input_lineedit_notification_action_warning_string], self.window.input_checkbox_notification_action_critical: [ self.window.input_lineedit_notification_action_critical_string], self.window.input_checkbox_notification_action_down: [ self.window.input_lineedit_notification_action_down_string], self.window.input_checkbox_notification_action_ok: [ self.window.input_lineedit_notification_action_ok_string], # single custom notification action self.window.input_checkbox_notification_custom_action: [self.window.notification_custom_action_groupbox], # use event separator or not self.window.input_checkbox_notification_custom_action_single: [ self.window.label_notification_custom_action_separator, self.window.input_lineedit_notification_custom_action_separator], # customized color alternation self.window.input_checkbox_show_grid: [self.window.input_checkbox_grid_use_custom_intensity], self.window.input_checkbox_grid_use_custom_intensity: [self.window.input_slider_grid_alternation_intensity, self.window.label_intensity_information_0, self.window.label_intensity_information_1, self.window.label_intensity_warning_0, self.window.label_intensity_warning_1, self.window.label_intensity_average_0, self.window.label_intensity_average_1, self.window.label_intensity_high_0, self.window.label_intensity_high_1, self.window.label_intensity_critical_0, self.window.label_intensity_critical_1, self.window.label_intensity_disaster_0, self.window.label_intensity_disaster_1, self.window.label_intensity_down_0, self.window.label_intensity_down_1, self.window.label_intensity_unreachable_0, self.window.label_intensity_unreachable_1, self.window.label_intensity_unknown_0, self.window.label_intensity_unknown_1], self.window.input_radiobutton_use_custom_browser: [self.window.groupbox_custom_browser, self.window.input_lineedit_custom_browser, self.window.button_choose_browser]} self.TOGGLE_DEPS_INVERTED = [self.window.input_checkbox_notification_custom_action_single] # because this makes only sense in macOS these dependencies will be added here if OS == OS_MACOS: # offer an option to hide icon in dock on macOS self.TOGGLE_DEPS.update({ self.window.input_radiobutton_icon_in_systray: [self.window.input_checkbox_hide_macos_dock_icon]}) # show an option to enable position fix only on Unices if not OS in OS_NON_LINUX: self.window.input_checkbox_enable_position_fix.show() else: self.window.input_checkbox_enable_position_fix.hide() # set title to the current version self.window.setWindowTitle(' '.join((AppInfo.NAME, AppInfo.VERSION))) # connect server buttons to server dialog self.window.button_new_server.clicked.connect(self.new_server) self.window.button_edit_server.clicked.connect(self.edit_server) self.window.button_copy_server.clicked.connect(self.copy_server) self.window.button_delete_server.clicked.connect(self.delete_server) # double-click on server to edit self.window.list_servers.doubleClicked.connect(self.edit_server) # connect check-for-updates button to update check # self.window.button_check_for_new_version_now.clicked.connect(check_version.check) self.window.button_check_for_new_version_now.clicked.connect(self.button_check_for_new_version_clicked) self.check_for_new_version.connect(check_version.check) # avoid offset spinbox if offset is not enabled self.window.input_radiobutton_windowed.clicked.connect(self.toggle_systray_icon_offset) self.window.input_radiobutton_fullscreen.clicked.connect(self.toggle_systray_icon_offset) self.window.input_radiobutton_icon_in_systray.clicked.connect(self.toggle_systray_icon_offset) self.window.input_radiobutton_statusbar_floating.clicked.connect(self.toggle_systray_icon_offset) # connect font chooser button to font choosing dialog self.window.button_fontchooser.clicked.connect(self.font_chooser) # connect revert-to-default-font button self.window.button_default_font.clicked.connect(self.font_default) # store font as default self.font = font # show current font in label_font self.window.label_font.setFont(font) # connect action buttons to action dialog self.window.button_new_action.clicked.connect(self.new_action) self.window.button_edit_action.clicked.connect(self.edit_action) self.window.button_copy_action.clicked.connect(self.copy_action) self.window.button_delete_action.clicked.connect(self.delete_action) # double-click on action to edit self.window.list_actions.doubleClicked.connect(self.edit_action) # connect custom sound file buttons self.window.button_choose_warning.clicked.connect(self.choose_sound_file_warning) self.window.button_choose_critical.clicked.connect(self.choose_sound_file_critical) self.window.button_choose_down.clicked.connect(self.choose_sound_file_down) # connect custom sound file buttons self.window.button_play_warning.clicked.connect(self.play_sound_file_warning) self.window.button_play_critical.clicked.connect(self.play_sound_file_critical) self.window.button_play_down.clicked.connect(self.play_sound_file_down) # only show desktop notification on systems that support it if not dbus_connection.connected: self.window.input_checkbox_notification_desktop.hide() # set folder and play symbols to choose and play buttons self.window.button_choose_warning.setText('') self.window.button_choose_warning.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)) self.window.button_play_warning.setText('') self.window.button_play_warning.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) self.window.button_choose_critical.setText('') self.window.button_choose_critical.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)) self.window.button_play_critical.setText('') self.window.button_play_critical.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) self.window.button_choose_down.setText('') self.window.button_choose_down.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)) self.window.button_play_down.setText('') self.window.button_play_down.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)) # set browser file chooser icon and current custom browser path self.window.button_choose_browser.setText('') self.window.button_choose_browser.setIcon( self.window.button_play_warning.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon)) self.window.input_lineedit_custom_browser.setText(conf.custom_browser) # connect choose browser button with file dialog self.window.button_choose_browser.clicked.connect(self.choose_browser_executable) # QSignalMapper needed to connect all color buttons to color dialogs self.signalmapper_colors = QSignalMapper() # connect color buttons with color dialog for widget in [x for x in self.window.__dict__ if x.startswith('input_button_color_')]: button = self.window.__dict__[widget] item = widget.split('input_button_color_')[1] # multiplex slot for open color dialog by signal-mapping self.signalmapper_colors.setMapping(button, item) button.clicked.connect(self.signalmapper_colors.map) # connect reset and defaults buttons self.window.button_colors_reset.clicked.connect(self.paint_colors) self.window.button_colors_reset.clicked.connect(self.paint_color_alternation) self.window.button_colors_reset.clicked.connect(self.change_color_alternation_by_value) self.window.button_colors_defaults.clicked.connect(self.colors_defaults) self.window.button_colors_defaults.clicked.connect(self.paint_color_alternation) self.window.button_colors_defaults.clicked.connect(self.change_color_alternation_by_value) # paint alternating colors when example is wanted for customized intensity self.window.input_checkbox_grid_use_custom_intensity.clicked.connect(self.paint_color_alternation) self.window.input_checkbox_grid_use_custom_intensity.clicked.connect(self.change_color_alternation_by_value) self.window.input_checkbox_grid_use_custom_intensity.clicked.connect(self.toggle_zabbix_widgets) # finally, map signals with .sender() - [] is important! self.signalmapper_colors.mappedString[str].connect(self.color_chooser) # connect slider to alternating colors self.window.input_slider_grid_alternation_intensity.valueChanged.connect(self.change_color_alternation) # apply toggle-dependencies between checkboxes and certain widgets self.toggle_toggles() # workaround to avoid a gigantic settings dialog # list of Zabbix-related widgets, only to be shown if there is a Zabbix monitor server configured self.ZABBIX_WIDGETS = [self.window.input_checkbox_filter_all_average_services, self.window.input_checkbox_filter_all_disaster_services, self.window.input_checkbox_filter_all_high_services, self.window.input_checkbox_filter_all_information_services, self.window.input_checkbox_notify_if_average, self.window.input_checkbox_notify_if_disaster, self.window.input_checkbox_notify_if_high, self.window.input_checkbox_notify_if_information, self.window.input_button_color_average_text, self.window.input_button_color_average_background, self.window.input_button_color_disaster_text, self.window.input_button_color_disaster_background, self.window.input_button_color_high_text, self.window.input_button_color_high_background, self.window.input_button_color_information_text, self.window.input_button_color_information_background, self.window.label_color_average, self.window.label_color_disaster, self.window.label_color_high, self.window.label_color_information] # Labes for customized color intensity self.ZABBIX_COLOR_INTENSITY_LABELS = [self.window.label_intensity_average_0, self.window.label_intensity_average_1, self.window.label_intensity_disaster_0, self.window.label_intensity_disaster_1, self.window.label_intensity_high_0, self.window.label_intensity_high_1, self.window.label_intensity_information_0, self.window.label_intensity_information_1] # the next workaround... self.OP5MONITOR_WIDGETS = [self.window.input_checkbox_re_groups_enabled, self.window.input_lineedit_re_groups_pattern, self.window.input_checkbox_re_groups_reverse] # ...and another... self.EXPIRE_TIME_WIDGETS = [self.window.input_checkbox_defaults_acknowledge_expire, self.window.label_expire_in, self.window.label_expire_in_hours, self.window.label_expire_in_minutes, self.window.input_spinbox_defaults_acknowledge_expire_duration_hours, self.window.input_spinbox_defaults_acknowledge_expire_duration_minutes] def initialize(self): # apply configuration values # start with server tab self.window.tabs.setCurrentIndex(0) for widget in dir(self.window): if widget.startswith('input_'): if widget.startswith('input_checkbox_'): if conf.__dict__[widget.split('input_checkbox_')[1]] is True: self.window.__dict__[widget].toggle() elif widget.startswith('input_radiobutton_'): if conf.__dict__[widget.split('input_radiobutton_')[1]] is True: self.window.__dict__[widget].toggle() elif widget.startswith('input_lineedit_'): # older versions of Nagstamon have a bool value for custom_action_separator # which leads to a crash here - thus str() to solve this self.window.__dict__[widget].setText(str(conf.__dict__[widget.split('input_lineedit_')[1]])) elif widget.startswith('input_spinbox_'): self.window.__dict__[widget].setValue(int(conf.__dict__[widget.split('input_spinbox_')[1]])) elif widget.startswith('input_slider_'): self.window.__dict__[widget].setValue(int(conf.__dict__[widget.split('input_slider_')[1]])) # bruteforce size smallification, lazy try/except variant try: self.window.__dict__[widget].adjustSize() except: pass # fill default order fields combobox with s names # kick out empty headers for hosts and services flags sort_fields = copy(HEADERS_HEADERS) while '' in sort_fields: sort_fields.remove('') self.window.input_combobox_default_sort_field.addItems(sort_fields) # catch exception which will occur when older settings are used which have real header names as values try: self.window.input_combobox_default_sort_field.setCurrentText(HEADERS_KEYS_HEADERS[conf.default_sort_field]) except: self.window.input_combobox_default_sort_field.setCurrentText(conf.default_sort_field) # fill default sort order combobox self.window.input_combobox_default_sort_order.addItems(['Ascending', 'Descending']) # .title() to get upper first letter self.window.input_combobox_default_sort_order.setCurrentText(conf.default_sort_order.title()) # fill combobox with screens for fullscreen for screen in app.screens(): self.window.input_combobox_fullscreen_display.addItem(str(screen.name())) self.window.input_combobox_fullscreen_display.setCurrentText(str(conf.fullscreen_display)) # fill servers list widget with servers self.fill_list(self.window.list_servers, conf.servers) # select first item self.window.list_servers.setCurrentRow(0) # fill actions list widget with actions self.fill_list(self.window.list_actions, conf.actions) # select first item self.window.list_actions.setCurrentRow(0) # paint colors onto color selection buttons and alternation example self.paint_colors() self.paint_color_alternation() self.change_color_alternation(conf.grid_alternation_intensity) # hide keyring setting if keyring is not available if KEYRING: self.window.input_checkbox_use_system_keyring.show() else: self.window.input_checkbox_use_system_keyring.hide() # hide 'Hide macOS Dock icon' if not on macOS if OS != OS_MACOS: self.window.input_checkbox_hide_macos_dock_icon.hide() # avoid showing offset setting if not icon in systray is configured if not OS in OS_NON_LINUX and not conf.icon_in_systray: self.toggle_systray_icon_offset() # important final size adjustment self.window.adjustSize() def show(self, tab=0): # hide them and thus be able to fix size if no extra Zabbix/Op5Monitor/IcingaWeb2 widgets are shown self.toggle_zabbix_widgets() self.toggle_op5monitor_widgets() self.toggle_expire_time_widgets() # tell the world that dialog pops up self.show_dialog.emit() # jump to requested tab in settings dialog self.window.tabs.setCurrentIndex(tab) super().show() @Slot() def show_new_server(self): """ opens settings and new server dialogs - used by dialogs.server_missing """ # emulate button click self.window.button_new_server.clicked.emit() @Slot() def show_filters(self): """ opens filters settings after clicking button_filters in the top area """ self.show(tab=2) @Slot() def show_defaults(self): """ opens default settings after clicking button in acknowledge/downtime dialog """ self.show(tab=6) def ok(self): """ what to do if OK was pressed """ # store position of statuswindow/statusbar only if statusbar is floating if conf.statusbar_floating: self.settings_ok.emit() # store hash of all display settings as display_mode to decide if statuswindow has to be recreated display_mode = str(conf.statusbar_floating) + \ str(conf.icon_in_systray) + \ str(conf.fullscreen) + \ str(conf.fullscreen_display) + \ str(conf.windowed) # do all stuff necessary after the OK button was clicked # and put widget values into conf for widget in self.window.__dict__.values(): if widget.objectName().startswith('input_checkbox_'): conf.__dict__[widget.objectName().split('input_checkbox_')[1]] = widget.isChecked() elif widget.objectName().startswith('input_radiobutton_'): conf.__dict__[widget.objectName().split('input_radiobutton_')[1]] = widget.isChecked() elif widget.objectName().startswith("input_lineedit_"): conf.__dict__[widget.objectName().split('input_lineedit_')[1]] = widget.text() elif widget.objectName().startswith('input_spinbox_'): conf.__dict__[widget.objectName().split('input_spinbox_')[1]] = str(widget.value()) elif widget.objectName().startswith('input_slider_'): conf.__dict__[widget.objectName().split('input_slider_')[1]] = str(widget.value()) elif widget.objectName().startswith('input_combobox_'): conf.__dict__[widget.objectName().split('input_combobox_')[1]] = widget.currentText() elif widget.objectName().startswith('input_button_color_'): # get color value from color button stylesheet color = self.window.__dict__[widget.objectName()].styleSheet() color = color.split(':')[1].strip().split(';')[0] conf.__dict__[widget.objectName().split('input_button_')[1]] = color # convert some strings to integers and bools for item in conf.__dict__: if type(conf.__dict__[item]) == str: # when an item is not one of those which always have to be strings, then it might be OK to convert it if not item in CONFIG_STRINGS: if conf.__dict__[item] in BOOLPOOL: conf.__dict__[item] = BOOLPOOL[conf.__dict__[item]] elif conf.__dict__[item].isdecimal(): conf.__dict__[item] = int(conf.__dict__[item]) # convert sorting fields to simple keys - maybe one day translated conf.default_sort_field = HEADERS_HEADERS_KEYS[conf.default_sort_field] # apply font conf.font = self.font.toString() # update global font and icon font font.fromString(conf.font) font_icons.setPointSize(font.pointSize() + 2) # save configuration conf.save_config() # when display mode was changed, it's the easiest to destroy the old status window and create a new one # store display_mode to decide if statuswindow has to be recreated if display_mode != str(conf.statusbar_floating) + \ str(conf.icon_in_systray) + \ str(conf.fullscreen) + \ str(conf.fullscreen_display) + \ str(conf.windowed): self.changed_display_mode.emit() # tell statuswindow to reinitialize due to new settings self.changed.emit() # call close and macOS dock icon treatment from ancestor super().ok() @Slot() def cancel(self): """ check if there are any usable servers configured """ # call close and macOS dock icon treatment from ancestor self.cancelled.emit() super().cancel() @Slot() def new_server(self): """ create new server """ self.server_created.emit() @Slot() def edit_server(self): """ edit existing server """ # issue #1114 - do not allow editing of servers when no server is selected nor doesn't exist if self.window.list_servers.currentItem(): self.server_edited.emit(self.window.list_servers.currentItem().text()) @Slot() def copy_server(self): """ copy existing server """ # issue #1114 - do not allow copying of servers when no server is selected nor doesn't exist if self.window.list_servers.currentItem(): self.server_copied.emit(self.window.list_servers.currentItem().text()) @Slot() def delete_server(self): """ delete server, stop its thread, remove from config and list """ # issue #1114 - do not allow editing of servers when no server is selected nor doesn't exist if self.window.list_servers.currentItem(): # server to delete from current row in servers list server = conf.servers[self.window.list_servers.currentItem().text()] reply = QMessageBox.question(self.window, 'Nagstamon', f'Do you really want to delete monitor server {server.name}?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: # in case server is enabled to delete its vbox if server.enabled: self.server_deleted.emit(server.name) # kick server out of server instances servers.pop(server.name) # dito from config items conf.servers.pop(server.name) # refresh list # row index 0 to x row = self.window.list_servers.currentRow() # count real number, 1 to x count = self.window.list_servers.count() # if deleted row was the last line, the new current row has to be the new last line, accidentally the same as count if row == count - 1: # use the penultimate item as the new current one row = count - 2 else: # go down one row row = row + 1 # refresh list and mark new current row self.update_list.emit('list_servers', 'servers', self.window.list_servers.item(row).text()) del row, count # delete server config file from disk conf.delete_file('servers', 'server_{0}.conf'.format(quote(server.name, safe=''))) del server @Slot(str, str, str) def refresh_list(self, list_widget_str, list_conf_str, current=''): """ refresh given 'list_widget' from given 'list_conf' and mark 'current' as current """ # convert strings from signal to widgets and config items list_widget = self.window.__dict__[list_widget_str] list_conf = conf.__dict__[list_conf_str] # clear list of servers list_widget.clear() # fill servers listwidget with servers self.fill_list(list_widget, list_conf) # select current edited item # activate currently created/edited server monitor item by first searching it in the list list_widget.setCurrentItem(list_widget.findItems(current, Qt.MatchFlag.MatchExactly)[0]) @Slot() def new_action(self): """ create new action """ self.action_created.emit() @Slot() def edit_action(self): """ edit existing action """ if self.window.list_actions.currentItem(): self.action_edited.emit(self.window.list_actions.currentItem().text()) @Slot() def copy_action(self): """ copy existing action and edit it """ if self.window.list_actions.currentItem(): self.action_copied.emit(self.window.list_actions.currentItem().text()) @Slot() def delete_action(self): """ delete action remove from config and list """ # action to delete from current row in actions list action = conf.actions[self.window.list_actions.currentItem().text()] reply = QMessageBox.question(self.window, 'Nagstamon', f'Do you really want to delete action {action.name}?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.Yes: # kick action out of config items conf.actions.pop(action.name) # refresh list # row index 0 to x row = self.window.list_actions.currentRow() # count real number, 1 to x count = self.window.list_actions.count() # if deleted row was the last line, the new current row has to be the new last line, accidentally the same as count if row == count - 1: # use the penultimate item as the new current one row = count - 2 else: # go down one row row = row + 1 # refresh list and mark new current row self.update_list.emit('list_actions', 'actions', self.window.list_actions.item(row).text()) del row, count # delete the action config file from disk conf.delete_file('actions', 'action_{0}.conf'.format(quote(action.name, safe=''))) del action def choose_sound_file_decoration(method): """ try to decorate sound file dialog """ def decoration_function(self): # execute decorated function method(self) # shortcut for widget to fill and revaluate widget = self.window.__dict__['input_lineedit_notification_custom_sound_%s' % self.sound_file_type] # use 2 filters, sound files and all files file = self.file_chooser.getOpenFileName(self.window, filter='Sound files (*.mp3 *.MP3 *.mp4 *.MP4 ' '*.wav *.WAV *.ogg *.OGG);;' 'All files (*)')[0] # only take filename if QFileDialog gave something useful back if file != '': widget.setText(file) return decoration_function @choose_sound_file_decoration @Slot() def choose_sound_file_warning(self): self.sound_file_type = 'warning' @choose_sound_file_decoration @Slot() def choose_sound_file_critical(self): self.sound_file_type = 'critical' @choose_sound_file_decoration @Slot() def choose_sound_file_down(self): self.sound_file_type = 'down' def play_sound_file_decoration(method): """ try to decorate sound file dialog """ def decoration_function(self): # execute decorated function method(self) # shortcut for widget to fill and revaluate widget = self.window.__dict__['input_lineedit_notification_custom_sound_%s' % self.sound_file_type] # get the file path from widget file = widget.text() # tell mediaplayer to play a file only if it exists if mediaplayer.set_media(file) is True: mediaplayer.play() return decoration_function @play_sound_file_decoration @Slot() def play_sound_file_warning(self): self.sound_file_type = 'warning' @play_sound_file_decoration @Slot() def play_sound_file_critical(self): self.sound_file_type = 'critical' @play_sound_file_decoration @Slot() def play_sound_file_down(self): self.sound_file_type = 'down' def paint_colors(self): """ fill color selection buttons with appropriate colors """ # color buttons for color in [x for x in conf.__dict__ if x.startswith('color_')]: self.window.__dict__['input_button_%s' % (color)].setStyleSheet('''background-color: %s; border-width: 1px; border-color: black; border-style: solid;''' % conf.__dict__[color]) # example color labels for label in [x for x in self.window.__dict__ if x.startswith('label_color_')]: status = label.split('label_color_')[1] self.window.__dict__[label].setStyleSheet('color: %s; background: %s' % (conf.__dict__['color_%s_text' % (status)], (conf.__dict__['color_%s_background' % (status)]))) @Slot() def colors_defaults(self): """ apply default colors to buttons """ # color buttons for default_color in [x for x in conf.__dict__ if x.startswith('default_color_')]: # cut 'default_' off to get color color = default_color.split('default_')[1] self.window.__dict__['input_button_%s' % (color)].setStyleSheet('''background-color: %s; border-width: 1px; border-color: black; border-style: solid;''' % conf.__dict__[default_color]) # example color labels for label in [x for x in self.window.__dict__ if x.startswith('label_color_')]: status = label.split('label_color_')[1] # get color values from color button stylesheets color_text = self.window.__dict__['input_button_color_' + status + '_text'].styleSheet() color_text = color_text.split(':')[1].strip().split(';')[0] color_background = self.window.__dict__['input_button_color_' + status + '_background'].styleSheet() color_background = color_background.split(':')[1].strip().split(';')[0] # apply color values from stylesheet to label self.window.__dict__[label].setStyleSheet('color: %s; background: %s' % (color_text, color_background)) @Slot(str) def color_chooser(self, item): """ open QColorDialog to choose a color and change it in the settings dialog """ color = conf.__dict__['color_%s' % (item)] new_color = QColorDialog.getColor(QColor(color), parent=self.window) # if canceled, the color is invalid if new_color.isValid(): self.window.__dict__[f'input_button_color_{item}'].setStyleSheet(f'''background-color: {new_color.name()}; border-width: 1px; border-color: black; border-style: solid; ''') status = item.split('_')[0] # get color value from stylesheet to paint example text = self.window.__dict__[f'input_button_color_{status}_text'].styleSheet() text = text.split(':')[1].strip().split(';')[0] background = self.window.__dict__[f'input_button_color_{status}_background'].styleSheet() background = background.split(':')[1].strip().split(';')[0] # set example color self.window.__dict__[f'label_color_{status}'].setStyleSheet(f'''color: {text}; background: {background}; ''') # update alternation colors self.paint_color_alternation() self.change_color_alternation(self.window.input_slider_grid_alternation_intensity.value()) def paint_color_alternation(self): """ paint the intensity example color labels taking actual colors from color chooser buttons these labels have the color of alteration level 0 aka default """ for state in COLORS: # get text color from button CSS text = self.window.__dict__['input_button_color_{0}_text' .format(state.lower())] \ .styleSheet() \ .split(';\n')[0].split(': ')[1] # get background color from button CSS background = self.window.__dict__['input_button_color_{0}_background' .format(state.lower())] \ .styleSheet() \ .split(';\n')[0].split(': ')[1] # set CSS self.window.__dict__['label_intensity_{0}_0'.format(state.lower())] \ .setStyleSheet('''color: {0}; background-color: {1}; padding-top: 3px; padding-bottom: 3px; '''.format(text, background)) @Slot(int) def change_color_alternation(self, value): """ fill alternation level 1 labels with altered color derived from level 0 labels aka default """ for state in COLORS: # only evaluate colors if there is any stylesheet if len(self.window.__dict__['input_button_color_{0}_text' .format(state.lower())] \ .styleSheet()) > 0: # access both labels label_0 = self.window.__dict__['label_intensity_{0}_0'.format(state.lower())] label_1 = self.window.__dict__['label_intensity_{0}_1'.format(state.lower())] # get text color from text color chooser button text = self.window.__dict__['input_button_color_{0}_text' .format(state.lower())] \ .styleSheet() \ .split(';\n')[0].split(': ')[1] # get background of level 0 label background = label_0.palette().color(QPalette.ColorRole.Window) r, g, b, a = background.getRgb() # if a label background is too dark, lighten the color instead of darken it more if background.lightness() < 30: if value > 5: r += 30 g += 30 b += 30 r = round(r / 100 * (100 + value)) g = round(g / 100 * (100 + value)) b = round(b / 100 * (100 + value)) else: r = round(r / 100 * (100 - value)) g = round(g / 100 * (100 - value)) b = round(b / 100 * (100 - value)) # finally apply new background color # easier with style sheets than with QPalette/QColor label_1.setStyleSheet('''color: {0}; background-color: rgb({1}, {2}, {3}); padding-top: 3px; padding-bottom: 3px; '''.format(text, r, g, b)) @Slot() def change_color_alternation_by_value(self): """ to be fired up when colors are reset """ self.change_color_alternation(self.window.input_slider_grid_alternation_intensity.value()) @Slot() def font_chooser(self): """ use the font dialog to choose a font """ self.font = QFontDialog.getFont(self.font, parent=self.window)[0] self.window.label_font.setFont(self.font) @Slot() def font_default(self): """ reset font to default font which was valid when Nagstamon was launched """ self.window.label_font.setFont(font_default) self.font = font_default @Slot() def button_check_for_new_version_clicked(self): """ at this point, start_mode for version check is definitively False """ self.check_for_new_version.emit(False, self.window) @Slot() def choose_browser_executable(self): """ show dialog for selection of non-default browser """ # present dialog with OS-specific sensible defaults if OS == OS_WINDOWS: file_filter = 'Executables (*.exe *.EXE);; All files (*)' directory = environ['ProgramFiles'] elif OS == OS_MACOS: file_filter = '' directory = '/Applications' else: file_filter = '' directory = '/usr/bin' file = self.file_chooser.getOpenFileName(self.window, directory=directory, filter=file_filter)[0] # only take filename if QFileDialog gave something useful back if file != '': self.window.input_lineedit_custom_browser.setText(file) @Slot() def toggle_zabbix_widgets(self): """ depending on the existence of an enabled Zabbix monitor, the Zabbix widgets are shown or hidden """ use_zabbix = False for server in servers.values(): if server.enabled: if server.type.startswith('Zabbix'): use_zabbix = True break # remove extra Zabbix options if use_zabbix: for widget in self.ZABBIX_WIDGETS: widget.show() else: for widget in self.ZABBIX_WIDGETS: widget.hide() # remove custom color intensity labels if use_zabbix and self.window.input_checkbox_grid_use_custom_intensity.isChecked(): for widget in self.ZABBIX_COLOR_INTENSITY_LABELS: widget.show() else: for widget in self.ZABBIX_COLOR_INTENSITY_LABELS: widget.hide() @Slot() def toggle_op5monitor_widgets(self): """ depending on the existence of an enabled Op5Monitor monitor, the Op5Monitor widgets are shown or hidden """ use_op5monitor = False for server in servers.values(): if server.enabled: if server.type == 'op5Monitor': use_op5monitor = True break if use_op5monitor: for widget in self.OP5MONITOR_WIDGETS: widget.show() else: for widget in self.OP5MONITOR_WIDGETS: widget.hide() @Slot() def toggle_expire_time_widgets(self): """ depending on the existence of an enabled IcingaWeb2 or Alertmanager monitor, the expire_time widgets are shown or hidden """ use_expire_time = False for server in servers.values(): if server.enabled: if server.type in ['IcingaWeb2', 'Icinga2API', 'Alertmanager']: use_expire_time = True break if use_expire_time: for widget in self.EXPIRE_TIME_WIDGETS: widget.show() else: for widget in self.EXPIRE_TIME_WIDGETS: widget.hide() @Slot() def toggle_systray_icon_offset(self): """ only show offset spinbox when offset is enabled """ if self.window.input_checkbox_systray_offset_use.isVisible(): if self.window.input_checkbox_systray_offset_use.isChecked(): self.window.input_spinbox_systray_offset.show() self.window.label_offset_statuswindow.show() else: self.window.input_spinbox_systray_offset.hide() self.window.label_offset_statuswindow.hide() else: self.window.input_spinbox_systray_offset.hide() self.window.label_offset_statuswindow.hide() Nagstamon-master/Nagstamon/qui/dialogs/submit.py000066400000000000000000000106321505160700500223230ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.qui.dialogs.dialog import Dialog from Nagstamon.qui.qt import (Signal, Slot) class DialogSubmit(Dialog): """ dialog for submitting arbitrarily chosen results """ # store host and service to be used for OK button evaluation server = None host = service = '' submit = Signal(dict) def __init__(self): Dialog.__init__(self, 'dialog_submit') @Slot(object, str, str) def initialize(self, server=None, host='', service=''): # store server, host and service to be used for OK button evaluation self.server = server self.host = host self.service = service # if service is "" it must be a host if service == "": # set label for acknowledging a host self.window.setWindowTitle('Submit check result for host') self.window.input_label_description.setText('Host %s' % (host)) # services do not need all states self.window.input_radiobutton_result_up.show() self.window.input_radiobutton_result_ok.hide() self.window.input_radiobutton_result_warning.hide() self.window.input_radiobutton_result_critical.hide() self.window.input_radiobutton_result_unknown.show() self.window.input_radiobutton_result_unreachable.show() self.window.input_radiobutton_result_down.show() # activate first radiobutton self.window.input_radiobutton_result_up.setChecked(True) else: # set label for acknowledging a service on host self.window.setWindowTitle('Submit check result for service') self.window.input_label_description.setText('Service %s on host %s' % (service, host)) # hosts do not need all states self.window.input_radiobutton_result_up.hide() self.window.input_radiobutton_result_ok.show() self.window.input_radiobutton_result_warning.show() self.window.input_radiobutton_result_critical.show() self.window.input_radiobutton_result_unknown.show() self.window.input_radiobutton_result_unreachable.hide() self.window.input_radiobutton_result_down.hide() # activate first radiobutton self.window.input_radiobutton_result_ok.setChecked(True) # clear text fields self.window.input_lineedit_check_output.setText('') self.window.input_lineedit_performance_data.setText('') self.window.input_lineedit_comment.setText(conf.defaults_submit_check_result_comment) self.window.input_lineedit_check_output.setFocus() def ok(self): """ submit an arbitrary check result """ # default state state = "ok" for button in ["ok", "up", "warning", "critical", "unreachable", "unknown", "down"]: if self.window.__dict__['input_radiobutton_result_' + button].isChecked(): state = button break # tell worker to submit self.submit.emit({'server': self.server, 'host': self.host, 'service': self.service, 'state': state, 'comment': self.window.input_lineedit_comment.text(), 'check_output': self.window.input_lineedit_check_output.text(), 'performance_data': self.window.input_lineedit_performance_data.text()}) # call close and macOS dock icon treatment from ancestor super().ok()Nagstamon-master/Nagstamon/qui/globals.py000066400000000000000000000072371505160700500210300ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Global variables used in different modules from dataclasses import dataclass from Nagstamon.config import (conf, OS, OS_MACOS, RESOURCES) from Nagstamon.helpers import FilesDict from Nagstamon.qui.dbus import DBus from Nagstamon.qui.qt import QFont from Nagstamon.qui.widgets.app import app # save default font to be able to reset to it font_default = app.font() # take global font from conf if it exists if conf.font != '': font = QFont() font.fromString(conf.font) else: font = font_default # always stay in normal weight without any italic font_icons = QFont('Nagstamon', font.pointSize() + 2, QFont.Weight.Normal, False) # DBus initialization dbus_connection = DBus() # check ECP authentication support availability try: from requests_ecp import HTTPECPAuth ecp_available = True except ImportError: ecp_available = False # flag to keep track of Kerberos availability kerberos_available = False if OS == OS_MACOS: # requests_gssapi is newer but not available everywhere try: # extra imports needed to get it compiled on macOS import numbers import gssapi.raw.cython_converters from requests_gssapi import HTTPSPNEGOAuth as HTTPSKerberos kerberos_available = True except ImportError as error: print(error) else: # requests_gssapi is newer but not available everywhere try: # requests_gssapi needs installation of KfW - Kerberos for Windows # requests_kerberoes doesn't from requests_kerberos import HTTPKerberosAuth as HTTPSKerberos kerberos_available = True except ImportError as error: print(error) @dataclass class StatusWindowProperties: """ storing statuswindow related variables globally available for several classes """ icon_x: int = 0 icon_y: int = 0 is_shown: bool = False is_shown_timestamp: float = 0.0 is_hiding_timestamp: float = 0.0 moving: bool = False relative_x: int = 0 relative_y: int = 0 status_ok: bool = True top: bool = False # flag about current notification state is_notifying: bool = False # current worst state worth a notification worst_notification_status: str = 'UP' notifying_server: str = '' debug_loop_looping = False # shared status window properties statuswindow_properties = StatusWindowProperties() # access to clipboard clipboard = app.clipboard() # QBrushes made of QColors for treeview model data() method # 2 flavours for alternating backgrounds # filled by create_brushes() qbrushes = {0: {}, 1: {}} # store default sounds as buffers to avoid https://github.com/HenriWahl/Nagstamon/issues/578 # meanwhile used as backup copy in case they had been deleted by macOS # https://github.com/HenriWahl/Nagstamon/issues/578 resource_files = FilesDict(RESOURCES)Nagstamon-master/Nagstamon/qui/helpers.py000066400000000000000000000065721505160700500210500ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.qui.qt import (QObject, QPoint, Signal, Slot) from Nagstamon.qui.widgets.app import app from Nagstamon.Servers import servers # make icon status in macOS dock accessible via NSApp, used by set_macos_dock_icon_visible() if OS == OS_MACOS: from AppKit import (NSApp, NSApplicationPresentationDefault, NSApplicationPresentationHideDock) class CheckServers(QObject): """ check if there are any servers configured and enabled """ # signal to emit if no server is configured or enabled checked = Signal(str) @Slot() def check(self): """ check if there are any servers configured and enabled """ # no server is configured if len(servers) == 0: # emit signal that no server is configured self.checked.emit('no_server') # no server is enabled elif len([x for x in conf.servers.values() if x.enabled is True]) == 0: # emit signal that no server is enabled self.checked.emit('no_server_enabled') def hide_macos_dock_icon(hide=False): """ small helper to make dock icon visible or not in macOS inspired by https://stackoverflow.com/questions/6796028/start-a-gui-process-in-mac-os-x-without-dock-icon """ if hide: NSApp.setActivationPolicy_(NSApplicationPresentationHideDock) else: NSApp.setActivationPolicy_(NSApplicationPresentationDefault) def get_screen_name(x, y): """ find out which screen the given coordinates belong to gives back 'None' if coordinates are out of any known screen """ # integerify these values as they *might* be strings x = int(x) y = int(y) # QApplication (using Qt5 and/or its Python binding on RHEL/CentOS 7) has no attribute 'screenAt' try: screen = app.screenAt(QPoint(x, y)) del x, y if screen: return screen.name else: return None except: return None def get_screen_geometry(screen_name): """ set screen for fullscreen """ for screen in app.screens(): if screen.name() == screen_name: return screen.geometry() # if screen_name didn't match available use primary screen return app.primaryScreen().geometry() # to be used in nagstamon.py and qui/widgets/dialogs/settings.py check_servers = CheckServers() Nagstamon-master/Nagstamon/qui/qt.py000066400000000000000000000244361505160700500200310ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Select Qt version based on installation found # Prefer in this order: PyQt6 - PyQt5 from pathlib import Path import sys from statistics import median_low # Enough to handle with differences between PyQt5 + PyQt6, so PySide6 will be # ignored right now # by the little import the appropriate PyQt version will be loaded try: from PyQt6.QtCore import PYQT_VERSION_STR as QT_VERSION_STR # get int-ed version parts QT_VERSION_MAJOR, QT_VERSION_MINOR = [int(x) for x in QT_VERSION_STR.split('.')[0:2]] # for later decision which differences have to be considered QT_FLAVOR = 'PyQt6' except ImportError: try: from PyQt5.QtCore import PYQT_VERSION_STR as QT_VERSION_STR # get int-ed version parts QT_VERSION_MAJOR, QT_VERSION_MINOR = [int(x) for x in QT_VERSION_STR.split('.')[0:2]] # for later decision which differences have to be considered QT_FLAVOR = 'PyQt5' except ImportError: sys.exit('Qt is missing') # because 'PyQt6' is in sys.modules even if the import some line before failed # the backup PyQt5 should be loaded earlier if it exists due to exception treatment if QT_FLAVOR == 'PyQt5': from PyQt5.QtCore import pyqtSignal as Signal, \ pyqtSlot as Slot, \ PYQT_VERSION_STR as QT_VERSION_STR, \ QAbstractTableModel, \ QByteArray, \ QDateTime, \ QModelIndex, \ QObject, \ QPoint, \ QSignalMapper, \ Qt, \ QThread, \ QTimer, \ QUrl, \ QVariant, \ QXmlStreamReader from PyQt5.QtGui import QBrush, \ QColor, \ QCursor, \ QFont, \ QFontDatabase, \ QIcon, \ QKeySequence, \ QPainter, \ QPalette, \ QPixmap from PyQt5.QtMultimedia import QMediaContent, \ QMediaPlayer, \ QMediaPlaylist from PyQt5.QtSvg import QSvgRenderer, \ QSvgWidget from PyQt5.QtWidgets import QAbstractItemView, \ QAction, \ QApplication, \ QColorDialog, \ QComboBox, \ QDialog, \ QFileDialog, \ QFontDialog, \ QHBoxLayout, \ QHeaderView, \ QListWidgetItem, \ QMenu, \ QMenuBar, \ QMessageBox, \ QLabel, \ QPushButton, \ QScrollArea, \ QSizePolicy, \ QSpacerItem, \ QToolButton, \ QTreeView, \ QStyle, \ QSystemTrayIcon, \ QVBoxLayout, \ QWidget from PyQt5 import uic class QSignalMapper(QSignalMapper): """ QSignalMapper has method mappedString since Qt 5.15 which is not available in Ubuntu 20.04 See https://github.com/HenriWahl/Nagstamon/issues/865 for details """ def __init__(self): super().__init__() # map mappedString onto mapped self.mappedString = self.mapped class MediaPlayer(QObject): """ play media files for notification """ # needed to show error in a thread-safe way send_message = Signal(str, str) def __init__(self, resource_files): QObject.__init__(self) self.player = QMediaPlayer(parent=self) self.player.setVolume(100) self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.resource_files = resource_files @Slot(str) def set_media(self, media_file): """ Give media_file to player and if it is one of the default files check first if still exists :param media_file: :return: """ if media_file in self.resource_files: # by using RESOURCE_FILES the file path will be checked on macOS and the file restored if necessary media_file = self.resource_files[media_file] # only existing file can be played if Path(media_file).exists: url = QUrl.fromLocalFile(media_file) media_content = QMediaContent(url) self.player.setMedia(media_content) del url, media_content return True else: # cry and tell no file was found self.send_message.emit('warning', 'Sound file \'{0}\' not found for playback.'.format(media_file)) return False @Slot() def play(self): # just play sound self.player.play() def get_global_position(event): """ Qt5 uses other method than Qt6 """ return event.globalPos() def get_sort_order_value(sort_order): """ Qt5 has int for Qt.SortOrder but Qt6 has Qt.SortOrder.[Ascending|Descending]Order """ return sort_order elif QT_FLAVOR == 'PyQt6': # PySide/PyQt compatibility from PyQt6.QtCore import pyqtSignal as Signal, \ pyqtSlot as Slot, \ PYQT_VERSION_STR as QT_VERSION_STR, \ QAbstractTableModel, \ QByteArray, \ QDateTime, \ QModelIndex, \ QObject, \ QPoint, \ QSignalMapper, \ Qt, \ QThread, \ QTimer, \ QUrl, \ QVariant, \ QXmlStreamReader from PyQt6.QtGui import QAction, \ QBrush, \ QColor, \ QCursor, \ QFont, \ QFontDatabase, \ QIcon, \ QKeySequence, \ QPainter, \ QPalette, \ QPixmap from PyQt6.QtMultimedia import QAudioOutput, \ QMediaPlayer from PyQt6.QtSvg import QSvgRenderer from PyQt6.QtSvgWidgets import QSvgWidget from PyQt6.QtWidgets import QAbstractItemView, \ QApplication, \ QColorDialog, \ QComboBox, \ QDialog, \ QFileDialog, \ QFontDialog, \ QHBoxLayout, \ QHeaderView, \ QListWidgetItem, \ QMenu, \ QMenuBar, \ QMessageBox, \ QLabel, \ QPushButton, \ QScrollArea, \ QSizePolicy, \ QSpacerItem, \ QToolButton, \ QTreeView, \ QStyle, \ QSystemTrayIcon, \ QVBoxLayout, \ QWidget from PyQt6 import uic # for later decision which differences have to be considered QT_FLAVOR = 'PyQt6' class MediaPlayer(QObject): """ play media files for notification """ # needed to show error in a thread-safe way send_message = Signal(str, str) def __init__(self, resource_files): QObject.__init__(self) self.audio_output = QAudioOutput() self.player = QMediaPlayer(parent=self) self.player.setAudioOutput(self.audio_output) self.resource_files = resource_files @Slot(str) def set_media(self, media_file): """ Give media_file to player and if it is one of the default files check first if still exists :param media_file: :return: """ if media_file in self.resource_files: # by using RESOURCE_FILES the file path will be checked on macOS and the file restored if necessary media_file = self.resource_files[media_file] # only existing file can be played if Path(media_file).exists(): self.player.setSource(QUrl.fromLocalFile(media_file)) return True else: # cry and tell no file was found self.send_message.emit('warning', f'Sound file \'{media_file}\' not found for playback.') return False @Slot() def play(self): # just play sound self.player.play() def get_global_position(event): """ Qt5 uses other method than Qt6 """ return event.globalPosition() def get_sort_order_value(sort_order): """ Qt5 has int for Qt.SortOrder but Qt6 has Qt.SortOrder.[Ascending|Descending]Order """ return sort_order.value # elif 'PySide6' in sys.modules: # from PySide6.QtCore import Signal, \ # Slot, \ # QAbstractTableModel, \ # QByteArray, \ # QDateTime, \ # QModelIndex, \ # QObject, \ # QPoint, \ # QSignalMapper, \ # Qt, \ # QThread, \ # QTimer, \ # QUrl, \ # QVariant, \ # QXmlStreamReader # from PySide6.QtGui import QAction, \ # QBrush, \ # QColor, \ # QCursor, \ # QFont, \ # QFontDatabase, \ # QIcon, \ # QKeySequence, \ # QPainter, \ # QPalette, \ # QPixmap # from PySide6.QtMultimedia import QAudioOutput, \ # QMediaPlayer # from PySide6.QtSvg import QSvgRenderer # from PySide6.QtSvgWidgets import QSvgWidget # from PySide6.QtWidgets import QAbstractItemView, \ # QApplication, \ # QColorDialog, \ # QComboBox, \ # QDialog, \ # QFileDialog, \ # QFontDialog, \ # QHBoxLayout, \ # QHeaderView, \ # QListWidgetItem, \ # QMenu, \ # QMenuBar, \ # QMessageBox, \ # QLabel, \ # QPushButton, \ # QScrollArea, \ # QSizePolicy, \ # QSpacerItem, \ # QToolButton, \ # QTreeView, \ # QStyle, \ # QSystemTrayIcon, \ # QVBoxLayout, \ # QWidget # # for later decision which differences have to be considered # QT_FLAVOR = 'PySide6' Nagstamon-master/Nagstamon/qui/widgets/000077500000000000000000000000001505160700500204705ustar00rootroot00000000000000Nagstamon-master/Nagstamon/qui/widgets/__init__.py000066400000000000000000000014711505160700500226040ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Nagstamon-master/Nagstamon/qui/widgets/app.py000066400000000000000000000040261505160700500216240ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from platform import release from os import sep from sys import argv from Nagstamon.config import (OS, OS_WINDOWS, RESOURCES) from Nagstamon.qui.qt import (Qt, QApplication, QFontDatabase, QT_VERSION_MAJOR) # since Qt6 HighDPI-awareness is default behaviour if QT_VERSION_MAJOR < 6: # enable HighDPI-awareness to avoid https://github.com/HenriWahl/Nagstamon/issues/618 try: QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) except AttributeError: pass QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) # global application instance app = QApplication(argv) # set style for tooltips globally - to sad not all properties can be set here app.setStyleSheet('''QToolTip { margin: 3px; }''') # as long as Windows 11 + Qt6 looks that ugly it's better to choose another app style # might be mitigated with sometimes, so commented out now if OS == OS_WINDOWS and release() >= '11': app.setStyle('fusion') # add nagstamon.ttf with icons to fonts QFontDatabase.addApplicationFont(f'{RESOURCES}{sep}nagstamon.ttf')Nagstamon-master/Nagstamon/qui/widgets/buttons.py000066400000000000000000000116131505160700500225420ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.helpers import webbrowser_open from Nagstamon.qui.qt import (QMenu, QPushButton, QToolButton, Signal, Slot) class FlatButton(QToolButton): """ A QToolButton that visually and functionally acts as a push button. Args: text (str, optional): The button label text. Defaults to an empty string. parent (QWidget, optional): The parent widget. Defaults to None. server (optional): Optional server reference for context. Defaults to None. url_type (str, optional): Optional URL type for button context. Defaults to an empty string. Attributes: Inherits all attributes from QToolButton. """ def __init__(self, text='', parent=None, server=None, url_type=''): QToolButton.__init__(self, parent=parent) self.setAutoRaise(True) self.setStyleSheet('''padding: 3px;''') self.setText(text) # OSX does not support flat QToolButtons so keep the neat default ones if OS == OS_MACOS: Button = QPushButton CSS_CLOSE_BUTTON = '''QPushButton {border-width: 0px; border-style: none; margin-right: 5px;} QPushButton:hover {background-color: white; border-radius: 4px;}''' CSS_HAMBURGER_MENU = '''QPushButton {border-width: 0px; border-style: none;} QPushButton::menu-indicator{image:url(none.jpg)}; QPushButton:hover {background-color: white; border-radius: 4px;}''' else: Button = FlatButton CSS_CLOSE_BUTTON = '''margin-right: 5px;''' CSS_HAMBURGER_MENU = '''FlatButton::menu-indicator{image:url(none.jpg);}''' class PushButtonHamburger(Button): """ A push button styled as a hamburger menu button with an attached menu. Attributes: pressed (Signal): Emitted when the button is pressed. Methods: mousePressEvent(event): Emits the `pressed` signal and shows the menu. set_menu(menu): Sets the menu to be displayed when the button is pressed. Usage: Use this button to provide a compact menu access point, typically represented by a hamburger icon, in toolbars or application headers. """ pressed = Signal() def __init__(self): super().__init__() self.setStyleSheet(CSS_HAMBURGER_MENU) def mousePressEvent(self, event): self.pressed.emit() self.showMenu() @Slot(QMenu) def set_menu(self, menu): self.setMenu(menu) class PushButtonBrowserURL(Button): """ QPushButton for ServerVBox which opens certain URL if clicked """ # send when a web browser is opened # this is used to hide the status window in fullscreen mode webbrowser_opened = Signal() def __init__(self, text='', parent=None, server=None, url_type=''): Button.__init__(self, text, parent=parent) self.server = server self.url_type = url_type self.parent_statuswindow = self.parentWidget().parentWidget() @Slot() def open_url(self): """ open URL from BROWSER_URLS in webbrowser """ # BROWSER_URLS come with $MONITOR$ instead of real monitor url - heritage from actions url = self.server.BROWSER_URLS[self.url_type] url = url.replace('$MONITOR$', self.server.monitor_url) url = url.replace('$MONITOR-CGI$', self.server.monitor_cgi_url) if conf.debug_mode: self.server.debug(server=self.server.get_name(), debug='Open {0} web page {1}'.format(self.url_type, url)) # use Python method to open browser webbrowser_open(url) # hide status window to get screen space for browser if not conf.fullscreen and not conf.windowed: self.webbrowser_opened.emit()Nagstamon-master/Nagstamon/qui/widgets/combobox_servers.py000066400000000000000000000044401505160700500244250ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.helpers import webbrowser_open from Nagstamon.qui.qt import (QComboBox, Signal, Slot) from Nagstamon.Servers import servers class ComboBoxServers(QComboBox): """ combobox which does lock status window so it does not close when opening combobox """ monitor_opened = Signal() # flag to avoid silly focusOutEvent freshly_opened = False def __init__(self, parent=None): QComboBox.__init__(self, parent=parent) # react to clicked monitor self.activated.connect(self.response) def mousePressEvent(self, event): # first click opens combobox popup self.freshly_opened = True # tell status window that there is no combobox anymore self.showPopup() def fill(self): """ fill default order fields combobox with server names """ self.clear() self.addItem('Go to monitor...') self.addItems(sorted([x.name for x in conf.servers.values() if x.enabled], key=str.lower)) @Slot() def response(self): """ response to activated item in servers combobox """ if self.currentText() in servers: # open webbrowser with server URL webbrowser_open(servers[self.currentText()].monitor_url) # hide window to make room for webbrowser self.monitor_opened.emit() self.setCurrentIndex(0) Nagstamon-master/Nagstamon/qui/widgets/draggables.py000066400000000000000000000145431505160700500231440ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from time import time from Nagstamon.config import conf from Nagstamon.qui.globals import statuswindow_properties from Nagstamon.qui.qt import (get_global_position, QLabel, QSizePolicy, QSvgWidget, QWidget, Qt, Signal) class DraggableWidget(QWidget): """ used to give various top area and statusbar widgets draggability """ # yell if statusbar is moved window_moved = Signal() # needed for popup after hover mouse_entered = Signal() # needed for popup after click mouse_pressed = Signal() mouse_released = Signal() # needed to close window in some configurations mouse_released_in_window = Signal() # keep state of right button pressed to avoid dragging and # unwanted repositioning of statuswindow right_mouse_button_pressed = False parent_statuswindow = None def __init__(self, parent=None): QWidget.__init__(self, parent=parent) def set_menu(self, menu): self.menu = menu def mousePressEvent(self, event): """ react differently to mouse button presses: 1 - left button, move window 2 - right button, popup menu """ # update access to status window self.parent_statuswindow = self.parentWidget().parentWidget() if event.button() == Qt.MouseButton.LeftButton: self.mouse_pressed.emit() if event.button() == Qt.MouseButton.RightButton: self.right_mouse_button_pressed = True # keep x and y relative to statusbar # if not set calculate relative position if not statuswindow_properties.relative_x and \ not statuswindow_properties.relative_y: # Qt5 & Qt6 have different methods for getting the global position so take it from qt.py global_position = get_global_position(event) statuswindow_properties.relative_x = global_position.x() - self.parent_statuswindow.x() statuswindow_properties.relative_y = global_position.y() - self.parent_statuswindow.y() def mouseReleaseEvent(self, event): """ decide if moving or menu should be treated after mouse button was released """ if event.button() == Qt.MouseButton.LeftButton: # if popup window should be closed by clicking do it now if statuswindow_properties.is_shown and \ (conf.close_details_clicking or conf.close_details_clicking_somewhere) and \ not conf.fullscreen and not conf.windowed: statuswindow_properties.is_hiding_timestamp = time() self.mouse_released_in_window.emit() elif not statuswindow_properties.is_shown: self.mouse_released.emit() # reset all helper values statuswindow_properties.moving = False statuswindow_properties.relative_x = 0 statuswindow_properties.relative_y = 0 if event.button() == Qt.MouseButton.RightButton: self.right_mouse_button_pressed = False self.menu.show_at_cursor() def mouseMoveEvent(self, event): """ do the moving action """ # if window should close when being clicked it might be problematic if it # will be moved unintendedly so try to filter this events out by waiting 0.5 seconds if not (conf.close_details_clicking and statuswindow_properties.is_shown and statuswindow_properties.is_shown_timestamp + 0.5 < time()): if not conf.fullscreen and not conf.windowed and not self.right_mouse_button_pressed: # Qt5 & Qt6 have different methods for getting the global position so take it from qt.py global_position = get_global_position(event) # lock window as moving # if not set calculate relative position if not statuswindow_properties.relative_x and not statuswindow_properties.relative_y: statuswindow_properties.relative_x = global_position.x() - self.parent_statuswindow.x() statuswindow_properties.relative_y = global_position.y() - self.parent_statuswindow.y() statuswindow_properties.moving = True # TODO: shall become a signal self.parent_statuswindow.move(int(global_position.x() - statuswindow_properties.relative_x), int(global_position.y() - statuswindow_properties.relative_y)) self.window_moved.emit() def enterEvent(self, event): """ tell the world that mouse entered the widget - interesting for hover popup and only if top area hasn't been clicked a moment ago """ if statuswindow_properties.is_shown is False and \ statuswindow_properties.is_hiding_timestamp + 0.2 < time(): self.mouse_entered.emit() class DraggableLabel(QLabel, DraggableWidget): """ label with dragging capabilities used by top area """ # yell if statusbar is moved window_moved = Signal() # needed for popup after hover mouse_entered = Signal() # needed for popup after click mouse_pressed = Signal() mouse_released = Signal() # needed to close window in some configurations mouse_released_in_window = Signal() def __init__(self, text='', parent=None): QLabel.__init__(self, text, parent=parent) Nagstamon-master/Nagstamon/qui/widgets/icon.py000066400000000000000000000027031505160700500217740ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.qui.qt import QIcon class QIconWithFilename(QIcon): """ Extends QIcon with a filename attribute. This class allows assigning the filename of the icon file as an attribute to a QIcon object. Args: *args: Arguments passed to QIcon. If the first argument is a string, it is stored as the filename. **kwargs: Keyword arguments passed to QIcon. Attributes: filename (str): The filename of the icon, if specified during creation. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if type(args[0]) == str: self.filename = args[0] Nagstamon-master/Nagstamon/qui/widgets/labels.py000066400000000000000000000122431505160700500223060ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from time import time from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.qui.constants import COLOR_STATUS_LABEL from Nagstamon.qui.globals import (font, statuswindow_properties) from Nagstamon.qui.qt import (QLabel, QSizePolicy, Qt, Signal, Slot) class LabelAllOK(QLabel): """ label which is shown in fullscreen and windowed mode when all is OK - pretty seldomly """ def __init__(self, text='', parent=None): QLabel.__init__(self, text='OK', parent=parent) self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setFont(font) self.set_color() @Slot() def set_color(self): self.setStyleSheet(f'''padding-left: 1px; padding-right: 1px; color: {conf.__dict__['color_ok_text']}; background-color: {conf.__dict__['color_ok_background']}; font-size: 92px; font-weight: bold; ''') class ClosingLabel(QLabel): """ modified QLabel which might close the status window if left-clicked """ parent_statuswindow = None # neede to close status window mouse_released = Signal() def __init__(self, text='', parent=None): QLabel.__init__(self, text, parent=parent) def mouseReleaseEvent(self, event): """ left click and configured close-if-clicking-somewhere makes status window close """ # update access to status window self.parent_statuswindow = self.parentWidget().parentWidget() if event.button() == Qt.MouseButton.LeftButton and conf.close_details_clicking_somewhere: # if popup window should be closed by clicking do it now if statuswindow_properties.is_shown and \ not conf.fullscreen and \ not conf.windowed: statuswindow_properties.is_hiding_timestamp = time() self.mouse_released.emit() class ServerStatusLabel(ClosingLabel): """ label for ServerVBox to show server connection state extra class to apply simple slots for changing text or color """ # storage for label text if it needs to be restored text_old = '' stylesheet_old = None def __init__(self, parent=None): QLabel.__init__(self, parent=parent) @Slot(str, str) def change(self, text, style=''): # store old text and stylesheet in case it needs to be reused self.text_old = self.text() self.stylesheet_old = self.styleSheet() # set stylesheet depending on submitted style if style in COLOR_STATUS_LABEL: if OS == OS_MACOS: self.setStyleSheet(f'''background: {COLOR_STATUS_LABEL[style]}; border-radius: 3px; ''') else: self.setStyleSheet(f'''background: {COLOR_STATUS_LABEL[style]}; margin-top: 8px; padding-top: 3px; margin-bottom: 8px; padding-bottom: 3px; border-radius: 4px; ''') elif style == '': self.setStyleSheet('') # in case of unknown errors try to avoid freaking out status window with too # big status label if style != 'unknown': # set new text with some space self.setText(' {0} '.format(text)) self.setToolTip('') else: # set new text to first word of text, delegate full text to tooltip self.setText(text.split(' ')[0]) self.setToolTip(text) @Slot() def reset(self): self.setStyleSheet(self.stylesheet_old) self.setText('') @Slot() def restore(self): # restore text, used by recheck_all of tablewidget worker self.setStyleSheet(self.stylesheet_old) self.setText(self.text_old)Nagstamon-master/Nagstamon/qui/widgets/layout.py000066400000000000000000000026431505160700500223640ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.qui.qt import QHBoxLayout class HBoxLayout(QHBoxLayout): """ Custom QHBoxLayout with zero spacing and margins by default. This layout is used to create a horizontal box. Args: spacing (int, optional): Space between child widgets. Defaults to 0. parent (QWidget, optional): Parent widget. """ def __init__(self, spacing=None, parent=None): QHBoxLayout.__init__(self, parent) if spacing is None: self.setSpacing(0) else: self.setSpacing(spacing) # no margin self.setContentsMargins(0, 0, 0, 0) Nagstamon-master/Nagstamon/qui/widgets/mediaplayer.py000066400000000000000000000017501505160700500233410ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.qui.globals import resource_files from Nagstamon.qui.qt import MediaPlayer # to be used when sounds should be played mediaplayer = MediaPlayer(resource_files)Nagstamon-master/Nagstamon/qui/widgets/menu.py000066400000000000000000000151261505160700500220130ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.qui.qt import (QAction, QMenu, \ QCursor, QPoint, Signal, Slot) from Nagstamon.Servers import servers class MenuAtCursor(QMenu): """ Displays the menu at the current mouse pointer position. Signals: is_shown (bool): Emitted with True when the menu is shown, and with False when it is closed. Args: parent (QWidget, optional): The parent widget of the menu. Methods: show_at_cursor(): Shows the menu at the current mouse pointer position and emits is_shown signals. """ # flag to avoid too fast popping up menus # available = True is_shown = Signal(bool) def __init__(self, parent=None): QMenu.__init__(self, parent=parent) @Slot() def show_at_cursor(self): """ Pop up at mouse pointer position, lock itself to avoid constantly popping menus on Windows """ # get cursor coordinates and decrease them to show menu under mouse pointer x = QCursor.pos().x() - 10 y = QCursor.pos().y() - 10 # tell the world that the menu will be shown self.is_shown.emit(True) # show menu self.exec(QPoint(x, y)) # tell world that menu will be closed self.is_shown.emit(False) del x, y class MenuContext(MenuAtCursor): """ class for universal context menu, used at systray icon and hamburger menu """ menu_ready = Signal(QMenu) def __init__(self, parent=None): MenuAtCursor.__init__(self, parent=parent) self.parent_statuswindow = self.parentWidget() # connect all relevant widgets which should show the context menu for widget in [self.parent_statuswindow.toparea.button_hamburger_menu, self.parent_statuswindow.toparea.label_version, self.parent_statuswindow.toparea.label_empty_space, self.parent_statuswindow.toparea.logo, self.parent_statuswindow.statusbar.logo, self.parent_statuswindow.statusbar.label_message]: self.menu_ready.connect(widget.set_menu) for color_label in self.parent_statuswindow.statusbar.color_labels.values(): self.menu_ready.connect(color_label.set_menu) self.initialize() @Slot() def initialize(self): """ add actions and servers to menu """ # first clear to get rid of old servers self.clear() self.action_refresh = QAction('Refresh', self) self.action_refresh.triggered.connect(self.parent_statuswindow.refresh) self.addAction(self.action_refresh) self.action_recheck = QAction('Recheck all', self) self.action_recheck.triggered.connect(self.parent_statuswindow.recheck_all) self.addAction(self.action_recheck) self.addSeparator() # dict to hold all servers - more flexible this way self.action_servers = dict() # connect every server to its monitoring webpage for server in sorted([x.name for x in conf.servers.values() if x.enabled], key=str.lower): self.action_servers[server] = QAction(server, self) self.action_servers[server].triggered.connect(servers[server].open_monitor_webpage) self.addAction(self.action_servers[server]) self.addSeparator() self.action_settings = QAction('Settings...', self) self.action_settings.triggered.connect(self.parent_statuswindow.hide_window) self.action_settings.triggered.connect(self.parent_statuswindow.injected_dialogs.settings.show) self.addAction(self.action_settings) self.action_save_position = QAction('Save position', self) # TODO: remove action from menu if not needed aka not floating if conf.statusbar_floating: self.addAction(self.action_save_position) self.action_save_position.triggered.connect(self.parent_statuswindow.save_position_to_conf) self.addSeparator() self.action_about = QAction('About...', self) self.action_about.triggered.connect(self.parent_statuswindow.hide_window) self.action_about.triggered.connect(self.parent_statuswindow.injected_dialogs.about.show) self.addAction(self.action_about) self.action_exit = QAction('Exit', self) self.action_exit.triggered.connect(self.parent_statuswindow.exit) self.addAction(self.action_exit) # tell all widgets to use the new menu self.menu_ready.emit(self) class MenuContextSystrayicon(MenuContext): """ Necessary for Ubuntu 16.04 new Qt5-Systray-AppIndicator meltdown Maybe in general a good idea to offer status window popup here """ action_status = None def __init__(self, parent=None): """ clone of normal MenuContext which serves well in all other places but no need of signal/slots initialization """ QMenu.__init__(self, parent=parent) self.parent_statuswindow = self.parentWidget() # initialize as default + extra self.initialize() def initialize(self): """ initialize as inherited + a popup menu entry mostly useful in Ubuntu Unity """ MenuContext.initialize(self) # makes even less sense on OSX if OS != OS_MACOS: self.action_status = QAction('Show status window', self) self.action_status.triggered.connect(self.parent_statuswindow.show_window_systrayicon) self.insertAction(self.action_refresh, self.action_status) self.insertSeparator(self.action_refresh)Nagstamon-master/Nagstamon/qui/widgets/model.py000066400000000000000000000107301505160700500221430ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import conf from Nagstamon.qui.constants import HEADERS_HEADERS from Nagstamon.qui.globals import font_icons from Nagstamon.qui.qt import (QAbstractTableModel, Qt, QVariant, Signal, Slot) class Model(QAbstractTableModel): """ model for storing status data to be presented in Treeview-table """ model_data_array_filled = Signal() # list of lists for storage of status data data_array = list() # cache row and column count row_count = 0 column_count = len(HEADERS_HEADERS) # tell treeview if flags columns should be hidden or not hosts_flags_column_needed = Signal(bool) services_flags_column_needed = Signal(bool) def __init__(self, server, parent=None): QAbstractTableModel.__init__(self, parent=parent) self.server = server def rowCount(self, parent): """ overridden method to get number of rows """ return self.row_count def columnCount(self, parent): """ overridden method to get number of columns """ return self.column_count def headerData(self, column, orientation, role): """ overridden method to get headers of columns """ if role == Qt.ItemDataRole.DisplayRole: return HEADERS_HEADERS[column] return None @Slot(list, dict) # @Slot(list) def fill_data_array(self, data_array, info): """ fill data_array for model """ # tell treeview that model is about to change - necessary because # otherwise new number of rows would not be applied self.beginResetModel() # first empty the data storage del self.data_array[:] # use delivered data array self.data_array = data_array # cache row_count self.row_count = len(self.data_array) # tell treeview if flags columns are needed self.hosts_flags_column_needed.emit(info['hosts_flags_column_needed']) self.services_flags_column_needed.emit(info['services_flags_column_needed']) # new model applied self.endResetModel() self.model_data_array_filled.emit() def data(self, index, role): """ overridden method for data delivery for treeview """ if role == Qt.ItemDataRole.DisplayRole: return self.data_array[index.row()][index.column()] elif role == Qt.ItemDataRole.ForegroundRole: return self.data_array[index.row()][10] elif role == Qt.ItemDataRole.BackgroundRole: return self.data_array[index.row()][11] elif role == Qt.ItemDataRole.FontRole: if index.column() == 1: return font_icons elif index.column() == 3: return font_icons else: return QVariant # provide icons via Qt.UserRole elif role == Qt.ItemDataRole.UserRole: # depending on host or service column return host or service icon list return self.data_array[index.row()][7 + index.column()] elif role == Qt.ItemDataRole.ToolTipRole: # only if tooltips are wanted show them, combining host + service + status_info if conf.show_tooltips: return (f'

' f'{self.data_array[index.row()][0]}: {self.data_array[index.row()][2]}' f'
' f'{self.data_array[index.row()][8]}') else: return QVariant return NoneNagstamon-master/Nagstamon/qui/widgets/nagstamon_logo.py000066400000000000000000000041271505160700500240550ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.qui.qt import (QSvgWidget, QSizePolicy, Signal) from Nagstamon.qui.widgets.draggables import DraggableWidget class NagstamonLogo(QSvgWidget, DraggableWidget): """ SVG based logo, used for statusbar and top area logos """ # yell if statusbar is moved window_moved = Signal() # needed for popup after hover mouse_entered = Signal() # needed for popup after click mouse_pressed = Signal() # needed to close window in some configurations mouse_released = Signal() mouse_released_in_window = Signal() def __init__(self, file, width=None, height=None, parent=None): QSvgWidget.__init__(self, parent=parent) # either filepath or QByteArray for top area logo self.load(file) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) # size needed for small Nagstamon logo in statusbar if width is not None and height is not None: self.setMinimumSize(width, height) self.setMaximumSize(width, height) def adjust_size(self, height=None, width=None): if width is not None and height is not None: self.setMinimumSize(width, height) self.setMaximumSize(width, height) Nagstamon-master/Nagstamon/qui/widgets/server_vbox.py000066400000000000000000000311221505160700500234050ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Nagstamon.config import (conf, OS, OS_MACOS) from Nagstamon.qui.constants import (HEADERS, HEADERS_KEYS_COLUMNS, HEADERS_HEADERS_COLUMNS, SORT_ORDER, SPACE) from Nagstamon.qui.qt import (QPushButton, QSizePolicy, Qt, QVBoxLayout, Signal, Slot) from Nagstamon.qui.widgets.buttons import (Button, PushButtonBrowserURL) from Nagstamon.qui.widgets.labels import (ClosingLabel, ServerStatusLabel) from Nagstamon.qui.widgets.layout import HBoxLayout from Nagstamon.qui.widgets.treeview import TreeView class ServerVBox(QVBoxLayout): """ one VBox per server containing buttons and hosts/services listview """ # used to update status label text like 'Connected-' change_label_status = Signal(str, str) # signal to submit server to authentication dialog authenticate = Signal(str) # handle TLS error button button_fix_tls_error_show = Signal() button_fix_tls_error_hide = Signal() # buttons pressed, which need dialogs button_edit_pressed = Signal(str) button_fix_tls_error_pressed = Signal(str, bool) # open dialog - may need closing the statuswindow open_dialog = Signal() def __init__(self, server, parent=None): QVBoxLayout.__init__(self, parent) self.parent_statuswindow = parent # no space around self.setSpacing(0) self.setContentsMargins(0, 0, 0, 0) # server the vbox belongs to self.server = server # header containing monitor name, buttons and status self.header = HBoxLayout(spacing=SPACE, parent=parent) self.addLayout(self.header) # top and bottom should be kept by padding self.header.setContentsMargins(0, 0, SPACE, 0) self.label = ClosingLabel(parent=parent) self.label.mouse_released.connect(self.parent_statuswindow.hide_window) self.update_label() self.button_monitor = PushButtonBrowserURL(text='Monitor', parent=parent, server=self.server, url_type='monitor') self.button_monitor.webbrowser_opened.connect(self.parent_statuswindow.hide_window) self.button_hosts = PushButtonBrowserURL(text='Hosts', parent=parent, server=self.server, url_type='hosts') self.button_hosts.webbrowser_opened.connect(self.parent_statuswindow.hide_window) self.button_services = PushButtonBrowserURL(text='Services', parent=parent, server=self.server, url_type='services') self.button_services.webbrowser_opened.connect(self.parent_statuswindow.hide_window) self.button_history = PushButtonBrowserURL(text='History', parent=parent, server=self.server, url_type='history') self.button_history.webbrowser_opened.connect(self.parent_statuswindow.hide_window) self.button_edit = Button('Edit', parent=parent) # use label instead of spacer to be clickable self.label_stretcher = ClosingLabel('', parent=parent) self.label_stretcher.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Expanding) self.label_stretcher.mouse_released.connect(self.parent_statuswindow.hide_window) self.label_status = ServerStatusLabel(parent=parent) self.label_status.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.button_authenticate = QPushButton('Authenticate', parent=parent) self.button_fix_tls_error = QPushButton('Fix error', parent=parent) # avoid useless spaces in macOS when server has nothing to show # see https://bugreports.qt.io/browse/QTBUG-2699 self.button_monitor.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_history.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_services.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_hosts.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_edit.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_authenticate.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_fix_tls_error.setAttribute(Qt.WidgetAttribute.WA_LayoutUsesWidgetRect) self.button_monitor.clicked.connect(self.button_monitor.open_url) self.button_hosts.clicked.connect(self.button_hosts.open_url) self.button_services.clicked.connect(self.button_services.open_url) self.button_history.clicked.connect(self.button_history.open_url) self.button_edit.clicked.connect(self.edit_server) self.header.addWidget(self.label) self.header.addWidget(self.button_monitor) self.header.addWidget(self.button_hosts) self.header.addWidget(self.button_services) self.header.addWidget(self.button_history) self.header.addWidget(self.button_edit) self.header.addWidget(self.label_stretcher) self.header.addWidget(self.label_status) self.header.addWidget(self.button_authenticate) self.header.addWidget(self.button_fix_tls_error) self.open_dialog.connect(self.parent_statuswindow.hide_window) # attempt to get header strings try: # when stored as simple lowercase keys sort_column = HEADERS_KEYS_COLUMNS[conf.default_sort_field] except Exception: # when as legacy stored as presentation string sort_column = HEADERS_HEADERS_COLUMNS[conf.default_sort_field] # convert sort order to number as used in Qt.SortOrder sort_order = SORT_ORDER[conf.default_sort_order.lower()] self.table = TreeView(len(HEADERS) + 1, 0, sort_column, sort_order, self.server, parent=parent) # delete vbox if thread quits self.table.worker_thread.finished.connect(self.delete) # connect worker to status label to reflect connectivity self.table.worker.change_label_status.connect(self.label_status.change) self.table.worker.restore_label_status.connect(self.label_status.restore) # care about authentications self.button_authenticate.clicked.connect(self.authenticate_server) # somehow a long way to connect the signal with the slot but works self.authenticate.connect(self.parent_statuswindow.injected_dialogs.authentication.show_auth_dialog) self.parent_statuswindow.injected_dialogs.authentication.update.connect(self.update_label) # start ignoring TLS trouble when button clicked self.button_fix_tls_error.clicked.connect(self.fix_tls_error) # connect button signals to dialogs self.button_edit_pressed.connect(self.parent_statuswindow.injected_dialogs.server.edit) self.button_fix_tls_error_pressed.connect(self.parent_statuswindow.injected_dialogs.server.edit) self.addWidget(self.table, 1) # as default do not show anything self.show_only_header() def get_real_height(self): """ return summarized real height of hbox items and table """ height = self.table.get_real_height() if self.label.isVisible() and self.button_monitor.isVisible(): # compare item heights, decide to take the largest and add 2 time the MARGIN (top and bottom) if self.label.sizeHint().height() > self.button_monitor.sizeHint().height(): height += self.label.sizeHint().height() + 2 else: height += self.button_monitor.sizeHint().height() + 2 return height @Slot() def show_all(self): """ show all items in server vbox """ self.button_authenticate.hide() self.button_edit.show() self.button_fix_tls_error.hide() self.button_history.show() self.button_hosts.show() self.button_monitor.show() self.button_services.show() self.label.show() self.label_status.show() self.label_stretcher.show() # special table treatment self.table.show() # self.table.is_shown = True @Slot() def show_only_header(self): """ show all items in server vbox except the table - not needed if empty or major connection problem """ self.button_authenticate.hide() self.button_edit.show() self.button_history.show() self.button_hosts.show() self.button_fix_tls_error.hide() self.button_monitor.show() self.button_services.show() self.label.show() self.label_status.show() self.label_stretcher.show() # special table treatment self.table.hide() @Slot() def hide_all(self): """ hide all items in server vbox """ self.button_authenticate.hide() self.button_edit.hide() self.button_fix_tls_error.hide() self.button_history.hide() self.button_hosts.hide() self.button_monitor.hide() self.button_services.hide() self.label.hide() self.label_status.hide() self.label_stretcher.hide() # special table treatment self.table.hide() @Slot() def delete(self): """ delete VBox and its children """ for widget in (self.label, self.button_monitor, self.button_hosts, self.button_services, self.button_history, self.button_edit, self.label_status, self.label_stretcher, self.button_authenticate, self.button_fix_tls_error): widget.hide() widget.deleteLater() self.removeItem(self.header) self.header.deleteLater() self.table.hide() self.table.deleteLater() self.deleteLater() def edit_server(self): """ call dialogs.server.edit() with server name """ if not conf.fullscreen and not conf.windowed: self.open_dialog.emit() self.button_edit_pressed.emit(self.server.name) def authenticate_server(self): """ send signal to open authentication dialog with self.server.name """ self.authenticate.emit(self.server.name) @Slot() def update_label(self): self.label.setText(' {0}@{1}'.format(self.server.username, self.server.name)) # let label padding keep top and bottom space - apparently not necessary on OSX if OS != OS_MACOS: self.label.setStyleSheet('''padding-top: {0}px; padding-bottom: {0}px;'''.format(SPACE)) @Slot() def fix_tls_error(self): """ call dialogs.server.edit() with server name and showing extra options """ if not conf.fullscreen and not conf.windowed: self.open_dialog.emit() self.button_fix_tls_error_pressed.emit(self.server.name, True)Nagstamon-master/Nagstamon/qui/widgets/statusbar.py000066400000000000000000000216471505160700500230640ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from os import sep from collections import OrderedDict from Nagstamon.config import (conf, RESOURCES) from Nagstamon.qui.constants import (COLORS, COLOR_STATE_NAMES) from Nagstamon.qui.globals import statuswindow_properties, font from Nagstamon.qui.qt import (QSizePolicy, QTimer, QWidget, Signal, Slot) from Nagstamon.qui.widgets.draggables import DraggableLabel from Nagstamon.qui.widgets.layout import HBoxLayout from Nagstamon.qui.widgets.nagstamon_logo import NagstamonLogo from Nagstamon.Servers import servers, get_errors class StatusBarLabel(DraggableLabel): """ one piece of the status bar labels for one state """ # yell if statusbar is moved window_moved = Signal() # needed for popup after hover mouse_entered = Signal() # needed for popup after click mouse_pressed = Signal() mouse_released = Signal() # needed to close window in some configurations mouse_released_in_window = Signal() def __init__(self, state, parent=None): DraggableLabel.__init__(self, parent=parent) self.setStyleSheet(f'''padding-left: 1px; padding-right: 1px; color: {conf.__dict__[f'color_{state.lower()}_text']}; background-color: {conf.__dict__[f'color_{state.lower()}_background']}; ''') # just let labels grow as much as they need self.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) # hidden per default self.hide() # default text - only useful in case of OK Label self.setText(state) # number of hosts/services of this state self.number = 0 # store state of label to access long state names in .summarize_states() self.state = state @Slot() def invert(self): self.setStyleSheet(f'''padding-left: 1px; padding-right: 1px; color: {conf.__dict__[f'color_{self.state.lower()}_background']}; background-color: {conf.__dict__[f'color_{self.state.lower()}_text']}; ''') @Slot() def reset(self): self.setStyleSheet(f'''padding-left: 1px; padding-right: 1px; color: {conf.__dict__[f'color_{self.state.lower()}_text']}; background-color: {conf.__dict__[f'color_{self.state.lower()}_background']}; ''') class StatusBar(QWidget): """ status bar for short display of problems """ # send signal to statuswindow resize = Signal() # needed to maintain flashing labels labels_invert = Signal() labels_reset = Signal() def __init__(self, parent=None): QWidget.__init__(self, parent=parent) self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.hbox = HBoxLayout(spacing=0, parent=parent) self.setLayout(self.hbox) # define labels first to get their size for svg logo dimensions self.color_labels = OrderedDict() self.color_labels['OK'] = StatusBarLabel('OK', parent=parent) for state in COLORS: self.color_labels[state] = StatusBarLabel(state, parent=parent) self.labels_invert.connect(self.color_labels[state].invert) self.labels_reset.connect(self.color_labels[state].reset) # label for error message(s) self.label_message = StatusBarLabel('error', parent=parent) self.labels_invert.connect(self.label_message.invert) self.labels_reset.connect(self.label_message.reset) # derive logo dimensions from status label self.logo = NagstamonLogo('{0}{1}nagstamon_logo_bar.svg'.format(RESOURCES, sep), self.color_labels['OK'].fontMetrics().height(), self.color_labels['OK'].fontMetrics().height(), parent=parent) # add logo self.hbox.addWidget(self.logo) # label for error messages self.hbox.addWidget(self.label_message) self.label_message.hide() # add state labels self.hbox.addWidget(self.color_labels['OK']) for state in COLORS: self.hbox.addWidget(self.color_labels[state]) # timer for singleshots for flashing self.timer = QTimer() self.adjust_size() @Slot() def summarize_states(self): """ display summaries of states in statusbar """ # initial zeros for label in self.color_labels.values(): label.number = 0 # only count numbers of enabled monitor servers for server in (filter(lambda s: s.enabled, servers.values())): for state in COLORS: self.color_labels[state].number += server.__dict__[state.lower()] # summarize all numbers - if all_numbers keeps 0 everything seems to be OK all_numbers = 0 # repaint colored labels or hide them if necessary for label in self.color_labels.values(): if label.number == 0: label.hide() else: label.setText(' '.join((str(label.number), COLOR_STATE_NAMES[label.state][conf.long_display]))) label.show() label.adjustSize() all_numbers += label.number if all_numbers == 0 and not get_errors() and not self.label_message.isVisible(): self.color_labels['OK'].show() self.color_labels['OK'].adjustSize() else: self.color_labels['OK'].hide() # fix size after refresh - better done here to avoid ugly artefacts hint = self.sizeHint() self.setMaximumSize(hint) self.setMinimumSize(hint) del hint # tell statuswindow its size might be adjusted self.resize.emit() @Slot() def flash(self): """ send color inversion signal to labels """ # only if currently a notification is necessary if statuswindow_properties.is_notifying: self.labels_invert.emit() # fire up a singleshot to reset color soon self.timer.singleShot(500, self.reset) @Slot() def reset(self): """ tell labels to set original colors """ self.labels_reset.emit() # only if currently a notification is necessary if statuswindow_properties.is_notifying: # even later call itself to invert colors as flash self.timer.singleShot(500, self.flash) @Slot() def adjust_size(self): """ apply new size of widgets, especially Nagstamon logo run through all labels to the the max height in case not all labels are shown at the same time - which is very likely the case """ # take height for logo # height = 0 # run through labels to set font and get height for logo for label in self.color_labels.values(): label.setFont(font) # if label.fontMetrics().height() > height: # height = label.fontMetrics().height() self.label_message.setFont(font) height = self.label_message.sizeHint().height() # adjust logo size to fit to label size - due to being a square height and width are the same self.logo.adjust_size(height, height) # avoid flickering/artefact by updating immediately self.summarize_states() @Slot(str) def set_error(self, message): """ display error message if any error exists """ self.label_message.setText(message) self.label_message.show() @Slot() def reset_error(self): """ delete error message if there is no error """ if not get_errors(): self.label_message.setText('') self.label_message.hide() Nagstamon-master/Nagstamon/qui/widgets/statuswindow.py000066400000000000000000002060431505160700500236220ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from os import sep from pathlib import Path from subprocess import Popen import sys from sys import stdout from time import (sleep, time) from traceback import print_exc from Nagstamon.config import (AppInfo, conf, DESKTOP_WAYLAND, OS, OS_MACOS, OS_NON_LINUX, OS_WINDOWS, RESOURCES, debug_queue) from Nagstamon.helpers import (STATES, STATES_SOUND) from Nagstamon.qui.constants import WINDOW_FLAGS from Nagstamon.qui.globals import (dbus_connection, statuswindow_properties) from Nagstamon.qui.helpers import (get_screen_geometry, get_screen_name, hide_macos_dock_icon) from Nagstamon.qui.qt import (QAction, QCursor, QIcon, QMenuBar, QMessageBox, QObject, QVBoxLayout, QScrollArea, QSizePolicy, QSpacerItem, QThread, QWidget, Qt, QT_VERSION_MAJOR, QT_VERSION_MINOR, QTimer, Signal, Slot) from Nagstamon.qui.widgets.app import app from Nagstamon.qui.widgets.labels import LabelAllOK from Nagstamon.qui.widgets.server_vbox import ServerVBox from Nagstamon.qui.widgets.statusbar import StatusBar from Nagstamon.qui.widgets.toparea import TopArea from Nagstamon.Servers import (get_enabled_servers, get_status_count, servers) # only on X11/Linux thirdparty path should be added because it contains the Xlib module # needed to tell window manager via EWMH to keep Nagstamon window on all virtual desktops if OS not in OS_NON_LINUX and not DESKTOP_WAYLAND: # extract thirdparty path from resources path - make submodules accessible by thirdparty modules sys.path.insert(0, sep.join(RESOURCES.split(sep)[0:-1] + ['thirdparty'])) # Xlib for EWMH needs the file ~/.Xauthority and crashes if it does not exist xauthority_file = Path().home() / '.Xauthority' if not xauthority_file.exists(): open(xauthority_file, 'a').close() from Nagstamon.thirdparty.ewmh import EWMH class StatusWindow(QWidget): """ Consists of statusbar, toparea and scrolling area. Either statusbar is shown or (toparea + scrolling area) """ # sent by .resize_window() resizing = Signal() # send when windows opens, e.g. for stopping notifications showing = Signal() # send when window shrinks down to statusbar or closes hiding = Signal() # signal to be sent to all server workers to recheck all recheck = Signal() # signal to submit server to authentication dialog authenticate = Signal(str) # signal to be sent to all treeview workers to clear server event history # after 'Refresh'-button has been pressed clear_event_history = Signal() # signals for changing the state of the system tray icon systrayicon_enabled = Signal() systrayicon_disabled = Signal() # signal to request systray icon position for storing it in statuswindow_properties request_systrayicon_position = Signal() # shortcut to systray icon needed for connecting signals/slots injected_systrayicon = None # cached coordinates stored_x = 0 stored_y = 0 stored_height = 0 stored_width = 0 def __init__(self, dialogs=None, systrayicon=None): """ Status window combined from status bar and popup window """ QWidget.__init__(self) # immediately hide to avoid flicker on Windows and OSX self.hide() # avoid quitting when using Qt.Tool flag and closing settings dialog app.setQuitOnLastWindowClosed(False) # show tooltips even if popup window has no focus self.setAttribute(Qt.WidgetAttribute.WA_AlwaysShowToolTips) # title + icon self.setWindowTitle(AppInfo.NAME) self.setWindowIcon(QIcon(f'{RESOURCES}{sep}nagstamon.svg')) if OS == OS_MACOS: # avoid hiding window if it has no focus - necessary on OSX if using flag Qt.Tool self.setAttribute(Qt.WidgetAttribute.WA_MacAlwaysShowToolWindow) # ewmh.py in thirdparty directory needed to keep floating statusbar on all desktops in Linux if not OS in OS_NON_LINUX and not DESKTOP_WAYLAND: self.ewmh = EWMH() # set shortcut for dialogs icon to be used for connecting signals/slots self.injected_dialogs = dialogs # set shortcut for systray icon to be used for connecting signals/slots self.injected_systrayicon = systrayicon self.vbox = QVBoxLayout(self) # global VBox self.vbox.setSpacing(0) # no spacing self.vbox.setContentsMargins(0, 0, 0, 0) # no margin self.statusbar = StatusBar(parent=self) # statusbar HBox self.toparea = TopArea(parent=self) # toparea HBox # no need to be seen first self.toparea.hide() self.servers_scrollarea = QScrollArea(self) # scrollable area for server vboxes # avoid horizontal scrollbars self.servers_scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # necessary widget to contain vbox for servers self.servers_scrollarea_widget = QWidget(self.servers_scrollarea) self.servers_scrollarea.hide() self.vbox.addWidget(self.statusbar) self.vbox.addWidget(self.toparea) self.vbox.addWidget(self.servers_scrollarea) self.servers_vbox = QVBoxLayout(self.servers_scrollarea) # VBox full of servers self.servers_vbox.setSpacing(0) self.servers_vbox.setContentsMargins(0, 0, 0, 0) self.label_all_ok = LabelAllOK(parent=self) self.label_all_ok.hide() self.servers_vbox.addWidget(self.label_all_ok) # test with OSX top menubar if OS == OS_MACOS: self.menubar = QMenuBar() action_exit = QAction('exit', self.menubar) action_settings = QAction('settings', self.menubar) self.menubar.addAction(action_settings) self.menubar.addAction(action_exit) # stored x y values for systemtray icon statuswindow_properties.icon_x = 0 statuswindow_properties.icon_y = 0 # if status_ok is true no server_vboxes are needed statuswindow_properties.status_ok = True # timer for waiting to set is_shown flag self.timer = QTimer(self) # react to open button in notification bubble dbus_connection.open_statuswindow.connect(self.show_window_from_notification_bubble) # connect logo of statusbar self.statusbar.logo.window_moved.connect(self.save_position) self.statusbar.logo.window_moved.connect(self.hide_window) self.statusbar.logo.window_moved.connect(self.correct_moving_position) self.statusbar.logo.window_moved.connect(self.update) self.statusbar.logo.mouse_pressed.connect(self.save_position) # after status summarization check if window has to be resized self.statusbar.resize.connect(self.adjust_size) # statusbar label has been entered by mouse -> show for label in self.statusbar.color_labels.values(): label.mouse_entered.connect(self.show_window_after_checking_for_hover) label.mouse_released.connect(self.show_window_after_checking_for_clicking) label.mouse_released_in_window.connect(self.hide_window) # connect message label to hover self.statusbar.label_message.mouse_entered.connect(self.show_window_after_checking_for_hover) self.statusbar.label_message.mouse_released.connect(self.show_window_after_checking_for_clicking) self.statusbar.label_message.mouse_released_in_window.connect(self.hide_window) # when logo in toparea was pressed hurry up to save the position so the statusbar will not jump self.toparea.logo.window_moved.connect(self.save_position) self.toparea.logo.window_moved.connect(self.hide_window) self.toparea.logo.window_moved.connect(self.correct_moving_position) self.toparea.logo.window_moved.connect(self.update) self.toparea.logo.mouse_pressed.connect(self.save_position) self.toparea.logo.mouse_released_in_window.connect(self.hide_window) # when version label in toparea was pressed hurry up to save the position so the statusbar will not jump self.toparea.label_version.window_moved.connect(self.save_position) self.toparea.label_version.window_moved.connect(self.hide_window) self.toparea.label_version.window_moved.connect(self.correct_moving_position) self.toparea.label_version.window_moved.connect(self.update) self.toparea.label_version.mouse_pressed.connect(self.save_position) self.toparea.label_version.mouse_released_in_window.connect(self.hide_window) # when empty space in toparea was pressed hurry up to save the position so the statusbar will not jump self.toparea.label_empty_space.window_moved.connect(self.save_position) self.toparea.label_empty_space.window_moved.connect(self.hide_window) self.toparea.label_empty_space.window_moved.connect(self.correct_moving_position) self.toparea.label_empty_space.window_moved.connect(self.update) self.toparea.label_empty_space.mouse_pressed.connect(self.save_position) self.toparea.label_empty_space.mouse_released_in_window.connect(self.hide_window) # buttons in toparea self.toparea.button_recheck_all.clicked.connect(self.recheck_all) self.toparea.button_refresh.clicked.connect(self.refresh) self.toparea.button_settings.clicked.connect(self.hide_window) self.toparea.button_close.clicked.connect(self.hide_window) # if monitor was selected in combobox its monitor window is opened self.toparea.combobox_servers.monitor_opened.connect(self.hide_window) self.initialize() def initialize(self): """ initialize the volatile widgets of the status window """ # needed to store position of status window self.stored_y = 0 self.stored_x = 0 self.stored_width = 0 self.stored_height = 0 # worker and thread duo needed for notifications self.worker_notification_thread = QThread(parent=self) self.worker_notification = self.WorkerNotification(statuswindow_properties) # clean shutdown of thread self.worker_notification.finish.connect(self.finish_worker_notification_thread) # flashing statusbar self.worker_notification.start_flash.connect(self.statusbar.flash) self.worker_notification.stop_flash.connect(self.statusbar.reset) # desktop notification self.worker_notification.desktop_notification.connect(self.desktop_notification) # stop notification if window gets shown or hidden self.hiding.connect(self.worker_notification.stop) self.hiding.connect(self.move_while_hiding) self.worker_notification.moveToThread(self.worker_notification_thread) # start with low priority self.worker_notification_thread.start(QThread.Priority.LowestPriority) self.create_server_vboxes() # connect status window server vboxes to systray for server_vbox in self.servers_vbox.children(): if 'server' in server_vbox.__dict__.keys(): # tell systray after table was refreshed server_vbox.table.worker.new_status.connect(self.injected_systrayicon.show_state) # show error icon in systray server_vbox.table.worker.show_error.connect(self.injected_systrayicon.set_error) server_vbox.table.worker.hide_error.connect(self.injected_systrayicon.reset_error) self.servers_scrollarea_widget.setLayout(self.servers_vbox) self.servers_scrollarea.setWidget(self.servers_scrollarea_widget) self.servers_scrollarea.setWidgetResizable(True) # needed for moving the statuswindow statuswindow_properties.moving = False statuswindow_properties.relative_x = 0 statuswindow_properties.relative_y = 0 # helper values for QTimer.singleShot move attempt self.move_to_x = self.move_to_y = 0 # flag to mark if window is shown or not if conf.windowed: statuswindow_properties.is_shown = True else: statuswindow_properties.is_shown = False # store show_window timestamp to avoid flickering window in KDE5 with systray statuswindow_properties.is_shown_timestamp = time() # store timestamp to avoid reappearing window shortly after clicking onto toparea statuswindow_properties.is_hiding_timestamp = time() # a thread + worker is necessary to do actions thread-safe in background # like debugging self.worker_thread = QThread(parent=self) self.worker = self.Worker() self.worker.moveToThread(self.worker_thread) # start thread and debugging loop if debugging is enabled if conf.debug_mode: self.worker_thread.started.connect(self.worker.debug_loop) # start with low priority self.worker_thread.start(QThread.Priority.LowestPriority) # clean shutdown of thread self.worker.finish.connect(self.finish_worker_thread) # finally show up self.set_mode() @Slot() def reinitialize(self): """ delete volatile widgets and initialize them again """ # stop both workers if hasattr(self, 'worker'): self.worker.running = False self.worker.finish.emit() if hasattr(self, 'worker_notification'): self.worker_notification.running = False self.worker_notification.finish.emit() # remove all server vboxes if hasattr(self, 'servers_vbox'): for vbox in self.servers_vbox.children(): if hasattr(vbox, 'table') and hasattr(vbox.table, 'worker'): vbox.table.worker.finish.emit() vbox.deleteLater() # initialize all volatile widgets self.initialize() def get_screen(self): """ very hackish fix for https://github.com/HenriWahl/Nagstamon/issues/865 should actually fit into qt.py but due to the reference to `app` it could only be solved here """ # Qt6 has .screen() as replacement for QDesktopWidget... if QT_VERSION_MAJOR > 5: return self.screen() # ...and .screen() exists since Qt5 5.15... elif QT_VERSION_MINOR < 15: return app.desktop() # ...so newer ones can use .screen() again else: return self.screen() def set_mode(self): """ apply presentation mode """ # hide everything first self.hide() self.statusbar.hide() self.toparea.hide() self.servers_scrollarea.hide() # restore to normal window in case it was fullscreen self.showNormal() if conf.statusbar_floating: # show icon in dock if window is set if OS == OS_MACOS: # in floating mode always show dock icon - right now I am not able to # get the icon hidden hide_macos_dock_icon(False) # no need for systray self.systrayicon_disabled.emit() self.statusbar.show() # show statusbar/statuswindow on last saved position # when coordinates are inside known screens if get_screen_name(conf.position_x, conf.position_y): self.move(conf.position_x, conf.position_y) else: # get available desktop specs available_x = self.get_screen().availableGeometry().x() available_y = self.get_screen().availableGeometry().y() self.move(available_x, available_y) # statusbar and detail window should be frameless and stay on top # tool flag helps to be invisible in taskbar self.setWindowFlags(WINDOW_FLAGS) # show statusbar without being active, just floating self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating) # necessary to be shown before Linux EWMH-mantra can be applied self.show() # X11/Linux needs some special treatment to get the statusbar floating on all virtual desktops if OS not in OS_NON_LINUX and not DESKTOP_WAYLAND: # get all windows... self.ewmh.setWmDesktop(self.winId().__int__(), 0xffffffff) self.ewmh.display.flush() # show statusbar/statuswindow on last saved position # when coordinates are inside known screens if get_screen_name(conf.position_x, conf.position_y): self.move(conf.position_x, conf.position_y) else: # get available desktop specs available_x = self.get_screen().availableGeometry().x() available_y = self.get_screen().availableGeometry().y() self.move(available_x, available_y) # need a close button self.toparea.button_close.show() elif conf.icon_in_systray: # no need for icon in dock if in systray if OS == OS_MACOS: hide_macos_dock_icon(conf.hide_macos_dock_icon) # statusbar and detail window should be frameless and stay on top # tool flag helps to be invisible in taskbar self.setWindowFlags(WINDOW_FLAGS) # show statusbar without being active, just floating self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating) # yeah! systray! self.systrayicon_enabled.emit() # need a close button self.toparea.button_close.show() # reset systray icon position statuswindow_properties.icon_x = statuswindow_properties.icon_y = 0 elif conf.fullscreen: # no need for systray self.systrayicon_disabled.emit() # needed permanently self.toparea.show() self.servers_scrollarea.show() # get screen geometry to get right screen to position window on screen_geometry = get_screen_geometry(conf.fullscreen_display) self.move(screen_geometry.x(), screen_geometry.y()) # keep window entry in taskbar and thus no Qt.Tool self.setWindowFlags(Qt.WindowType.Widget | Qt.WindowType.FramelessWindowHint) # show statusbar actively self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating, False) # newer Qt5 seem to be better regarding fullscreen mode on non-OSX self.show_window() # fullscreen mode is rather buggy on everything other than OSX so just use a maximized window if OS == OS_MACOS: self.showFullScreen() # in fullscreen mode dock icon does not disturb because the dock is away anyway hide_macos_dock_icon(False) else: self.show() self.showMaximized() # no need for close button self.toparea.button_close.hide() elif conf.windowed: # show icon in dock if window is set if OS == OS_MACOS: # in windowed mode always show dock icon hide_macos_dock_icon(False) self.systrayicon_disabled.emit() # no need for close button self.toparea.button_close.hide() self.toparea.show() self.servers_scrollarea.show() # keep window entry in taskbar and thus no Qt.Tool self.setWindowFlags(Qt.WindowType.Widget) # show statusbar actively self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating, False) # some maybe sensible default self.setMinimumSize(700, 300) self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) # default maximum size self.setMaximumSize(16777215, 16777215) self.move(conf.position_x, conf.position_y) self.resize(conf.position_width, conf.position_height) # make sure window is shown self.show() self.showNormal() self.show_window() # make sure window comes up self.raise_() # force correct position of statuswindow self.adjust_size() # store position for showing/hiding statuswindow self.stored_x = self.x() self.stored_y = self.y() self.stored_width = self.width() def sort_server_vboxes(self): """ sort ServerVBoxes alphabetically """ # shortly after applying changes a QObject might hang around in the children list which should # be filtered out this way vboxes_dict = dict() for child in self.servers_vbox.children(): if 'server' in child.__dict__.keys(): vboxes_dict[child.server.name] = child # freshly set servers_scrollarea_widget and its layout servers_vbox servers_vbox_new = QVBoxLayout() # VBox full of servers servers_vbox_new.setContentsMargins(0, 0, 0, 0) servers_vbox_new.setSpacing(0) # sort server vboxes for vbox in sorted(vboxes_dict): vboxes_dict[vbox].setParent(None) servers_vbox_new.addLayout(vboxes_dict[vbox]) # add expanding stretching item at the end for fullscreen beauty servers_vbox_new.addSpacerItem(QSpacerItem(0, self.get_screen().availableGeometry().height(), QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)) # switch to new servers_vbox self.servers_vbox = servers_vbox_new # necessary widget to contain vbox for servers self.servers_scrollarea_widget = QWidget() self.servers_scrollarea_widget.setLayout(self.servers_vbox) self.servers_scrollarea.setWidget(self.servers_scrollarea_widget) self.servers_scrollarea.contentsMargins().setTop(0) self.servers_scrollarea.contentsMargins().setBottom(0) del vboxes_dict @Slot(str) def create_server_vbox(self, name): """ internally used to create enabled servers to be displayed """ server = servers[name] # create server vboxed from current running servers if server.enabled: # display authentication dialog if password is not known if not conf.servers[server.name].save_password and \ not conf.servers[server.name].use_autologin and \ conf.servers[server.name].password == '' and \ not conf.servers[server.name].authentication == 'kerberos': self.authenticate.emit(server.name) # without parent, there is some flickering when starting server_vbox = ServerVBox(server, parent=self) # important to set correct server to worker, especially after server changes server_vbox.table.worker.server = server # connect to global resize signal server_vbox.table.ready_to_resize.connect(self.adjust_size) # tell statusbar to summarize after table was refreshed server_vbox.table.worker.new_status.connect(self.statusbar.summarize_states) server_vbox.table.worker.new_status.connect(self.raise_window_on_all_desktops) # if problems go themselves there is no need to notify user anymore server_vbox.table.worker.problems_vanished.connect(self.worker_notification.stop) # show the error message in statusbar server_vbox.table.worker.show_error.connect(self.statusbar.set_error) server_vbox.table.worker.hide_error.connect(self.statusbar.reset_error) # tell notification worker to do something AFTER the table was updated server_vbox.table.status_changed.connect(self.worker_notification.start) # and to update status window server_vbox.table.refreshed.connect(self.update_window) # hide statuswindow if authentication dialog is to be shown server_vbox.button_authenticate.clicked.connect(self.hide_window) # tell table it should remove freshness of formerly new items when window closes # because apparently the new events have been seen now self.hiding.connect(server_vbox.table.worker.unfresh_event_history) # stop notification if statuswindow pops up self.showing.connect(self.worker_notification.stop) # tell server worker to recheck all hosts and services self.recheck.connect(server_vbox.table.worker.recheck_all) # refresh table after changed settings self.injected_dialogs.settings.changed.connect(server_vbox.table.refresh) # listen if statuswindow cries for event history clearance self.clear_event_history.connect(server_vbox.table.worker.unfresh_event_history) # statuswindow.servers_vbox.addLayout(statuswindow.create_server_vbox(servers[server_name])) self.servers_vbox.addLayout(server_vbox) self.sort_server_vboxes() return server_vbox else: return None def create_server_vboxes(self): """ create VBox for each enabled server """ for server in servers.values(): if server.enabled: server_vbox = self.create_server_vbox(server.name) self.servers_vbox.addLayout(server_vbox) self.sort_server_vboxes() @Slot(str) def delete_server_vbox(self, name): """ delete VBox for server with given name - called by signal from settings dialog """ for vbox in self.servers_vbox.children(): if vbox.server.name == name: # stop thread by falsificate running flag vbox.table.worker.running = False vbox.table.worker.finish.emit() break @Slot() def show_window_after_checking_for_clicking(self): """ being called after clicking statusbar - check if window should be shown """ if conf.popup_details_clicking: self.show_window() @Slot() def show_window_after_checking_for_hover(self): """ being called after hovering over statusbar - check if window should be shown """ if conf.popup_details_hover: self.show_window() @Slot() def show_window_from_notification_bubble(self): """ show status window after button being clicked in notification bubble """ if conf.statusbar_floating: self.show_window() elif conf.icon_in_systray: self.show_window_systrayicon() @Slot() def show_window_systrayicon(self): """ handle clicks onto systray icon """ if not statuswindow_properties.is_shown: # under unfortunate circumstances statusbar might have the the moving flag true # fix it here because it makes no sense but might cause non-appearing statuswindow‚ statuswindow_properties.moving = False # hopefully no race condition here and the systray icon position will be retrieved just in time self.request_systrayicon_position.emit() # already show here because was closed before in hide_window() # best results achieved when doing .show() before .show_window() self.show() self.show_window() else: self.hide_window() @Slot() def show_window(self, event=None): """ used to show status window when its appearance is triggered, also adjusts geometry """ # do not show up when being dragged around if not statuswindow_properties.moving: # check if really all is OK for vbox in self.servers_vbox.children(): if vbox.server.all_ok and \ vbox.server.status == '' and \ not vbox.server.refresh_authentication and \ not vbox.server.tls_error: statuswindow_properties.status_ok = True else: statuswindow_properties.status_ok = False break # here we should check if scroll_area should be shown at all if not statuswindow_properties.status_ok: # store timestamp to avoid flickering as in https://github.com/HenriWahl/Nagstamon/issues/184 statuswindow_properties.is_shown_timestamp = time() if not conf.fullscreen and not conf.windowed: # attempt to avoid flickering on MacOSX - already hide statusbar here self.statusbar.hide() # show the other status window components self.toparea.show() self.servers_scrollarea.show() else: self.label_all_ok.hide() for vbox in self.servers_vbox.children(): if not vbox.server.all_ok: vbox.show_all() # show at least server vbox header to notify about connection or other errors elif vbox.server.status != '' or vbox.server.refresh_authentication or vbox.server.tls_error: vbox.show_only_header() elif vbox.server.all_ok and vbox.server.status == '': vbox.hide_all() # depending on authentication state show reauthentication button if vbox.server.refresh_authentication: vbox.button_authenticate.show() else: vbox.button_authenticate.hide() # depending on TLS error show fix-TLS-button if vbox.server.tls_error: vbox.button_fix_tls_error.show() else: vbox.button_fix_tls_error.hide() if not conf.fullscreen and \ not conf.windowed: # theory... width, height, x, y = self.calculate_size() # ...and practice self.resize_window(width, height, x, y) # switch on if OS == OS_MACOS: # delayed because of flickering window in OSX self.timer.singleShot(200, self.set_shown) else: self.set_shown() # avoid horizontally scrollable tables self.adjust_dummy_columns() self.show() # Using the EWMH protocol to move the window to the active desktop. # Seemed to be a problem on XFCE # https://github.com/HenriWahl/Nagstamon/pull/199 if not OS in OS_NON_LINUX and conf.icon_in_systray: try: winid = self.winId().__int__() deskid = self.ewmh.getCurrentDesktop() self.ewmh.setWmDesktop(winid, deskid) self.ewmh.display.flush() # makes the window manager switch to the desktop where this widget has appeared self.raise_() except Exception: # workaround for https://github.com/HenriWahl/Nagstamon/issues/246#issuecomment-220478066 pass # tell others like notification that statuswindow shows up now self.showing.emit() else: # hide vboxes in fullscreen and whole window in any other case if all is OK for vbox in self.servers_vbox.children(): vbox.hide_all() if conf.fullscreen or conf.windowed: self.label_all_ok.show() if conf.icon_in_systray or conf.statusbar_floating: self.hide_window() # If the mouse cursor drives too fast over and out the window will not be hidden. # Thus, we check again with this timer to catch missed mouse-outs. # causes trouble in Wayland so is disabled for it if conf.close_details_hover and \ conf.statusbar_floating and \ statuswindow_properties.is_shown and \ not DESKTOP_WAYLAND: self.periodically_check_window_under_mouse_and_hide() def periodically_check_window_under_mouse_and_hide(self): """ Periodically check if window is under mouse and hide it if not """ if not self.hide_window_if_not_under_mouse(): self.timer.singleShot(1000, self.periodically_check_window_under_mouse_and_hide) def hide_window_if_not_under_mouse(self): """ hide window if it is under mouse pointer """ mouse_pos = QCursor.pos() # Check mouse cursor over window and an opened context menu or dropdown list if self.geometry().contains(mouse_pos.x(), mouse_pos.y()) or \ not app.activePopupWidget() is None or \ statuswindow_properties.is_shown: return False self.hide_window() return True @Slot() def update_window(self): """ redraw window content, to be effective only when window is shown """ if statuswindow_properties.is_shown or \ conf.fullscreen or \ (conf.windowed and statuswindow_properties.is_shown): self.show_window() @Slot() def hide_window(self): """ hide window if not needed """ if not conf.fullscreen and not conf.windowed: # only hide if shown and not locked or if not yet hidden if moving if statuswindow_properties.is_shown is True or \ statuswindow_properties.is_shown is True and \ statuswindow_properties.moving is True: # only hide if shown at least a fraction of a second # or has not been hidden a too short time ago if statuswindow_properties.is_shown_timestamp + 0.5 < time() or \ statuswindow_properties.is_hiding_timestamp + 0.1 < time(): if conf.statusbar_floating: self.statusbar.show() self.toparea.hide() self.servers_scrollarea.hide() # macOS needs this since Qt6 to avoid statuswindow size changeability # looks silly but works to force using the own hint as hint if OS == OS_MACOS: self.setMinimumSize(self.sizeHint()) self.setMaximumSize(self.sizeHint()) else: self.setMinimumSize(1, 1) self.adjustSize() if conf.icon_in_systray: self.close() else: # show again after reorganizing widgets self.show() # switch off statuswindow_properties.is_shown = False # flag to reflect top-ness of window/statusbar statuswindow_properties.top = False # reset icon x y statuswindow_properties.icon_x = 0 statuswindow_properties.icon_y = 0 if conf.windowed: self.hide() # store time of hiding statuswindow_properties.is_hiding_timestamp = time() # tell the world that window goes down self.hiding.emit() @Slot() def move_while_hiding(self): """ silly workaround to avoid flickering window in floating mode """ # give some rest to avoid flickering sleep(0.0125) # move the window to the last stored position self.move(self.stored_x, self.stored_y) @Slot() def correct_moving_position(self): """ correct position if moving and cursor started outside statusbar """ if statuswindow_properties.moving: mouse_x = QCursor.pos().x() mouse_y = QCursor.pos().y() # when cursor is outside moved window correct the coordinates of statusbar/statuswindow if not self.geometry().contains(mouse_x, mouse_y): rect = self.geometry() corrected_x = int(mouse_x - rect.width() // 2) corrected_y = int(mouse_y - rect.height() // 2) # calculate new relative values statuswindow_properties.relative_x = mouse_x - corrected_x statuswindow_properties.relative_y = mouse_y - corrected_y self.move(corrected_x, corrected_y) del mouse_x, mouse_y, corrected_x, corrected_y def calculate_size(self): """ get size of popup window """ # only consider offset if it is configured if conf.systray_offset_use and conf.icon_in_systray: available_height = self.get_screen().availableGeometry().height() - conf.systray_offset else: available_height = self.get_screen().availableGeometry().height() available_width = self.get_screen().availableGeometry().width() available_x = self.get_screen().availableGeometry().x() available_y = self.get_screen().availableGeometry().y() # Workaround for Cinnamon + GNOME Flashback if OS not in OS_NON_LINUX and conf.enable_position_fix: if available_x == 0: available_x = available_width if available_y == 0: available_y = available_height # take whole screen height into account when deciding about upper/lower-ness # add available_y because it might vary on differently setup screens # calculate top-ness only if window is closed if conf.statusbar_floating: if self.y() < self.get_screen().geometry().height() // 2 + available_y: statuswindow_properties.top = True else: statuswindow_properties.top = False # always take the stored position of the statusbar x = self.stored_x elif conf.icon_in_systray or conf.windowed: if statuswindow_properties.icon_y < self.get_screen().geometry().height() // 2 + available_y: statuswindow_properties.top = True else: statuswindow_properties.top = False # just a little buffer to let systrayicon.retrieve_icon_position() time to do its work, just in case while statuswindow_properties.icon_x == 0: sleep(0.01) x = statuswindow_properties.icon_x # get height from table widgets real_height = self.get_real_height() # width simply will be the current screen maximal width - less hassle! if self.get_real_width() > available_width: width = available_width x = available_x else: width = self.get_real_width() if width < self.toparea.sizeHint().width(): width = self.toparea.sizeHint().width() # always take the stored width of the statusbar into account x -= int(width // 2) + int(self.stored_width // 2) # check left and right limits of x if x < available_x: x = available_x if x + width > available_x + available_width: x = available_x + available_width - width if conf.statusbar_floating: # when statusbar resides in the uppermost part of current screen extend from top to bottom if statuswindow_properties.top: y = self.y() if self.y() + real_height < available_height + available_y: height = real_height else: height = available_height - self.y() + available_y # when statusbar hangs around in lowermost part of current screen extend from bottom to top else: # when height is too large for current screen cut it if self.y() + self.height() - real_height < available_y: height = self.get_screen().geometry().height() - available_y - ( self.get_screen().geometry().height() - (self.y() + self.height())) y = available_y else: height = real_height y = self.y() + self.height() - height elif conf.icon_in_systray or conf.windowed: # when systrayicon resides in the uppermost part of current screen extend from top to bottom if statuswindow_properties.top: # when being top y is of course the available one y = available_y if self.y() + real_height < available_height + available_y: height = real_height else: # if bigger than screen shrink to maximal real_height height = available_height - available_y # when statusbar hangs around in lowermost part of current screen extend from bottom to top else: if available_height < real_height: y = available_y height = available_height else: y = available_height - real_height height = real_height return width, height, x, y def resize_window(self, width, height, x, y): """ resize status window according to its new size """ # store position for restoring it when hiding - only if not shown of course if statuswindow_properties.is_shown is False: self.stored_x = self.x() self.stored_y = self.y() self.stored_width = self.width() self.stored_height = self.height() if OS == OS_WINDOWS: # absolutely strange, but no other solution available # - Only on Windows the statusbar is moving FIRST before resizing - no matter which # order was used # - Dirty workaround: # - store x and y in .move_to_* # - start helper move_timer by timer singleshot to give statusbar some time to hide self.move_to_x, self.move_to_y = x, y self.timer.singleShot(10, self.move_timer) else: self.move(x, y) self.setMaximumSize(width, height) self.setMinimumSize(width, height) self.adjustSize() return True @Slot() def move_timer(self): """ helper for move by QTimer.singleShot - attempt to avoid flickering on Windows """ self.move(self.move_to_x, self.move_to_y) @Slot() def adjust_size(self): """ resize window if shown and needed """ # avoid race condition when waiting for password dialog if 'is_shown' in statuswindow_properties.__dict__: if not conf.fullscreen and not conf.windowed: # fully displayed statuswindow if statuswindow_properties.is_shown is True: width, height, x, y = self.calculate_size() self.adjust_dummy_columns() else: # statusbar only hint = self.sizeHint() # on MacOSX and Windows statusbar will not shrink automatically, so this workaround hopefully helps width = hint.width() height = hint.height() x = self.x() y = self.y() self.setMaximumSize(hint) self.setMinimumSize(hint) del hint self.resize_window(width, height, x, y) del width, height, x, y else: self.adjust_dummy_columns() @Slot() def adjust_dummy_columns(self): """ calculate the widest width of all server tables to hide dummy column at the widest one """ max_width = 0 max_width_table = None for server in self.servers_vbox.children(): # if table is wider than current max_width take its width as max_width if server.table.get_real_width() > max_width: max_width = server.table.get_real_width() max_width_table = server.table # widest table does not need the dummy column #9 for server in self.servers_vbox.children(): if max_width_table == server.table and max_width == server.table.width(): # hide dummy column as here is the most stretched table server.table.setColumnHidden(9, True) server.table.header().setStretchLastSection(False) else: # show dummy column because some other table is wider server.table.setColumnHidden(9, False) server.table.header().setStretchLastSection(True) del max_width, max_width_table return True @Slot() def save_position(self): """ store position for restoring it when hiding """ if not statuswindow_properties.is_shown: self.stored_x = self.x() self.stored_y = self.y() self.stored_width = self.width() self.stored_height = self.height() def leaveEvent(self, event): """ check if popup has to be hidden depending on mouse position """ # depending on display mode the leave time offset shall be different because # it may be too short in systray mode and lead to flickering window if conf.statusbar_floating: leave_time_offset = 0.25 elif conf.icon_in_systray: # offset is max 1 and smaller if window is smaller too leave_time_offset = self.height() / self.get_screen().availableGeometry().height() else: leave_time_offset = 0 # check first if popup has to be shown by hovering or clicking if conf.close_details_hover and \ not conf.fullscreen and \ not conf.windowed and \ statuswindow_properties.is_shown_timestamp + leave_time_offset < time(): # only hide window if cursor is outside of it mouse_x = QCursor.pos().x() mouse_y = QCursor.pos().y() # <= and >= necessary because sometimes mouse_* is the same as self.*() if mouse_x <= self.x() or mouse_x >= self.x() + self.width() or \ mouse_y <= self.y() or mouse_y >= self.y() + self.height(): self.hide_window() def closeEvent(self, event): """ window close """ # check first if popup has to be shown by hovering or clicking if conf.windowed: exit() def get_real_width(self): """ calculate the widest width of all server tables """ width = 0 for server in self.servers_vbox.children(): # if table is wider than window adjust with to table if server.table.isVisible() and server.table.get_real_width() > width: width = server.table.get_real_width() # if header in server vbox is wider than width adjust the latter if server.header.sizeHint().width() > width: width = server.header.sizeHint().width() return width def get_real_height(self): """ calculate summary of all heights of all server tables plus height of toparea """ height = 0 for vbox in self.servers_vbox.children(): height += vbox.get_real_height() # add size of toparea and 2 times the MARGIN (top and bottom) height += self.toparea.sizeHint().height() + 2 return height def set_shown(self): """ might help to avoid flickering on MacOSX, in cooperation with QTimer """ statuswindow_properties.is_shown = True @Slot() def save_position_to_conf(self): """ store position of statuswindow/statusbar """ # only useful if statusbar is floating if conf.statusbar_floating: # minimize window to statusbar only to get real position self.hide_window() conf.position_x = self.x() conf.position_y = self.y() if conf.windowed: conf.position_x = self.x() conf.position_y = self.y() conf.position_width = self.width() conf.position_height = self.height() # store position of statuswindow/statusbar conf.save_config() @Slot(str, str) def show_message(self, msg_type, message): """ show message from other thread like MediaPlayer """ title = " ".join((AppInfo.NAME, msg_type)) if msg_type == 'warning': return QMessageBox.warning(self, title, message) elif msg_type == 'information': return QMessageBox.information(self, title, message) return None @Slot() def recheck_all(self): """ tell servers to recheck all hosts and services """ self.recheck.emit() @Slot() def refresh(self): """ tell all enabled servers to refresh their information """ # unfresh event history of servers self.clear_event_history.emit() for server in get_enabled_servers(): if conf.debug_mode: server.debug(server=server.name, debug='Refreshing all hosts and services') # manipulate server thread counter so get_status loop will refresh when next looking # at thread counter server.thread_counter = conf.update_interval_seconds @Slot(dict) def desktop_notification(self, current_status_count): """ show desktop notification - must be called from same thread as DBus intialization """ # compile message from status counts message = '' for state in ['DOWN', 'UNREACHABLE', 'DISASTER', 'CRITICAL', 'HIGH', 'AVERAGE', 'WARNING', 'INFORMATION', 'UNKNOWN']: if current_status_count[state] > 0: message += '{0} {1} '.format(str(current_status_count[state]), state) if not message == '': # due to mysterious DBus-Crashes # see https://github.com/HenriWahl/Nagstamon/issues/320 try: dbus_connection.show(AppInfo.NAME, message) except Exception: print_exc(file=stdout) @Slot() def raise_window_on_all_desktops(self): """ experimental workaround for floating-statusbar-only-on-one-virtual-desktop-after-a-while bug see https://github.com/HenriWahl/Nagstamon/issues/217 """ if conf.windowed: return # X11/Linux needs some special treatment to get the statusbar floating on all virtual desktops if OS not in OS_NON_LINUX and not DESKTOP_WAYLAND: # get all windows... winid = self.winId().__int__() self.ewmh.setWmDesktop(winid, 0xffffffff) self.ewmh.display.flush() # apparently sometime the floating statusbar vanishes in the background # lets try here to keep it on top - only if not fullscreen if not conf.fullscreen and not conf.windowed and not OS == OS_WINDOWS: self.setWindowFlags(WINDOW_FLAGS) # again and again try to keep that statuswindow on top! if OS == OS_WINDOWS and \ not conf.fullscreen and \ not conf.windowed and \ app.activePopupWidget() is None: try: self.raise_() except Exception as error: # apparently a race condition could occur on set_mode() - grab it here and continue print(error) @Slot() def finish_worker_thread(self): """ attempt to shut down thread cleanly """ # stop debugging statuswindow_properties.worker_debug_loop_looping = False # tell thread to quit self.worker_thread.quit() # wait until thread is really stopped self.worker_thread.wait() @Slot() def finish_worker_notification_thread(self): """ attempt to shut down thread cleanly """ # tell thread to quit self.worker_notification_thread.quit() # wait until thread is really stopped self.worker_notification_thread.wait() @Slot(str) def remove_previous_server_vbox(self, previous_server_name): # remove old server vbox from the status window if still running for vbox in self.servers_vbox.children(): if vbox.server.name == previous_server_name: # disable server vbox.server.enabled = False # stop thread by falsificate running flag vbox.table.worker.running = False vbox.table.worker.finish.emit() # nothing more to do break @Slot() def decrease_shown_timestamp(self): """ small workaround for the timestamp trick to avoid flickering if the 'Settings' button was clicked too fast the timestamp difference is too short and the statuswindow will keep open modifying the timestamp could help """ statuswindow_properties.is_shown_timestamp -= 1 @Slot() def exit(self): """ stop all child threads before quitting instance """ # store position of statuswindow/statusbar self.save_position_to_conf() # save configuration conf.save_config() # hide statuswindow first to avoid lag when waiting for finished threads self.hide() # stop statuswindow workers self.worker.finish.emit() self.worker_notification.finish.emit() # tell all treeview threads to stop for server_vbox in self.servers_vbox.children(): server_vbox.table.worker.finish.emit() app.exit() class Worker(QObject): """ run a thread, for example, for debugging """ # send signal if ready to stop finish = Signal() def __init__(self): QObject.__init__(self) # flag to decide if the thread has to run or to be stopped self.running = True # flag if debug_loop is looping self.debug_loop_looping = False # default debug dile does not exist self.debug_file = None def open_debug_file(self): # open file and truncate self.debug_file = open(conf.debug_file, "w") def close_debug_file(self): # close and reset file self.debug_file.close() self.debug_file = None @Slot() def debug_loop(self): """ if debugging is enabled, poll debug_queue list and print/write its contents """ if conf.debug_mode: statuswindow_properties.worker_debug_loop_looping = True # as long thread is supposed to run while self.running and statuswindow_properties.worker_debug_loop_looping: # only log something if there is something to tell while len(debug_queue) > 0: # always get the oldest item of queue list - FIFO debug_line = (debug_queue.pop(0)) # output to console print(debug_line) if conf.debug_to_file: # if there is no file handle available get it if self.debug_file is None: self.open_debug_file() # log line per line self.debug_file.write(debug_line + "\n") # wait second until the next poll sleep(1) # unset looping statuswindow_properties.worker_debug_loop_looping = False # close file if any if self.debug_file is not None: self.close_debug_file() else: # set the flag to tell debug loop it should stop, please self.debug_loop_looping = False class WorkerNotification(QObject): """ run a thread for doing all notification stuff """ # tell statusbar labels to flash start_flash = Signal() stop_flash = Signal() # tell mediaplayer to load and play sound file load_sound = Signal(str) play_sound = Signal() # tell statuswindow to use desktop notification desktop_notification = Signal(dict) # only one enabled server should have the right to send play_sound signal notifying_server = '' # desktop notification needs to store count of states status_count = dict() # send signal if ready to stop finish = Signal() def __init__(self, statuswindow_properties = None): QObject.__init__(self) self.statuswindow_properties = statuswindow_properties @Slot(str, str, str) def start(self, server_name, worst_status_diff, worst_status_current): """ start notification """ if conf.notification: # only if not notifying yet or the current state is worse than the prior AND # only when the current state is configured to be honking about if (STATES.index(worst_status_diff) > STATES.index(self.statuswindow_properties.worst_notification_status) or self.statuswindow_properties.is_notifying is False) and \ conf.__dict__[f'notify_if_{worst_status_diff.lower()}'] is True: # keep last worst state worth a notification for comparison 3 lines above self.statuswindow_properties.worst_notification_status = worst_status_diff # set flag to avoid innecessary notification self.statuswindow_properties.is_notifying = True if self.statuswindow_properties.notifying_server == '': statuswindow_properties.notifying_server = server_name # flashing statusbar if conf.notification_flashing: self.start_flash.emit() # Play default sounds via mediaplayer if conf.notification_sound: sound_file = '' # at the moment there are only sounds for down, critical and warning # only honk if notifications are wanted for this state if worst_status_diff in STATES_SOUND: if conf.notification_default_sound: # default .wav sound files are in resources folder sound_file = '{0}{1}{2}.wav'.format(RESOURCES, sep, worst_status_diff.lower()) elif conf.notification_custom_sound: sound_file = conf.__dict__[ 'notification_custom_sound_{0}'.format(worst_status_diff.lower())] # only one enabled server should access the mediaplayer if statuswindow_properties.notifying_server == server_name: # once loaded file will be played by every server, even if it is # not the statuswindow_properties.notifying_server that loaded it self.load_sound.emit(sound_file) self.play_sound.emit() # Notification actions if conf.notification_actions: if conf.notification_action_warning is True and worst_status_diff == 'WARNING': self.execute_action(server_name, conf.notification_action_warning_string) if conf.notification_action_critical is True and worst_status_diff == 'CRITICAL': self.execute_action(server_name, conf.notification_action_critical_string) if conf.notification_action_down is True and worst_status_diff == 'DOWN': self.execute_action(server_name, conf.notification_action_down_string) # Notification action OK if worst_status_current == 'UP' and \ conf.notification_actions and conf.notification_action_ok: self.execute_action(server_name, conf.notification_action_ok_string) # Custom event notification - valid vor ALL events, thus without status comparison if conf.notification_actions is True and conf.notification_custom_action is True: # temporarily used to collect executed events events_list = [] events_string = '' # if no single notifications should be used (default) put all events into one string, separated by separator if conf.notification_custom_action_single is False: for server in get_enabled_servers(): # list comprehension only considers events which are new, ergo True events_list += [k for k, v in server.events_notification.items() if v is True] # create string for no-single-event-notification of events separated by separator events_string = conf.notification_custom_action_separator.join(events_list) # clear already notified events setting them to False for server in get_enabled_servers(): for event in [k for k, v in server.events_notification.items() if v is True]: server.events_notification[event] = False else: for server in get_enabled_servers(): for event in [k for k, v in server.events_notification.items() if v is True]: custom_action_string = conf.notification_custom_action_string.replace('$EVENT$', '$EVENTS$') custom_action_string = custom_action_string.replace('$EVENTS$', event) # execute action self.execute_action(server_name, custom_action_string) # clear already notified events setting them to False server.events_notification[event] = False # if events got filled display them now if events_string != '': # in case a single action per event has to be executed custom_action_string = conf.notification_custom_action_string.replace('$EVENT$', '$EVENTS$') # insert real event(s) custom_action_string = custom_action_string.replace('$EVENTS$', events_string) # execute action self.execute_action(server_name, custom_action_string) else: # set all events to False to ignore them in the future for event in servers[server_name].events_notification: servers[server_name].events_notification[event] = False # repeated sound # only let one enabled server play sound to avoid a larger cacophony if statuswindow_properties.is_notifying and \ conf.notification_sound_repeat and \ statuswindow_properties == server_name: self.play_sound.emit() # desktop notification if conf.notification_desktop: # get status count from servers current_status_count = get_status_count() if current_status_count != self.status_count: self.desktop_notification.emit(current_status_count) # store status count for next comparison self.status_count = current_status_count del current_status_count @Slot() def stop(self): """ stop notification if there is no need anymore """ if self.statuswindow_properties.is_notifying: self.statuswindow_properties.worst_notification_status = 'UP' self.statuswindow_properties.is_notifying = False # no more flashing statusbar and systray self.stop_flash.emit() # reset notifying server, waiting for next notification statuswindow_properties.notifying_server = '' def execute_action(self, server_name, custom_action_string): """ execute custom action """ if conf.debug_mode: servers[server_name].debug(debug='NOTIFICATION: ' + custom_action_string) Popen(custom_action_string, shell=True)Nagstamon-master/Nagstamon/qui/widgets/system_tray_icon.py000066400000000000000000000251351505160700500244430ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from os import (environ, sep) from sys import stdout from traceback import print_exc from Nagstamon.config import (conf, debug_queue, OS, OS_MACOS, RESOURCES, OS_NON_LINUX) from Nagstamon.qui.globals import (resource_files, statuswindow_properties) from Nagstamon.qui.qt import (QCursor, QMenu, QPainter, QPixmap, QSvgRenderer, QSystemTrayIcon, Qt, QTimer, QXmlStreamReader, Signal, Slot) from Nagstamon.qui.widgets.icon import QIconWithFilename from Nagstamon.Servers import get_worst_status class SystemTrayIcon(QSystemTrayIcon): """ Icon in system tray, works at least in Windows and OSX Several Linux desktop environments have different problems For some dark, very dark reason systray menu does NOT work in Windows if run on commandline as nagstamon.py - the binary .exe works """ show_popwin = Signal() hide_popwin = Signal() # flag for displaying error icon in case of error error_shown = False def __init__(self): # debug environment variables if conf.debug_mode: for environment_key, environment_value in environ.items(): debug_queue.append(f'DEBUG: Environment variable: {environment_key}={environment_value}') # initialize systray icon QSystemTrayIcon.__init__(self) # icons are in dictionary self.icons = {} self.create_icons() # empty icon for flashing notification self.icons['EMPTY'] = QIconWithFilename(f'{RESOURCES}{sep}nagstamon_systrayicon_empty.svg') # little workaround to match statuswindow.worker_notification.worst_notification_status self.icons['UP'] = self.icons['OK'] # default icon is OK if conf.icon_in_systray: self.setIcon(self.icons['OK']) # store icon for flashing self.current_icon = None # no menu at first self.menu = None # timer for singleshots for flashing self.timer = QTimer() # treat clicks self.activated.connect(self.icon_clicked) def current_icon_name(self): """ internal function useful for debugging, returns the name of the current icon """ current_account_icon = self.icon() if current_account_icon is None: return '' return str(current_account_icon) @Slot(QMenu) def set_menu(self, menu): """ create current menu for right clicks """ # store menu for future use, especially for MacOSX self.menu = menu # MacOSX does not distinguish between left and right click so menu will go to upper menu bar # update: apparently not, but own context menu will be shown when icon is clicked an all is OK = green if OS != OS_MACOS: self.setContextMenu(self.menu) @Slot() def create_icons(self): """ create icons from template, applying colors """ svg_template = f'{RESOURCES}{sep}nagstamon_systrayicon_template.svg' # get template from file # by using RESOURCE_FILES the file path will be checked on macOS and the file restored if necessary with open(resource_files[svg_template]) as svg_template_file: svg_template_xml = svg_template_file.readlines() # create icons for all states for state in ['OK', 'INFORMATION', 'UNKNOWN', 'WARNING', 'AVERAGE', 'HIGH', 'CRITICAL', 'DISASTER', 'UNREACHABLE', 'DOWN', 'ERROR']: # current SVG XML for state icon, derived from svg_template_cml svg_state_xml = list() # replace dummy text and background colors with configured ones for line in svg_template_xml: line = line.replace('fill:#ff00ff', 'fill:' + conf.__dict__['color_' + state.lower() + '_text']) line = line.replace('fill:#00ff00', 'fill:' + conf.__dict__['color_' + state.lower() + '_background']) svg_state_xml.append(line) # create XML stream of SVG svg_xml_stream = QXmlStreamReader(''.join(svg_state_xml)) # create renderer for SVG and put SVG XML into renderer svg_renderer = QSvgRenderer(svg_xml_stream) # pixmap to be painted on - arbitrarily choosen 128x128 px svg_pixmap = QPixmap(128, 128) # fill transparent backgound svg_pixmap.fill(Qt.GlobalColor.transparent) # initiate painter which paints onto paintdevice pixmap svg_painter = QPainter(svg_pixmap) # render svg to pixmap svg_renderer.render(svg_painter) # close painting svg_painter.end() # put pixmap into icon self.icons[state] = QIconWithFilename(svg_pixmap) debug_queue.append(f'DEBUG: SystemTrayIcon created icon {self.icons[state]} for state "{state}"') @Slot(QSystemTrayIcon.ActivationReason) def icon_clicked(self, reason): """ evaluate mouse click """ # retrieve icon position and store it in statuswindow_properties self.retrieve_icon_position() if reason in (QSystemTrayIcon.ActivationReason.Trigger, QSystemTrayIcon.ActivationReason.DoubleClick, QSystemTrayIcon.ActivationReason.MiddleClick): # when green icon is displayed and no popwin is about to pop up... if get_worst_status() == 'UP': # ...nothing to do except on macOS where menu should be shown if OS == OS_MACOS: # in case there is some error show popwin rather than context menu if not self.error_shown: self.menu.show_at_cursor() else: self.show_popwin.emit() else: # show status window if there is something to tell if statuswindow_properties.is_shown: self.hide_popwin.emit() else: self.show_popwin.emit() @Slot() def retrieve_icon_position(self): """ get the coordinates of the systray icon and store it in statuswindow_properties """ # where is the pointer which clicked onto systray icon icon_x = self.geometry().x() icon_y = self.geometry().y() if OS in OS_NON_LINUX: if statuswindow_properties.icon_x == 0: statuswindow_properties.icon_x = QCursor.pos().x() elif icon_x != 0: statuswindow_properties.icon_x = icon_x else: # strangely enough on KDE the systray icon geometry gives back 0, 0 as coordinates # also at Ubuntu Unity 16.04 if icon_x == 0 and statuswindow_properties.icon_x == 0: statuswindow_properties.icon_x = QCursor.pos().x() elif icon_x != 0: statuswindow_properties.icon_x = icon_x if icon_y == 0 and statuswindow_properties.icon_y == 0: statuswindow_properties.icon_y = QCursor.pos().y() if OS in OS_NON_LINUX: if statuswindow_properties.icon_y == 0: statuswindow_properties.icon_y = QCursor.pos().y() elif icon_y != 0: statuswindow_properties.icon_y = icon_y pass @Slot() def show_state(self): """ get the worst status and display it in systray """ if not self.error_shown: worst_status = get_worst_status() self.setIcon(self.icons[worst_status]) # set current icon for flashing self.current_icon = self.icons[worst_status] del worst_status else: self.setIcon(self.icons['ERROR']) @Slot() def flash(self): """ send color inversion signal to labels """ # only if currently a notification is necessary if statuswindow_properties.is_notifying: # store current icon to get it reset back if self.current_icon is None: if not self.error_shown: self.current_icon = self.icons[statuswindow_properties.worst_notification_status] else: self.current_icon = self.icons['ERROR'] # use empty SVG icon to display emptiness if resource_files[self.icons['EMPTY'].filename]: self.setIcon(self.icons['EMPTY']) # fire up a singleshot to reset color soon self.timer.singleShot(500, self.reset) @Slot() def reset(self): """ tell labels to set original colors """ # only if currently a notification is necessary if statuswindow_properties.is_notifying: try: # set curent status icon self.setIcon(self.current_icon) # even later call itself to invert colors as flash self.timer.singleShot(500, self.flash) except: print_exc(file=stdout) else: if self.current_icon is not None: self.setIcon(self.current_icon) self.current_icon = None @Slot() def set_error(self): self.error_shown = True @Slot() def reset_error(self): self.error_shown = False Nagstamon-master/Nagstamon/qui/widgets/toparea.py000066400000000000000000000145361505160700500225060ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from base64 import b64encode from os import sep from Nagstamon.config import AppInfo, RESOURCES from Nagstamon.qui.constants import SPACE from Nagstamon.qui.globals import app from Nagstamon.qui.qt import (QAction, QByteArray, QHBoxLayout, QIcon, QPainter, QPixmap, QPalette, QSizePolicy, QSvgRenderer, Qt, QXmlStreamReader, QWidget, Signal, Slot) from Nagstamon.qui.widgets.buttons import (Button, CSS_CLOSE_BUTTON, PushButtonHamburger) from Nagstamon.qui.widgets.combobox_servers import (ComboBoxServers) from Nagstamon.qui.widgets.draggables import DraggableLabel from Nagstamon.qui.widgets.layout import HBoxLayout from Nagstamon.qui.widgets.menu import MenuAtCursor from Nagstamon.qui.widgets.nagstamon_logo import NagstamonLogo class TopArea(QWidget): """ top area of status window """ mouse_entered = Signal() def __init__(self, parent=None): QWidget.__init__(self) self.hbox = HBoxLayout(spacing=SPACE, parent=self) # top HBox containing buttons self.hbox.setSizeConstraint(QHBoxLayout.SizeConstraint.SetMinimumSize) self.icons = dict() self.create_icons() # top button box self.logo = NagstamonLogo(self.icons['nagstamon_logo_toparea'], width=150, height=42, parent=self) self.label_version = DraggableLabel(text=AppInfo.VERSION, parent=self) self.label_empty_space = DraggableLabel(text='', parent=self) self.label_empty_space.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored) self.combobox_servers = ComboBoxServers(parent=self) self.button_filters = Button("Filters", parent=self) self.button_recheck_all = Button("Recheck all", parent=self) self.button_refresh = Button("Refresh", parent=self) self.button_settings = Button("Settings", parent=self) # fill default order fields combobox with server names self.combobox_servers.fill() # hamburger menu self.button_hamburger_menu = PushButtonHamburger() self.button_hamburger_menu.setIcon(self.icons['menu']) self.hamburger_menu = MenuAtCursor() self.action_exit = QAction("Exit", self) self.hamburger_menu.addAction(self.action_exit) self.button_hamburger_menu.setMenu(self.hamburger_menu) # X self.button_close = Button() self.button_close.setIcon(self.icons['close']) self.button_close.setStyleSheet(CSS_CLOSE_BUTTON) self.hbox.addWidget(self.logo) self.hbox.addWidget(self.label_version) self.hbox.addWidget(self.label_empty_space) self.hbox.addWidget(self.combobox_servers) self.hbox.addWidget(self.button_filters) self.hbox.addWidget(self.button_recheck_all) self.hbox.addWidget(self.button_refresh) self.hbox.addWidget(self.button_settings) self.hbox.addWidget(self.button_hamburger_menu) self.hbox.addWidget(self.button_close) self.setLayout(self.hbox) def enterEvent(self, event): # unlock statuswindow if pointer touches statusbar self.mouse_entered.emit() @Slot() def create_icons(self): """ create icons from template, applying colors """ # get rgb values of current foreground color to be used for SVG icons (menu) r, g, b, a = app.palette().color(QPalette.ColorRole.Text).getRgb() for icon in 'nagstamon_logo_toparea', 'close', 'menu': # get template from file svg_template_file = open(f'{RESOURCES}{sep}{icon}_template.svg') svg_template_xml = svg_template_file.readlines() # current SVG XML for state icon, derived from svg_template_cml svg_icon_xml = list() # replace dummy text and background colors with configured ones for line in svg_template_xml: line = line.replace('fill:#ff00ff', 'fill:#{0:x}{1:x}{2:x}'.format(r, g, b)) svg_icon_xml.append(line) # create XML stream of SVG svg_xml_stream = QXmlStreamReader(''.join(svg_icon_xml)) # create renderer for SVG and put SVG XML into renderer svg_renderer = QSvgRenderer(svg_xml_stream) # pixmap to be painted on - arbitrarily choosen 128x128 px svg_pixmap = QPixmap(128, 128) # fill transparent backgound svg_pixmap.fill(Qt.GlobalColor.transparent) # initiate painter which paints onto paintdevice pixmap svg_painter = QPainter(svg_pixmap) # render svg to pixmap svg_renderer.render(svg_painter) # close painting svg_painter.end() # two ways... if icon == 'nagstamon_logo_toparea': # first get a base64 version of the SVG svg_base64 = b64encode(bytes(''.join(svg_icon_xml), 'utf8')) # create a QByteArray for NagstamonLogo aka QSvgWidget svg_bytes = QByteArray.fromBase64(svg_base64) self.icons[icon] = svg_bytes else: # put pixmap into icon self.icons[icon] = QIcon(svg_pixmap) Nagstamon-master/Nagstamon/qui/widgets/treeview.py000066400000000000000000001771641505160700500227140ustar00rootroot00000000000000# Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from copy import deepcopy from datetime import datetime from subprocess import Popen from sys import stdout from traceback import print_exc from urllib.parse import quote from Nagstamon.config import conf from Nagstamon.helpers import (is_found_by_re, STATES, SORT_COLUMNS_FUNCTIONS, urlify, webbrowser_open) from Nagstamon.qui.constants import (COLORS, HEADERS, SORT_COLUMNS_INDEX, SORT_ORDER) from Nagstamon.qui.globals import (clipboard, font, qbrushes, statuswindow_properties) from Nagstamon.qui.qt import (get_sort_order_value, QAbstractItemView, QAction, QColor, QHeaderView, QKeySequence, QMenu, QObject, QSignalMapper, QSizePolicy, Qt, QThread, QTimer, QTreeView, Signal, Slot) from Nagstamon.qui.widgets.app import app from Nagstamon.qui.widgets.menu import MenuAtCursor from Nagstamon.qui.widgets.model import Model from Nagstamon.Servers import SERVER_TYPES, servers class TreeView(QTreeView): """ attempt to get a less resource-hungry table/tree """ # tell global window that it should be resized ready_to_resize = Signal() # sent by refresh() for statusbar refreshed = Signal() # tell worker to get status after a recheck has been solicited recheck = Signal(dict) # tell notification that status of server has changed status_changed = Signal(str, str, str) # action to be executed by worker # 2 values: action and host/service info request_action = Signal(dict, dict) # tell worker it should sort columns after someone pressed the column header sort_data_array_for_columns = Signal(int, int, bool) # mouse clicked on cell mouse_released = Signal() # action menu option was selected action_menu_clicked = Signal() # action to edit actions in settings dialog action_edit_triggered = Signal(int) # action acknowledge triggered, needs to be initialized and shown action_acknowledge_triggered_initialize = Signal(object, list, list) action_acknowledge_triggered_show = Signal() # action downtime triggered, needs to be initialized and shown action_downtime_triggered_initialize = Signal(object, list, list) action_downtime_triggered_show = Signal() # action submit check result triggered, needs to be initialized and shown action_submit_triggered_initialize = Signal(object, str, str) action_submit_triggered_show = Signal() def __init__(self, columncount, rowcount, sort_column, sort_order, server, parent=None): QTreeView.__init__(self, parent=parent) self.parent_statuswindow = self.parentWidget() self.sort_column = sort_column self.sort_order = sort_order self.server = server # no handling of selection by treeview self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # disable space on the left side self.setRootIsDecorated(False) self.setIndentation(0) self.setUniformRowHeights(True) # no scrollbars at tables because they will be scrollable by the global vertical scrollbar self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setAutoScroll(False) self.setSortingEnabled(True) self.sortByColumn(0, Qt.SortOrder.AscendingOrder) self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding) self.header().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.header().setSortIndicatorShown(True) self.header().setStretchLastSection(True) self.header().setSortIndicator(sort_column, SORT_ORDER[self.sort_order]) # small method needed to tell worker which column and sort order to use self.header().sortIndicatorChanged.connect(self.sort_columns) # set overall margin and hover colors - to be refined self.setStyleSheet('''QTreeView::item {margin: 5px;} QTreeView::item:hover {margin: 0px; color: white; background-color: dimgrey;} QTreeView::item:selected {margin: 0px; color: white; background-color: grey;} ''') # set application font self.set_font() # change font if it has been changed by settings self.parent_statuswindow.injected_dialogs.settings.changed.connect(self.set_font) # create brushes if colors have been changed self.parent_statuswindow.injected_dialogs.settings.changed.connect(self.create_brushes) # create brushes for treeview self.create_brushes() # action context menu self.action_menu = MenuAtCursor(parent=self) # signalmapper for getting triggered actions self.signalmapper_action_menu = QSignalMapper() # connect menu to responder self.signalmapper_action_menu.mappedString[str].connect(self.action_menu_custom_response) # clipboard actions self.clipboard_menu = QMenu('Copy to clipboard', self) self.clipboard_action_host = QAction('Host', self) self.clipboard_action_host.triggered.connect(self.action_clipboard_action_host) self.clipboard_menu.addAction(self.clipboard_action_host) self.clipboard_action_service = QAction('Service', self) self.clipboard_action_service.triggered.connect(self.action_clipboard_action_service) self.clipboard_menu.addAction(self.clipboard_action_service) self.clipboard_action_statusinformation = QAction('Status information', self) self.clipboard_action_statusinformation.triggered.connect(self.action_clipboard_action_statusinformation) self.clipboard_menu.addAction(self.clipboard_action_statusinformation) self.clipboard_action_all = QAction('All information', self) self.clipboard_action_all.triggered.connect(self.action_clipboard_action_all) self.clipboard_menu.addAction(self.clipboard_action_all) self.setModel(Model(server=self.server, parent=self)) self.model().model_data_array_filled.connect(self.adjust_table) self.model().hosts_flags_column_needed.connect(self.show_hosts_flags_column) self.model().services_flags_column_needed.connect(self.show_services_flags_column) # a thread + worker is necessary to get new monitor server data in the background and # to refresh the table cell by cell after new data is available self.worker_thread = QThread(parent=self) self.worker = self.Worker(server=server, sort_column=self.sort_column, sort_order=self.sort_order, status_window=self.parent_statuswindow) self.worker.moveToThread(self.worker_thread) # if worker got new status data from monitor server get_status # the treeview model has to be updated self.worker.worker_data_array_filled.connect(self.model().fill_data_array) # fill array again if data has been sorted after a header column click self.worker.data_array_sorted.connect(self.model().fill_data_array) # tell worker to sort data_array depending on sort_column and sort_order self.sort_data_array_for_columns.connect(self.worker.sort_data_array) # if worker got new status data from monitor server get_status the table should be refreshed self.worker.new_status.connect(self.refresh) # quit thread if worker has finished self.worker.finish.connect(self.finish_worker_thread) # get status if started self.worker_thread.started.connect(self.worker.get_status) # start with priority 0 = lowest self.worker_thread.start() # connect signal for acknowledge self.parent_statuswindow.injected_dialogs.acknowledge.acknowledge.connect(self.worker.acknowledge) # connect signal to get start end time for downtime from worker self.parent_statuswindow.injected_dialogs.downtime.get_start_end.connect(self.worker.get_start_end) self.worker.set_start_end.connect(self.parent_statuswindow.injected_dialogs.downtime.set_start_end) # connect signal for downtime self.parent_statuswindow.injected_dialogs.downtime.downtime.connect(self.worker.downtime) # connect signal for submit check result self.parent_statuswindow.injected_dialogs.submit.submit.connect(self.worker.submit) # connect signal for recheck action self.recheck.connect(self.worker.recheck) # execute action by worker self.request_action.connect(self.worker.execute_action) # hide status window if mouse was clicked ant it is configured to do so self.mouse_released.connect(self.parent_statuswindow.hide_window) # option of action menu was selected self.action_menu_clicked.connect(self.parent_statuswindow.hide_window) # edit actions in settings dialog in tab #3 self.action_edit_triggered.connect(self.parent_statuswindow.injected_dialogs.settings.show) # intialize and open acknowledge dialog self.action_acknowledge_triggered_initialize.connect(self.parent_statuswindow.injected_dialogs.acknowledge.initialize) self.action_acknowledge_triggered_show.connect(self.parent_statuswindow.injected_dialogs.acknowledge.show) # intialize and open downtime dialog self.action_downtime_triggered_initialize.connect(self.parent_statuswindow.injected_dialogs.downtime.initialize) self.action_downtime_triggered_show.connect(self.parent_statuswindow.injected_dialogs.downtime.show) # intialize and open submit check result dialog self.action_submit_triggered_initialize.connect(self.parent_statuswindow.injected_dialogs.submit.initialize) self.action_submit_triggered_show.connect(self.parent_statuswindow.injected_dialogs.submit.show) @Slot() def set_font(self): """ change font if it has been changed by settings """ self.setFont(font) @Slot() def create_brushes(self): """ fill static brushes with current colors for treeview """ # if not customized, use default intensity if conf.grid_use_custom_intensity: intensity = 100 + conf.grid_alternation_intensity else: intensity = 115 # every state has 2 labels in both alteration levels 0 and 1 for state in STATES[1:]: for role in ('text', 'background'): qbrushes[0][COLORS[state] + role] = QColor(conf.__dict__[COLORS[state] + role]) # if the background is too dark to be litten split it into RGB values # and increase them separately # light/darkness spans from 0 to 255 - 30 is just a guess if role == 'background' and conf.show_grid: if qbrushes[0][COLORS[state] + role].lightness() < 30: r, g, b, a = (qbrushes[0][COLORS[state] + role].getRgb()) r += 30 g += 30 b += 30 qbrushes[1][COLORS[state] + role] = QColor(r, g, b).lighter(intensity) else: # otherwise just make it a little bit darker qbrushes[1][COLORS[state] + role] = QColor(conf.__dict__[COLORS[state] + role]).darker(intensity) else: # only make the background darker; the text should stay as it is qbrushes[1][COLORS[state] + role] = qbrushes[0][COLORS[state] + role] @Slot(bool) def show_hosts_flags_column(self, value): """ show hosts flags column if needed 'value' is True if there is a need so it has to be converted """ self.setColumnHidden(1, not value) @Slot(bool) def show_services_flags_column(self, value): """ show service flags column if needed 'value' is True if there is a need so it has to be converted """ self.setColumnHidden(3, not value) def get_real_height(self): """ calculate real table height as there is no method included """ height = 0 # only count if there is anything to display - there is no use of the headers only if self.model().rowCount(self) > 0: # height summary starts with headers' height # apparently height works better/without scrollbar if some pixels are added height = self.header().sizeHint().height() + 2 # maybe simply take nagitems_filtered_count? height += self.indexRowSizeHint(self.model().index(0, 0)) * self.model().rowCount(self) return height def get_real_width(self): width = 0 # avoid the last dummy column to be counted for column in range(len(HEADERS) - 1): width += self.columnWidth(column) return width @Slot() def adjust_table(self): """ adjust table dimensions after filling it """ # force table to its maximal height, calculated by .get_real_height() self.setMinimumHeight(self.get_real_height()) self.setMaximumHeight(self.get_real_height()) self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Maximum) # after setting table whole window can be repainted self.ready_to_resize.emit() def count_selected_rows(self): """ find out if rows are selected and return their number """ rows = [] for index in self.selectedIndexes(): if index.row() not in rows: rows.append(index.row()) return len(rows) def mouseReleaseEvent(self, event): """ forward clicked cell info from event """ # special treatment if window should be closed when left-clicking somewhere # it is important to check if CTRL or SHIFT key is presses while clicking to select lines modifiers = event.modifiers() if conf.close_details_clicking_somewhere: if event.button() == Qt.MouseButton.LeftButton: # count selected rows - if more than 1 do not close popwin if modifiers or self.count_selected_rows() > 1: super(TreeView, self).mouseReleaseEvent(event) else: self.mouse_released.emit() return elif event.button() == Qt.MouseButton.RightButton: self.cell_clicked() return elif not modifiers or \ event.button() == Qt.MouseButton.RightButton: self.cell_clicked() return else: super(TreeView, self).mouseReleaseEvent(event) def wheelEvent(self, event): """ avoid scrollable single treeview in Linux and GNOME3 by simply do nothing when getting a wheel event """ event.ignore() def keyPressEvent(self, event): """ Use to handle copy from keyboard """ if event.matches(QKeySequence.StandardKey.Copy): self.action_clipboard_action_all() return super(TreeView, self).keyPressEvent(event) @Slot() def cell_clicked(self): """ Windows reacts differently to clicks into table cells than Linux and MacOSX Therefore the .available flag is necessary """ # empty the menu self.action_menu.clear() # clear signal mappings self.signalmapper_action_menu.removeMappings(self.signalmapper_action_menu) # add custom actions actions_list = list(conf.actions) actions_list.sort(key=str.lower) # How many rows do we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) # dummy definition to avoid crash if no actions are enabled - asked for some lines later miserable_service = None # Add custom actions if all selected rows want them, one per one for a in actions_list: # shortcut for next lines action = conf.actions[a] # check if current monitor server type is in action # second check for server type is legacy-compatible with older settings if action.enabled is True and (action.monitor_type in ['', self.server.TYPE] or action.monitor_type not in SERVER_TYPES): # menu item visibility flag item_visible = None for lrow in list_rows: # temporary menu item visibility flag to collect all visibility info item_visible_temporary = False # take data from model data_array miserable_host = self.model().data_array[lrow][0] miserable_service = self.model().data_array[lrow][2] miserable_duration = self.model().data_array[lrow][6] miserable_attempt = self.model().data_array[lrow][7] miserable_status_information = self.model().data_array[lrow][8] # check if clicked line is a service or host # it is checked if the action is targeted on hosts or services if miserable_service: if action.filter_target_service is True: # only check if there is some to check if action.re_host_enabled is True: if is_found_by_re(miserable_host, action.re_host_pattern, action.re_host_reverse): item_visible_temporary = True # dito if action.re_service_enabled is True: if is_found_by_re(miserable_service, action.re_service_pattern, action.re_service_reverse): item_visible_temporary = True # dito if action.re_status_information_enabled is True: if is_found_by_re(miserable_status_information, action.re_status_information_pattern, action.re_status_information_reverse): item_visible_temporary = True # dito if action.re_duration_enabled is True: if is_found_by_re(miserable_duration, action.re_duration_pattern, action.re_duration_reverse): item_visible_temporary = True # dito if action.re_attempt_enabled is True: if is_found_by_re(miserable_attempt, action.re_attempt_pattern, action.re_attempt_reverse): item_visible_temporary = True # dito - how is this supposed to work? if action.re_groups_enabled is True: if is_found_by_re(miserable_service, action.re_groups_pattern, action.re_groups_reverse): item_visible_temporary = True # fallback if no regexp is selected if action.re_host_enabled == action.re_service_enabled == \ action.re_status_information_enabled == action.re_duration_enabled == \ action.re_attempt_enabled == action.re_groups_enabled is False: item_visible_temporary = True else: # hosts should only care about host specific actions, no services if action.filter_target_host is True: if action.re_host_enabled is True: if is_found_by_re(miserable_host, action.re_host_pattern, action.re_host_reverse): item_visible_temporary = True else: # a non-specific action will be displayed per default item_visible_temporary = True # when item_visible has never been set it shall be false # also if at least one row leads to not-showing the item it will be false if item_visible_temporary and item_visible is None: item_visible = True if not item_visible_temporary: item_visible = False else: item_visible = False # populate context menu with service actions if item_visible: # create action action_menuentry = QAction(a, self) # add action self.action_menu.addAction(action_menuentry) # action to signalmapper self.signalmapper_action_menu.setMapping(action_menuentry, a) action_menuentry.triggered.connect(self.signalmapper_action_menu.map) del action, item_visible # create and add default actions action_edit_actions = QAction('Edit actions...', self) action_edit_actions.triggered.connect(self.action_edit_actions) self.action_menu.addAction(action_edit_actions) # put actions into menu after separator self.action_menu.addSeparator() if 'Monitor' in self.server.MENU_ACTIONS and len(list_rows) == 1: action_monitor = QAction('Monitor', self) action_monitor.triggered.connect(self.action_monitor) self.action_menu.addAction(action_monitor) if 'Recheck' in self.server.MENU_ACTIONS: action_recheck = QAction('Recheck', self) action_recheck.triggered.connect(self.action_recheck) self.action_menu.addAction(action_recheck) if 'Acknowledge' in self.server.MENU_ACTIONS: action_acknowledge = QAction('Acknowledge', self) action_acknowledge.triggered.connect(self.action_acknowledge) self.action_menu.addAction(action_acknowledge) if 'Downtime' in self.server.MENU_ACTIONS: action_downtime = QAction('Downtime', self) action_downtime.triggered.connect(self.action_downtime) self.action_menu.addAction(action_downtime) # special menu entry for Checkmk Multisite for archiving events if self.server.type == 'Checkmk Multisite' and len(list_rows) == 1: if miserable_service == 'Events': action_archive_event = QAction('Archive event', self) action_archive_event.triggered.connect(self.action_archive_event) self.action_menu.addAction(action_archive_event) # not all servers allow to submit fake check results if 'Submit check result' in self.server.MENU_ACTIONS and len(list_rows) == 1: action_submit = QAction('Submit check result', self) action_submit.triggered.connect(self.action_submit) self.action_menu.addAction(action_submit) # experimental clipboard submenu self.action_menu.addMenu(self.clipboard_menu) # show menu self.action_menu.show_at_cursor() @Slot(str) def action_menu_custom_response(self, action): # How many rows do we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: miserable_host = self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole) miserable_service = self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole) miserable_status_info = self.model().data(self.model().createIndex(lrow, 8), Qt.ItemDataRole.DisplayRole) # get data to send to action server = self.server.get_name() address = self.server.get_host(miserable_host).result monitor = self.server.monitor_url monitor_cgi = self.server.monitor_cgi_url username = self.server.username password = self.server.password comment_ack = conf.defaults_acknowledge_comment comment_down = conf.defaults_downtime_comment comment_submit = conf.defaults_submit_check_result_comment # send dict with action info and dict with host/service info self.request_action.emit(conf.actions[action].__dict__, {'server': server, 'host': miserable_host, 'service': miserable_service, 'status-info': miserable_status_info, 'address': address, 'monitor': monitor, 'monitor-cgi': monitor_cgi, 'username': username, 'password': password, 'comment-ack': comment_ack, 'comment-down': comment_down, 'comment-submit': comment_submit } ) # if action wants a closed status window it should be closed now if conf.actions[action].close_popwin and not conf.fullscreen and not conf.windowed: self.action_menu_clicked.emit() # clean up del list_rows @Slot() def action_response_decorator(method): """ decorate repeatedly called stuff """ def decoration_function(self): # run decorated method method(self) # default actions need closed statuswindow to display own dialogs if not conf.fullscreen and not conf.windowed and \ not method.__name__ == 'action_recheck' and \ not method.__name__ == 'action_archive_event': self.action_menu_clicked.emit() return decoration_function @action_response_decorator def action_edit_actions(self): # buttons in toparee if not conf.fullscreen and not conf.windowed: self.action_menu_clicked.emit() # open actions tab (#3) of settings dialog self.action_edit_triggered.emit(3) @action_response_decorator def action_monitor(self): # only on 1 row indexes = self.selectedIndexes() if len(indexes) > 0: index = indexes[0] miserable_host = self.model().data(self.model().createIndex(index.row(), 0), Qt.ItemDataRole.DisplayRole) miserable_service = self.model().data(self.model().createIndex(index.row(), 2), Qt.ItemDataRole.DisplayRole) # open host/service monitor in browser self.server.open_monitor(miserable_host, miserable_service) @action_response_decorator def action_recheck(self): # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: miserable_host = self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole) miserable_service = self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole) # send signal to worker recheck slot self.recheck.emit({'host': miserable_host, 'service': miserable_service}) @action_response_decorator def action_acknowledge(self): list_host = [] list_service = [] # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_host.append(self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole)) list_service.append(self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole)) # running worker method is left to OK button of dialog self.action_acknowledge_triggered_initialize.emit(self.server, list_host, list_service) self.action_acknowledge_triggered_show.emit() @action_response_decorator def action_downtime(self): list_host = [] list_service = [] # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_host.append(self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole)) list_service.append(self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole)) # running worker method is left to OK button of dialog self.action_downtime_triggered_initialize.emit(self.server, list_host, list_service) self.action_downtime_triggered_show.emit() @action_response_decorator def action_archive_event(self): """ archive events in Checkmk Multisite Event Console """ # fill action and info dict for thread-safe action request action = { 'string': '$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=ec_events_of_monhost&host=$HOST$&_mkeventd_comment=archived&_mkeventd_acknowledge=on&_mkeventd_state=2&_delete_event=Archive Event&event_first_from=&event_first_until=&event_last_from=&event_last_until=', 'type': 'url', 'recheck': True} list_host = [] list_service = [] list_status = [] # How many rows we have list_rows = [] indexes = self.selectedIndexes() for index in indexes: if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_host.append(self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole)) list_service.append(self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole)) list_status.append(self.model().data(self.model().createIndex(lrow, 8), Qt.ItemDataRole.DisplayRole)) for line_number in range(len(list_host)): host = list_host[line_number] service = list_service[line_number] status = list_status[line_number] info = {'server': self.server.get_name(), 'host': host, 'service': service, 'status-info': status, 'address': self.server.get_host(host).result, 'monitor': self.server.monitor_url, 'monitor-cgi': self.server.monitor_cgi_url, 'username': self.server.username, 'password': self.server.password, 'comment-ack': conf.defaults_acknowledge_comment, 'comment-down': conf.defaults_downtime_comment, 'comment-submit': conf.defaults_submit_check_result_comment } # tell worker to do the action self.request_action.emit(action, info) # clean up del index, indexes, list_rows, list_host, list_service, list_status @action_response_decorator def action_submit(self): # only on 1 row indexes = self.selectedIndexes() index = indexes[0] miserable_host = self.model().data(self.model().createIndex(index.row(), 0), Qt.ItemDataRole.DisplayRole) miserable_service = self.model().data(self.model().createIndex(index.row(), 2), Qt.ItemDataRole.DisplayRole) # running worker method is left to OK button of dialog self.action_submit_triggered_initialize.emit(self.server, miserable_host, miserable_service) self.action_submit_triggered_show.emit() @Slot() def action_clipboard_action_host(self): """ copy host name to clipboard """ list_host = [] text = '' # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_host.append(self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole)) for line_number in range(len(list_host)): text = text + list_host[line_number] if line_number + 1 < len(list_host): text += '\n' clipboard.setText(text) @Slot() def action_clipboard_action_service(self): """ copy service name to clipboard """ list_service = [] text = '' # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_service.append(self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole)) for line_number in range(len(list_service)): text = text + list_service[line_number] if line_number + 1 < len(list_service): text += '\n' clipboard.setText(text) @Slot() def action_clipboard_action_statusinformation(self): """ copy status information to clipboard """ list_status = [] text = '' # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_status.append(self.model().data(self.model().createIndex(lrow, 8), Qt.ItemDataRole.DisplayRole)) for line_number in range(len(list_status)): text = text + list_status[line_number] if line_number + 1 < len(list_status): text += '\n' clipboard.setText(text) @Slot() def action_clipboard_action_all(self): """ copy all information to clipboard """ list_host = [] list_service = [] text = '' # How many rows we have list_rows = [] for index in self.selectedIndexes(): if index.row() not in list_rows: list_rows.append(index.row()) for lrow in list_rows: list_host.append(self.model().data(self.model().createIndex(lrow, 0), Qt.ItemDataRole.DisplayRole)) list_service.append(self.model().data(self.model().createIndex(lrow, 2), Qt.ItemDataRole.DisplayRole)) for line_number in range(len(list_host)): host = list_host[line_number] service = list_service[line_number] # item to access all properties of host/service object # defaults to host item = self.server.hosts[host] text += f'Host: {host}\n' # if it is a service switch to service object if service != '': if item.services.get(service): item = item.services[service] text += f'Service: {service}\n' # finally solve https://github.com/HenriWahl/Nagstamon/issues/1024 elif self.server.TYPE == 'Zabbix': for service_item in item.services.values(): if service_item.name == service: item = service_item text += f'Service: {service}\n' break # the other properties belong to both hosts and services text += 'Status: {0}\n'.format(item.status) text += 'Last check: {0}\n'.format(item.last_check) text += 'Duration: {0}\n'.format(item.duration) text += 'Attempt: {0}\n'.format(item.attempt) text += 'Status information: {0}\n'.format(item.status_information) if line_number + 1 < len(list_host): text += '\n' # copy text to clipboard clipboard.setText(text) @Slot() def refresh(self): """ refresh status display """ # avoid race condition when waiting for password dialog if self.parent_statuswindow is not None: # do nothing if window is moving to avoid lagging movement if not statuswindow_properties.moving: # tell statusbar it should update self.refreshed.emit() # check if status changed and notification is necessary # send signal because there are unseen events # status has changed if there are unseen events in the list OR (current status is up AND has been changed since last time) if (self.server.get_events_history_count() > 0) or \ ((self.server.worst_status_current == 'UP') and ( self.server.worst_status_current != self.server.worst_status_last)): self.status_changed.emit(self.server.name, self.server.worst_status_diff, self.server.worst_status_current) @Slot(int, Qt.SortOrder) def sort_columns(self, sort_column, sort_order): """ forward sorting task to worker """ # better int() the Qt.* values because they partly seem to be # intransmissible # get_sort_order_value() cures the differences between Qt5 and Qt6 self.sort_data_array_for_columns.emit(int(sort_column), int(get_sort_order_value(sort_order)), True) @Slot() def finish_worker_thread(self): """ attempt to shut down thread cleanly """ # tell thread to quit self.worker_thread.quit() # wait until thread is really stopped self.worker_thread.wait() class Worker(QObject): """ attempt to run a server status update thread - only needed by table so it is defined here inside table """ # send signal if monitor server has new status data new_status = Signal() # send signal if next cell can be filled next_cell = Signal(int, int, str, str, str, list, str) # send signal if all cells are filled and table can be adjusted table_ready = Signal() # send signal if ready to stop finish = Signal() # send start and end of downtime set_start_end = Signal(str, str) # try to stop thread by evaluating this flag running = True # signal to be sent to slot "change" of ServerStatusLabel change_label_status = Signal(str, str) # signal to be sent to slot "restore" of ServerStatusLabel restore_label_status = Signal() # send notification a stop message if problems vanished without being noticed problems_vanished = Signal() # flag to keep recheck_all from being started more than once rechecking_all = False # signals to control error message in statusbar show_error = Signal(str) hide_error = Signal() # sent to treeview with new data_array worker_data_array_filled = Signal(list, dict) # sendt to treeview if data has been sorted by click on column header data_array_sorted = Signal(list, dict) # keep track of last sorting column and order to pre-sort by it # start with sorting by host last_sort_column_cached = 0 last_sort_column_real = 0 last_sort_order = 0 # keep track of action menu being shown or not to avoid refresh while selecting multiple items # action_menu_shown = False def __init__(self, parent=None, server=None, sort_column=0, sort_order=0, status_window=None): QObject.__init__(self) self.server = server # needed for update interval self.timer = QTimer(self) self.server.init_config() self.sort_column = sort_column self.sort_order = sort_order self.parent_statuswindow = status_window @Slot() def get_status(self): """ check every second if thread still has to run if interval time is reached get status """ # if counter is at least update interval get status if self.server.thread_counter >= conf.update_interval_seconds: # only if no multiple selection is done at the moment and no context action menu is open if not app.keyboardModifiers() and app.activePopupWidget() is None: # reflect status retrieval attempt on server vbox label self.change_label_status.emit('Refreshing...', '') status = self.server.get_status() # all is OK if no error info came back if self.server.status_description == '' and \ self.server.status_code < 400 and \ not self.server.refresh_authentication and \ not self.server.tls_error: # show last update time self.change_label_status.emit(f"Last updated at {datetime.now().strftime('%X')}", '') # reset server error flag, needed for error label in statusbar self.server.has_error = False # tell statusbar there is no error self.hide_error.emit() else: # try to display some more user-friendly error description if self.server.status_code == 404: self.change_label_status.emit('Monitor URL not valid', 'critical') elif status.error.startswith('requests.exceptions.ConnectTimeout'): self.change_label_status.emit('Connection timeout', 'error') elif status.error.startswith('requests.exceptions.ConnectionError'): self.change_label_status.emit('Connection error', 'error') elif status.error.startswith('requests.exceptions.ReadTimeout'): self.change_label_status.emit('Connection timeout', 'error') elif status.error.startswith('requests.exceptions.ProxyError'): self.change_label_status.emit('Proxy error', 'error') elif status.error.startswith('requests.exceptions.MaxRetryError'): self.change_label_status.emit('Max retry error', 'error') elif self.server.tls_error: self.change_label_status.emit('SSL/TLS problem', 'critical') elif self.server.status_code in self.server.STATUS_CODES_NO_AUTH or \ self.server.refresh_authentication: self.change_label_status.emit('Authentication problem', 'critical') elif self.server.status_code == 503: self.change_label_status.emit('Service unavailable', 'error') else: # kick out line breaks to avoid broken status window if self.server.status_description == '': self.server.status_description = 'Unknown error' self.change_label_status.emit(self.server.status_description.replace('\n', ''), 'error') # set server error flag, needed for error label in statusbar self.server.has_error = True # tell statusbar there is some error to display self.show_error.emit('ERROR') # reset counter for this thread self.server.thread_counter = 0 # if failures have gone and nobody took notice switch notification off again if len([k for k, v in self.server.events_history.items() if v is True]) == 0 and \ self.parent_statuswindow and \ statuswindow_properties.is_notifying is True and \ statuswindow_properties.notifying_server == self.server.name: # tell notification that unnoticed problems are gone self.problems_vanished.emit() # stuff data into array and sort it self.fill_data_array(self.sort_column, self.sort_order) # tell news about new status available self.new_status.emit() # increase thread counter self.server.thread_counter += 1 # if running flag is still set call myself after 1 second if self.running: self.timer.singleShot(1000, self.get_status) else: # tell treeview to finish worker_thread self.finish.emit() @Slot(int, int) def fill_data_array(self, sort_column, sort_order): """ let worker do the dirty job of filling the array """ # data_array to be evaluated in data() of model # first 9 items per row come from current status information self.data_array = list() # dictionary containing extra info about data_array self.info = {'hosts_flags_column_needed': False, 'services_flags_column_needed': False, } # only refresh table if there is no popup opened if not app.activePopupWidget(): # avoid race condition when waiting for password dialog if len(qbrushes[0]) > 0: # cruising the whole nagitems structure for category in ('hosts', 'services'): for state in self.server.nagitems_filtered[category].values(): for item in state: self.data_array.append(list(item.get_columns(HEADERS))) # hash for freshness comparison hash = item.get_hash() if item.is_host(): if hash in self.server.events_history and \ self.server.events_history[hash] is True: # second item in last data_array line is host flags self.data_array[-1][1] += 'N' else: if hash in self.server.events_history and \ self.server.events_history[hash] is True: # fourth item in last data_array line is service flags self.data_array[-1][3] += 'N' # add text color as QBrush from status self.data_array[-1].append( qbrushes[len(self.data_array) % 2][COLORS[item.status] + 'text']) # add background color as QBrush from status self.data_array[-1].append( qbrushes[len(self.data_array) % 2][COLORS[item.status] + 'background']) # add text color name for sorting data self.data_array[-1].append(COLORS[item.status] + 'text') # add background color name for sorting data self.data_array[-1].append(COLORS[item.status] + 'background') # check if hosts and services flags should be shown if self.data_array[-1][1] != '': self.info['hosts_flags_column_needed'] = True if self.data_array[-1][3] != '': self.info['services_flags_column_needed'] = True self.data_array[-1].append('X') # sort data before it gets transmitted to treeview model self.sort_data_array(self.sort_column, self.sort_order, False) # give sorted data to model self.worker_data_array_filled.emit(self.data_array, self.info) @Slot(int, int, bool) def sort_data_array(self, sort_column, sort_order, header_clicked=False): """ sort list of lists in data_array depending on sort criteria used from fill_data_array() and when clicked on table headers """ # store current sort_column and sort_data for next sort actions self.sort_column = sort_column self.sort_order = sort_order # to keep GTK Treeview sort behaviour first by hosts first_sort = sorted(self.data_array, key=lambda row: SORT_COLUMNS_FUNCTIONS[self.last_sort_column_real]( row[SORT_COLUMNS_INDEX[self.last_sort_column_real]]), reverse=self.last_sort_order) # use SORT_COLUMNS from Helpers to sort column accordingly self.data_array = sorted(first_sort, key=lambda row: SORT_COLUMNS_FUNCTIONS[self.sort_column]( row[SORT_COLUMNS_INDEX[self.sort_column]]), reverse=self.sort_order) # fix alternating colors for count, row in enumerate(self.data_array): # change text color of sorted rows row[10] = qbrushes[count % 2][row[12]] # change background color of sorted rows row[11] = qbrushes[count % 2][row[13]] # if header was clicked tell model to use new data_array if header_clicked: self.data_array_sorted.emit(self.data_array, self.info) # store last sorting column for next sorting only if header was clicked if header_clicked: # last sorting column needs to be cached to avoid losing it # effective last column is self.last_sort_column_real if self.last_sort_column_cached != self.sort_column: self.last_sort_column_real = self.last_sort_column_cached self.last_sort_order = self.sort_order self.last_sort_column_cached = self.sort_column @Slot(dict) def acknowledge(self, info_dict): """ slot waiting for 'acknowledge' signal from ok button from acknowledge dialog all information about target server, host, service and flags is contained in dictionary 'info_dict' """ # because all monitors are connected to this slot we must check which one sent the signal, # otherwise there are several calls and not only one as wanted if self.server == info_dict['server']: # pass dictionary to server's acknowledge machinery self.server.set_acknowledge(info_dict) @Slot(dict) def downtime(self, info_dict): """ slot waiting for 'downtime' signal from ok button from downtime dialog all information about target server, host, service and flags is contained in dictionary 'info_dict' """ # because all monitors are connected to this slot we must check which one sent the signal, # otherwise there are several calls and not only one as wanted if self.server == info_dict['server']: # pass dictionary to server's downtime machinery self.server.set_downtime(info_dict) @Slot(dict) def submit(self, info_dict): """ slot waiting for 'submit' signal from ok button from submit dialog all information about target server, host, service and flags is contained in dictionary 'info_dict' """ # because all monitors are connected to this slot we must check which one sent the signal, # otherwise there are several calls and not only one as wanted if self.server == info_dict['server']: # pass dictionary to server's downtime machinery self.server.set_submit_check_result(info_dict) @Slot(dict) def recheck(self, info_dict): """ Slot to start server recheck method, getting signal from TableWidget context menu """ if conf.debug_mode: # host if info_dict['service'] == '': self.server.debug(server=self.server.name, debug='Rechecking host {0}'.format(info_dict['host'])) else: self.server.debug(server=self.server.name, debug='Rechecking service {0} on host {1}'.format(info_dict['service'], info_dict['host'])) # call server recheck method self.server.set_recheck(info_dict) @Slot() def recheck_all(self): """ call server.set_recheck for every single host/service """ # only if no already rechecking if self.rechecking_all is False: # block rechecking self.rechecking_all = True # change label of server vbox self.change_label_status.emit('Rechecking all...', '') if conf.debug_mode: self.server.debug(server=self.server.name, debug='Start rechecking all') # special treatment for Checkmk Multisite because there is only one URL call necessary if self.server.type != 'Checkmk Multisite': # make a copy to preserve hosts/service to recheck - just in case something changes meanwhile nagitems_filtered = deepcopy(self.server.nagitems_filtered) for status in nagitems_filtered['hosts'].items(): for host in status[1]: if conf.debug_mode: self.server.debug(server=self.server.name, debug='Rechecking host {0}'.format(host.name)) # call server recheck method self.server.set_recheck({'host': host.name, 'service': ''}) for status in nagitems_filtered['services'].items(): for service in status[1]: if conf.debug_mode: self.server.debug(server=self.server.name, debug='Rechecking service {0} on host {1}'.format( service.get_service_name(), service.host)) # call server recheck method self.server.set_recheck({'host': service.host, 'service': service.name}) del nagitems_filtered, status else: # Checkmk Multisite does it its own way self.server.recheck_all() # release rechecking lock self.rechecking_all = False # restore server status label self.restore_label_status.emit() else: if conf.debug_mode: self.server.debug(server=self.server.name, debug='Already rechecking all') @Slot(str, str) def get_start_end(self, server_name, host): """ Investigates start and end time of a downtime asynchronously """ # because every server listens to this signal the name has to be filtered if server_name == self.server.name: start, end = self.server.get_start_end(host) # send start/end time to slot self.set_start_end.emit(start, end) @Slot(dict, dict) def execute_action(self, action, info): """ runs action, may it be custom or included like the Checkmk Multisite actions """ # first replace placeholder variables in string with actual values # # Possible values for variables: # $HOST$ - host as in monitor # $SERVICE$ - service as in monitor # $MONITOR$ - monitor address - not yet clear what exactly for # $MONITOR-CGI$ - monitor CGI address - not yet clear what exactly for # $ADDRESS$ - address of host, investigated by Server.GetHost() # $STATUS-INFO$ - status information # $USERNAME$ - username on monitor # $PASSWORD$ - username's password on monitor - whatever for # $COMMENT-ACK$ - default acknowledge comment # $COMMENT-DOWN$ - default downtime comment # $COMMENT-SUBMIT$ - default submit check result comment try: # used for POST request if 'cgi_data' in action: cgi_data = action['cgi_data'] else: cgi_data = '' # mapping of variables and values mapping = {'$HOST$': info['host'], '$SERVICE$': info['service'], '$ADDRESS$': info['address'], '$MONITOR$': info['monitor'], '$MONITOR-CGI$': info['monitor-cgi'], '$STATUS-INFO$': info['status-info'], '$USERNAME$': info['username'], '$PASSWORD$': info['password'], '$COMMENT-ACK$': info['comment-ack'], '$COMMENT-DOWN$': info['comment-down'], '$COMMENT-SUBMIT$': info['comment-submit']} # take string form action string = action['string'] # mapping mapping for i in mapping: # mapping with urllib.quote string = string.replace("$" + i + "$", quote(mapping[i])) # normal mapping string = string.replace(i, mapping[i]) # see what action to take if action['type'] == 'browser': # debug if conf.debug_mode is True: self.server.debug(server=self.server.name, host=info['host'], service=info['service'], debug='ACTION: BROWSER ' + string) webbrowser_open(string) elif action['type'] == 'command': # debug if conf.debug_mode is True: self.server.debug(server=self.server.name, host=info['host'], service=info['service'], debug='ACTION: COMMAND ' + string) Popen(string, shell=True) elif action['type'] == 'url': # Checkmk uses transids - if this occurs in URL its very likely that a Checkmk-URL is called if '$TRANSID$' in string: transid = servers[info['server']]._get_transid(info['host'], info['service']) string = string.replace('$TRANSID$', transid).replace(' ', '+') else: # make string ready for URL string = urlify(string) # debug if conf.debug_mode is True: self.server.debug(server=self.server.name, host=info['host'], service=info['service'], debug='ACTION: URL in background ' + string) servers[info['server']].fetch_url(string) # used for example by Op5Monitor.py elif action['type'] == 'url-post': # make string ready for URL string = urlify(string) # debug if conf.debug_mode is True: self.server.debug(server=self.server.name, host=info['host'], service=info['service'], debug='ACTION: URL-POST in background ' + string) servers[info['server']].fetch_url(string, cgi_data=cgi_data, multipart=True) if action['recheck']: self.recheck(info) except Exception: print_exc(file=stdout) @Slot() def unfresh_event_history(self): # set all flagged-as-fresh-events to un-fresh for event in self.server.events_history.keys(): self.server.events_history[event] = False Nagstamon-master/Nagstamon/resources/000077500000000000000000000000001505160700500202365ustar00rootroot00000000000000Nagstamon-master/Nagstamon/resources/CREDITS000066400000000000000000000112701505160700500212570ustar00rootroot00000000000000Some lines of code are written by Henri Wahl @HenriWahl.

Thanks a lot for contributions, patches, hints, packaging, testing and ideas go at least to:

Andreas Ericsson @ageric,
Antoine Jacoutot,
Anton LÃļfgren @catharsis,
Arkadiy @arkadiyvleonov,
Arnaud Gomes @nono-gdv,
@aspel,
Benoit Poulet @BenoitPoulet,
BenoÃŽt Soenen,
Carl Chenet @chaica,
Carl Helmertz @chelmertz,
Christian @idl0r,
Christian Gierschner ,
Christoph Handel @fragfutter,
@Coolgeek789,
@cventastic,
Davide Cecchetto @dcec,
Duncan Ferguson @duncs,
Emile Heitor,
Erinn Looney-Triggs @erinn,
Fabian RÃļhl @froehl,
Fkhan @Skym3,
@foscarini,
Gabriele Tozzi @gtozzi,
Geir Andre Tufte @gtufte,
Georges Toth @sim0nx,
@janhenkins,
Jan-Philipp Litza @jplitza,
Jake Murphy @Murph9,
@jeromelebleu,
John Conroy,
Lars Michelsen @LarsMichelsen,
@limes007,
Luca @ldellefemmine,
M. Cigdem Cebe,
Maik LÃŧdeke @MaikL,
Marcel Freundl @Massl123,
Martin Campbell @martin-css,
Mattias RyrlÊn @mattiasr,
Michał Rzeszut,
@minibbjd,
Michael Richter @ponchofiesta,
Michael Wieland @Programie,
Moritz Schlarb @moschlar,
Nikita Klimov,
@oernii,
Patrick Cernko,
Pawel Połewicz,
Robin @DeadHunter,
Robin Sonefors,
@rwha,
Rym Rabehi @RymRabehi,
@Sakerdotes,
Salvatore LaMendola @vt0r,
Sandro Tosi,
Sidney Harrell @sidharrell,
Simon Oxwell @soxwellfb,
Stefano Stella @mprenditore,
Stephan Schwarz, @stearz,
Sven Nierlein @sni,
Thomas Gelf @Thomas-Gelf,
Tobias Scheerbaum,
Udo Stachowiak @studost, @UntrustedRoot,
Vladimir Lazarenko @favoretti,
@vsalvador,
@weeboo,
Vyron Tsingaras@vtsingaras,
Wouter Schoot,
@Wulthan,
Yannick Charton,
@yasa1987

...and all the bug reporters at GitHub!

Also thanks to the great thirdparty modules used by Nagstamon:

BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/
Requests: http://www.python-requests.org
Keyring: https://github.com/jaraco/keyring
emwh.py: https://github.com/parkouss/pyewmh Nagstamon-master/Nagstamon/resources/LICENSE000066400000000000000000001323301505160700500212450ustar00rootroot00000000000000Nagstamon is licensed under the following GPL2: GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. 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 this service 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. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey 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 General Public License as published by the Free Software Foundation; either version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Nagstamon uses BeautifulSoup under the following license: Copyright (c) 2004-2010, Leonard Richardson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. Nagstamon's experimental Zabbix support is based on zabbix_api.py, which is licensed under LGPL 2.1: GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey 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 library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it!Nagstamon-master/Nagstamon/resources/close_template.svg000066400000000000000000000132131505160700500237570ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/critical.wav000066400000000000000000000440541505160700500225560ustar00rootroot00000000000000RIFF$HWAVEfmt +"VdataHēŽ(õ—ų~â-×yņ})Wúĩí›Ö;ád#÷væ Ūq#Õí]ã/÷æė+´åŖęŪ-ķ™āt(öú×n". cĪR§MĪvL ĪĻ Ą%gø'âæ\Ee"įö‰ãJÜ, š4$]GéƒĪ˜1¸(6cđĪ[ú$#Ŋģō=ŲMíÕjĩ Z'!˙ûKõįô,õ9ę”ÕÕũ‚ūRũ F"đ˙ûđ÷‰ô"æįÕ°ûáČf$9ŅúpÜāWkq$ũ×åŠÔjfæ)<Zîôˀü%˜  `õJĶoęš#Gám(ŲUũf-ÉÚXđx+édã°%SōÜN.áÃθ’ŖĶ^6ŸĘ– AĐÎúd ØbPđĶßcė#!Á(˙uåųĐIĄ])ö˙;ëļĪÚ˙T# ~đ9Öjûė Ũ!uúļünüĐđŨåôŅUũŠ9t ķ¤)#* øÛö!ûËøqōÆŌ…ņũęP ĢįË%ũjõĐ×ŖđĒ Ÿ%ŊŽōØâÄĸW(ĒûíŨ-×į,(ĒėŅėũe '‹æ-ßt!ëėäĻ,~įËí ,ĻæÖëĨ+‹ėWæ-āö˜ŲB*|Ō¸´$Đ}“Č­ Ņ'Āô@įgåÃØ:&4úķä}Ōĩ[> &÷žĪˇ‡ ˛…ę6ĶŅw K$Ėđė‘Õ°īäY2ĸܨ 2üČī4ûũøŨBÕ1&ôé Ãâ-‹eüåú\đÂđŠågÖ¤‘øîûĪįiå1 Ia Đ÷ÃéAĪQķ'°ũļķŌq˙S] ÁdôÔĪâė+äڐ)uÕØ˙Í#ĀÚŊü&ÅßÔėD'ÛîZæŸ%8úÚn& <Ėe(×Ęî&)̈́ũĮ DđÜįK •m$Pmã2Ņzûj!רī(ŌÛü&ģ æė Ķ—ôJ qí"ũ‘üYüÔéßígÛøÖ'á!Ÿ6BũÔûõåķjķ¤Ų)í4Ĩ˙đ 7 ķ‰'ĘũJøĩÕ=÷Í ÎĄ*öÆŨ8äü ~÷˙áÛ×zī ,öūXė8Ԙũa Š%…¸ãķä!‡éĖč&)ÚKú‚0åæj/ŋī3ÚÍ-š˙ÄÕ +_GĪ™&RTŅ,ÔÁÍ.O* úlŲ8ę&š "ôôá%Ô[Æ#Ą×äËeö-a úėŨËô… Ž+ ‚îzāč[ũŒ K õ‹*[ņ_ūdú`öŲëŽĖ×û=Oŧ ; p#o ũÜúĩû=í°ėŪ&đŗūPe$C@ã~Û°Ūd&køkæ­ØŗĀ›.TnãZČoũsâ+ōzë'āIëĘ(tå,ųœ,“ŨZōe,ėä¤é.-Ëč%âę0PõīŲ)w•Ņ‚"UŌĖ4M{Ęā„ûÍ.ŋ qNęNŪjō@'Ãŋč|ÔŖY ä)‡0ôרmúwBđ &ūW×éįŖ{Ö>gî ī2öúŌ§ô_ ‰t ,zYņ˜ú‹ūzđÚîâß âėúœ )_ m÷“ÖÛö÷ęÖxüĄÕæáÄūjá$A˛í­ÔĸŽŅ"øķ‡Õ úĄ Ļ+ Žãęäŋ!Sã*ō\*Ņāô+á/ęą2…ė°ÛY1rúįՐ/ĸžË­$Āy͚&8ĖŽš*>ũ$ÜËä &ŧīâ{ЧÁâ$X;îØËÄ˙Æh"’ ôĖÉČđg;'@>ōŲŪ|ęR‡˛ Ž"(Ŗō[û‹ô~÷§ícÚĨī}ū¯  Ą áûũ9õĐô0öUëÜÁũlR´shëĩ؏= ´–ōUĪhūā{"Š*öĪKúœËŸđõ;āaāH Ū$!ãÄü*wāĶõÚ(^įķå!+%đŲŪ*$$ūÛÆ"D„×…ėĶĐc„˜ÕTŨ"\É õÄr"/đâ%âœ˙€.ŌŒįzĪkäČļ -ōsĪv+„A|öCŌßëJ0áûdú¨ôXéßæ æŋũ< }üˇ ãį S%1 úōø[kîmá Ūęüđ÷Oy ŗ &Á dėåŲ¤ü¸¨"ŪîŲĶxå.ļ)ΨæÉŸŽ0 õBČMÉĒ$Īâá`áų$ŋí{į>)įČīŌ+véŨâ)4îđãÔf1ÂPČ+x‘ĀL!b Į” (ƒĘn&ú>î"âe "ûųgė•ĐžŦ-A%úéĮ|÷8J•OúqĖ›äū !öOãá{Ŋü,K š !RãūĸųÜôĸđ†ōSÖęé€=aķĢo!üvö+úËû×÷BŌqđOũŲn)ė(ÕŊŊ"Ļ =ö:ËÔūTRˇ †ü™Ō}đ2ÖNÚúŦáŲā Â#iįūô.á]đ“$éč¤įC(ų.ä$"u˙ßŲqÍ ũÕxŌ’‚˙ĶĩŠ"ÄŅÚņöV3ë´Ũ—ä4ßĸ. āäķĐ0 Ō6ĒpėZÉ_ũM´‡‘÷ۈī0G ×ũAú4ōKë,éĘØˆ, 6˙55Čō#ŅeõˇķëõlįКô[˙í)A(R4ëÛŽķ /Ņčų‰×%ÖĪFčy éMĖÜ߈öîĪK÷n 1_^ę#Út'‚÷Úß+îãGč*51åRã-4@đk×G3 ūlΛ*Œ ‡ÉŦÃ‡Ėˆã#Ė+ËũĢķ­ÕŦūōņ" ŨėÅ(ũĢ”ũ͏ņ\Ę !ĸøÜß+ßÔ ÄøūŠņęĪ=úí‰5Ø Ö ŠxNđíĩķųėöČxųÉtũø ĸĒŅë 3ûĨūNūbû5â„×ú đ)ô3Лsū aúfËūö\XÁü4Ô¯čƒ5*Û÷ į†ÛbŊ"ÂöBä:3åéˆįE!īRč>#´˙Ōß×á˙؊ÄYÖĶ I)ĖÜ*)[ˏqÔÜōūÍŗžę÷ւęU — m)ŗ zá€Đ´ ąíņ3Ëtü6 $!¯+ō­Ų°íRĻ }q[ü*ėqīĀëFĐoL œúövĨė'J+ņuņÔüâũqí'ĪÔë[ ŗ X$” é%Ū¯û%ĀĮ˛ú.Û~ÛEv€ģˆčŅÎHāîÛšõĪŗųã ŗF„ėáÛÅ&ōíŠâx0äēęÅ5ŲâŖß96Lņ>Øc,>ūJŌ¸- ŗÆŖ([Ōū#ĖBV,ˆ÷Nī%ÜÉū}×!‰=í"Ę*˙z ŋœ!œø×Ėpęˆ Ō#ÎūEá}Ų@¸¨!ŲŸī_͘í" %Æ,x5ũöžöJûë0ÕëčŪũ1ųÜûŽ )–úZüûú üė{Đ*˙â*#ļjđĄŌ‡æĪ7š÷÷ĖRôpĸ šü‰åyā“ãķ&ŧņËĶŠũĄ!e ā}'Âö¯Ú—%Ä÷fŪŊ&†ŌŗeÕĐjŽēβáÃX >-Ųԏũ &UØdäKo īį¸Örōø Á + üØ:Ųs đ˜#—į&Õ8øy #ëHō‚âí Ų_‡ÁžķÜé$õdãmÖ+Ō¯õ”\ ].ü÷íüųuú˙Ōõų΄îAö’> 7-. õíŠĶ,ũĶŪ!BüĮĶ;âØˆöoßVĶË~7ōZŌz˙_ks¨äâ(–ã­ėô*{āâķJ0ŗßôč2‘î¨āÉ6ō$Íe.÷ >Đsž ũĖÃm)ÉĶt ‚%fîVīFØf@#ryčŅę) 8œņÄŌģđą %˙süãųŨĖ>Â%L–íˆÖ íEú›°ĄOÜŽ˙úû û_û č…ÖJÛĘ‚5‘îe"Ųbö0øpķ?ķ´Ô˙ĮēæņøÕ"ũLMöT÷Äןī~ œô Fđ¸éâāŦ.)Hō€ÍeāŲå×$iûšÜØ!˛÷ÜÜŽ)ÅŅa ØŌĢž! Đė ƒßÅŗę/øÖÖúI&NÛXㆉ.ŨîŲTė8 (" üäC؊ Ŋ$ÚëÂŲHøWÂ#5üōĩØ9ī~ HĘtPû˜ö“îôė6ę&âŽ˙=|jx'íÅė(ô‚û‰˙ øÉԜđBųéŸ F .aˆōË܁ķ2W b|ķßæ K’(ûá՗ X ÷#÷%ë÷ŌČũ ß#° uęķāG*‹åėâT(˛ågôC0¸â¨ãû,¸õ÷įI*īîIÔŌ0' ÃŲvZÆ~!Q(ŨĶöĢnčcķMÕθ•#ŸūķŅâô‘Ë$b]÷’Ī ķØō`ô3ÛįŽĸ‚'=üqë—Ø’÷Ēķ÷. U×!Å _ō„+ų›īzëžÖWį˜˙[¯4āíÛ9û$õxņ|čĶÄĐƒ#UķŌôfØŦų€u.×ūËōæÚŌô[ eŲĒöŗā€â´ Ģ‘!,úÛæÃŌŪēÎķÍö<*˜ŪÖõų%äÛįņū1;æ’â6+ŒëŲf/#cÕ§ Æö%˛ĐŨ õ§Ī5ķ/@? ’ęŦßĐéÁ"0čōßë&ÕļÔ'@ũëAĶ?ũžŗ!˛“ķ‡ÖžōŗŸ Ē!Ŗœô ûđåæ]é–ū×Ûü’ )4ęō\ũržųS҆ņLīŗ °3FzîĪŪüųk–$ØąëMۍä ž#‹m÷lâ{Õ  á#¤IéqÕ9ū Æ&­ `į\Ūj$Víŗäũ&Øãö(*Āä,ėÔ,íOå:0fė‡Õ 5^QÔrÄÆM"l!ĐĖf%Æá:ņëŲŖøYj)Ûų.īoÖüVr"\ +đ–Ī7øÔ!§ųE×:ë §Ļ\ūĒëNŅwøŋ˙nüRŅ ŒF]í´ūf„öpđmØÔÚúÅ=ˆ ™F~%ŊîũĄæætŲ¨œ,+÷ęēÛßû ?0 Ūņ×-ôĻŸ6#õëâęß° į!Qû€ë9ÉW F)îōôę52Kã ån-æãqåĖ4N÷­Ú.čīˇŲ=2ˇjŅ?“OÃĀ%(ÆŌƒ†Öäũ|L *ßZßõįē8!Øō‡ápÜ69M&•ügč@Úūg â&<hķßÍcųg üȝųˇôÁõĘé#Ū¯įŽˇü ü„ÆĪũ'Në$õãû˜ õĒŌÍđđą ˛7¸oŪ÷ÜŦĢŊ$"â,Ö[ėŗĪM°õĘŲõŲÉ7= {ũTãÄĐW;Ÿ­‡ęTæ˙účíõâ0ã#ë"Č×å÷Œ.üŪAđ\2†č˙Ûĩ1šūmØ^#âÆN'­ųː#â(¯Ü!í~ßüšą,āúr樯ûË›"” ōĒÍúō˜ŧžāø¤Ûīė|@îhūžîĖ9ôúú…„9 ‰!.ęęxöctøņ Îá|ų5ņ )[y%l°˙oįķÖēŪŖ ęœ)õôķâGÚæāŽ0 āëŖÖˇôu¸(ŦËņ Û/č ”!Ɛķé'ĶŽ ˜)bņŖéč0#ãdëŅ*Ûېėö8Ëí4āÜ#¤æ°× 0ßÕf`Äã%­"ŌČ-Įč5;øč ŅŅö{ ē­$UđĮß*Ûęā$oü¸îų͈$â$üĶņ4ŲņČ g3ė'÷ĩëDķ,įBŪf˙Ëú€§ Į ,-€í°õ/ųrúFīâŌnūˆö˛üƒH<+3 Aī3ÎëU4YÔė‚ÉņîŽŪ5!Ö÷…×sŲ6U!JûAŪ÷ŅNí*ŸÖáī€(“ÛUú)â҉ū…+č۟'Îבī5õæŠŨ67úøk׃*"ĮĀâFÖUü&ė8åŽØ× \"˛-ëmĪZŗ!ŌåpÆuūķĪėšŨíī w W#ë/íŊŪėčŦĶƒ¨ Čņˇņ'õ_ū~úĘõŽ×éØqû›Į  ģ#cûCúzųŲCÍ]š)˙§đŠĶČú5¨+{3ō˛ÕNđg)"Ō`ōÛŨké Ũ &Īé“č‚Ųq˙+ZtŨc+ōyáē"Žėqë^55ōŪĐ#EčEŌ /čÔüžŠÃH#ū"ˆÆšs ŲČ(û,$–›ëȎúĢ!OįCá:ÛU .#ŋ!‹ôhíЛ:)&î\č“ÜĢô œ œ7 ÷÷SîąųTŨļŨAÃ˙7 >.'ncôķ¯ũŦôč@Ųüø+ü. &œų÷ŠĖ.šw°éqČ!ųģũķlî8ÛQܡ é"uôŸåGԈŠö,ŗũ=Ķ>÷Õ'ÁÕįũÄ(ôÉü,YׄS(jÔđųn9ņæ'ßú3öīÔž*ã#ĮRįŋÍR"'ēäáë+ߍ [$øqâx×{ 4C$y ģãÂĘ.ũū Æĩí@Ņ(ņÅ "‰ęī`ęĖÔôøĢÁ‹ ( ņ%ą ō úŦyôŗí§ĪČė ũœüø wŨiÜShü|ū!đ\ŌøßĘ Æk$āø_čÜh—Ē-ŸûÄëÎÖąõ2,5ĨėãŨįÃ{+AMíÂæĩÛU“-¤é:ô˜ ÕÕCų)ģÜûĮ,KŨ^éP,]áÛåB+˙īįÂ"&ųԂ!Öß UÉĄ7QnkæcĐ;ųJW&ųëqÛ|ŪĘ š&>đ)îČÍl#: zõEķíÍNō E' œ“­úqöųāܰĶÂ%ü 8Ķ"×ĪũęáûsõātŨ7 šíáûF“]2ŊîđčĶWYtW &î1Éęô/īâŧđÅßÔ ˇ%$ŸđBë’ĮHũm#„'Îû;Ũų÷s :Ũtü#ØŌĒK-öŌUúō"„×u÷”<é.ßō7•đĶŌ†/ ZÂ!ĸČŌ$*.fåfęÚ5 O$]øsęŸÛŽž!°ëÍ7÷ō -ņ¸ĶŦč+ÖvúČâŌ7øÉķlŠü q‚,˙×ũgũ‚ņHņÅІŪ~ŗú ˜´U#"ąö–üyėŠŪՒü*×-[÷CîŨÚ)˙mĒ/¸æ:ÛÉęÖ—(–čė§āüې´)IëŪåÅێË+éöߨéäđī 1įę^ë&Jėgä!(÷kÜq# {ÚWë6ĖHt!ųĐü Ø…Ō5ø_˙ Åį\Ō–úš`­ō“ÚāíŅ&šė.đ ŌXJ'‰VđiņqÕz÷n,Gí¤øņķôŌĶå˛ē Û˙ĶՁī"‚ čöHīÁü6õRŪ:å˙˜÷l˙7 ąõ)Ë˙xđNĪtg÷4ėčFŅŠī™Ã( ŠņÜAܙ”&įjīÔå ČH“ ,*ŪĶļķq×Ō'č$hŅK|ÄĘå0 Íw˙†9Ø'æâ93évâŧ8#ø Æ*z˙5×î/%č4â9áš =p&ŋõŨ•ßû ūWũˇíŸÎŽ­ĶĮīËÔôFî*ō,ĘŽÜ #H ¨īB$& ˙ümņíôå&Ë„û­Ũö!Ē:ø8r˙JäAÍÅė?Ų+žį ßøé Ę#č'ëcā•āŦĸ´4*÷påsՁķM?8ƒ ƒãÔŌ…ôüô'ųÚ÷b¸ĐĖ#ĀÎ {!,ׇ kÔjúY-:č/í%ôégá„'Î˙+ܚDĘÜfūO”éÕŲûÄŲŪ€öÎŲņĶN&ŸÂđQô1Î)Q&˛ļņXī—×.ķú c.ī@ëŠķ.Ú#æ= )äķē C)”ĄņŋíŽüūķDã3áVūeķB˙eĸû*Čūë^֑ˇō!ōfæŽĖÉđ@&E&o —ō›Ķ×Ûž,(öôâä­ĘW ! ' WÍõīŋ¤Ö–x)&Îä ´ ĮĄ3ĀË q:HÕ9æ¯8rãßĒ;ƒú–Ėú1yúhĘî,Ætę]čÜá,t§&įō-ŪLāŪ v‚âũ•įųĘŋH†& ÕđŧĮ9ö{˜% gî<Ũ|Ųg Ų$DÍ>,{÷#ûÅņ¯÷…íTĮxöŲPũQûP$(ĸņ— ^˙PėŽ×ÛE×´%îËāGä3 ž)Œīé"ÔqüR!h+jGî‚ĶėėŽä.ØVîĶŪKā* S!‘į—ū()šÛŒ>#ΰ[/Đãō. Ôã“íc(Æõã‹ûûīÔķq[Ø0XŦÉĸD#ąžUęMÎ.ų+E¤•ņaÚ-á$ öÄäâЄ #Į úhę-ŌBũŗ"ÃũIûLæĻđvüųâ,éĘwīpO!@0** nõ!ņN īTâ§ŨŅũŲöOŸ–T*÷ĮįIŲéžÂ$°ûŌæÍSõÁ'$`ąđÔŧڀQ%Ä™ųÛāÚČk›%l*uūéÍ<îĄ(ÜoĒ%Í“?YĖh <5ʁË68Ԓīš7šäMãũ9 ō Ė#/_ü•Ņß)3Ãįččųâ8+ņôä"Ú „EįžÆ&ĐĐĒôŋÂü  # ņö߈؇>T’ŽĢ&˜øšø0ųŗû.æĢĘĐîAW­ß!=|ōd ĨũcîžÚGÜŅŠ#Áņ æ:ߍ ģ'ĄøČęķŌč˙Ÿ!Å"aûōĸÔŪæŠĩ$Čå÷Íß,ÜŌ 9,áįˆōY&ŅÚ_,÷Ōø‰0'åĩđ"2čņäc(¸˙×ņ!ú)ŌĒ›ÎsÄŦ ¨šđ $ãķĐš=‘ đųĶkëží öā×ķ )LĄųčüŌ×ūŌÛį)“˙ė čpķÛ=ëC ąÕ÷øFų%'€0ņ€ų4ūę”ßdæBûkúx ( Đ -bķ˛éŲc ėÎÅå{Áų<)›$•$ųåË­Ūy)m9ũ4ÜŊÉį FÜ+IÖĪ˙ ąŅÛ˛!öÖõļļŊ^|,āĒ Õ8†Įķv:cŲÁęŠ@įôÕģ1„ö´Ø\0œ¯ÛįÅņŪĄ Cė8â˙ßc‚;áųāâVĮ $.) 4öļÁ"˙Ļ ;,c ĮåžĪęÚņô Æ F Ä%Čxü_ųÅöCö4ÖāŅŠūͨ1 uŪ.í›ųūÃû¯åŪËīøjā$ļŧįĩÖåôã Ī+LėŠâXØĩ Š'Â*’ôč?ĘĘú.4ūŪįãŅë÷B˙!HÚĘ ‚×Ę#Đ4ÎöQ!!Ø›ãΎ˙á*éķę"„āâæ‹*åû‰Õ.Į÷āĘCÚ›ŌĪänÎCīûûøôxŌmņAi:HõvÜ.؍ ΧŸøāįŧŲJn >b gÉŒķôį ķęŪã⏠Ąô-V –*”Aí"÷Sũ9íTáŧ⠝õhÔ×-|ųgæĸŨ °ŽÆķīîJÍßúe%Ã$5ķáÍ0ä˙K%’ üÜČĪø ßŅ'‘Ōh]HÎN īŅ …ŧš/`Ãi C6›ÆÚö9Õī?íáŖß.,Šņ‰ŪL/3äķáîŅp #ļī¨ä*Ü÷ņō÷{â’ÉG ŊĪäõĮŦø9, ŪōڏĖÎúæÄ ™ø ÚūėúōøÆúSãuÅ\õ…ūŠņįím%æ 6´÷węĪHãGî 1Fđ­ÛzâĘ>("ŧņôâRŅō˜#ž)Ũũ~ęĖņįs5õĐærĖžįŌ62'ãüúà O$ ׍ +7Ũ=˙lÔÔAōz-ō˛č$ģëÖא-Í`׹&§˙ÜĪŌJAuxå Ņ„2™\īëĪ3đøņ)õ'Ũ×Ũ.hî^ķ_ãÕ⥎ƒƒđüjũ„öšņ­ö3Øxã  ī6 ö'Ā đäđŸũųīDã§áÎôųü†s)Cķáû×p S=$Öõ3é˙Å^˙¸(ā)¤˙sķôČÜä_s$ąö€ŲĄÚâzÄ"zū]Ōë FÕÍŦŗ\Đo tŊ­ņ+nğ`5bÄņų9}Øõí :‹Ūũæč*(ėŨä1- åbâÖí9 =jī„âOڊĒĨÅüŋãČZ }EŠņÉcųJ*ëyõÎßlĘšúa\\ö/ z* AûŖųĸûlúß‘Ę–ú€û]ēü J* Ųū“Nõ+įąĪŗí• "lAë5ÚÛä¸ĸ,uLė|áŗŅ$h&(¯ėÅɸõ¨Ę6Štį`Ī0åëĪ.ÅŪō#áĈĘŗÚßt+ÛÕÃûE#ÁÔãķ¨/ŗėĸđ–$•ë;Ų]/ú)ÖÅ)ÍũÄÎäDŦYæąŌU̐šKķ(Ö7íã;ō¤Õ‡á!FęwÛËŨÔ?ˆŽ Øũöų÷āëōüęØ äW ã%ôR0''( AōZ÷ēū´ë$ßúåŪū_õŽ Ā#r*Z÷/æŊÚg!~–øDékÄ¡ ]&Č”ö…Č>ä[$$öõ Î;Ü^Q.#fôĐÍ< +Ā×BÆļ ߥžvĸ$ÉÅ60–Á–§;.Ī8öõ3>āîãØ0…ęFāŋ6‘āāęâ¸ë S ŦøüŲöŪŪÃ%ûŨĘí }Ąđí:Жŗ›'šöæyĐ ũ‚j×ŧŗmō lũÛúpĐųYÜ1͌÷Ŋ÷ą.Ú¯ E ‘ NT÷ėÎĒåôFšĘëęâæŨŋÍ' #žņtā”ĪŽ¤#é& ĀëĒːņP5ÕėzÎånŒ*RâĢÖËí l)Ķs(AÕPĖ$aŅÉųf0ę†é$zėPÚÉ.{ũėÔô,Æ˙Į¨ŸUbäįÎ˙ÅāßíåÎôö ūį~íCԈãŪ­#”įĻŌiâx ūŨ°ķwûwôŅęöũ´ÚEė‡HŠõŪ B N†åķÃ÷k˙Ëķœ×Įé‚ü!ų]{ &s$ôĪæ‰Ũ‚ "öŖņÜį3Τ R˜gđ@ČØõlcŲYđĪuä‡ûø%˜ø,ĘL n=Ō€'0ČÅ"÷ēÚW‹Į%!ī'HÂ_/UĶ`0ŅŨŌčŖ-dãëú7*Cæuįøä´ ˜"Ģ@ī˙á—įgŅ2 Cüė×jԋ Q–ĮŪėÅÎĖ 5(<°čŪÚČáúX¨˛Âz!p˙2üsú"ũúÎÚIÎĮųšú. ęId#ąÁ?îė4Ö0įj s-0įæeæč=ž*R+ĸå@ĶTÚ s!b4ãũXäĪ‘ņŲ8ņũäŸŅ#ųWâŪC GŅÅž 9ĶA]!Ģ˝Á&ÅÆ-W/Ų¸÷Ø(Ûܤã/o÷dØw-ôûÉû ø 1øę{Ôš!<ĘújæPÕ"ū6š záßvÔqęëQ(XęlĶâžÕ ‰L)Ĩđũ=ųūâWûäKé—˙㠐˙ÖÁą" ĒûũíšøS÷§ŲŦįæũ´ūQüĢx+ų#¸î[čŠáß' Â"4í‰ãŌ@>Ö#Íé†ĘëûVSØđŧÎĶŪ_Pņ&”ũ-Æm \ŋÎ6'~§Î8 – Ā’?`ÍRI"5ÍĐX(Öŋæ0Áß÷ëŧ+3ā ë6+Ėåiå-ęV l"…ęŖá¸ęęĀĐ$āöĶ˙Ō“ Ōˇ!ũ îÆĖžķ/%w´îŪ ׅüŖ• h Rh/ ö˙!÷âzĐSé˙s æ0 ( 8ŗNōÍęR×e䞆gŨęĻîâf â!÷& ęŪˆÚ´ éÔ+Œū´ėƒĐ„īË/ÚlëNŨĻäcĄ/ėØ9 \ZĐ qŠĶ/*]Ö2ũ'ĐĖōq4œéî¤$čÃāg*8ør՝)GūWЉôTZûŪtĐ^ߘÉę]ĐGũæ!ōíÕÕčĒģ&ȝęąÚ‹Ųx îGĖ<úMõÖ÷ęđBāÜô…˙V Žü÷ @&ލņ6ė}ķIĖmũ [īĸĮD;!ōúŪÕááâä"X)ķŖĶ–ÂT z …*0 ’é ÁøīÅĘ(å ‰ô{Ћāŗ(áđŧÜŅ˙QØA(›˜Û '•ĘׄÔf¨ sĮ*6/ÎÎŨū[3Ø"đC1@āĒé$5Ė_ŨŨŲZ 1!Đ%āNØÎųĄ#ÃÉ5įíĮĘâŦ4$e]ųëÔĐ)oO(ī,đkĖ'æū UĪeÔūËöÕf˙­īÎjâŨû ž,+ ¯§˙Ã(ú–ŨPÎà P NJížÛÜY%-ÎߨŌ4ķn(F#čĮߊÖ$ũČ'C5čōYė+ŲËķęî’×#)Ä˙Ņ"+û3՞)ųĐdÂi‹+đĘD 2 ‡Åëq/7Ī•ø•2 ÚDî & ę Íø:å ڎ ķą˙|īÉŅ&ö8(ķ,īĻרåŋé&Å °ī–Ũr׎ž'žū.ķęÆéáŪ‡*Ž˙ÁŦ1ē žûZķ¤é‹ôĄÛĸëģ¤YüœB×ļoûēßlÚ`5f÷¯Ū,Íé å$+DöAõ ķĻø Ėjø˙ ø…ņRāgúáƒüiõ[ūęūĢÁĄŨ!ū÷Nø€~Ę÷Ä,˙¨īĖ `ųn <ō ˙]ƒa %ųŽīØŦhūÜråõãũ˧ ĸĢø$ņņõ&ūHųšø˙+˙Šũ€ mˇČú)ö úŌū˜üRđ7÷Sü˙ūrūũ Ãũ ėũĶņÜų ŋūÁ˙ŲōŊûļ7íxôāëŽt Ô úņėJũâ` UÂûĀæ u˙Áų—ĩæéžü7ōŋ@Ių ķKNû×øß _ƒúÚũŸų*üņąũ‚ (ũņūáŒôõäüæū‚´úˇņ_+ĖōĒ˙e>ūA­[úËęĐ˙čą lZíĶėīū˙ūũZŌŦōeúûŲ÷n ô đōöÆøŊ˙ß˙Æ ‰˛üÉü$Āôæ NũÕüĸ˙Ôņ÷k&OūË ˙ õlí <öYÔņåö2ëžŨĢõsûá;úŊųōZøõ2v,Žü%üĩņžo 3ģû5đ*ųL N~ū˙ú™ųBũČ—+åŸú(÷@2˙äãrķøˇüYüWWÚû:ųXŋũËÖđ>ķ˜dßúÀČņ˙P&˙{Ėôhí>Á-:úyū0÷wũ„‚q˙6ųdûŠũ‰ú:×ũuąũHö ĒõŠ į Vüz÷~]öŽüė#ū#úûũ{ûæŒéū@û/ÜũÜüĮ;6ņœ‹ Ü\ŗė öU9 æņ—øģvúF – @÷ũøÉ=ú“¨ŧūáūØv÷÷øÛĄüKß Ĩûųø ,ų9ûį ą%úŖ÷Äöūũ– “Ī˙Úíî­ũ RúBjõ3Ļ Yü‘ø¯ôŽ÷mĀ™<øaöm˙´˙áĒõ"õûÛũå’ ū7ūc˙ úŠĪŧûũā˙dø#úŋĩ¨f‹û“ų8˙ŗžÆ8üôhũņ_§hĻųXüŦ4LųüOöãų4[Úũą˙‚ųõK 1ū'¸üyō,úŖ ēJũWGûcųĻū™ŠģūđüsøĄüj“ã"øoų˙ųwkûjø0€ũdđg÷žķiėũ™ĸūķĨ™ ŠųkJôPōÅŨčüņüÛõÆ˙Rôßo•øÅûiũČûrˇųŽøęúē˙Ķú Wf˙ŠûœúPøųÄøäø8˙¨ū  û öNũÍü†>ÍüËķÜÁŊ %ūuėDųį˙I ˜Āô>÷šũüĪ ĩ ûmõr˙!ũ)¤įøü˜ûIüúQ¤˙īđÖüqūgüķ÷…č ‰ũĶüūūø?ũņûę˙Ũ;ötûEũÁõ"Âũû_ Ųôĩõø÷I˙Y7 *2ų'øĻ˙VēĐąųĮôģúíû‡4;˙Aũ_˙Hų+q ˙;û12üVõ˙”Öė¸ úĢú h%Á^ú­ôg˙WgŲËũ'÷xũ> šFųĮüøôÜų ā Öüļ˙NųœôŪ“ h%0üûōxúŦ ãũ•ú<øūžöt^sūįũÁ÷„ũSâØŖ÷ÉúŸ`˙Ÿâæû‚úg%˙ūMËõ'ôū%Y˙ŠĘũ[öÔđ š ”˙Gū öfķ˜üRũûõä˙¨æė*Oųyüâũšũh˙1ûrûf˙#ûö˜˙ų ûfų* ­÷ûûEËũÔ„rų*÷­˙cü7Z•ûŌô”[ã÷ —ü)íōúÜĐ˙– ÍŠķMų:˙čü c šøŒöm˙ü!˙‘@âũĒûũ%ü6˙Žū”îü1˙ßüBųxv˙0ũÕũ‹úE˙ßĸũŲ˙ÕūöåŸUū÷xúũí wTõ˛ô4øÂĩ. čFúŌõ§z˛KøÜõĩúūŸ}ūÍ˙9ū9uúšÕ˙ú?Vūŗø{ũßœ˙dügúE?D˙”ĩûOõ4˙, NŨūüøãüÜ Äąøļüĩö ú7  ũ“­ų˙õ#y +)ūûķ@ũęÔĸ˙Ž_ú%ú`ūįveNēū*ü úá˙Ŧ@Hûų§ûĒĐũ\ĸúĐûE!ü˛^Sô[÷ŽĮūûKüîõQ~ ÃQ˙ųūCöŽõ/žŖūũHũâö>*+šŦ˙Đø¤üjũwū™ĮŧĖųŗû{ą˙üé~˙Aú…üšûc­Ņ€ųzú\ūFhĨúĐų÷˙;ū }„û‰÷ŪÁą įûîČū-or úõJúūũØũX ÷ųVøú˙GüĖUø`ũkü‰üWüŠú˙TĖmü(ūŸüFųƒķÜü‘ūô˙æų ū8ũü˙œ+øSĢ ũZ÷ūüüåmģõzøúúø˙Ļ ŋ&úĪų"|ūKøæ÷€ü‹ü ˆ`˙HũūyûÔŋ€ũĻûܝũķúÅũä žäüļûĻnî˙ė&ünø@˙û˛V˙#úŋüX0ōûũ÷ üœ 9 )ü†úÚö6žE˙Á™úwô…üˇō(Gú`úŅą:ÁE˙ūš˙÷ųUūĀ˙KAųCü ũĐhüüå˙>ũÄÎöÍ÷Öž˙løüTöfG!\{ūšô?÷ž’ÍūMû~÷šéYÅū_ų\ũ×ũY˙#  Ōú9ũ‹ÍũŠâg€ü_û‘ü0ŦŌû ũŠūęũ­Z,üßú˜ū‚ũ×ŗMüŌųÕšĀ\püOōûŽ™ĩ [÷"ú¨ũĸ˜ûĸ÷ßüIũuip~üâúÖũŖūŖx˙$ã_ü›Õũoų!"lũ ˙øū7ûÍūIÜũdPí÷ą‚ /ũ/úÖ6ü×üúŽõRø€ú…ZÕ Í úų˙ŗžĐųČöûūũ2iŪū!ũÅü‰˙ęũGĪūNûfŨūÂú ū&ŧ7ũÂüI¨ /ü–÷EYļūųųŧūë“?üāûøOũ3 ^cūéūHøĀų  ×ôø*õ“ūûÕ×s˙ž÷ėüĸzÜjũ.Tũ(ú†|ũũ_øēų>ũūîķŗûûûûŦũœŌ÷‰ų™j˙?Ÿũø¯įû÷üA˙’öˆ÷š’OaũRũĖøÜž ˇÖũÃ÷|ũ1˙Ļ Nũû}ũI˙‘üĄįą˙RūĐüģû6E ú˛ũ'˙ķũÁŧ—ûyü›˙†ũŋė3ûHúíĀ˙3tĖüŊōY˙ũĩ˙ÔÃõ¤úV˙Ŗ˙ ,Bų1÷’˙OūQKæúüÅũ™ū1Iįąü€ ˙ßú›|[ũ‡˙‹-ú+ūâŨüD+úņô*ü!úŪ—ūûwžÆöúūĖ˙?ø āūŅųCû˙ūūâ ųwøUû|ũíĖ ˙ūúŗū˜˙Hi#ūųN˙ĮüüūŒk˙T˙Æ˙Éüt~Ÿũ`—ū7ų^Ŋuū­ ˙*û˙“ūļüQūÔųŽũÅgūQ)ųúg55¯Žųö˙´=O¸ūŒ÷ŖûūŒ=šDũ4ūsūĒü[OĪū”äFūŅū°(ū-§5˙ƒ˙Ņūũj˙Ėųūgåūnū ū.ûĪng˙žūCøŽúŗÃĨ˙šüųå˙RÉK0ü?ų˙)ūPŠk<ũûėüģūĶ Y˙ˆ˙Īũĸū;`÷üĶî˙lũëûÆüū:ũÉ˙qũküŠ ´üšö9˙ä˙Šv™ ÷yüZũ"˙Ž yųéų8 ũŗÔ˙ú&ũÎü6˙Č<›ĄũvũūuēūČ˙ûŠüČũ[Jũ,ŪDüË9ų„üé÷ü=ũp€˙Jø°ûōūÅîv˙‡ûåú_˙2ĸÚúËų*ũõ˙/ä˙,ũáû,,WéoüâųqÁ üÁūFëũÕz3€üm6Aūž MúŽ˙ĸ}ū>âü\ūåũŌũĶ˙Îúūé˧üÚˇûÚúéÛą>įüsøZņlÍū˙ęøÃũW™ąffûQ˙ŧëũŸ˙đ˙sûŪ~nūeū€Yû”˙9ŨūĘū'vüoūáöü‰ū°Áū2ũ[Ģ'û+ŠØũÉđų%üåûVâũˇũRû: Ūø˙øũ…ûĮūÍ˙Āō1[ũ¯üF˙ íüQ€f˙=ūrũĢũ÷ÍBũYķüØîaũđ˙œ=ü˙ŧüũ¤L˙ŒnĀüPøXŅ˙ņšūcúlūžūŨ˙‹‘ûHüĨKūŒEļū<üäũæüČ˙*´ũŽ—~û&ūRĩũđšīü€ũH˙Œũ éBũGŌúyũ)ŋū9ũĪļ˙9øUū÷Ŋü<ũ˜ũm˙!†ÍûëüæũyūiëûũIūČü­ū Æ]ēü!û"Õ{ū˙ÍũÛĐaKūp˙r˙˙ mū_˙ø˙˙/Ã˙Á˙ŋ˙8Wū.ũ˙Ąũå˙ļü {üæüČ @DËüÆųo˙$ĐĒūsVúũÂjP"žũéũh˙Ũū€˙Ĩgū2ū ƒũÅ›ū°ü™’˙Ņũ÷ũ}ūũs˙~Ëũ™ū?čü0Ro™ūžûļũAÉÎĨ˙'˙ĸüV˙pŊ^“ũXüį˙ZūØt\ÄũŅüÔũÄËŊū˙R™˙ũ˙y˙ÎũđũY…{ũIœ4ü īūŽ˙šũęūÚũ5ūŠš˙;Ëü“úY¨ūæÅÛûĶũĒū–ūĶ‚jü‹üč™ūTÅ*˙ĐûMū4ū"˙đm˙Ų˙˛Ŗ˙7#ūZũåœūh˙ "˙oũJū°˙f”=˙’$,û˙ëČ˙'˙?,ūŽúEūÜ_d€ū3ū˛ü Ë!Đü†üPũ´_$ūÉũøüūī_™üüß˙Äw˙d˙'l˙Î˙–ęDũ ŋũs’ũ`˙Ô˙÷ũŧņūƒöĮüĒũí˙ˆũŽ˙ZXūĪ_ũãûžÅFtĘũ\ûڐ˙v˙Čú×ū÷fŠŗũ˙Ĩ˙{ūÎ˙îŠũÛū$ŋūđūpčũĨūÚ ą˙Nāũöũ ˙sũÉļW˙Ŋü‘˙¸ūkũK&˙†ūN˙ ûū…ÕK‹˙:ũ"ũbWÄ˙÷üíü€-˙ĩAYxū5ūIūpk˙(ŲĢũ ˙´ū$˙ĄĐūlũ˛ú˙öũšECũMę'˙mū|ūúüāüŧä_˙ôûŗüÆo`gūCũ ˙š˙ŒĪî˙#ũËū2˙ƒÂģūĪũīū?ūÚūū-ƒĢķū|˙Ú˙“ūĩj˙Øū…˙Zļ˙Ëūąū:c˙7¤cüÅũɯ¯­ūœúÛūMßōîü5ũ¯ũ˙đrū‡ü„˙áũ)Ą˙nū•ũ’ūė˙ K˙…˙Ž˙öũ ˙,’ŠÁūëũ0‹Fū ú˙[ūëUÅūH˙FL’@ūHū5ū˙‹äDūīū§ūĪūŨrę˙*_˙Bū†˙¤‘˙]˙flū0˙ô˙Å˙ j˙ßū ˙ūüūĖĶ˙~˙ŽšūoūWJ˙jūĻ˙ ž›ūĪūD˙:ūž ũŋüÁ˙Šū˙ŗü˜ūßũĐüzŧ˙Įa˙˛ũož\ūč˙ŠũÁ˙@Wũ­˙wđū=˙Wū6˙ūõ˙Œaū4˙“ūE˙ú˙ÜŖ˙ÁũxŽ<˙ öOūU˙¯6˙’ūž1˙‰˙ČÖâ˙g$ūPũ8#Õū‡ž÷ū ˙ôūg˙įH˙m˙lūę˙D3l˙4˙€ũļūŸ ō”ØũZ˙Á˙lhzūg˙õ˙˙U=˙-ū ×˙l˙ :öû’˙ŅŊ ˙8ũũ9ß˙Āūđũ.˙2˙ŗ˙īũ§P+ūú˙‹IūTÜrūįū ˙ƒØ]ūÖ˙Ī˙Ķũ9ŦQo0ū‰ū<FÍūŗ˙×˙Lūįc’˙IĻūģ˙ĸĐū^ū­˙˙•Y"˙Ûū ˙uū Q˙œ˙ g˙›ũSÜ˙œ˙Žß}ũˇ˙ū˙Õ˙N‰tūî˙L˙Úũ0JĶū`qũ ˙ė…˙Ĩ0ū˜˙ŽË˙Ũ˙›ũ˙ƒ˙pū¸˙e˙Ųū%ÉĖ˙Ąū=ū f°˙Ū˙ųūžūcGG˙ūĐūsLËũ¸˙˜¸ū>˙ņ˙˙||Ģū˙=˙úũ43˙zũš˙á˙P]čũ’˙QŪ˙—R/ūí7d˙“¸ũyū ÎūE’˙aūUŦū7OJūg˙ūŧ†‰ūģ˙šĐũn˙H ˙Ÿ?ū˙+U˙ĸ°˙šūãūčū^˜ŲūÖūŸ˙N˙Å„ũ0˙4î˙c˙JŲũ/ũo˙z˙ p-ŨūŌ˙=˙t˙“­eū ˙ę˙†ūM63ūÜ˙ .˙JyŠū‹˙‚˙HŌū)˙Œ–ō˙‹ūëū,w˙•ū~˙j˙؈˙I˙˙Z˙_#˙V˙B˙06˙¨˙Eôũāū¨™˙˙Î˙ˆūuQĩū0ĩū‚:ŦūÜ˙XîūĀ˙Å˙Sū˙9p˙‰ūßū dŠŊ˙3ū¤˙ūŊ˙Ū˙Ž˙Cũß˙=Ęū/˙—°ūĒ˙ûû˙Ÿ˙P5˙¯ūˇ-˙‚ˆ˙XūŒÛŸūɈ}ū–ųČũėūœĩ˙•âčū,˙ģ˙ŧ˙‘˙ųūŽūųūĨÛūõūž˙˜˙ŧĒB˙­ūšĩ˙ļ*˙îūڔcūé˙ŠAūĒ˙QE˙ˇ˙Y<˙đūÅūŽū@7˙˙F$ūũ°ú˙üū#Ãũôū'˙ņi†ū=˙ī˙‰ūžûūöūė˙ƒ˙—õ˙íū@˙Á˙ĩ˙§˜øũŦ˙´ģ˙Æ˙§Vū˙å˙¸˙Í˙šÅ˙E˙Šĩ˙|˙žA˙<ū´\Õūe=%ūŋŧ;˙ģ˙ ^ū~˙ô…§ũI˙ÚE<&÷ũ(˙ļ˙Ä˙?–˙P˙*˙˙/Ĩ˙˙ÍÜ˙˙ļīūnēĻūų˙utū‹˙đÎūŌ˙‚˙ĸ˙Ž˙˙0KQ˙qwū6ĉ˙Ķ3ŪũĻ˙ĩ˙Ē×rū1˙hŅ˙÷˙) ˙Ü˙ēÉ˙8˙}˙øūwŖ)˙_˙ü˙W˙B¤Ę˙*#˙(˙ŖÍ˙—˙ƒ˙ãūåx×ūqG‡ūÖÔąū§˙qņū:Ũūˆ˙˙ŸKíūV˙Œ˙/˙cÉ6˙ ˙dj˙Š˙ų˙Œ˙’˙Ō ‘˙.Z˙]˙ g˙0Č˙˙ūoq‹˙ )˙É˙/ÔūŸ˙Œ˜˙û˙ۛ˙3˙_Š˙;˙TB ˙•˙%˙¸P˙5å˙˙OK˙ë˙ã˙+ ˙ŋ˙ˇ {ū(“˙˙g˙ā˙‰˙ė˙bÔ˙ˇ˙–›˙/˙Ą˙˙9˙[0žū9"˙ú˙š;˙ë˙Sb˙kRp˙02Ãū›˙“Č˙û˙Ī˙J˙ú˙a¨˙Kô˙ ˙Æ˙%M˙Ä˙2Ž˙ˆ‡˙h˙KĻ˙Pã˙˙õ˙D˙Ņ˙ZA˙ ֌˙l˙;Ī˙˙w1˙S8ķūyŠY˙Lųūt˙‰6˙°˙~Ē˙Č˙.?˙ė˙˙œ˙°˙^˙šM5˙Š˙´˙•˙RtŸ˙Õ˙ß˙­˙Ú˙q<˙[˙DĀ˙ĄŨ˙éū|CA˙Z9˙Ų˙Qc˙TQA˙¸˙§˙S˙YΊ˙€˙ß˙ā˙K"t˙}˙Ņ˙Ų˙ä˙Í˙´˙L)ũ˙å˙˛˙…˙*(Ž˙ļ˙˙˙ą˙$p”˙g˙mÜ˙ˇ˙Pm˙I˙L˙˙­˙zÕ˙™˙LÔ˙i˙vk-˙ū˙Í˙X˙p=q˙ ˙h›˙(ö˙ĸ˙ø˙ ˙ū˙ų˙ß˙AÅ˙5˙AFû˙ß˙2˙ã˙ô˙›˙æ˙ƒ˙Ã˙Ą˙gú˙O˙û˙&U˙¤˙&Gb˙Ü˙#ģ˙=A9˙„˙D*Š˙–˙$%Á˙}˙Á˙Á˙"ų˙&ž˙Ĩ˙ >Ũ˙˙N˙ũ˙WÃ˙Ū˙”˙>{Ē˙Ā˙:Ä˙ž˙ •˙Ž˙k!p˙ß˙¨˙Ā=˙˜˙rX6˙Į˙ū˙Ģ˙ 8Ÿ˙÷˙)¤˙Î˙™˙TF…˙™˙Ō˙Ŋ˙.SØ˙˛˙Ü˙=˛˙–˙î˙ž˙Bä˙Ž˙Vü˙^˙2ž˙æ˙`˙:WŅ˙ø˙Č˙{˙(cŗ˙ä˙Ķ˙­˙0Ŧ˙õ˙Š˙č˙'ā˙ã˙*í˙Î˙Æ˙Ī˙Ā˙-ä˙Ę˙û˙´˙ā˙*Ô˙˙˙U˙Ø˙}č˙Â˙+Õ˙ļ˙fô˙{˙I5˜˙Ū˙v˙:3ī˙1Ų˙z˙&:č˙ å˙Ŋ˙Ŋ˙ä˙ æ˙ž˙KČ˙é˙đ˙Ô˙1 Ú˙ē˙ę˙á˙'Õ˙˙ų˙ é˙¸˙,ŧ˙â˙ã˙â˙&ŗ˙Ņ˙á˙>#Ļ˙ä˙ā˙ŋ˙Ü˙ Ä˙ž˙Ė˙ũ˙ü˙ņ˙ø˙É˙Ņ˙ë˙á˙Ų˙Á˙'ô˙đ˙û˙Ã˙!0Ī˙Ũ˙ü˙ø˙Õ˙ æ˙Ō˙Eˇ˙â˙û˙=ū˙Ô˙î˙Ī˙ķ˙6¸˙Ų˙â˙!Ë˙ô˙Ņ˙ö˙ū˙ũ˙ ų˙´˙į˙ũ˙ú˙×˙û˙!ķ˙đ˙ã˙í˙%Ö˙õ˙ā˙ö˙#Ų˙ø˙ü˙ü˙ä˙Ø˙í˙ö˙Ų˙Û˙'ō˙ã˙ō˙ü˙î˙ø˙ø˙č˙į˙û˙ķ˙ ô˙ū˙é˙ũ˙ū˙˙˙ã˙ø˙ā˙ī˙ ú˙Ų˙ī˙ų˙ë˙ü˙õ˙é˙í˙č˙û˙E Ė˙î˙ō˙Mô˙Ķ˙ķ˙* ë˙0Û˙ķ˙"Û˙ū˙û˙č˙˙˙ķ˙ ø˙ū˙˙˙ ú˙û˙ü˙˙˙÷˙ũ˙û˙˙˙ũ˙ũ˙ü˙ø˙ú˙ų˙ø˙û˙ũ˙ũ˙û˙˙˙ū˙ũ˙˙˙ũ˙Nagstamon-master/Nagstamon/resources/down.wav000066400000000000000000001122541505160700500217310ustar00rootroot00000000000000RIFF¤”WAVEfmt +"Vdata€”˙ū˙˙˙ū˙˙˙ū˙ūūũūũũū˙ũūū˙ũū˙ũũū˙üũ˙ũũ˙üũ˙ûüũüüûûüũûüûüüüü˙ûũūûüūüüü˙ũũūüũũ˙üũũüũūũūūũũūū˙ūū˙ūūũūũ˙˙˙˙˙ũ˙ü˙ūũü˙˙ūũ˙ũ˙ū˙˙˙˙˙ũū˙˙˙˙˙˙ũū˙˙˙ūũūū˙ûü˙ū˙ūū˙ũūūū˙ū˙˙üū˙˙ū˙ūüüû˙ũ˙˙˙˙ūū˙ũū˙˙ũūūūūũūūũū˙ũũūūūūūũũũũûũüûüüûüüûüūûüüû˙úû˙úü˙úü˙ûü˙ûü˙ûüūūũũ˙ũūūũūūũūūū˙ũ˙ūũ˙˙ũ˙ũüūū˙ū˙˙˙˙ūū˙ūū˙˙˙ũ˙ũū˙üū˙˙ũ˙˙˙˙ū˙˙ūūū˙˙˙ũ˙ū˙˙˙˙˙ũûü˙ũüūūūūū˙˙˙ü˙ũ˙˙˙ũũūüũũũū˙ū˙ūū˙˙ū˙ūü˙ũ˙˙˙˙˙ū˙ūūũū˙ũū˙ū˙˙ū˙ūüūüũū˙üūũūũūūũüūüüüũü˙ûũũúû˙ũøōū  ūøõ˙÷ķđôūûũø˙ôë˙ō˙ū÷˙ī ūũđ˙įōõûūúû˙ūû˙÷ū˙üúų˙˙úõ ˙˙ūū üũú ū˙ōúū˙øųūūüũû˙úūū˙úû˙ ˙ũü˙ũüūūûūö˙ūüû˙ųøųúã˙˙ ūôũ˙ õđōū˙˙ūëūâôüü˙û˙ũ˙÷ī˙ō˙˙æ˙éūđõūūūöøøųūûûūøųų˙˙õķ ūũûũ÷˙÷ū øûîųøüđ÷˙ûųøũüūũú÷˙ũũüåėûūōđūûņüū˙ū ûûôūųũöũũūū÷˙ķũ˙ öūû˙ ņī˙˙û˙ųûúûüųūūūūū˙ûūųũķ˙˙ ū˙˙ ˙ü˙ũ üøūõúũ˙˙øũü˙ũûũûúũķū˙øũ˙ûũūūū˙üū˙úūúūũũ˙ũūũüųüüũ˙üūũ˙üūúũũûüūûû˙ųüũüûūûû˙úú˙ûúûüûüūúúúũ˙ũü˙ųü˙üúų˙øū˙ûû˙ûũūûųú˙úúû˙ũū˙üũū˙ũũû˙üū˙˙ūũ÷ûũūü˙ū˙û˙÷øųū˙ũ˙ũūųûū˙˙ûüũüũū˙úũ˙ūũūöøū˙ūū˙ū˙üí˙ ũė˙˙%ö˙õųūö˙ūûô˙˙ íũëųũ˙øöũū˙ö˙ōūūđūė ü ėũėúųô˙˙öåå˙ũ ûũūû˙˙ė ˙ũķî˙ęõ˙öũ˙ūú˙éūæôíũũūįôũû÷ūũũņķūņŅ˙éũ÷ūæ÷˙čúûū ûöü"üũäũ ũäũū ūú ˙î˙å˙īā˙îŲũ%˙˙˙Ūųū˙˙ûåūę!ūį0 ėã ũ öüũũ#ųūŌæ˙âũé˙ ˙ ˙úô˙"˙öūņũÉįü  ūâį˙ũøÜūã2˙ ķũ 8ū$ų˙ėčũū÷˙Ų˙Îé˙˙ūũõũčū üû üüÔĮüü˙ō˙ßčūũöü˙ øō(˙ėä˙īëũÖŪũûūøũô ũ÷˙ę˙õ˙ ˙ūđ˙ãŅü˙á˙å ˙īđū  ūö˙čũ ņüßõũ÷ęüßė˙čūãōûũũõų üũ$˙%˙ųøũüôüōëũįîūīæä˙į˙ ũ ū ū ũû˙ûũ ūũōī˙īė˙įķ˙öõ˙đõ˙ûū˙ūû˙˙ ũū˙˙ũüûü˙˙ø˙ų ˙üöūū ņúÜāū0 ūõ˙!ęņ÷ûūøûū ūūđôūûũũķüū  ūøõ ëđū ūķâūō˙ßöũņūî÷üũņôüđôūčōūôū˙ øũũūüüũū ü÷˙ûüķūéōú÷đųéđûũųũîđķūîî˙û˙úô˙ī ūū ū ˙˙ ū˙ō˙īöūīéũīúüđôüũüüøũõū˙  ū ˙ ˙ūú˙ūũųöūôøúüúúûö˙ü˙ ū ˙ ˙˙ū˙ũ˙ũ˙ũũ˙ü˙úũū˙˙˙ ˙˙ū˙ü  ˙ ˙üūũũū˙ø÷˙˙ũ˙ūũ˙ūū˙÷˙ô˙ø˙˙ķôũøøūôųûū÷öųūū˙˙˙üūų˙˙ûũūū˙÷˙ķü˙ņÚ˙ęũ ōüôū ėäõūķû˙üũūū˙ūúü˙ũ˙÷ū ˙ū˙īõß˙ôũčũå"˙đáä˙øũūø ū ķķîū˙ûúūõ ˙˙īû ˙õ˙$ī˙ä ˙ ī˙å÷˙ūõ˙ķú˙üøôũøûūôũņūūūūūčūö$ū*øũ"üé˙Ķ˙äķüįßü÷ūøõņ˙īîü˙˙˙$'ũū'˙-ũ$ō˙î˙ũíÚūæüč˙Ûâøîåå˙ųđü÷ũøū ũūũüūō ū üíüũëūŨėũčā˙ßņųūîę˙öō˙áë˙û˙ö˙"ū ˙ũ  ūū˙ū ˙úîîūņé˙ßáđéåķũ÷ô˙ūųūōøūũ",ü+˙)ū˙õõūūđ˙îôõßÔâōôūéö˙ ũķūŲũA˙A˙ A˙Dã˙ķô˙ķũöūüé˙ŅŅŪ˙Õ˙Õåūũ!˙đéū6ũúūL(üú ũ áũõ˙÷í˙ÜūĖß˙ÛØ˙Úņ˙˙ø˙ō"˙ũ(û(ųû<ü;ū9˙üūųčā˙ßÛŲÖ˙ÛĪ˙ËÜâ˙Õõ%ūũũë˙IR˙ūO>˙ ķ˙îåáūâĪüæå˙āĐģũŖŊ˙áÜãũ0˙ ę˙2J@F˙R>+˙, ũĐęūúįŪ˙ųîũÜŋūž¸ŧũŦŅūúėE˙ø+˙1ųũ<C˙%3ūL"ũōŲđüÛÅúŋíü×üÄĪüßėũŅÚūū;ūA*#˙ũ"4ū.ûķũđ˙ä ˙Ø˙Õđéū×ÔûåŨûāôûũü˙ūķī!˙B=ū&>@˙!4˙Õ˙ūķöũúŪļūŊË˙ŲŅ˙âíüõåûô÷ũ˙FaPYW;-Ņįū ũđüéũÕēū°¨Éūē˛Ōūųü/.ũ<ū=ū8_ũQ$˙3<˙˙čąūÔė˛ũ˜ĖüķĘü§š˙ÆÖ÷ũúíüõ&üCNũ;>ūc? ˙B0Ûķ˙ đŅŊĪÁūĻŽ˙ØßÎ˙Á×ã˙į˙˙,B/˙,R˙S ˙IgM˙ũD&ūŋŧūīßūÄĀÉūÉ´˙™˙ŖģūĘÜ˙øų˙:ūoFü;˙ns6ū.ūN˙äį ūāĶ˙ĖÍŋŋū¤ĨũŦ°ũˇŲķ˙˙$˙GRYRSdVALB6)ķßŧŽŦŖŠŒŗÉ¸ŊÛīŨŪūø!0,hū~Zū2lbG+%ūûü÷âūļŖ˙Åąū¯ģū§ūĻÃ˙Ëâü )6ū4#ūNw˙~]˙H[6ū ųũáČüŲČũĩģĀūŖ†ũˆž˙ŗË˙Ōķū ũ 3ū@+˙[ū~V˙@B˙P(˙ūņ˙āæÖ˙ģ­¯ũ´’ü„–˙´ļČé˙ ūIū`:ū^ū~Jū-eū]>ūҏŪ˙ËąūŠšūš…ŽūĨ˛ūŧ×ö$I?˙8YūoEūGiiũ1 üEūúÚÖĘžŸ–˙˛­Œ˙ĻūŗŌūė÷˙:K˙pv˙F<˙h˙j9˙T+˙æ˙ķÂŦ˙°ū›°ūą¨˙˜ĻūĢĖūáö˙DN˙7Xū]|ū~vQūMRüF'üīüÍŽūŦ­ūšŠũ” ˙•ĻČÚ˙Ūū!/˙<jvūM`ũ~~ūQB˙N(˙ũáđÍūš¯ũ’˙  ”•˙§žūČÕūô$˙#!KyZ<sūXũAQ˙ øí˙Ã˛ÂūŽĒûĨĨũ˜˜ŦūŊĖũÛ˙˙9˙f_5ūd˙u6C˙`>˙ųūĐĮūĀŦĸĒĸŽŠŖ¸˙ÂĐ˙î E\Ei˙~ûaBüdVū1˙ŪÎūÔĀ˙ŦĄūĨŽ€˙­ūŊĘ˙Ūũû ú7ZūOP˙o˙~e˙`X8˙ų˙×ŋËŧ˙Ą‘ĢūĨŽŠūĄŗũģŌ˙ņ˙'?˙LhZ˙Qd˙{z˙seAũ*"ū˙Ũ¸ũĩ¯ũšŒū˜­§˙ŽŽĄūŗĪũčũ8˙X˙mJbcūQZ˙_=-ū)ūæËüĄ€ũ˜´˙‡ūˇÅũ  ˙ļÎ˙äí ū*MũK{ū_?ũ_cūP5˙/ ėĐĪūĒ™˙ž”‰ū›ĩ˙­¯˙ÅŌūįü˙<˙SIs˙vQ`ũ~pũI46˙÷ā˙Õ¯˙“ĨŸ˙›ĸ˙ĢŦŦ˙˛Čũáôū$ũE@ûRüpHūvūc'#ũ>úōÛüåÃĻũ§Ēü ¤°˙¨ŠūŧÔ˙âņũ&ü6CūXkb˙czûtQú2ü7ūęëĘ˙¨ŗ˙­— ˙šĸ™üŦËũÚėūü9ũ2;˙lsA_{ūJ+ūG.ūŅūæÎ˙ŋŅēĄ¤ą—ū‡˙š˙ÍÚüķ ũ;(˙9gb4YyrM˙;Dū"˙čĖūŸ¤ūŧ›Œ¤˙ą—˙Š¸˙ÅÛû˙*˙Ei˙_Jū_sũW\˙Z8˙ îŌ˙Žœũžšūƒ‚Ą˙´šū˜ąūÃÜūøū,M˙urhj]GQQ- đØŋū̇ūŠŖ˙…ąĐššĖã˙˙/Qt˙`|˙~h˙=e˙Y>ū˙ úôÚüžą™ū‘˙Ą˜ĻÂ˙ŋ¸˙Đęũ3ũSdc˙~˙IX˙~`˙6$,˙ í˙ÍĀũ›„ũ—™˙•˙œŗũ°ŗūĀÖūíü˙2TūQj˙~Tūjx˙U ū1ūãŌüͨūŸ˙œŒū̏˙ļÍūäøūûū,G˙Kp`ūa˙l˙@ū0īũÍĶū˛¤Ē˙”Ž §”˙—ŽūÎäūī<Q2˙YuNJ|˙}^˙*%ū3Ø˙Õ×˙­ˇūŧŸ–˙Ŧ ū‹˜¸Ō˙Üú˙ F6˙AlūrEũ_ūyT˙8B˙& ūįЧ˙ ĩũœŒū—˛ ˙Œ ŧ˙ĪÛūų2ũ=bū~zTb˙wna˙_G˙$ū÷Öũ¯–ũŦ§ũŠĨū´›ũ–¯ũÅØ˙ö)˙E˙hü~aũ_qYVWūBüūũ߯˙°Ž€Ž€˙žĮūÂŗūÄÜüųü'Gūkpr^^^˙I-ū˙ūÜ˙ĀšũĻŒūĸ“ū–ą˙Á¸˙Ėįûū(ūF]gūfūWLsūlJũ"&˙ íĪ˙šūœü‘ ü¯ąũĢŗČũæõû +˙LYZ˙~M[w[)ũ+ ûäĮũÖ´˜ū—ĸ˙ŽŠūŗ´ũÄŪũîûũ/ũ@Rũiũldũ~rũJũ'&ūüŲÖū¤ũĨž˙‘˙œ§ü—šũŦĘäū÷ ü(Cũ3PwũgJüwũo@ū/AüéûŅŨūŦ´˙ÆĒū—Ļûą”û¨ũÉÛũë˙2>Ab˙~Y˙Irh˙A9˙2ņÕ°˙šąĄŠū‹Žũ¨’œ˙šŌ˙Ûü2˙=^~˙~^ũc{w˙]^Gū# ų˙ØŊũžž˙Ą”†ū“ŽüĢšũ¨Âū×õū)ū?^ū~˙nehūROK˙<ūøØûŋŽû‘€û–€›ũ¸Äũ¯ÉūŲø˙$Cūgnūooū?W_ũ@#úūũ÷Ų˙ŧą¤ū‰’ũĄšũ ŧūČŧüÆáüöū)Lūjd˙gū_CūnoH˙!ūōÔÄ˙˛‹‹˙¤šūœąū¸´ū¸Ī˙éüū%H˙XMw^˙Mvūa1˙˙2ũõüĶÛūģ›œ˙Ĩš—ūŠšũ´Å˙ßôúũ )˙BMjūlZũ{xS˙"˙+ūŲŪĮŦ˙¯Ĩũ’ü°ūšŦÍįūöú:Xü;Y}ūaIũyũm=ũ&9ū áĪũÛ¯ūąʚ˙¨Ģū—˙ą˙Đâ˙ô?E?a˙yNR}}\ū28ū%ũßŅĸ–ŗū¨ū˜´˙Ŗ˙šūˇŅūÚû˙5>^|vūSTūpkūXWū=ūīūˍ‘ĢĄŒū‰Ĩ˙°›˜˙¯ĮŲ˙ų˙)Gūh˙_fūpSP˙R: õÚ­މĸ‹˙…§Ė˙Á¸˙Ęã*Lqbqū~rüMWũQ?ūôūūöÔļūēĒ–¯š¤ÂŌÄ˙Õī˙˙2ūQbbg˙{PūJrdū=˙$˙ęũĘģ˙Ÿ†žĨūœĒ˙š˙ˇ˛ü¸Īũđū5ûZbųWū~˙QYũqV˙!.ū čūËØūĩ™ūœ§“ū’ŽūģˇūĮãũōø ü&>üHbūug\˙tiūCũûņÎüŌŧ˙ĨūĒü ūĢ—šūŦË˙äô˙.˙M4Iüq`û<gũ~h˙8 5 ˙ŨÆūŲĒ­ÆūŠ˜ūǏ–•¯˙ÎāũôũBD?a˙~Qü@mūx\4-,˙ėĐūŦ”ūŦŦū’´˙ŗšūĄĀÖßû˙3ū<_˙~˙~YūXoūgQ˙T?ūö˙Õš˙šŖ˙¨š˙‹œũļ˛ūŸ¯ĘũÜûū+ūCfū~jūck˙S˙LLü<ūüÚũĀ´ũ•‡˙Ÿ˙™„ŖũÄÕüŧĖ˙Ũ˙ųũ#Düfqūi|QT˙^C˙&ū˙Ũ˙ĀŗŽ˙”›˙Š˙ŸĸũŊĐüŋĘūå÷˙%I˙efũbū[>b˙c<˙˙ęĘŊ˙džū¨˙›Ąĩ˙Āž˙ÁØđ˙ū ũ*Rū[Jr˙~OB˙fMũ÷ū!ã˙ÉŌū¯‘ū™§˜˙—¸ûÍÂúÔđ˙ú˙ūū.LM˙_{iFh˙c;˙ûū öÕūØÁü§­ūĨ™§˙¸¨ūĨļū×đũûũ7Sū5Fuūe7û[ũf9ū2˙æĘā˙ŗ­˙͸ ūŦŋũĸļüØíũõAüP?û_˙Y9n~˙_2). ë˙Ұ“ūǏũ”“˙´ˇŸ˙žģ˙×åūû˙>J˙e]X˙fc˙OS>˙˙ôÕ˙ĩ˜ŸũĻ™ũŠšģˇ˙ĸ˛˙ĖŨ˙üũ(@ũaū~e˙[hN˙59˙4 ˙ķõ˙Ņē˙Ž•€ū”Ļũ…ŸüÃåũÆĖūßū˙Aūet^˙yv˙U˙<Aũ2 üûÜū÷Ú˙ŧŖŧ˙”˙ĨŗĸũˇÕüŌŅīũúũ)IWe˙Vkū]=E˙bDũ üīūŅŧ˛‘ŠŦНĀŌÍ˙ĘÖ˙î ũ!GūjV]ü~mū>DO-˙ķø˙ØĖ˙ͨ’˙Ĩ˛žū­Ö˙ŨÕ˙ė ˙7˙RL^`>ūcWū%ņūīūŅÍŗ§ū¯§žąš­˙ŦŋūŪûū3˙TF˙>uy˙=@˙ygū; ū) ũßŊũŪˇ˙§ÍÁ˙œ§Į°ūŸ´üÕíûôũ:PũHRûuküC4ūZTū7! ˙÷îūÔ¸–˙˜¯ž˙Œ—ūļβ˙ŗŅ˙įđū+:ūYzü~oX˙d_ũ)ū'˙ûŨũŌÎü줐˙–ĻĄ˙­Î˙âäūæ$ũ0)ũI`l˙huūmTũ42ū)ūĪÚũãÁü˜ĄũČ´ū˜šūļÃūËÖđ˙˙:GIZdM˙/9Y˙@˙ øũ×Ā˙šģŗū§ŽüĮÚũŨÚūäųūø˙ ˙<PūJj˙e˙A2ū9-ūūú˙čāÕŋŦ¨ŗÂÎÖåü˙ ˙ū"*˙;TF6D˙@" ÷˙ß×üÍĖüÕß˙ØËĘ˙ČÆĘūŪôũũ˙0@˙45ū:4ũ+)ū)67ū!ūėūĪË˙ČšŊūĮČ˙ÃÎÔ˙ÖÕ˙ÔÛ˙ë˙2ü>IũF<4ū0,ũ$)˙& ũ÷ãūĐÂ˙´´ŧ¸šËØ˙ÕŌūÚäūėū-=˙IIūFLūNE840 ūôčüĐŧüļŊüą­û°Ŋ˙ˇ˙ēÄũŅå˙õ+ū8HüONũOTRH=4ū$ūûį˙ĶĮŊū´¯ū­¯ü°˛ü¸Á˙ĖŲ˙ī ū.ūFTS˙W[ũ\VūPL˙?*˙ũÖ˙ČΝ˙Žš˙ĩąŠĢ˙´ĘØ˙åū:˙FELSūXQüX^ũS>1ūūįåĶūŋ°ü­¨ũļē˙ˇļÄŨßūįöūü˙7>=^ūdWũF?˙4&˙ū ōũߨūēŠüˇĀúš´ûÂČĮũĖéũū˙6:ū35ũ0,>ũ>*ü!/!˙čŨ˙ŌŅ˙ÎŲ˙Ũ×˙ĘÍË˙šÄ˙ë ø˙ 5˙G&˙.0ū!#2˙4-ü*ũũëä˙ØÔ˙ĪØÛū×ÍüÎäüëãũæę˙đ˙ü$û71ū('˙+3'˙ ūôúúđįâØĶūŲä˙ââßöøôūüũ%˙"!ūũū˙í˙âÜŨūęųī˙î÷˙ņáŨũŪãũū˙˙ū˙ üûū˙ôč˙á÷ū ũûúũöđËūÆüūũ"˙üõ˙åôūūũūé×ūéüūōø˙ũ˙ņđũûõüũéũį ˙îâ˙ņöīũčęûéöūúīũčöũūųøū&ū!˙  ũũ"īūäņ˙íá˙ØāūáÜūÛíå˙Öû˙ūôūC*ūúũū ,˙ ëáįéŪŨôū˙îéūņí˙Ûāũ˙ũ-7ü÷ū0ũåũB5˙˙õūöūØÕ˙Ėâđã˙Øôūö˙Ķßú˙˙&0ū84˙ ,˙- ūöū( Ũ˙âüŨĮûĪÕûšŪüũōũÕōũ ˙éîūü$ũ;LE˙< ũū˙ęūüß˙ëķ˙ÔË˙ÖÛ˙ĖĪūįüũįÛūūú˙ėč ũ#)!/Cü=&ũ îŲūæũ˙čņ úØÅČĘËÎâ ˙  ü #˙˙ū-73˙ ˙ ėÖ˙âį˙ÉēÍũÛäÚūØņũėė˙čÚ˙éC˙;BũKQū+ ˙ ˙˙ũüũũâļūĒÎ˙ĶÃ˙ÕüūũãŲũøū˙˙˙üLūS=ũ#)ũ9˙īîā˙Ô÷˙îķßŊ˙ŋÉūÂ×øū*˙)˙-0ūîū,˙04B<˙é˙úØáúß˙ÎÜēŽÜūä˙Ë<˙.˙ėũâ4ū''ūMP9üøÚû÷'ũúč˙ ˙ôôđÍ˙­Ėūôęūâë˙ ûÍ˙ŧõũüü˙;˙-˙Üō˙+0˙)˙!0ũô˙ķ˙߯üÍüęĖėūęũËō˙ ķ˙Ø 0˙!˙ >"ô˙PūčūįũúØ˙čķ˙ŌÆ˙ëé˙âäũđåũūūøå˙ũū øũF˙`:ũ˙÷ãūí0ūßūĪ ūÍū¯ŨūûęĖû˙/ũéū%îū˙C4˙ūđ˙+Nũ#ū).ũäßūŨŪ˙âã˙âėč˙ŨĐŅÛß˙Öîũû2Mū+úū]üOũ"X˙A˙ņũęüũņÕ˙ÅÕ˙ÖĀ˙´Į˙Ęā˙ķķũëūū"3.˙#&ũ9JūE.!ū$ūčŪ˙ōüūęÔûÃÂūČļļũÛįũÛņū˙û(!ū˙ û6ûA+ũ*ũ%ū˙Öä˙ŌŧūĖåũíøũ  í˙ÔĶ˙āíî˙(3ū  ūúūķū üüûũčķ˙ņëÜâöūūüúūũō˙Ũ˙Úâũ÷ ū˙$˙ ū ūķ ũ ũ/,˙ õūōõũđëūīú˙ųíũéöū÷ņéũæßæ˙ö˙ ūũ"ûũ˙˙ üü÷üëäūįķ˙äæøņ˙ãėū ūũü ˙ ˙ü ũūūøđüņöūöø˙öøų÷˙øū üųõũūũüúüü˙øöõėäîū˙˙˙üųöũü ˙ūū ÷ė˙éîūņôđ˙ōņūíëî˙đųūöûú˙ūũū ˙ ˙ ôõ˙ôōė˙ííūčéūīķūņôņ˙îņūû ˙ũ 'û$üūũ ûúøûņīūįãāßŲŲ˙æíé˙đúũũü˙ ūûüū#+ũ'ü ˙účôũ˙įáūíõũįäûæŨûÛŨūæô ūũúû˙úūįėūüû˙āāíäÚäũôõüú ˙ ˙ ˙˙ũũ ũüū÷í˙îíäß˙ëë˙ŪŪíõčŅ˙č ū˙˙ (ū ˙ ˙ ū ū ÷ūŨāũíđ˙áæéūãäūōîüÜāü˙˙"ü#˙ņũūöėūãîđíåūéęūå×üŪķûöúū ūū˙%#ú÷ūíæūææ˙ãäņčūØäū˙éî ūū "˙"ūųüūūúīūėėũęí˙čâčûūūūüûûũ ˙ ūũųũũ˙ûúô˙ôųõí˙ņūūüūũ˙˙ ū˙ ū ũ ũ˙˙ü˙ķō˙đđī˙ëäí˙ų˙Úä˙û"˙ũüũ ÷ũöûü˙ã˙åėüëæ˙ë˙ķũūūíũį ūûūö˙$ķü ˙ôîūũ˙û˙÷ũũ÷đđų˙˙öķ˙ūũ÷˙ū ūũö˙˙˙üüū ũûū˙˙üûūúú÷˙õņ˙ę˙åōūöđũîüôūņ ũ  û%ũ%ū˙ ˙ ū ˙īëūíčåįęæåđķūíęũų˙ ˙*&˙˙%˙˙üö˙đė˙îė˙éė˙ëį˙éé˙æô˙˙ū˙ ˙˙˙˙˙÷˙ûūô˙ėëįâáūßâäæūīōüņōũũūũųüüüũū ūøņ˙đ˙ëæüáäũäææëëņũķúüöüũ ūũ ū  ˙ ũųôīūíė˙ęíīūííõū÷÷ūû  ū  ˙  ūūũũũ˙øôõķ˙ōķūõöũöøüųû˙ų˙ũũ  ũ  ũ ˙˙ū˙ūūūũ˙÷ú˙üúųûũūûûūüüūúũū˙ū˙˙ ū˙ũ˙üûūųú˙øú˙ø÷ũōōūõ÷ú˙˙ū ũ û ũ  ˙˙úø˙ķîī˙īđ˙ėęč˙åéí˙đõūû  ū  ˙  ˙˙˙ûô˙íėîūíë˙éėéūéíũíōúūøũûü ˙ū˙  ˙ūūúũōđũíėūėīî˙éđūōķ˙ôųúūũū˙ū  ü ũūų˙ķėé˙įęūéęüéíūđöûūũ ū˙˙˙ ū ũ˙øũõņ˙ņō˙îņ˙ōņîîđ˙õūū ū ū ˙ū  ˙ ˙ųûũ˙ûūņķ˙ôôũķōüīņüņ÷ūú˙˙ ˙  üüūūū˙ūų˙îíūīö˙éęęî˙ëėũėõūôöūų ˙ ū˙  ū ˙û÷öũķđüíīũīņėūæéũņé˙ã˙ūũ˙ ũ ū  ũ ūū ˙ô˙íīôõō˙ņö˙đëė˙åŪ˙ė˙ũ÷˙ü˙ū˙ ˙ũ ũûúūũöō˙ņöūųōūįķūüī˙îų÷ūøū˙ ˙ ˙ ū  ũ ũü˙õø˙ôí˙æō˙ųÜ˙Ôũûũå ū!˙õ˙ü ˙ ˙ ˙'ūũųũ˙ūūōõũ˙ūõú˙˙įŪ˙õ õ˙ę˙˙˙îōū!ũũ˙˙˙ũû˙õô˙ōúũîūãøūũíđüõîũô˙ûôū˙ ˙˙ūūũų˙˙÷ųūûįáûûôø˙ūøūúøūåø ˙ûũû ũ ũūėč˙üúėũ˙ųāäųö˙ô˙ū˙ũ˙˙ ˙ū˙˙ūũ÷ü  ˙ ūūüũęįūčūũų˙ø˙˙˙ũūü ũ ˙ ˙úũū ūú˙ųūíėūéėũöüú÷û ˙úô ˙ū˙ ũ ûûøú øûėųüô˙ë˙î÷ūõũūū ūū ˙üû˙öú˙÷ôôūņđ÷û÷ôũķúũúúúũûūũũü˙ūķđ˙ü ūķūîįūīõ˙úũū öüīöũøų˙ũū˙ ô˙û ūņúū ˙íí˙úūīđü˙üûũú˙÷˙ũūūũūūų˙ ˙ūũüúūũų˙öü˙˙ûũú˙÷ōūõ˙˙üü˙ūūū ˙ųüü üū˙˙˙üū˙ũūũ˙ũũúüøųūôņũũ˙ũūūũ ü ũūũû˙úúüū˙ūũ˙üüųö÷ūū  ū˙ũūüũø˙÷úüõđüķ÷đūôûūųøūųüūú˙ūū˙˙ü÷ø˙÷õōūōņūķņũņöüûüûûüû˙˙  ˙  ū ˙ ˙ü˙üũũúōũķöũôņ˙ķõ˙õõ˙õūũūũ˙ ˙   ˙  ū ˙˙ũ˙˙ũøųũ˙ũų˙ōøų˙ôō˙øũ˙ųũū˙ū ū  ˙ū!˙ūũōø˙ėîī˙ëđūõúüņėũįî˙øõūū ūũü ˙ ˙˙˙ķũėûäōũôč˙ėîîãūįöęČû˙.+÷ūLüEũ ˙  ūîŌáūäŪūËÕîđũįöüõæ˙īņ˙-˙7˙÷,ũ>ü÷˙˙÷ņäâčĮ˙ąØū ˙Ú˙Ôū0úūãíūū$ũ2ûKũīō˙˙ōí˙ ųũīåüÛėüņåũ×į˙ ˙éÕ˙* õ&˙8ū ˙ũėū0ū Üūäđ˙ÖŲøū÷îū˙õâ ˙ ÷üæũ)(#%˙ķ÷˙˙æÜũúãĐũí˙õč˙Ëč$ūķûû@ū;îũøũ ũū ūūßîū˙ņņ ˙ ûūõīũāâũ ˙û˙ ˙ãÎõ˙(÷˙!˙üåõ˙( ũķėūķÕūŪũ ņę˙üø˙ũŪÎčũ%+˙.˙ ū˙ûõöūüũīîūüōūÆŊūíôūãāä˙ ũū ūũ ū˙˙ū˙éáâ˙Ũãäæū˙ũôîųū˙ųũ˙ūũ˙õø˙ ū ūūķķũųôüėįũ˙ ˙ ˙ķø˙ūõ˙˙ ˙˙˙˙úđ˙į˙ ˙˙ ū˙ũ˙ķ˙é˙ûũ÷éûëđųđđøîöúũüüøū˙ū˙  ˙  ūũũøū˙ūôü˙˙ãį˙üč˙é˙ü˙ėū ˙ũũũüûũųņūķū÷ú˙üķūíëė˙īüū˙öûũũōüūîŲūûūú˙ü˙Ûāũëōũķúũ˙˙˙ų˙ņøū˙ ū˙ úūëūüũķũũ úũŲæū ę˙ėú˙õđû˙ûüūũ˙ũū˙öôü"ü ûūũųūũûú˙ûéãō˙øõõúú˙˙ūūõ˙˙üũ ū ūūūų÷˙ųöėņôūėėūü˙ũüū˙ũū ˙ûú˙ūũ˙ųūøøöķ˙ėîūôũüū ū˙ūüüūöņôūņđ˙īôõô÷÷˙ôō÷õøūū˙ųū ūūū˙ũų˙ú÷ūøõ˙ķöų˙÷ö˙ōō˙ęë˙íøūũ˙˙ ˙ėūúūũúū ķ˙æđëėūīîūīđ˙÷íūčí˙øū ū˙ đ"ūâķūūūãâęë˙îõõūųüûũņüãé ˙˙,1#˙˙ ũūķ2û"ņüŪũŨÕûåęũãėë˙īîūųūķîū ūúū4-˙*%ūōũ˙"˙úį ˙ė˙ĐĶũíâũãņú˙ķųūķīũ˙˙2-ū ˙ ū ũū ˙īāđéÜãôūįæūđô˙îøūû˙íū ūõū*#˙ ˙õũ˙  ũũũũüüūõī˙äæí˙éīú˙øđ˙üôņ˙ ūüūũũ ũü˙ūķ÷úö˙ķôô˙ņö˙öö˙ūüū˙üü ū ūûō˙ō ūú÷ūđūûûūøū ˙öūõ˙û˙õū˙öūį˙Å˙8 ūęüC+ūčįúū÷õũ äūÕâüæá˙Ú˙õũõū úūĘņū07ū ML î˙úâ˙ü˙ î˙ÉŊŅŅ˙ÍŲũũúî˙ úū ˙65˙ü1$ûëŌ˙ōūįüÍāũöėÉūžÛ˙ÔŌāūėę˙3ū2˙"ü 1ū6')˙1˙ū úÛ˙ØöũÚŪū×áÜÍ˙ŗĪ˙éŲę˙#ūøũīūI\˙B'˙AG,ūßũĖõūááũōúũŌ´ũŠÁũÍČä˙ü ü(åūũ<e&˙2fū-ūūîūūöãūķ˙ë˙ĮĘū×Ŋ˙š˙Øūü ũüū˙÷ūT˙7˙SV3%ūčûį ˙ņ˙āũ˙é˙ČÉũĐÖūÉÍæ˙ëëũ"üüüū0/GK˙B<;.ô˙ō˙įüã÷ūđšŸ˙ÄÍŽĘõ˙âØ5åũUü9ũBaũ=üôũī ūėūäáũÍĄūŸË˙äĒūŧ÷ūūŲ˙<˙ í#Y˙a6ũ"TMũ ˙ÚĶ˙˙í˙ŗĮ˙ō˙ĶŖüšĪūš­˙Øųūû˙=F˙-ū+3üYkũ=˙:$đļūÕíũĮĸļūęä˙ĩ¤´ˇūĮđ˙đ÷(˙VR<GūPFũ&I[ū6˙7&č×˙›¨Ûū˛ĻūÄážũŠšūĐį˙č˙#OR˙]^ūQCM˙G<ü- ü4ūņŲŨ˙´¨ũ°Ēū“ ˙¸ŋ˙ĩÆ˙Ņė˙õ˙2˙1Dũ_Qū1fqū[?˙4.ūöũåŋŽ˙ÄĮ˙žļÎÎ˙¯Ŧ˙ÁĶĖ˙ßū0ū,gUūR˙y]˙ū@-˙úå˙õ߸ˇŧĩ­šŠ§¯ĖĮŲú˙/˙LNū1Vū~y˙CūDLũ&˙ūÜ˙žģ˙ˇ¤˙ގĒ˙–ĄūšĪ˙Üäú ˙#T˙{_ū;Ws˙]NBũA'ūņĘüŠŧũĮŖū“ŗũ¸›û—ĩüĐÔũßũ*>˙v˙D6p˙~W˙(H˙<˙õá˙ÎËũÃĒũœ¨ĸ˙—›ū¤ŧ˙ÉŌūé ū&6ũ^j^Pkũ~lũREū1ūúÛ˙˛ŒŠūŠ…ūŠ´üĩžũ’¤˙ŧ˙Õđü +úFbû|˙u˙ihūd]˙QBũ$ũķØũž¤ūŒžū‰ū­ŊũޏĘ×ö˙=]d˙~ūNCūtZD$ūáŊūĀĸū˜¤œūŽŠūš°´ÅËä˙û˙Bp[ũ]bũTV˙hS<1ū˙ęĶūą–ū•ŸūœũĄ´ūššŧÔ˙đũ@J˙;kN<zi89%ŨĖÁŽŦŖ›ĸ´ĢŠ˙žÔūßä˙ų˙CūCMq˙@dũ~dū$˙F˙øęũÜļũНũŸ˙ą¯¤˙ŠĀŅŨņ ˙,&<k˙h@gũ~xûBũA;ũęüíČûļĘū°•˜ũ°ž˙‘ŸÂũØāũđū7)˙;fũf>ūhy˙I3Q2˙ņŪĒ˙¨Ōũ°šû§ģū›‡œ˙šÍŅ˙ī˙(/Q˙scLūB^ū`fa˙A!˙øØ˙˛¤ūĻ˙ƒ€ũ›žüĻ—¨˙¸Ėëũ ˙=fū~pũXixūH;˙O?˙˙ųŨ˙Æ´‘€§ũ™„˙ŖČĶģūÁÚû˙ū9\_˙[aASN˙.˙ûîúâĮûŦŊūŸ™˙§ ˜ū´É˙ŧÄâ˙ī˙0G˙_jw˙DDj˙{Tũ-0ū ˙ũÜĮũ´”ü™°ūĢąÃ˙Åĩš˙ÂÛũëū$O˙i_n˙~fũ\UO˙2˙˙ūßūÍŧũ“‡˙ĄŖ‹˙ĨĪĪŌ˙ėū ˙=˙NHūgũ^Fũna˙."˙ųÚûžĸû­´ūĄžũÃĮũŽŠÅ˙ãø˙õ-˙O2ũIūhū;tũZ$ūũ1˙ÛÂ˙īēũĨÉũģ’ũĀũ¨š˙¯Īßūė ˙.8<˙K˙{\ũ3T˙}`7/ũūÚąū“ĩ˙˛’ˆĨūˇ¨ū˜ą˙Ō˙Ūį˙$:ũZxũ~e˙`aaSūQJ˙& ũūÛÁ›ˆœĒ˙’˙¯ĘũލûŋÛüīū%;˙\˙bQ˙SI˙AB#ūūįüΎũ¨€„Ē˙”˙ÂīÜ˙ĖĶ˙ßū˙+R˙~lpū}d˙GJū3'ũ÷˙ûū˙×ē˙ĩĄŠ˙™ˇ˙ž§˙ČáÍĶüđ û$0_ūl]ũR`˙5P˙fA˙# ˙ņĐ˙ŗ­˙‘‹ũĒĒũ­ĀÎ˙ĖË˙Õčø˙ũGiũE[ū~YūZü^&ũī$˙üÛËĀ—˙–ŽũĨĄũĀÕũžÉūâéö˙&DũXZūuqūJOdF öũ# úëÔûĮŖüž­ü“ũĒēū°ĩūĐīū ü9ũE4ū`ũ];ūnvSū˙(øÍÄūÃĸ˙Āȟū—ˇ˙¸ ˙ĻÉūëøũ-ũZP˙;j|˙='eūmKüũúũŨĘūŖ•ũššũ‘›˙ĘŌ˙­˛˙Îë˙ī -˙JPũlūoON˙SB9˙F*ũųūëËŠ˙•™˙ œ˙ ˙ÍÔ˙ąē×˙å$ū,Lũr˙~h[ũ[>ũ+7ū1˙ũúÛ˙ž˙‹€ĸũŦŽūŦā÷˙ÍŌ˙ã˙ū8e˙iYtünDũ-J˙;˙éä˙ŨŧüĄŽü™Ÿ¯˙Ē­Įū×ĮĮ˙äöū%ūK`ū]_ūrE˙5˙X\ü2˙ ˙ėüŞ˙­…‰ĩũ°ŦũÄ×ËũĘÜ˙ô )˙\vūHZũ~\ü+=˙L˙ôūūūđÔ˙Ā˙ŗ‹ūŠŽŽ˙žžëŨ˙Üû˙ ū E˙XHūi}ũI,ûaQúįūí˙ÎÆ˙­ĢļŦĄ˙ļÅļĩÍ˙ī ˙D^˙3Rū~WūT˙uQ˙.˙ũĐÆ˙ŲĨ°Ņ˙Ē“°˙ÄŽ˙ĒÉ˙īūü"üQSū?l˙N-iūuMũ'ūūäũÆĄ˙•ŧąūŒ˜ūÆČ˙̰Îūéî˙$F˙IiūsN˙GTūG9˙>)ūđä˙Ť‡•ū¤™ūŖ˙ĐĪ­ģ×čū"˙/Fl˙~˙gR˙WB&˙-0ũíūöĶŧ˙Ŧ•€˙ąŒĨ˙Õ˙ÕūÎŪûõü>ūgc˙xt˙V>˙6+˙÷˙Ũúâ˙Ÿũšžū Â˙¤ˇ˙ØëÕ˙î˙"3T˙dbūSdb˙2:ūeN+˙˙īāŧũĢŒ˙„˙ŦšũŗŅáÛĘÖæ˙˙ū"L˙z\T˙h"6ūV*ũöíüûũÚŊ˙ÆĨūŠĄüŧŖũŦÜ˙æ˙Öôũ˙)˙Cc˙^d˙{˙c?˙XRũåû ūīŅÂ˙Ŗ›ūŽĒũ–­ũôũŗËūë˙-ūWFū4pq˙*)˙mūY)ûõ%ũ×´Ķ˙ĨĄūĐŧ˙“Ē˙Ôĩū›¸üÛđüķũEUū@W˙~Rū;p˙L&ū%˙čËĻ˙Ž´¸‘˙ŒēŅūšŽūĘįüęúü8˙=fū~}TūT\˙N˙7C˙0˙÷ōũĐĩũ–•ĸŖ’œÆÔūĩˇ˙Īä˙.Eülū~lX˙SBũ39ū6˙ųÛ˙Âĩũ”€ü›­ũ‹ŽØūīĘ˙ÕāūûūC˙mvkgDEJ6 ÷˙õūūØšṳ̄ū‘ĸˇ˙¤ĒūĪá˙ČÍč˙˙0b{˙b^˙~J2˙`a˙4ū đ˙Ėžū¯Š’ū˛ŦŗČÎË˙Íāũī˙#Uūc>˙^˙H&˙[Q˙ī˙˙īĶūŰ˙†”˙ŽĄũĀ˙ØĀ˙Īíüķųū)LũPZzūk9ūOd:ũø#˙āĪūÁ ũ¨ļũŸšũ¸¸Š˙ŗØöū˙ ū+TE˙3no˙3=ũxeũ5*˙åũŋÜ˙ą§˙ŅŊ˙š§Į˙¨Ÿēß˙õõü>ũX8˙NW%ūQyũ\.˙.˙īÖ˙ģ•˙Ēʤ˙‘¸Õūĩ¤ŋūŲčũí2˙DPuũ~^ũDQa˙HNūI'˙ūâĀũ—ĸũ¯ ˙‰’ũ¸ĘūŦĻÂũĶé ˙#5ūY˙~v˙^h];˙AK˙&ũ˙īÖ˙ŧĒ€„üŽū¸č˙ĘēĖ˙įū-˙Rx˙[xzaūDLü@.ũåūå˙ĮĒēū‘‰˙¯™ũŦŅũÎÃūßõ˙ !˙F[˙[S˙oW˙8S˙rM˙$ũķÖūģĻ…ũ‰˛ūŠą˙ÃĐŋūšĖũįû˙ )ūXmüL`ũ~Oū2WüS!ũôūōĶũĐēũ”ū´¤™ÁØÁ˙Đîũôøü #ūCLR˙zp˙Ia˙kB˙˙.˙ č˙×Å˙§š˙˛¨ĀŦ˙ŖĩūÖīú˙Fû36üor0ũGüm7 ˙= ˙īĮ˙íŊ˙ŽŲÂūĢūɤūĢūĐäęũ :úG<ũU[/˙V}ū_7˙%8˙üūÚˇ•˛˙ŧ˜ūˆ°ūĮŽ ˙ŧÕũÛęū )3X˙~ũRNdcJ˙OD˙ũüÛüģšū˜ĸ˜˙‚‘ūļžĸũĨŋũĶđū#9ũ^˙~hY˙\N=AEũ˙áũĮļûž€üŠĢ„˙šÄūâžũÃĪüë ú5ų`tū[o˙E@Uũ=0˙ķũéË˙°ˇũ˜ž˙ŗ¯ĸüšÚūĖÁŲũëū:dfJpūa0˙G˙uTü*ũ$ ūéËÅĄˆ˙Ĩ˛­˙¸Ë˙ÃŋËũāōũ˙6b˙KPū~tū4T˙m@˙/ėÚÉ›ūŽūĨ’¨ūÔÅũŧŪüīōüõ9˙VQũsūWQ˙x_û8˙õūŲÚū¯ǎĸš˛˙ļ¤ūĨÃäķ˙úūGC˙&˙_q78yūxL˙$-ūúËū×ÍŖÃÎūĨœŊū­’üŸÄũßčú˙ Iü2:ũkm˙-˙Iyūj=ū3"ũāÆü™ ūÃĒũ ūÉąū–ŽūĖß˙ß˙ ū7CūhūhF˙KcūTMũO/ū éūĮĸû–­ü¤ŒŒ˙ŽČũ°ĻũŧĐũâ˙*ūMyũ~sT`˙aA˙;J5˙ ų˙ÚÁ˙ļŒ˙‚ą ˙Œĩ˙ãÜ˙ŋË˙á˙ ˙Ht˙]e˙}gũ@HũD<ūđ˙öÚ˙ˇš˛˙“ũˇĻūĻÁūŌÂÎ˙įūū+ūPc]˙ZuūBBj˙mE˙'˙ķ˙Ņ˙‡—ŗŦ¸ÆūÆģũģĖûį÷ú (üXbūGb˙~H6W˙P ˙õ˙đÔūͲ˙ˆŽũ¯›û‘ģü×ŧûÉéüķöū!˙BMũQ|ũtJūco˙B˙˙-˙é˙ÔÂ˙§°Ē˙– ˙˛Ŗ˙œŦūĖęũøũD˙<8nūu9ũC|˙qC .ũōūŋŨĘūĻĮūČĸūšÃūą•ĻũČäüėū/N˙;˙Dus6Cx˙oG˙˙.!ūåũÅž˙Á§ũ ˙Ëē˙Ÿ´ūĐå˙ę ˙$:JūsûfTücgūPNN˙( ū˙äÉ˙§–˙ĻĻ˙Ž˙¯Ä̧˙žÖî(ū:\˙~|ū`cū[GūDH˙#˙ ũæĐūēŦ˙…‡ũ­Žũ“žę˙ĖÆ˙Ōë ˙1X˙wUvũrTū2T>˙. ˙åęūË­ūą™ūĨŽūš°ũĪĮûšÔüčū=˙ciūQub5LtũP'ũ˙û˙ÛĀūˇ•ūĄūŖ§´Ãģš˙ÆŪūņū>ūeOūQ˙r˙;Qüg@üū)ūäÕū˜Œ˙ĒĸūĒūŌÃŋŪđõūũ˙;QR˙u|VYv˙[!ú.˙öūØĶũ¯Ĩü° ™˙­ļ˙Ÿ˙ŖŊß˙ōūü;ũH0üX|üQ=ūtū^'ū8 ßĪܰˇ˙˯üš¯˙˛ū’–ũ˛×å˙ō<C˙>fū~SūCu˙^4ü-0ū đĶŦ˙”ĩ˛˙’ũ¸ŧū› Ŋ˙ÖŪūö2=azP˙Tdü\NũR9íÍ­“ ĄŒš˙ģ¯ŦÆŲ˙ų(Cg˙~gdūhP˙?F˙<üŲžūą‘€”˙¤€˙›˙ÉáûšÄú×øũ˙@k˙n]ū~wKũEXüF.ūō ß˙Āąūļū“°˙Ŗ ũēĐũĀÄūßķ˙ %Kcb^˙{Iū@lūvM#)ųØ˙¯ü‰“ũŦ¨˛ūÂÄūļšūËęūö ū+Yũ_PüqūPHd˙Y'˙ü(˙ôū×Ķû­‹ü˜Ģ—˙˜Ŋ˙СČ˙ãņ˙ō˙#D˙LdūwYũwuũL ũ1ū ęØ˙ŧ°˙Š–˙ˇĸ˙–Ē˙Ęčūø˙$Lū;;üpoū1˙Uũl:˙6č˙žß˙¸Ģ˙Įŗ˙•ĸ˙¸•˙¨˙Ëā˙ë˙:A˙3X~˙K6˙i}˙[2ū'0û îûΨûŽ´ũ°‘ū‰ēžũ™œũģÔ˙Ú˙ķ/:ū_R]l˙`SüX>ü˙ņ˙Ōĩũ–žü¤“ū—˙šļūžĒūÄŲų+Dūg˙~mb˙gUFH˙C˙ūŪ˙Åļ˙ž€˙Œ¯ū„™ūÄę˙ŋ˙ÄÖūķ9b|ūYyR9YūC4 î ė˙ΰ˙š˙ž“ũŠ­üĩũÎÅũģÔûįū=_ũe]ũyaũ;^ũ~\ũ.(&ūãũÉŊ–ū‹Ŧ˙Ŗ˙Šģ˙Á˙ĩļūÃßđ˙˙Gü\CūZ\?˙[fũ>ū%˙âÕŋ˙”Ŧ˙Ą”ũĢŅũŧģūĶę˙ėö˙/B˙EjsV˙Oqū_-ū˙*$âÛ˙ļ§ūą •ūϞ™ū™˛ūĶé˙ô ˙/@)˙Pw˙R9˙p˙b*˙>˙áĐūæˇž˙Īąš˙­ĩũ”ũ˛Ôũãīũ<˙@4Y˙}N=oū~eũ<-˙8˙úÜš–˙°žüœŽü´ÄũŸ›ũˇŌūØî˙ .˙7Yū~S˙RhiūU^ūM)üũãÆĸūŸŠ˙œ†ĩūģĸüžĩũÉå˙!.˙Q{ū~yg˙lc˙KNV:˙ ˙øŪÃ˙ŗ‰˙¯“ũ‡°ūáĘūŗÃ˙Ųū˙ E˙qWūiūc0ũORûC&ũų˙ŪģšŽ˙›Žũ˜ŖūžÄŗūĀØũî˙DYRū`p˙7G|k@!4ūö˙Ô˙ÆŦü‹ĸũǍ˙˛Å¸¯ĩĖäūîū)WGūDu˙|?JūiY˙"˙3˙úßÚ˙ąŽĄ˙¯—ūšÃũĮ˛ũÄß˙éėū!˙<A˙e`Q˙~r˙@+5 ëūãÆũŦ¸ũŠ™ūĨģūž–˙Ŧ˙Ōėüķũ*G'˙Bu˙\0j˙h,ũD˙˙äĖũėŋ˙Áךūž˛˙ˇ“˙ąŌŪũėû=7˙0˙\{ū?Fzū~_ū68:˙öØ­šÂ˙¸šū”Ä˙ŧ˜˙žžūŅÖũđ˙+5˙]sETūe_ūOZū@˙ûظ˜ ˙Ŗ”€“¸˙ą™ūĄŧĖ˙ë ˙4˙Y˙|d˙Za˙PBūFI˙ ūčĐŧ˙Š‚˙…ąŽū¸æũÅēūĮâ˙ $˙JqūPt˙{[˙1YN˙>˙ōũŨ¸ũ­ũ’¨ū´˙ĢÉ˙ôũĘŪûõ ü(J˙\˙Skūl7˙G˙~i˙<˙!3ũöũÕĘ˙­ūĸ˛­ļÉ˙Â˙¸ē˙Īęõ˙(YũSBūjL˙Edū].ũ)&ūäũÜŋ•˙—ˇ˙Ą˜˙šØūšŋŲ˙ęė˙õ˙7ūCNū{}N`~ũ^ü <˙$˙˙äũÛš˛ūļĄũœĩūą™ũžŧüÛíũõ˙59%\ūw<˙;˙v~ũOū)4ū˙ĖũŪŨąÍĖĻũ˜ēüĨˆü™ĀūÚáūø(Dū*?ür^ū(Wn˙A$ũ4ū˙Ūŋ˙“žÄũĨŒ˙žĪŗ˙•ŦūË×ûāû$4üEqūZIü\f˙PP˙P.ü ũëĖĻū™¨˙ĻŽūŽÂ˙¨ĄļūËåũ˙)Pz˙~r^`Y˙BGL/˙ôØ˙Āļ‰ū‚´ũ–˙¸˙čŌ˙ŊÉ˙ß#PũxUw˙}Z1üWJú<ūņūūڴç”ŦˇĄ´ĖūÄĩ˙ËŪú ū,N˙ZQl˙g5Kf9%ū2ūđŌ˙ÃŖū­ąũąŋ˙Ėŋ¸˙ŋÚ˙ėø ˙7_˙@H~ūg4˙CcF ,˙ëÖüĘžū‡¨ūĒ“¨˙ÔÉūšŌéüëņũ+FūCkü|XũBoü_*ūô!*ūåūÛģ˙Šą¨˙˜Ļ¸Ŗž´×îø˙)ūB'ũH}˙\˙*cû~bũ#AūŪūÅíšŊÜē¯ŋ™”ūŗØūåë˙ <ũA-ûJ{ūK˙'\ũ~cũ6 9ūüũÜģ˙”ŽĘ˙ĨŽūąÔ˙˛˙ĩŌ˙Ũę˙ ,˙7V˙~P˙R^ū[J˙OK˙#˙æÉ˙ĸšŖũĨŽũŒ˛ūČą˙ĨģĪ˙é˙ -˙T}˙~ub˙_]@˙?Gū+īŌŊ˙­€ū‚ˇû”ũÁīĪÃ˙Ėæū 'VsSvpO0Gū;1˙ äū÷Ōū§ģ˙¤˙–ĒũĩŖ˙ĩĪÉģ˙Đåū˙1QW˙J`ūX*˙<˙pVũ*ũ# ūčÉžĄŠĻ°ĩÃĪ˙ÄžūÃÜ˙î˙ü ū<a˙GK}ūi6˙8X@˙ū(˙ņ˙ÖÎĸ‹ĒüļüĢÛũÜĮūØī˙ō˙ôü(DũDa}ū^9ūabũ1øū0˙ ėÜūÃ˛ũļą˙Ą˛Ā˙Ŧ¨˙žāø˙û ˙*E$ũH}˙V"˙\ū`˙ AŨÆņĀēáÁžąÅž™ĩūØęūč:˙@*G˙zN˙%Vd6:üũÛˇû”´ũĖĄ˙ŊÖ˙˛Ąŧ˙Öã˙é ˙,8˙V|˙xH˙KRNũ=LūA˙ā˙ž˜ū”¤ūŖ‹˙ŊūŅŽū¤ÂÔūíũ/˙Yū~i˙UXLū/8˙>ú čĖ˙ŧπЏūŠœĘōĶČ˙Ęéū 'ūVtY˙upS5˙?<ũ4ūįūĶūĻžū¨Ąū¯ŋū¨¸˙Õ˙ÕÁūŌíūū3VWH]Uū*@wūY5˙$ūīÎ˙ÃĻ’´ŧŋÍ˙×ĘūÃÆũŲíũû û8gûI@ū{l˙+5[?ũ ˙2ö˙ÛŌĸ˙Œąģūœ´ũčŲ˙ÆÛęéūđ˙*FCūa˙[6jū`)ôū!4ū éūמūļžūŗ§ũēžū°¯˙Čįûūųū!:ũDvLūWũzX˙>ũ Õ˙ÍīģÅūáˇũœēūŋœšūšāũåéüBũ4%ũTqū2˙[vN"ū4ūîŌĨ˙—ÄũŊ’ũ˜ĪūÎŦ§˙ÄâũŪķû8ū9kũeEüMOúE@üK6ũúüŲĩũ•Ÿū¯Ą§ūÖĪūĒĩ˙Đ˙ā˙ū˙?˙lūxYūMW8ū+<ū:ūūäČ˙™€¨ĩ‘ˇŨ÷ūŌËūĶõ˙3ah˙XkbE1˙8;*˙ķūķĶũˇĘūļˇÉüÅšüËāũŅÂũÚîū0@ū:1üO5ũ!AũlMū&ũ ęũÎÉûĢũŋÆÉūÔŨ˙Î˙ĮĘüŨīüũ ũ:a<ũ=|ũVū2Vũ0ú.ū˙īÔüĪüގũļ›ūšėūĐÄ˙Ũįę˙ī,Eū7]üvAü$aūQį˙ 0ũäü×Ŋūļŋ¸ū¤ļūŋŗŦũÃäūûų˙>˙5o˙TũJ{ū[ũBūÛŅöÁ˙ÉîũÄĨüŊËū¤œ˙ēäūėå ūA:û Lúy<ūJ{X˙B˙üũŪĩū™ÄŅ˙Ą™˙ËŨūŊ§˙Āä˙äę˙75˙[|rūAHMūI<üJDũ ˙éĞĄ¯Ģū• ūĪŲũąŦũÆÔîūũ)Yū~g˙RXL˙&8G˙ üüô×ûIJ€Ž˙Ŋ—˙ĄËø˙ÛÅĮũâüũMqūVdūgS˙+2˙=5˙ î˙ Ö˙Ļŧ˙ޤ˙˛ÂĨ˙˛Ú˙ŌĩÎūįūũ 0ũJG˙5MFū%5ũnW˙/˙"ũėĘūÂŖ˙ŽŗÁ˙žĘØ˙ÉŋũŋÔũčų˙2˙c>ū5{˙a -ūV< ˙2ūöũÕØũĨ”ūšŸūˇîūÜĖßüåéûđūû!Dü5Oũ|VũSüb/ūņ9ũöūÜČ˙¸ÂũŋĒūąÃŧŽŋ˙ßü˙úüūB˙%%m˙k1zo0˙û=ū'î˙Ä˙õĐē˙įÔū§­Ô˙ŗ™ą˙×ėũŪúũ1Eū#3˙uW˙-ūqh˙1=* ˙įȞ˛Õ°“˙˛áũÎĸüąÖũäÛūũ$˙2FūouūI>ūEEü3;ũI,ūū÷ÕūŽœū ­˙œ”˙°×ũÄĻũŗĖūÛũ:˙g~˙kQKūO/ü&>ū9 ˙æÉũÁ›€ž˙ļŽ˙¨Ô˙øÍ˙žĖîũ  ũ)`˙iN˙gk˙J(˙;E2ũ ōũū˙ŅŖ˙Āą˙¨ŧÁ˙¤ŋŪ˙͸˙Ķė˙<ũNEū6XL+@x\˙4ü&ûīĐ˙É˙Ĩ˜ũÂĘūÄŅ˙ÛÍūÃÄū×íũũ7ũ_9;˙}^˙0T˙<ū0ũ÷ūÕÜŦ˙—šĮĄ¸ūëáĐÛä˙æ˙đüūA˙7O}]"Qūe6˙û<˙õŲ˙Įŧ˙Äŧ˙ĸąÆūš¯ūÃâúũöųūAû!lũe˙,tūe&˙÷:!˙įÂķĮūģčüČ ü´Õ˙¯˙˜ļūÜęūÜũū4@ū;x˙F ũ3s˙X#˙<˙ũ˙Ũēũ—ˇūĐĄ˙Ää˙ÁŸ˙ŊŪß˙ã ū05ūYxsūBIũJ@˙.F˙Fū˙ëÉĸū–ĸü¯›ü™ÂũŪžĒÄ×ņū&T|˙|lVQI)ū6B˙&ũ üôŲũÃļū‹ũÂŖ˙œÍü˙čĘ˙Čä ūũKnūR\p˙Z0˙6Cū<˙ô˙ŪŽ˙šÁ˙°ˇĮ˙ŠŧūØÕ˙¸Í˙č˙4˙H?ū9SüR0û?wūa<$˙ķũŅĘūŠ—ūģĖūÁÎ˙ÚĐ˙ÅÁĐčūúũ#\ũF2˙hq-ũBũJ˙ú˙*˙ÚŅ˙ē˙ŖÆĻ˙ĻÕ˙ëŌÖ˙Ūåî÷˙5˙>@˙ngü'0ü_D˙ô,ũ+ũÛÍūŊŋūŊ§˙ĸŋũŊ´ûžÖûņúūø/˙6˙Jūx<ũWzP ũ=üĪÜūåŊĶßĩ˙ĨÉÉ˙§§ūČčäūęũD7ū'ap,ū Q˙v˙Kü ;˙øÚ¯˙šÃ˙˞˙˜ŌũįÅúĨŋüŪáūä ũ18üWv˙uūHJüH>ū2L˙L%˙  ˙õŌ˙¨’˙°˙›”ũŧâÂ˙ŖēũŅę ˙˙Iw˙y_˙QS0ū9Fû3ũ â˙Įžū‹€ū¯ĸ˙šāîūËÄũŌ÷ũ˙6d˙WSūlaū<6˙=> ú˙íũäļú¨Įü­ŠžŽ˙¯Į˙Ōģ˙Ŋß˙ķ˙">:ū5>ũT4ū/^l˙D ū˙Ø˙Åĩ—ĄūČŊũÆŅ˙ĪÚÃ˙ÜôūRY2`?˙=Xū)ū1ũ áüËČũ“œūŞū Ëũņ×ũŌŪ˙įķ˙ø6˙O@üotû4(ūdPķũ,2ūß×ũÆÄûÁŽũŠÄÄ˙ēÃūÖņũûũū/9˙K˙|> ˙U|˙MB˙ĶŨ˙đ˙ÃØüŪŗũĒÎ˙ĮĄ§Æūäā˙čIū8%hq)Z|Kū*>ũ÷Ø˙Ģ›ĮūŘœūÖåũ´Ąŋ˙âŲęū9ũ4a{˙kC˙ED=˙8L?˙ūâüŊ•ûĻũ§ūŌ˙׎¨ūÅÔ˙ņ&Wūz`ũMP˙?$8<˙ũ ũķ×üİū†”˙ą’˙ĄÄí˙ÜČÃūä ûûLhũLc˙o[?˙@D7õ˙Ũ­˙ÂÂ˙Ѝ˙ÁŠūŽČūĶēũÄå˙ū'˙>;˙7EW˙?:a˙jL˙% ˙áʚĻ˙ÍÁūČŅ˙ÔĮŧũÅāũöúūQPũ2k˙~B"<˙S-ũū0 ūāŌ˙Ī˜ūšÉ˙ŗžÆ˙đÛĶūŲįüķøū5P˙<j}?˙^˙X$÷ū%6˙ âĐ˙Čŧģ˙°ŦģūŋÄÅūĘėũöđ˙ņ,˙Dz_˙jũd+û0û%īũŲö˙ÅŦÍ˙ĖŦÜÕ°ĩūÔåũāųü#?ū$Yq˙Y$ũ6Dü0ũ:ûøüõĐĒū™Ĩū­ŸĄ˙Ķä˙ÜĮũËĖūí )˙Wfj˙rcūAF,˙ô˙ū߲Ė˙Ú¨ūŋ˙¸˙ŦˇüÍßũæáū˙˙LQ˙/BOR˙A4˙˙)ūôâ÷ė˙ĖšūĮŅũÕŨüÉÎ˙đ˙ęŌūõ#N@? ˙3 ˙ č˙ŌĖ˙ĶĐũĐÜūėķ˙īī˙ōķ˙éōô˙"4.%%˙(  ũûđô˙ũ˙ķã×ĐĪ˙Ųũū˙đū 'ũü ū˙˙) ˙üöæūÜ×˙Ųā˙ÜÜūÜŪũ×Ũūëîé˙÷#˙$.-ũ+#!ū ũ˙˙ōáÚūĖŧüžÎũÎÉūÛė˙ņôūņōūų˙$<?<1-+!ūū ūûôüõäũÔÆÉūČĘūĮËØŲÖâ˙ōũü'/ū068˙6>ũ?9ü9-üü÷˙ãÜÕūĐĮÃÂÂ˙ēēũÅØūāō ˙ ˙18ũ6>ũGH@˙B?ū*ú˙ŌĢ˙Īøß˙ŊĀá˙Å̍˙˛ŋũŨøũũG`˙5˙%;˙=@˙S]K˙7'ũÚūÖâČ˙ŽÃũÆŌ˙ÅĖ˙ČÆ˙Ųéũíđđ˙ (û7#˙=˙;Gū,"(˙˙+ũõũķÖūšˇ˙Ũå˙ĪĪ˙Ũ÷˙Éšūįūîü?/ü#˙ ˙˙ũ  ˙$øũÜâūôōäūįæÜũéäüÔŌūúūôū,"˙öå˙ ūũ(,˙#ūüįüđ÷ūķāæūųū×Đôûäá˙ō˙ ū ūũ'û) ũü ūøũ üûīũäå˙å˙ėņüįėũęūūūû˙ ū!ü$ũ   ˙  õūįęâÎč˙ ôņũ÷˙čëūæôū ū+ũ˙üųũ ū ū÷ķ ūčūŪņüåíû˙ķÛūŪũ˙ũũū˙üũû˙'%˙ūōōūīôüņøũüūåÜāūīíüßŪ˙čņõūū ū ˙ ˙˙##ũ ˙ū ˙ųūŨÚūääũãå˙é˙åįũéėūīøøūķūũ ˙%˙/ũ"ū '˙,˙ũ ˙ķá˙ãÍĶÚ˙ÜÖūßķūõæ˙ãî ˙&=˙22ü6)üū*ūũéüßëũâÔÍĐŲâéíęķö˙ũ  ˙'.ū%&ūũũ˙õūųéūëõūņëũåęüīí˙ķ˙ú˙öņØü/-ũ ü?7˙˙õũūüöū ō˙ĶŌŨ˙ÔÁŪ˙ōî˙ķūéõ˙ )˙0ū*õ˙ōüūęę˙ä˙âØ˙áØ×Ų˙æčūâßũņ ûúü&˙!5˙* ˙˙ôŨÛ˙ÖŅĶŲŨ˙ÚāáŲÕūįũņ˙ė-M-ū!:Jū.ū  üũūøĐÅ˙Ķ˙ÎÉüŅčûîų˙ ÷˙!"ü0ūG;˙$˙˙ų˙ũëėũëâŨ˙ŲŅũĐÛũīōūúū˙˙üû"ũ˙û-ũ ūøęėūīî˙åéí˙ņđ˙đų˙ķé˙ķ˙˙ ˙û˙ė&1˙#˙/˙ũūūíũ˙ķãū÷ęÔÛéãßíū˙˙ņâ ū"ü ũōö ūøûūų˙˙ũ˙įëôŪ˙Æ˙ ˙ķ˙ūõ˙øņũųū ˙ū ūīėũūūū ū˙˙ķÚō íķķčüõūų üũüũßãü ū *˙&ėūųņūî˙˙˙ +˙ķõ˙÷čÜ˙ôū  ũí˙á˙õíūũįø˙ ˙īũû˙ ˙ üũûíūíöūėųū ü˙ ũõĶūĶâåũ(ū168ũüûųūķ˙ ũũûāũČØũčîũōúũũîíũū ˙˙ ˙ ÷ūũũüüūõđūäŲüåīüŲüū# ūîųũüūôūũūüũũûėüčū õ˙íõ˙˙îęũũôč˙ōūø˙ė ũčüč˙đ˙ ūū˙˙˙õ îîôüû ūōđįāúúî$äéöđCŲKŲųôüį ĸ(Ļ(ō ôûĐâ Û ŦŗĶģžJéŽÔPč&õqāFíķúļĐØÉQėJíJīûĘ ŋh!H(˛ũĸD&s­…f€˙4ö ūÚ!úšßãØ;öũ6÷úÖĪŪgīXōPķ´ä×ŪÉâ)d㊠Ŋ-)ž ŋO„ą Ĩ Œ¨ YŸ ŌåēÉ ė~BøŪ…đūá6Ũ ü‘ <ųĖéZö“ J ´Āĸ âœđķĩ°>8ū€ķ+ü'ü?ú¤đúčįūtõ×ėáëū˛ČîŨū d  É­Ķ#ũÛt m ¤l õIúJúū•ô`åâîÛįĖđŠķõí…öõâīÔđ˙˙ąäåĄv [ äČ U  ˇĮD Ļ/ũGû>üIûŌō=ėīĮķđšôĢõđ@üNûęņ īŊs ŦK%ģöę°< Gš5ũ˙(đĪæ´čZíƒëØæßæ<īØôcíŒë1đĮõČõ\û ļW0ųâ¤Y!B"$7VW7ũvú$ōÕé˙įNäČŪĖŪ}âlãZä\äčķôTün{#NÍį"&Ë#  ‚Š| ,ķõŠõ|å.Ū=čÜáÕ^Ō¨Ų)ßÃí9ķoņ˛øëH˙4ĪÍY$÷'S$š÷ké/ūYĪĖî+ë0ë ã—ÔøĐ¨Ũáäcāä;ë’ņ í”ņĮø€û°ų6OĖ0"b ^0à Žà ´ũ˙ĸú÷Åđšé!åĶį'åŦāëŪĸéWėáD܀ķe ë÷` Ģ#ĄĪ ęą "ąyÉĒsáø@îčCîĩęčÖéHîŧęŪéhí9įŨöđ÷ˇ — S#Š!— – %ųQŗ‰™gøĪōņˆíīę§åÉ䐿mįķãpā–æüņŨō>÷âųŊĸ {{j-Ã*­eã"&u }{cũ ųĐķfé6ŨØ'ׁÎÄĖ(Ū*ë"ōPę$ō˙˙šˆ îhb*ō,ķ,_0l6L*”ō Ë [ +Eū;ō0ė įá˜ŨMŲsŌvØÔîķįáÖîļûl÷$ķo÷G û÷îíĘ…˛‚Ų.ųy÷\ōöîããāPįšęŊęãésė5čÂęĐõôšđęúYæ?‘ )‘×ŗ × ­ `K ‰Ÿü üĮûĢņÂëßåČëãõõęįՏâPūRūōFų÷õļŽ÷úĪûĐ =v,(äøõÍöUūPųĪņûõRųûū˙ķUå0ëŠü1õ]ęöO n¯ü8đVūK&Į há[ę Ëš ôˆũgų]ūŋŒũ|ô…ôâûęûžø]õuđēķ`ūēü$ōÃķ(Wg Oĸ¸Í Ü{W ƒ ´ P Ž Ėģôë…ížô*ķŒíÃėdö—ųôœí7÷šū˙Ņüō yĒĻp `u s öŲ ØüEûŲüÄõxī!íˇî”ëĶęšëÅî‚čÔãĪîŨüõôņ]to jŲ͏ ŸÉ#ü™úbûæōīÄėjįYäâŨYáĘéiîgîô-üˇũ“㠑FøÄCd # Ãūü¸úõķíęŗč1įģå|ãč†ėHęčėJöŽõ‘ōĘú+ ÎŃ„ŧD|sēŽ ōũúøô"ė)ėæė¸ęDé7ėõė?ėsî¨đ!ōhņĄûĶx s ˙âS!LEūĄG Ŗ û˙tfü`÷b÷:õĢņ¨ņķfōlōl÷%øßø$ũ&ŗš f9 í  ŗ“™ ĮY<´L˙˜ū˜ū|üûøû—÷dķËôŅôôÖôPømú6ũ›ūęũyŽf x Ų Ķ ž Y f U Ū!oģEũ9û8û(÷ŊķČķõĐķķƒôôÛķ5õKûĻūĒūS˙Ģ[ŠOĒ÷¤ øW˙ üžøÂøø$ö'öÔö‚õåô9ôNōõō–õØøtûĻ–=ßÎŦ § Ÿ 2 Í ? bū˙/üŖųųeø÷āõüķxņ›īœīæđ,ōąôūúÛüd˙;Ä7~đ Đ  e Ä ] — | ėu*ūŗûâų¨øõ”ō^ņœōfņõŦõRöUųöųöųÆûi Í É Å%VQMą K € MŊŌū ũú(÷ĸķķŠķķJôāôPôTôöôôØ÷zĖC š ° @ Š ¤ Ą w ģ ļG)ũLúøčöôgōÜņķsōœķŊôQõ&øüąwˇ˜ ˇ = Ė ā ŠI j ,ķĻåūˆúČ÷\øõôõĒõ™ô›ôā÷€ųč÷Z÷Æüˆ%ÂäöŒ ÜTØ2†ņūz˙īū×üø¯ų7úš÷€õĸ÷÷õ˜öGúDúSûöū{˙ŽŽ—ŖĢ"••‹øūoûíúėúpúlųmøđøtųķøų÷öø€ü}üũ€ū{øyõtjīqķjttūū˙–üûûĻú+úŠúĢúŦúˇų°ú$üũĸü ˙prV×=Æˇ!!)ŖAÔÕŅl ˙œū”ūū¸üáúVûZûõųúųŌûÅü&ū‰˙w˙u$ŠĀŠŲMÔėZčã†Ņäv2ūÚü÷ûú/úĸúŲø‰÷ŨøûŠö@öNĮÅū˙< ˜õ˛°ųMĻ;įW˜Œü`úüŠüjúiúHûúŨú•ü,ųëôãúnũéúGrÕE‘wŦđoÄ?ŦÄūmû˛üÚûĪųT÷ûû\÷7øĩüTüûTüUü&ũž9;tĶĸĶŅ˙œ˙–˙˙ũĶüüŖûúxúÛúú°ųIûÚüƒúúüČ7˙×ūīIĩ~{ŖÜ uÚčŒĢ'ĩũ¸ũTũųüoûwûÕû˜üžü7ü!ûŲû#ū~ūĄ˙A˙~Züö¸ĐŗT°k ’e‘\_ĪũÔũčūŽūwũÖũÕũ{ũ4ū3ūÛũūqkšÆÖ|Õ/ˆ,†)Đŗ L˙õūéũ9ũęũ0ü)ûØû7ü+û×ú™ũŸū¤ūŦ˛pmkĀhšŧ^´ZUūūĢūWūVũũ°ûüĩû ûŧú ü ũcüļũW˙RIž›;îã8,:57FģūļūÆũŨû‹û>û=ûBûžúcųŽų=ü‰üüũúoūņím˙˙ŅĸĮîÚŽŧh˙˙†ËūįųÛúXüü(û]ų`ųŽü›ũû‚û5ūåæū˙Ų)nŲÕ ĪzĀ –ū˙ØūĖü¯ũCūōûcûĶüÔüüŒünũlũÚūo˙&˙’ū%Õ¯ŠĢ8]°X€IG+˙æū:ũĩüČũŠũ„ũFũ@ũÔũįūŒũÕũĄūéV寰iķņ!#ÛÍEv˙ôū'ū^ũĻũĸũ¨ũœü×û üĸübü ü˛ũ8˙ÄČŽL ˆ‰HEÃĀÂü@:„˙?˙ĮūÃūMũũũVü™üũŸü[üŖü“ũŲũZũĪū{;|´1jņōhhw=ū˙>˙ū(ū˛ũŽüũũXüŅüEũ™üœüœüLũ5ū˙Y˙û˙vâU454 ¤KÃōZ˙Y˙íūūĮüoũnũĪüÎüũ2ü/üŖüūŊūŠū4kĄĩą˛­Gqsĸl˙˙š˙ĘūfūËũšũ›ũũũũ ũ ũrũ:ū¤ūĐūËÅ]ļå‚vvŦBØLĨy€ôžB˙ū#ūSūÄũČũõũËũ÷ũ+ū+ū‰ū¸ūĨ˙Í˙5ŗÉW'$÷PôÅÄ–<†L˙ôūËū@ū˜ũëũ˜ũ™ũ—ũËũŸūNūĪūĒ˙*YU2˙„ŠĢĐ&ŖOųOĶĨU‚˙…˙Šūéũūëũ˜ũœũėũxũsũôũBūžū˙^˙Ø˙ą˙˙˙Č<dОˆ`vtÃuą˙A˙˙¨ū3ūūÁũĸũŸũ¤ũVũ6ũĻũ‰ūŠūeū!˙ļ˙%nnrŽē×l׎ג’˙s˙āūėũ0ūîũĘũ>ũˆũ‡ũhũ‹ũūƒū_ū1˙$cĶ–|0˜ÛˇĩPęé#ŧ˙˙;˙“ū™ū•ū6ūZū—ū}ū8ū}ūßū˙`˙`ŋß]|\[Ö÷Ô–•TWô~Yå˙E˙ņūëūōūĪū˙íūöūŗū˙.˙×ū2˙å˙VėĖ’Ė•“?¯=9;Œrū˙˙Ē˙™ū%ūžũÔũūķũßũ›ūđū&˙7ŧŋ&x­ZuŽŠoŽĄ=œ‚OĐ˙–˙œ˙}˙O˙2˙j˙3˙˙˙†˙f˙¸˙HMb|}ąwcz/3.n˙B˙øūĘūœūƒū ūƒūŒūŗūėūžūĨū˙C˙z˙‰˙/oŖmĸ˛û°Ÿ›Y[ė˙{˙“˙{˙˙āū˙˙˙˙čūéūB˙h˙™˙Ķ˙BĪ;ģøúöĪšŽ{PŲ˙ë˙Ä˙‡˙`˙M˙8˙N˙;˙%˙=˙c˙Ã˙Ę˙û˙#=p°Š›—Ē…ƒ`maH'˙˙ĩ˙§˙Ŗ˙“˙“˙˙–˙ļ˙¨˙Č˙đ˙É˙¨˙Ü˙$"Iˆ~CYfF34#ņ˙ū˙î˙Î˙Ŧ˙›˙˙Š˙~˙š˙Ÿ˙˙Ž˙Ņ˙Ũ˙Nul…œĄ~œ jb:3 !á˙Ã˙Ķ˙¤˙Ļ˙—˙Ŗ˙œ˙’˙Ģ˙Ņ˙Æ˙Ķ˙Ô˙ō˙á˙ô˙ū˙(˙˙˙˙ū˙Ø˙ņ˙Ö˙Ŧ˙‘˙˙Ž˙Ž˙ž˙Ą˙“˙„˙ˆ˙Ž˙Ž˙ą˙É˙ķ˙ )$61%æ˙™˙Ē˙Ĩ˙’˙r˙Ÿ˙Ž˙’˙›˙“˙›˙Ä˙“˙Š˙Ü˙L\KPc72:.ũ˙Ô˙Ž˙ą˙¯˙›˙˜˙ŗ˙Ž˙ĩ˙¯˙Ę˙Ķ˙Į˙Ā˙ū˙"5o_c`VK_bG8'ū˙ā˙×˙Ė˙Í˙š˙Ã˙Í˙ē˙Ö˙Û˙Õ˙ã˙/F/3U<AE80'$ä˙ė˙Ķ˙Ú˙Ņ˙Ë˙Æ˙Ę˙Ķ˙ŋ˙Â˙ˇ˙Ō˙Ũ˙÷˙-EQUi_W]`HR=0ũ˙ė˙Ô˙Ķ˙Ō˙Æ˙É˙Ī˙Ō˙Ö˙ã˙×˙Ú˙ā˙á˙ō˙ū˙ ũ˙ô˙č˙ķ˙ņ˙Ũ˙Õ˙Õ˙Č˙ģ˙ŋ˙Ō˙Ė˙Ķ˙Î˙Ō˙Ũ˙Ū˙æ˙ņ˙÷˙ ú˙ô˙ķ˙ô˙æ˙ã˙Ų˙Ø˙Í˙×˙Ö˙Ī˙Ņ˙Đ˙Ø˙Û˙ß˙ō˙  ö˙đ˙ī˙į˙Ū˙î˙ã˙î˙å˙á˙ė˙đ˙ö˙$õ˙đ˙đ˙õ˙í˙ë˙õ˙ū˙đ˙˙˙õ˙ų˙û˙˙˙  ũ˙˙˙ū˙í˙ņ˙é˙ë˙å˙á˙æ˙ë˙ę˙î˙í˙ų˙ !#  ÷˙õ˙î˙ę˙ė˙ë˙ę˙ņ˙å˙ī˙é˙÷˙÷˙û˙û˙ũ˙ū˙ũ˙˙˙ũ˙÷˙õ˙ō˙ķ˙ō˙ė˙ō˙î˙ė˙ī˙î˙ô˙ô˙ō˙ō˙ō˙û˙÷˙ô˙ö˙û˙˙˙˙˙˙˙ü˙˙˙˙˙ū˙ų˙ų˙ü˙õ˙÷˙ô˙ų˙ø˙÷˙û˙÷˙ũ˙ų˙ų˙ų˙ų˙ü˙ū˙˙˙ũ˙ū˙˙˙û˙ö˙û˙ū˙˙˙  ˙˙˙˙ũ˙˙˙˙˙ū˙˙˙˙˙˙˙ū˙ü˙ũ˙ū˙ü˙˙˙ü˙ū˙˙˙  ū˙ũ˙ū˙ü˙ũ˙û˙˙˙ü˙˙˙û˙û˙û˙ų˙ü˙û˙ū˙ũ˙ü˙ú˙ü˙ū˙ü˙ũ˙ü˙û˙ū˙û˙ũ˙û˙ų˙ú˙ü˙ø˙ū˙û˙ũ˙û˙ũ˙û˙ū˙ũ˙ũ˙ū˙˙˙˙˙˙˙ü˙ü˙ū˙˙˙ũ˙û˙˙˙û˙˙˙ú˙˙˙ũ˙ũ˙ũ˙ū˙ū˙ū˙˙˙ũ˙ü˙ū˙˙˙ũ˙ũ˙˙˙˙˙˙˙˙˙ũ˙ũ˙ũ˙ū˙˙˙˙˙˙˙˙˙˙˙˙˙ū˙ũ˙˙˙ū˙ü˙ũ˙ū˙ũ˙ū˙˙˙˙˙ū˙˙˙ú˙û˙˙˙ū˙ũ˙û˙ũ˙ũ˙˙˙˙˙û˙ũ˙˙˙ū˙ū˙ū˙ū˙ū˙ū˙ū˙ū˙ũ˙ũ˙ū˙ü˙˙˙ū˙ũ˙˙˙˙˙ū˙ū˙ū˙ū˙˙˙˙˙˙˙ū˙ü˙ú˙ũ˙ū˙ü˙ü˙˙˙˙˙ū˙ū˙ū˙˙˙˙˙˙˙˙˙ū˙ū˙ū˙˙˙˙˙˙˙ũ˙˙˙ū˙˙˙ū˙ü˙ũ˙ū˙ũ˙ū˙˙˙ũ˙ú˙ũ˙˙˙˙˙˙˙ũ˙ū˙˙˙˙˙ũ˙ũ˙ū˙˙˙ū˙ū˙ū˙˙˙˙˙ū˙ū˙ũ˙˙˙ū˙ū˙ü˙ū˙ū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ū˙ü˙ũ˙˙˙˙˙ū˙˙˙˙˙˙˙˙˙ū˙û˙û˙ū˙˙˙˙˙˙˙˙˙˙˙˙˙ũ˙ū˙˙˙ũ˙ü˙ū˙˙˙ũ˙ū˙˙˙˙˙˙˙˙˙ū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ū˙ũ˙ū˙˙˙ū˙˙˙ū˙˙˙ū˙ü˙˙˙˙˙ü˙ũ˙û˙˙˙˙˙ü˙ũ˙ū˙ū˙ū˙˙˙˙˙ū˙˙˙ū˙ũ˙ū˙ũ˙ū˙ü˙ü˙ũ˙ū˙ū˙ū˙ū˙ū˙ū˙ū˙˙˙˙˙ū˙ũ˙˙˙˙˙ũ˙˙˙˙˙ũ˙ũ˙˙˙ū˙ū˙ũ˙ü˙ü˙ü˙˙˙˙˙ū˙˙˙ū˙˙˙˙˙ū˙˙˙˙˙˙˙ū˙ũ˙˙˙˙˙ū˙ū˙˙˙˙˙˙˙˙˙Nagstamon-master/Nagstamon/resources/menu_template.svg000066400000000000000000000233061505160700500236220ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/nagstamon.1000066400000000000000000000031441505160700500223110ustar00rootroot00000000000000.\" Man page generated from reStructuredText. . .TH NAGSTAMON 1 "2016-09-05" "2.0" "" .SH NAME Nagstamon \- Nagios status monitor which takes place in systray or on desktop . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp nagstamon [alternate\-config] .SH DESCRIPTION .sp Nagstamon is a Nagios status monitor which takes place in systray or on desktop as floating statusbar to inform you in realtime about the status of your Nagios monitored network&. Nagstamon connects to multiple Nagios, Opsview, Icinga, Centreon, Op5Monitor, Checkmk Multisite and Thruk monitoring servers. Experimental support for Zabbix, Zenoss and Livestatus is included. .sp The command can optionally take one argument giving the path to an alternate configuration file. .SH RESOURCES .sp \fI\%https://nagstamon.de\fP .SH AUTHOR This manual page has been written by Carl Chenet and updated by Henri Wahl .SH COPYRIGHT This manual page is licensed under the GPL-2 license. .\" Generated by docutils manpage writer. . Nagstamon-master/Nagstamon/resources/nagstamon.appdata.xml000066400000000000000000000040751505160700500243660ustar00rootroot00000000000000 de.nagstamon.nagstamon de.ifw_dresden.nagstamon CC0-1.0 GPL-2.0 Nagstamon

The status monitor for the desktop
  • fix sound problem
  • fix IncingaDBWeb

Nagstamon is a status monitor for the desktop. It connects to multiple Nagios, Icinga, Opsview, Centreon, Op5 Monitor/Ninja, Check_MK Multisite and Thruk monitoring servers. It resides in systray, as a floating statusbar or fullscreen at the desktop showing a brief summary of critical, warning, unknown, unreachable and down hosts and services. It pops up a detailed status overview when being touched by the mouse pointer. Connections to displayed hosts and services are easily established by context menu via SSH, RDP, VNC or any self defined actions. Users can be notified by sound. Hosts and services can be filtered by category and regular expressions.

https://nagstamon.de https://nagstamon.de/documentation https://github.com/HenriWahl/Nagstamon/issues de.nagstamon.nagstamon.desktop #c9facc #59b65f Henri Wahl https://nagstamon.de/assets/images/teaser_1.png https://nagstamon.de/assets/images/teaser_2.png flathub@nagstamon.de Nagstamon-master/Nagstamon/resources/nagstamon.desktop000066400000000000000000000004001505160700500236120ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Nagstamon Comment=Nagios status monitor Icon=nagstamon Exec=nagstamon Terminal=false Categories=System;Monitor;GTK; Keywords=system;monitor;remote; StartupNotify=true GenericName=Nagios status monitor for the desktop Nagstamon-master/Nagstamon/resources/nagstamon.icns000066400000000000000000005267471505160700500231300ustar00rootroot00000000000000icns­įis32‰ŽH}€}}|}z{z{yvCAĪøŨÔŅĖÆÃĀšĩ´Æ¤<3ôĘÚāáŨŪáäáŌĸž80ë™˙˙|˙˙Ŧ†˙˙Ŗļ50ę˜˙˙\bĒ˙”!˙˙•ĩ50å’ŪÜEgNÕ"Εą51á•˙˙7ž1]K˙˙—­51Ū–˙˙fá˙˙˜Ģ61M܊˙ûV˙&˙˙š§62Ų˙˙U˙ß˙˙¤75Ų›ÃäëęäéôōįŞ§9AÆĮ¯ĒФœ™œ“‘“›GDƒ‰„€€}~|{~€wBƒ  Ž3Hƒ€€€~~}~|wCAĐūčâßÚÔŌÎČÄŋĖĨ=3ú›V^accddgh^¨Ã91ôW‡‹€Œ Ž‘”–’€ž62ôQln€sym4 sž62íi••Wf_x PG†ē72čvˇ¸Gv,'›ŒŠ´62ãr¯Ĩ(hŽ~‡°62ߎ˙ųX˙(˙˙ĄĒ62ڎ˙˙V˙ā˙˙ŸĨ75Ų›Ãäëęåéôō誧9AÆĮ¯ĒФœ™œ“‘“›GDƒ‰„€~}}{~‚wHƒ  Ž2Hƒ€€€~~}~|wCAŅūčâßÚÔŌÎČÄŋĖĨ=3ú›V_accdegh^¨Ã92ôXŠ€Œ ’•—”€ž62õUsx}r=wĀ73ņU[__dh3 ~ž73îTNRZJ‚€ē74íZSA„‚š84ėI‡…´84æK‡€¯86᠇!ŽŽ:BÉŌÅÆÅÁŧ¸ˇ°ŦĸžGD…Š‡†……ƒ„†yH   s8mkUĢ­­­­­­­­­­ŠH!ū˙˙˙˙˙˙˙˙˙˙˙˙û<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙<˙˙˙˙˙˙˙˙˙˙˙˙˙˙%˙˙˙˙˙˙˙˙˙˙˙˙˙ūqĪÛâęđöõđéâÚÍa$7GOMG6"il32‚ĀUC“ADU‚DI~—˜˜——–••””““’‘‘ŽŽŠnDD€JBMŲíŪÚØ×ÔĶŅĪÍËĘČÆÅÂĀžŊģš¸ēĐŽD9B›ëÎËÄŧŧē¸¸ļ´˛ą¯Ž­ĢЍĻĨŖĸŸžĖrCD¸×Ėˆ¨ŽŽ¯°€ą˛ŗ´´ĩĩŗ¨ĸ›ąDDēŅēŒų˙ ņĢšĻ’DDšĪ­ŧ€˙퀛Ŗü€˙ܛ›Ą˙ ļ›Ĩ’DD¸ÍŦĀ˙đ‚É€˙Ķ‚‚‰˙ ¸›Ŗ‘DD¸ÍŦÀ˙īx„ú˙˙Īxxr˙ ¨šĄ‘DDˇËŦÁ€˙î‚nŧ˙˙ËE˙ ¨˜ DDˇÉ̎€æ×€dkdmß␠Ö ¤˜ŸDDĩÆĢž€˙ė€ZĒZV“˙Ĩ˙ Š–DDĩÅĒŧ€˙ë€Rād íĨ˙ Ē–›ŽDD´ÃĒŊ€˙ ëRRMØ˛xĨ˙ Ģ”šŽDDŗÂŠŊ€˙ ëL*Ō˙7 ށ˙ Ģ“˜DDŗĀŠŊ€˙ äŌ˙š˙­’–DD˛žŠŠãââĮ€ēââ8 â Ģ‘”ŒDDąŊ¨¯€˙á€Ō˙˙˙ Ž“‹DDąŧ§°€˙á€Ō€˙G€˙ Ž‘‹DD°š¨Ē€˙á€Ō€˙ʁ˙ ¯ŠDD¯ˇŠ—ü˙ōž…ŽŠDDŽš­Ą™ŠŦŦ­­ŽŽ¯¯°°ą€˛ŗŗ˛ēĢ|”‡DB˜ĘĢŠĨ¤¤ŖŖĸĸ€ ŸŸ€œ'š›|zŽrEASÔÉ´­ĢǧĨŖĄŸœ™—•”‘Ž‹‰‡ŽĢ­FI€BQŽĸĄĄ  žžœ››š™˜˜—–••”“‘{HD‚(87689;=@CFILKHFB?<:86784"„ !$'*.001.,+($Œ   ČĀUC“ADU‚DI~—˜˜——–••””““’‘‘ŽŽŠnDD€QBMŲíŪÚØ×ÔĶŅĪÍËĘČÆÅÂĀžŊģš¸ēĐŽD9B›ëÎËÄŧŧē¸¸ļ´˛ą¯Ž­ĢЍĻĨŖĸŸžĖrCD¸×Ėm``abbccdeefg€hijn˜ĸ›ąDDēŅēgŽ– …›šĻ’DDšĪ­]‘Œ m›Ĩ’DD¸ÍŦ[‘‚ d›Ŗ‘DD¸ÍŦWŒxiK7+@šĄ‘DDˇËŦQ‰nlE‚ ?˜ DDˇÉĢ[€ts€dedet^ M˜ŸDDĩÆĢ…€°Ļ€Z„ZV_…V… w–DDĩÅĒ‚€ŦĄ€RœT |V… x–›ŽDD´Ãǃ€Ŧ ĄRRM‚^?V… y”šŽDDŗÂЃ€Ŧ ĄL*n…J…y“˜DDŗĀЃŦ§ž„n…`…{’–DD˛žŠŸĶŌŌš€­ŌŌ4 Ō ¤‘”ŒDDąŊ¨¯€˙á€Ō˙˙˙ Ž“‹DDąŧ§°€˙á€Ō€˙G€˙ Ž‘‹DD°š¨Ē€˙á€Ō€˙ʁ˙ ¯ŠDD¯ˇŠ—ü˙ōž…ŽŠDDŽš­Ą™ŠŦŦ­­ŽŽ¯¯°°ą€˛ŗŗ˛ēĢ|”‡DB˜ĘĢŠĨ¤¤ŖŖĸĸ€ ŸŸ€œ'š›|zŽrEASÔÉ´­ĢǧĨŖĄŸœ™—•”‘Ž‹‰‡ŽĢ­FI€BQŽĸĄĄ  žžœ››š™˜˜—–••”“‘{HD‚-8788:;=ADGKNMJGC@=;98784"„ !$),022100-*$!Œ   ČĀUC“ADU‚DI~—˜˜——–••””““’‘‘ŽŽŠnDD€QBMŲíŪÚØ×ÔĶŅĪÍËĘČÆÅÂĀžŊģš¸ēĐŽD9B›ëÎËÄŧŧē¸¸ļ´˛ą¯Ž­ĢЍĻĨŖĸŸžĖrCD¸×Ėm``abbccdeefg€hijn˜ĸ›ąDDēŅēgŽ– …›šĻ’DDšĪ­]‘Œ m›Ĩ’DD¸ÍŦ[‘‚ d›Ŗ‘DD¸ÍŦWŒxiK7+@šĄ‘DDˇËŦQ‰nlE‚ ?˜ DDˇÉĢOˆdK „ B˜ŸDDĩÆĢG†ZV&† @–DDĩÅĒC…RA ‡ A–›ŽDD´ÃĒCƒRM#ˆ B”šŽDDŗÂŠCRL*ŠB“˜DDŗĀŠCRG4Œ D’–DD˛žŠ  G‘”ŒDDąŊ¨‘ E“‹DDąŧ§‘ E‘‹DD°š¨ ‘ OŠDD¯ˇŠ\ĸ…ŽŠDDŽš­Ąb'$$%%&&''(()€*++5“Ģ|”‡DB˜ĘĢŠĨ¤¤ŖŖĸĸ€ ŸŸ€œ'š›|zŽrEASÔÉ´­ĢǧĨŖĄŸœ™—•”‘Ž‹‰‡ŽĢ­FI€BQŽĸĄĄ  žžœ››š™˜˜—–••”“‘{HD‚-8788:<>ADGKNNKGD@=;98784"„ !&),022320-*$!Œ   Čl8mk.3333333333333333333333)Rî˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ā8ö˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ä a˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙9x˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kx˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Kh˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙?+ü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ņtû˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ķR?|›Ĩ°ģÅĐÚåī÷÷íãŲÎÄšŽ¤™Žu4&5CQ^ju†‹ŽŠ…}ti\OA3#  ih32˙ŋFDŸCDBDG„ACRšēŊŧŧģģēšš¸¸ˇˇļ€ĩ´ŗŗ˛€ą °¯¯ŽŽ­­ŦĢĻ~FCE‚+BCjíúėäáāŪŨÜÛÚØ×ÖÕÔÔŌŅĪĪÍĖËĘÉĮÆÅÄÃÂĀĀÁĪŪĀOC€+BJéöÖÍĖËÉČÆÅÃÂÁŋžŧģš¸ˇĩ´˛ą°Ž­ĢĒŠ§ĻĨŖĸ ŸąÚŦCB+D‚ûØÎĖËÆē¸ˇļĩ´ŗŗ˛ąą¯ŽŽ­ŦŦĢŠŠ¨§§ĻĨĨŖŖ Ÿžœ¸ÚSD+BŸōÎÍʝ{|„…†‡‰‹ŒŽ‘‘“•–˜™šœžŸĄŖ¤Ļ§ŠŖœ›ŲjDBĨëÍËĸz˛õ—˙ėŽŦŸ™˜ŌoDB¤éËĘ…Ļš˙üŦ§˜—ŅoDB¤éĘÉ{܂˙ė‚Ŧģƒ˙úŦļƒ˙ÃĢ—–ĐnDB¤įÉČ}č‚˙デ߂˙ø‡•ƒ˙ËŦ–•ĪnDBŖįČÆ~į‚˙Ⴡ‚˙øƒ˙ĘŦ•“ÎnDBŖæĮÅæ‚˙ā„z؁˙÷zŠƒ˙ļ­”’ÍnDBŖåÆÄå‚˙Ū„sŽū€˙÷ssd?7ƒ˙ĩŽ“‘ÍmDBĸåÄÂä‚˙܅lĪ€˙öV#ƒ˙ĩ°‘ĖmDBĸãÄä‚˙ہfi€f€ū˙˙ōƒ˙ĩ¯ŽËmDBĄâÂĀ…Ö‚íˁ_†q€_šéãՁƒâŽąĘmDBĄáÁŋ‡â‚˙؁X‰ÁXXP:û˙đƒ˙ļ˛ŽŒÉlDBĄáŋžˆá‚˙ցR…ũm9›˙đƒ˙‹ČlDB āžŊ‰á‚˙ցR…˙ŗ€øđƒ˙ļŗ‹ŠČlDB ŪŊŧ‹á‚˙րRQm˙ũ/€“đƒ˙ˇ´Š‰ČlDB ŪŧģŒá‚˙ÖRQ6K˙˙ą€įƒ˙ˇ´‰‡ĮlDBŸŨģšŽá‚˙ Õ. K˙˙ū6€|ƒ˙ˇĩ‡†ÆlDBŸÜš¸â‚˙ÁK€˙¸€ ƒ˙ˇļ†…ÅkDBžÛ¸ˇŧĪÍ<Í2„ƒÍŦˇ…„ÄkDBžÚˇĩ’ׂ˙ÁK˙Á„ƒ˙¸ˇ„‚ÃkDBÚﴓׂ˙ÁK‚˙Fƒƒ˙¸¸ƒÂjDBØĩŗ•ׂ˙ÁK‚˙Č‚ƒ˙¸š‚€ÂjDBØŗ˛–ׂ˙ÁKƒ˙N‚ƒ˙¸ē€ÁjDBœ×˛ą˜Í‚˙ÁKƒ˙Ёƒ˙ĩģ~ĀjDBœÖą°›Ĩ›˙ŗĩ~|žjDB›Õ°ŽĄ›Ŋ˜˙ûąÎ}{ŋjD+B—ÚŽ­Ģ ŸšžžŸ  ĸĸŖ¤¤ĨĻϧ¨¨ŠĒĢĢŦ­ŽŽ°Āί~{|ÅgD B„æŗŦĒŠĻ¤Ĩƒ¤ĨĨ¤ĨŖ€¤Ĩ€¤Ĩ€¤ Ŗ¤¤ }|z“ÆWD+BRā×ŽŠ¨ĻĨ¤ĸĄŸžœ›š˜—•”“‘ŽŒŠ‰‡†…ƒ‚€}|{…ŊŽCD+DCãÜÂĩą°¯ŽŦĒǍ§Ļ¤ŖĄĄŸžœ›š˜˜–”“’‘‹ĸÃŊ[C9‚)DCj˛ŌÖÖÕÔĶŌŌŅĐĪĪÎÍĖĖËĘÉČČĮÆÅÄÃÂÂÁÁĀŋžĩ“SCD„=AĄC@4…'  "!$(,/239:?CEHKLLJHDB=984/-+&#  ‡# !&'+,/1355776331.-)($"   €   œ˙ ˙ŋFDŸCDBDG„ACRšēŊŧŧģģēšš¸¸ˇˇļ€ĩ´ŗŗ˛€ą °¯¯ŽŽ­­ŦĢĻ~FCE‚+BCjíúėäáāŪŨÜÛÚØ×ÖÕÔÔŌŅĪĪÍĖËĘÉĮÆÅÄÃÂĀĀÁĪŪĀOC€+BJéöÖÍĖËÉČÆÅÃÂÁŋžŧģš¸ˇĩ´˛ą°Ž­ĢĒŠ§ĻĨŖĸ ŸąÚŦCB+D‚ûØÎĖËÆē¸ˇļĩ´ŗŗ˛ąą¯ŽŽ­ŦŦĢŠŠ¨§§ĻĨĨŖŖ Ÿžœ¸ÚSD+BŸōÎÍʝ{|uuvwxz{|}ƒ…†ˆ‰ŠŒŽ‘’“›§ŠŖœ›ŲjDBĨëÍËĸz`Š–˜ —‚uŦŸ™˜ŌoDB¤éËʅ]š•ˆ§˜—ŅoDB¤éĘÉ{m›ŽgĢ—–ĐnDB¤įÉČ}q›‡dŦ–•ĪnDBŖįČÆ~l›_Ŧ•“ÎnDBŖæĮÅf•z wcOD8*$­”’ÍnDBŖåÆÄb’sd?‚Ž“‘ÍmDBĸåÄÂ\lV#…°‘ĖmDBĸãÄWŽf[ˆ¯ŽËmDBĄâÂĀ…”‚§–_sh€_“vlƒsfąĘmDBĄáÁŋ‡š‚¯›XrXXP+ƒ…}ƒ…n˛ŽŒÉlDBĄáŋžˆ˜‚Ŧ—RlĢ`9Q…}ƒ…n˛Œ‹ČlDB āžŊ‰˜‚Ŧ—RlĒm€}ƒ…nŗ‹ŠČlDB ŪŊŧ‹˜‚Ŧ—€RQPˆ„€M}ƒ…o´Š‰ČlDB Ūŧ쌘‚Ŧ—RQ6'……\€ xƒ…o´‰‡ĮlDBŸŨģšŽ˜‚Ŧ “. '……„€Aƒ…oĩ‡†ÆlD BŸÜš¸˜ŦĢĨ›g'€…`€ƒ…oÅkDBžÛ¸ˇĸ°­€Ŧ„3Ŧ*„ƒŦ™ˇ…„ÄkDBžÚˇĩ’ׂ˙ÁK˙Á„ƒ˙¸ˇ„‚ÃkDBÚﴓׂ˙ÁK‚˙Fƒƒ˙¸¸ƒÂjDBØĩŗ•ׂ˙ÁK‚˙Č‚ƒ˙¸š‚€ÂjDBØŗ˛–ׂ˙ÁKƒ˙N‚ƒ˙¸ē€ÁjDBœ×˛ą˜Í‚˙ÁKƒ˙Ёƒ˙ĩģ~ĀjDBœÖą°›Ĩ›˙ŗĩ~|žjDB›Õ°ŽĄ›Ŋ˜˙ûąÎ}{ŋjD+B—ÚŽ­Ģ ŸšžžŸ  ĸĸŖ¤¤ĨĻϧ¨¨ŠĒĢĢŦ­ŽŽ°Āί~{|ÅgD B„æŗŦĒŠĻ¤Ĩƒ¤ĨĨ¤ĨŖ€¤Ĩ€¤Ĩ€¤ Ŗ¤¤ }|z“ÆWD+BRā×ŽŠ¨ĻĨ¤ĸĄŸžœ›š˜—•”“‘ŽŒŠ‰‡†…ƒ‚€}|{…ŊŽCD+DCãÜÂĩą°¯ŽŦĒǍ§Ļ¤ŖĄĄŸžœ›š˜˜–”“’‘‹ĸÃŊ[C9‚)DCj˛ŌÖÖÕÔĶŌŌŅĐĪĪÎÍĖĖËĘÉČČĮÆÅÄÃÂÂÁÁĀŋžĩ“SCD„=AĄC@7…'  "!$(,/27;=@CHKLNNLJGD@;861/+)%!  ‡# $&)-0134678877530/,($"    €   œ˙ ˙ŋFDŸCDBDG„ACRšēŊŧŧģģēšš¸¸ˇˇļ€ĩ´ŗŗ˛€ą °¯¯ŽŽ­­ŦĢĻ~FCE‚+BCjíúėäáāŪŨÜÛÚØ×ÖÕÔÔŌŅĪĪÍĖËĘÉĮÆÅÄÃÂĀĀÁĪŪĀOC€+BJéöÖÍĖËÉČÆÅÃÂÁŋžŧģš¸ˇĩ´˛ą°Ž­ĢĒŠ§ĻĨŖĸ ŸąÚŦCB+D‚ûØÎĖËÆē¸ˇļĩ´ŗŗ˛ąą¯ŽŽ­ŦŦĢŠŠ¨§§ĻĨĨŖŖ Ÿžœ¸ÚSD+BŸōÎÍʝ{|uuvwxz{|}ƒ…†ˆ‰ŠŒŽ‘’“›§ŠŖœ›ŲjDBĨëÍËĸz`Š–˜ —‚uŦŸ™˜ŌoDB¤éËʅ]š•ˆ§˜—ŅoDB¤éĘÉ{m›ŽgĢ—–ĐnDB¤įÉČ}q›‡dŦ–•ĪnDBŖįČÆ~l›_Ŧ•“ÎnDBŖæĮÅf•z wcOD8*$­”’ÍnDBŖåÆÄb’sd?‚Ž“‘ÍmDBĸåÄÂ\lV#…°‘ĖmDBĸãÄWŽf[ˆ¯ŽËmDBĄâÂĀ…R_;‰"ąĘmDBĄáÁŋ‡K‹XP‹ ˛ŽŒÉlDBĄáŋžˆGŠR9Œ ˛Œ‹ČlDB āžŊ‰GˆRM Ž ŗ‹ŠČlDB ŪŊŧ‹G†RQ0!´Š‰ČlDB ŪŧģŒG„RQ6‘!´‰‡ĮlDBŸŨģšŽG‚RK. “!ĩ‡†ÆlD BŸÜš¸HRPC.•!ÅkDBžÛ¸ˇ ™'ˇ…„ÄkDBžÚˇĩ’›"ˇ„‚ÃkDBÚļ´“›"¸ƒÂjDBØĩŗ•›"š‚€ÂjDBØŗ˛–›"ē€ÁjDBœ×˛ą˜›+ģ~ĀjDBœÖą°›)›sĩ~|žjDB›Õ°ŽĄ”™8Ν}{ŋjD+B—ÚŽ­Ģ ŸvLIIJKKMMNOOPQQRSSTUVVWXYYfŠÎ¯~{|ÅgD B„æŗŦĒŠĻ¤Ĩƒ¤ĨĨ¤ĨŖ€¤Ĩ€¤Ĩ€¤ Ŗ¤¤ }|z“ÆWD+BRā×ŽŠ¨ĻĨ¤ĸĄŸžœ›š˜—•”“‘ŽŒŠ‰‡†…ƒ‚€}|{…ŊŽCD+DCãÜÂĩą°¯ŽŦĒǍ§Ļ¤ŖĄĄŸžœ›š˜˜–”“’‘‹ĸÃŊ[C9‚)DCj˛ŌÖÖÕÔĶŌŌŅĐĪĪÎÍĖĖËĘÉČČĮÆÅÄÃÂÂÁÁĀŋžĩ“SCD„=AĄC@7…'  "!$*,/37;>AEHLMOOMKGDA<:63/+)%!  ‡ $&)-01466€877532/,(&"     €   œ˙ h8mk (—ËŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨŨÛĀN÷˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ã%ô˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Đ{˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙>ĸ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙fĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙iĨ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙i‡˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙K1ū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙į „˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ųK hŲ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũÃE $6HS^it‰•ŸĒ´ŋÉŌÛáååāŲĪÆŧ˛§’ˆ|qg\PF1! '1ˆ€CKŸųûøāĪÎÎÍÍĖĖËËÅŊŗĻĄ  ĄĄ  „Ą €Ąĸ€ĄĸĸĄĸĄĄĸŖĸ„ŖĸˆŖ¤€Ŗ€¤Ŗƒ¤Ŗĸ Ÿ€žœœ››ĄĐÛÚŋPCCDDˆ€CqMŗųûõĶÎÎÍÍĖĖËÉŧĸ…zz{{|}}~~€€‚‚ƒ„„…††‡ˆˆ‰‰Š‹ŒŒŽŽ‘‘’““””•––—˜˜™™š››œžŸŸ  ĄĸĸŖ¤¤ĨĨϧ§¨¨ĻĸŸžœœ››ššĀØÚĪRCCDDˆ€COĀúúėÎ΀ÍĖËČŗ‰yz€{~‚……††€‡ˆˆ‰‰Š€‹ŒŽ€‘‘€’ “””••––——˜˜™€š›œœ€žŸŸ €ĄĸĸŖ¤¤€ĨĻ€§ϧ¨ŠĒФžœœ››šš™¯ÕÚÖWCCDDˆ€CPĮúųäÎÎÍÍĖËÉ´ƒyzz|‰–Ĩą‡¸‚š‚ē†ģŧŧ…Ŋƒžƒŋ„Ā„Á ÂĀŧš˛ĒŠĒĢĢŖ€› 𙙤ĶŲÖZDCDDˆ€CPĮú÷áÎÍÍĖËĘŊŠyzzžÎöÃ˙üãÁ¯ĢĢŦŦĸœ›š™™˜žŌŲÖZDCDDˆ€CPĮų÷ŪÍÍĖËËÆ¤yzzƒŗ÷Č˙ÔŗŦŦ­Šš™™˜˜›ŌŲÖZDCDDˆ€CPĮų÷ŪÍĖĖËËŋ‡zząũĘ˙Ø˛Ŧ­Ž š™€˜›ŌŲÕZDCDDˆ€COÆų÷ŪĖĖËËɡ{z|›õĖ˙Č¯ŽŽĻ›™˜˜—›ŌŲÕZDCDDˆ€COÆųöŨĖËËĘĮŦzz†É˙Œũū˙Šũ˙đšŽ¯Š›˜˜——šŅØÕZDCDDˆ€COÆøöŨËËĘĘÅĻ{{‘ōŒ˙øâ‹×æüŽ˙öāˆ×Ũö˙ŋ¯°Ģ›˜——–™ĐØÕZDCDDˆ€C OÆøöÛËĘĘÉŤ{}œ˙ė̊‰‹‰§įŽ˙åĨ‰†‹Šœå˙Ͳ°Ŧ›——––šŅØÔZDCDDˆ€C OÆøõÛĘĘÉÉÄĨ{Ĩ˙ėNJŠ‹Ãû˙æĻ‰†‹Š›å˙ÖĩąŦš—––•™Đ×ÔZDCDDˆ€C OÆ÷õÛĘĘÉČÄĨ|€§˙ėŠ‡Šˆ†Ŗä˙åŖ†‡ˆ™ä˙Ûˇą­š—–••˜Ī×ÔZDCDDˆ€C OÆ÷õÛĘÉČČÃĨ|€§˙맅‹†ŠĀúŒ˙åĸ„††…—ä˙Úˇ˛­š–••”˜Ī×ĶZDCDDˆ€C OÅ÷õÛÉČČĮÃĨ}§˙ëĨ‚‹ƒžáŒ˙䟁†ƒ‚•ã˙Úˇ˛Ž™••””˜Ī×ĶZDCDDˆ€COÅ÷ôڀČĮÃĨ~§˙ęŖ€Œ„ŧú‹˙䞆€’â˙ŲˇŗŽ™•€”˜ĪÖĶZDCDDˆ€C OÅöôÚČČĮĮÂĨ~‚§˙ęĄ}Œ~|˜Ū‹˙ã›|†~}â˙ŌļŗŽ™•””“—ÎÖĶZDCDDˆ€C OÄöôÚČĮĮÆÂ¤‚¨˙ę z|~¸ųŠ˙âšz†|{Žá˙É´´Ž˜””““–ÎÖŌZDCDDˆ€C OÄöôŲĮĮÆÆÂĨƒ¨˙éžxyw“ÛŠ˙â˜w‡yá˙Įĩ´Ž˜”““’–ÎÕŌZDCDDˆ€C OÄõķØĮÆÆÅÁĨ€ƒ§˙éœuŽwxŗø‰˙á–uƒwxywnzۍ˙Čĩĩ¯˜““’’•ÍÕŌZDCDDˆ€C OÄõķØÆÆÅÅÁĨ€ƒ§˙čšsŽtrØ‰˙ā”r€tuvtj\L7Aˍ˙Čļĩ¯—“’’‘•ÍÕŌYCCDDˆ€C OÄõķØÆÆÅÄĀĨ„§˙č˜pr¯öˆ˙ā’prstl\K2"ō˙Čļļ°—“’‘‘•ÍÕŅYDCDDˆ€C OÄõōׯÅÄÄĀĨ„§˙į–momˆÕˆ˙ߐnpeS:#ō˙Čļļą—’‘‘•ÍÔŅYDCDDˆ€C OÄôō×ÅÄÄÃĀĻ‚„§˙į”k‘mĢõ‡˙ߏ`L/„#ō˙ȡˇą–‘‘”ĖÔŅYDCDDˆ€COÄôō׀ÄÃĀĻ‚…§˙į’hjhŅ‡˙Ûr* †#ō˙ȸ¸°–‘€”ĖÔŅYDCDDˆ€C OÃôņÖÄÄÃÃŋĨƒ…§˙æ‘f‘hgĻô†˙Ī@ˆ#ō˙ȸ¸°–€ ’ĖÔĐYDCDDˆ€C OÃķņÕÄÃÃÂŋĨƒ†¨˙įc‡egh…ec|Ά˙Č:ˆ$Į˙É¸šą•Ž’ĖĶĐYDCDDˆ€C OÃķņÕÃÞĨ„‡¤ī؉a†cbowe…c aæđīīėįååæŗ4ˆ ࿌åŧˇšą•ŽŽ’ËĶĐYDCDDˆ€C OÂķđÕÃÂÂÁžĨ…‡ĸŒæįЄ_†`_wœ€^„` _pˇčææâÚ××Ų¨1ˆĻ،×ļˇš˛•ŽŽ’ËĶĪYDCDDˆ€C OÂōđÕÂÂÁÁŊĨ…ˆ§˙æŠ\†^\wÂŗ_]„^\žķ„˙É;ˆ$Į˙Éšēŗ”€Ž ‘ËŌĪYDCDDˆ€C OÂōđÕÂÁÁĀŊĨ†ˆ§˙ä‡Y†[YsĖŨ}X…[eģ„˙Į:ˆ#ō˙Čēģŗ”ŽŽ‘ĘŌĪYDCDDˆ€COÂōđÔ€ĀŧĨ†‰§˙ä†W†YWqČų¯[X‚YXH!håƒ˙Į:ˆ#ō˙Čģģŗ“€ ŒĘŌĪYDCDDˆ€C OÂōđÔÁĀĀŋŧĨ†ˆ§˙ã„T†VToĮ˙Ú|S€VWR;Ĩƒ˙Į:ˆ#ō˙Čģŧŗ“ŒŒĘŌÎYDCDDˆ€C OÂōīÔĀĀŋŋŧĨ‡‰§˙ã‚Q†T RmÆ˙÷ŽWSTUK. €]ã‚˙Į:ˆ#ō˙Čģŧ´“€ŒĘŌÎYDCDDˆ€COÂņīÔĀ€ŋģĨ‡Š§˙â€P†R PkÅ˙˙Û{OQ? ĸ‚˙Į:ˆ#ō˙ČŧŊ´“€Œ ‹ĘŅÎYDCDDˆ€C OÂņīĶĀŋžžēĨˆŠ§˙â€P†R PkÅ˙˙ų¯O1„Zá˙Į:ˆ#ō˙ČŧŊ´’ŒŒ‹ŠŽÉŅÎYDCDDˆ€C OÁņîŌŋžžŊēĨ‰‹§˙â€P†RPkŀ˙Û\…Ÿū€˙Į:ˆ#ō˙ÉŊž´’‹‹ŠŠŽÉŅÍXDCDDˆ€C OÁđîŅŋžŊŊēĨ‰‹§˙â€P†RPkŀ˙÷‹ †WŪ€˙Į:ˆ#ō˙ÉŊžĩ’‹‹ŠŠŽÉĐÍXDCDDˆ€COÁđîŅž€ŊēĨŠ‹¨˙â€P†RPlƁ˙ĪD†ũ˙˙Į:ˆ#ō˙Éžŋĩ‘€Š ‰ČĐÍXDCDDˆ€C OĀđîŅžŊŧŧšĨŠŒ¨˙â€P…RSO`ē˙ųކSÛ˙˙Į:ˆ#ō˙Éžŋļ‘ŠŠ‰‰ČĐĖXDCDDˆ€COĀđíĐŊ€ŧšĨ‹ŒŠ˙â€PƒRSRF+9Ģ‚˙ĶH†šü˙Į:ˆ#ō˙ÉžĀ‰ ˆŒČĪĖXDCDDˆ€C OĀīíĐŊŧŧģš¤‹Š˙â€PRSSI2#Ē‚˙ú‘†PØ˙Į:ˆ#ō˙ÉŋĀ‰ˆˆŒĮĪĖXDCDDˆ€C OĀīíĐŧŧģģ¸ĨŒŠ˙â€PRRSSI2&ǃ˙ÖK†—üČ:ˆ#ō˙ÉŋÁˆˆ‡‹ĮĪĖXDCDDˆ€C OŋīíĪŧģģēˇĨŽŠ˙â€PTRF1&ǃ˙û”†LŲË:ˆ#ō˙ÉĀ¡ˆˆ‡‡‹ĮĪËXDCDDˆ€C OŋīėĪŧģēšˇĨŽŠ˙ã‚N@,ƒ&Ē„˙ŲO†˜Ä;ˆ#ō˙ÉÁÃˇ€‡ †ŠÆÎËXDCDDˆ€C OŋîėÎģēēšˇĨŽŠ˙ßj!…&Ē„˙ũ—†L›>ˆ#ō˙ÉÁøŽ‡‡††ŠÆÎËXDCDDˆ€C OŋîėÎģēššļĨŽĒ˙ØKˆ&Ē…˙ÛR†Y5ˆ#ō˙ÉÁøŽ‡‡††ŠÆÎËXDCDDˆ€C OŋîëÎēšš¸ļ¤Ē˙ÕDˆ&Ē…˙ũ™†ˆ#ō˙ÉÁÄšŽ€† …‰ÆÎĘXDCDDˆ€C OŋíëÍēš¸¸ĩ¤‘Ģ˙ÖEˆ&̆˙āV“$ƍ˙ÉÂÄšŽ††……‰ÅÍĘXDCDDˆ€COŋíëÍš¸¸ˇĩ¤‘ŖčįቿčĀ>ˆ"™é…æčŠ’ ˛čŒæžÁÅš€… „ˆÅÍĘXDCDDˆ€COžíëÍš¸¸ˇĩĨĸîíí‰ėîÅ@ˆ#žī…ėīÔT’!ˇî‹ėíÁÂÆš…€„ˆÅÍĘXDCDDˆ€C Ožíę͸¸ˇļ´Ĩ‘‘Ĩ˙ÖEˆ&̈˙Ÿ‘$ƍ˙ĘÄÆē…„ƒƒ‡ÅÍÉXDCDDˆ€C Ožėęˏˇļļ´¤‘‘Ĩ˙ÕEˆ&Lj˙ä[‘#ō˙ĘÄĮē„„ƒƒ‡ÄĖÉXDCDDˆ€C OŊėę˸ļļĩŗĨ’’Ĩ˙ÕEˆ&lj˙ĸ#ō˙ĘÅĮ猃ƒ‚‚†ÄĖÉXDCDDˆ€C OŊėéˡļļĩŗĨ“’Ĩ˙ÕEˆ&lj˙į^#ō˙ĘÅČģ‹ƒƒ‚‚†ÄĖČXDCDDˆ€C OŊëéˡļĩĩŗĨ“’Ĩ˙ÕEˆ&NJ˙Ļ #ō˙ĘÅČģ‹ƒ€‚†ÃËČXDCDDˆ€C OŊëčĘļĩĩ´˛Ĩ”“Ĩ˙ÕEˆ&NJ˙ęa#ō˙ĘÆÉŧ‹‚‚…ÃËČXDCDDˆ€C OŊëčĘļĩ´´˛Ĩ”“Ļ˙ÕEˆ&Ē‹˙Š#Ž#ō˙ĘÆÉŧ‹‚€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗą¤•”Ļ˙ÕEˆ&Ē‹˙ėdŽ#ō˙ĘĮĘŧЁ€€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗąĨ••§˙ÕEˆ&nj˙Ŧ%#ō˙ĘĮĘŧЁ€€„ÂĘĮXDCDDˆ€C OŊęįÉ´´ŗ˛ąĨ–•§˙ÕEˆ&nj˙ífŒ#ō˙ĘĮËŊЁ€„ÂĘĮXDCDDˆ€C OŧęįČ´ŗ˛˛°Ĩ–•Ļ˙ÕEˆ&Ē˙°(Œ#ō˙ĘČËŊŠ€~ƒÂĘĮXDCDDˆ€C OŧęįČ´˛˛ą°Ĩ—–§˙ÕEˆ&Ē˙īi‹#ō˙ÉČĖŊ‰~~ƒÂĘÆXDCDDˆ€C OŧéæĮŗ˛˛ą°Ĩ—–§˙ÕCˆ#ŠŽ˙ŗ*‹!ō˙ÄĮÍžˆ~~ƒÁÉÆXDCDDˆ€CNģéæĮŗ˛ąą¯Ĩ˜—¨úŒ˙Ūl4†64SŧŽ˙ņ‚55ˆ65RŅ˙žĮÍžˆ~~}‚ĀÉÆXDCDDˆ€CNģéæĮ˛ąą°¯Ļ˜˜ĄÖŒ˙÷Û͆ÎÍÕī˙å͉ÎÍÕôŒ˙ũČĘÎē‡~~}|ĀÉÅXDCDDˆ€CNģéåĮ˛ą°°¯¨™™šąÍ˙ĶĘÍÎŗ…~}||ĀČÅXDCDDˆ€C NģčåÆą°°¯¯Ē€š ĐË˙îĮĘÎΤ‚}}||ŋČÅXDCDDˆ€CNēčåÆą°°¯ŽŦŸ››œ§ØÉ˙ņËĘÍÎˑ~}||{€ŋČÅXDCDDˆ€CNēč寰°¯ŽŽ­§›œœ¨ČųÆ˙ÜĖČÍÎΰ…}||{{€ŋČÄXDCDDˆ€CNēįåʰ¯ŽŽ­­ĢĸœžĸŗŋÖčô¤õ—ôėŪËČÉÉÍÎΐ~||{{z„ŋĮÄXDCDDˆ€CN¸įæÎ°ŽŽ€­ŦŠĸžŸŸĄĒąĩļ¸€šēģģ€ŧŊŊžžŋŋĀĀ€ÁÂÂÃÀÄÅÅÆÆĮĮČȀÉʀËĖĖÍ̀ÎĪĪĐЀŅŌŌĶŅĖÆÆĖÎÎĐØ€||{{zzŠĀĮÄXDCDDˆ€CM¯įįØ°ŽŽ­­ŦĢ̍¤ŸŸ  €ĄMĸĸŖ¤¤ĨĨĻϧ¨¨ŠŠĒĢĢŦ­­ŽŽ¯°°ąą˛ŗŗ´´ĩĩ¸šēēģŧŧŊžžŋŋĀÁÁÂÂÃÃÄÅÅÆÆĮČČÉÉĘËĖÍÎĪΎ”€||€{ zzšÂÆÁSCCDDˆ€CeK æįāˇŽ­­ŦĢĢĒǍϤĸĄĄĸŖŖ¤ĨĨĻϧ§¨ŠŠĒĢĢŦ­­Ž¯¯°ąą˛˛ŗ´ĩĩļ¸ššēēģŧŧŊžžŋĀÁÁÂÃÃÄÄÅÅÆĮĮČČÉÉĘËËĖÍÎÎĘÃŗ›Š~}||{{€z¯ÅÆšOCCDDˆ€C I‰åįåÍ­­ŦĢĢ€ĒŠŠ§ĻĻ€Ĩ‚¤ŖŖ€ĸĄ‚ ŸŸž‚œ››‚š€™‚˜—–‚•€”“”“’ˆ€}}||{{zzyŒžÆÅĻMCCBAˆ-BBCDcâįæŪēŦŦĢĢĒĒŠŠ¨§§ĻĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœ››šš™€˜+——–••””““’’‘ŽŒŒ‹‹ŠŠ‰‰ˆˆ‡††……„„ƒ‚‚€€€~}~}||{{zzy{ŽÄÆÅ…I€CDˆD€CPŧææåÖ˛ĢĢĒĒŠŠ¨§§ĻĻ€Ĩ¤ŖŖĸĸĄ  €Ÿ,žžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹‹ŠŠ‰‰ˆ‡††€…„ƒƒ‚‚€€~~}||{{zzyy ÁÆÆĀ`E€CYˆC GwãææãŌŗĒĒŠŠ¨€§2ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹€Š%‰‰ˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy{žÆÆÅ”LC‰AB€CPŠ€å'ãÖģŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–€• ””““’‘‘ŽŽ€%ŒŒ‹ŠŠ‰ˆˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy†¨ŋ€Æ´^DCCBBŠBBCCD^ŊååäãÜĐŧŽ€ĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœ€›-šš™™˜——––••””““’’‘ŽŽŒŒ‹‹Š‰‰ˆˆ‡‡††……„„ƒ‚‚€€€~~}}|{{zz}ŖšÃ€ÆžnG€CBAŠAB€CEcˇäâŨŲÔ΁ĖËĘʀɀȀĮƀŀÄÃÁ€ÁĀĀŋ€žŊ‚ŧģ€ē€š¸¸ˇƒļĩĩ´´ŗŗ€˛ąą°¯€Ž­ŽŗšŊÁÆˇrICD‹6‚CD[šŲåãâáß€Ū€Ũ€Ü€Û€Ú€Ų€Ø×€Ö€Õ€ÔĶŌԀҁĐĪĪ€Î€ÍĖ€Ë€Ę€ÉČ€Į ÆÆĮČČĮÆÆÅ eH€C€DŒED‚COižĮâ€ãâ€á€ā€ß€Ū€ŨÜÜ€ÛÚ€Ų€Ø€×Ö€Õ€Ô€Ķ€Ō€Ņ€ĐĪ€Î€Í€ĖË€Ę€É€ČĮģœpSECDFŽD?A€CBDOYhwƒ„І‰„ˆ€‡†††…„„ƒ…‚†…€€~|reXPGCB@@+=B€CBBDGI—K„JK¯JIGDBBC@2’-;ABÜCA>4%“  &,57:;:9€:99:€;-<<==>??@@AABCCDEEFGHHIIKLMMLKKJHHGFEEDCCBBAA??€>€=<<;;‚:9::9:9:;:9962( ’i   ! !"#$&&()*,,-/11346889<=>>@ABDFFGIJKKMNMMNMLKJJIHFECBA?>=;;9875321/..,++)''&%$""   ”/   "#%'((*,-/02346889;<>>?@ABCDE…F/EDCBB@?>=<;:97753210-,+)(&&%"  –€ '!"$'()**,,.001234667899;;€<=>=€<€;9€8 6543320/.,*)('&%$#"  €  ™ €  !#$%&()*)*+,./011€23343434332€1 0/-,-,+*)'&%#€"  € ›+  "#"$%&'(())*)*+*+†,+*+€*)('('&$$#"" € Ą  €  ""##Š"#"!" €  ¨  €€€Š€€€ ą   € €ƒƒ€ € €  Ā  € ‹    × ‹  ˙˙˙˙˙˙˙˙Ŗ˙˙˙˙˙˙˙˙˙ųA€C×DCBCDi—A€B€C×DCBCDFCB”D€BãCH9CCB‚CGNPSTTŸSŸRŒQOMID„CADBCDL[}§ēÉĪ‚Î€ÍĖƒË€ĘÉ€ČĮÆ‚ŀāÀÁĀ‚ŋ€ž€ŊƒŧģēšŽ ƒ`OEƒCFGDB€CG_Ÿæ€úû€ú€ų€ø€÷€ö€õ€ôķ€ō€ņđ€ī€î€í€ė€ë€ę€éč€į€æ€åä€ã€â€á€āßŪŪ׊iLC€D‹DCB€CLyŨûü€ûų÷÷ö€õ€ô€ķ€ō€ņ€đīîî€íė€ë€ę€éčįææåæå€ä€ãâ€áāāß€Ū€Ũ€Ü ÛÛÜÜŪßŪŪŨՏSDCB‹DCKˆėüüûú÷ķéâŪÜÛÛÚڀŲØØÖÕÔĶ€ŌŅŅ€Đ΁ÎÍÍĖËËĘĘɁČĮĮ€ÆÅÄÀÁĀŋž€Ŋ€ŧģēēšš¸ŧÂĪ×ÜŪŨŨÛ TDCCBB?‰HD€CGyėüüûøđŪŅÎÍÍĖĖËËĘĘÉČČĮĮ€ÆÅÄÄÃÃÂÂÁÁĀĀŋŋžŊŊŧŧģģēēš¸¸ˇļļĩĩ€´ŗ˛˛ąą°°¯ŽŽ­­ŦŦĢĢĒĒŠŠ¨€§ĻĻĨĨ¤ŖŖĸĸĄĄ  ­Æ×€Ũ۔N€CBA‰ DDCCD\Ûûüû÷įŌ€ÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄÄÃÃÂÂÁÁĀĀŋž€Ŋŧ€ģ ēšš¸¸ˇļļĩĩ€´ŗ˛˛ąą°°¯€Ž­­ŦĢĢĒĒŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  €ŸŗŅ€ÜØwGC‰7BBCCKœúüü÷äĐĪÎÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄÄÃÃÂÂÁÁĀĀŋŋžŊŊŧŧģģēšš¸¸ˇ€ļ4ĩĩ´´ŗ˛˛ąą°°¯¯ŽŽ­­ŦĢĢĒĒŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžĒĐÜÜÛšTC‰BBCCWâûüųčĐĪÎÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄĀÃÂÂÁĀĀŋŋžžŊŊŧŧģģēšš¸¸ˇˇļļĩĩ´ŗ˛˛€ą °°¯ŽŽ­­ŦĢĢĒĒ€Š!¨§§ĻĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœŽÔÜÛŲuGCCB9ˆ€C FvųüúņŌĪÎÎÍÍĖ€Ë ĘÉÅÃÃÂÂÁÁĀĀ€ŋžžŊ€ŧģģēē€š¸¸ˇļļ€ĩ´´€ŗ˛˛ą€°€¯ŽŽ­­ŦŦĢ€ĒŠŠ€¨§€ĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœœ›ŧØÛڟLCCA>ˆ€CKŸųûøāĪÎÎÍÍĖĖËËÅŊŗĻĄ  ĄĄ  „Ą €Ąĸ€ĄĸĸĄĸĄĄĸŖĸ„ŖĸˆŖ¤€Ŗ€¤Ŗƒ¤Ŗĸ Ÿ€žœœ››ĄĐÛÚŋPCCDDˆ€CqMŗųûõĶÎÎÍÍĖĖËÉŧĸ…zz{||}~~€‚‚ƒƒ„……†‡ˆˆ‰ŠŠ‹ŒŒŽŽ‘‘’““””•––—˜˜™™š›œœžŸŸ  ĄĄĸŖŖ¤ĨĨĻϧ§¨¨ĻĸŸžœœ››ššĀØÚĪRCCDDˆ€COĀúúėÎ΀Í"ĖËČŗ‰yz{|{xwwxxyyzz{{||}}~~€€‚‚ƒ€„……†‡‡€ˆ‰ŠŠ‹‹Œ€ Ž‘‘’’“€”••–——€˜™š›žĨ¨ŠŠĒФžœœ››šš™¯ÕÚÖWCCDDˆ€CPĮúųäÎÎÍÍĖËÉ´ƒyzzyw{smk†lƒm‚n…oppq€pq„r‚s„t„u vx™œĄŠĒĢĢŖ€› 𙙤ĶŲÖZDCDDˆ€CPĮú÷áÎÍÍĖËĘŊŠyzzwvq‹Ã™“{‡™ŠĢŦŦĸœ›š™™˜žŌŲÖZDCDDˆ€CPĮų÷ŪÍÍĖËËÆ¤yzzut‹™Å˜™–{•ĒŦ­Šš™™˜˜›ŌŲÖZDCDDˆ€CPĮų÷ŪÍĖĖËËŋ‡zzxt’ɘ™z˜Ģ­Ž š™€˜›ŌŲÕZDCDDˆ€COÆų÷ŪĖĖËËɡ{zyuˆ–Ë•}¤ŽŽĻ›™˜˜—›ŌŲÕZDCDDˆ€COÆųöŨĖËËĘĮŦz{wl“’Ë“­¯Š›˜˜——šŅØÕZDCDDˆ€COÆøöŨËËĘĘÅĻ{{zÍ‘€Ļ°Ģ›˜——–™ĐØÕZDCDDˆ€COÆøöÛËĘĘÉŤ{yqŽÍŽmŖąŦ›——––šŅØÔZDCDDˆ€C OÆøõÛĘĘÉÉÄĨ|xi΋ŒlŖ˛Ŧš—––•™Đ×ÔZDCDDˆ€COÆ÷õÛĘĘÉČÄĨ|xg‰Íˆ‰l¤˛­š—–••˜Ī×ÔZDCDDˆ€C OÆ÷õÛĘÉČČÃĨ}xfΆj¤ŗ­š–••”˜Ī×ĶZDCDDˆ€COÅ÷õÛÉČČĮÃĨ~ye„̓„i¤ŗŽ™••””˜Ī×ĶZDCDDˆ€COÅ÷ôڀČĮÃĨ~ye΁‚e¤´Ž™•€”˜ĪÖĶZDCDDˆ€COÅöôÚČČĮĮÂĨydÅ~€€zMŸ´Ž™•””“—ÎÖĶZDCDDˆ€C OÄöôÚČĮĮÆÂ¤zdĀ|}}~{wtplhd]G"™ĩޘ””““–ÎÖŌZDCDDˆ€COÄöôŲĮĮÆÆÂĨ€{dzģy$z{|zskd]XSLD;4*# ™ļޘ”““’–ÎÕŌZDCDDˆ€C OÄõķØĮÆÆÅÁĨ€{dšw xywodYM=-‡™ļ¯˜““’’•ÍÕŌZDCDDˆ€C OÄõķØÆÆÅÅÁĨ{cļtuvtj\L8" šˇ¯—“’’‘•ÍÕŌYCCDDˆ€C OÄõķØÆÆÅÄĀĨ{b´rstl\K2šˇ°—“’‘‘•ÍÕŅYDCDDˆ€C OÄõōׯÅÄÄĀĨ‚|a˛oppeS:“›¸ą—’‘‘•ÍÔŅYDCDDˆ€C OÄôō×ÅÄÄÃĀĻ‚|`°mnnbL/–›šą–‘‘”ĖÔŅYDCDDˆ€COÄôō׀ÄÃĀσ}`ŽjklaK, ˜œš°–‘€”ĖÔŅYDCDDˆ€C OÃôņÖÄÄÃÃŋĨƒ}`­hjdO. šœē°–€ ’ĖÔĐYDCDDˆ€C OÃķņÕÄÃÃÂŋĨ„~_Žd–edecT6œœēą•Ž’ĖĶĐYDCDDˆ€C OÃķņÕÃÞĨ…€gspgˆcfhd†c gpsstmZ9ˆ& ēą•ŽŽ’ËĶĐYDCDDˆ€C OÂķđÕÃÂÂÁžĨ…‚{Œž“q_†`_j|o_…` g‡Ÿ–‚i`bdMˆLdŒb[Šē˛•ŽŽ’ËĶĪYDCDDˆ€C OÂōđÕÂÂÁÁŊĨ†„„Œŗ´Ĩu]†^]k’Š_…^ ]Ģ¨•ˆ†‡‡‰iˆh‰Œ‡rŦģŗ”€Ž ‘ËŌĪYDCDDˆ€C OÂōđÕÂÁÁĀŊĨ†„„Œą˛ŖrZ†[Zh–žmZ„[\\{Ž„…‡hˆg‡Œ…q­ŧŗ”ŽŽ‘ĘŌĪYDCDDˆ€COÂōđÔ€ĀŧĨ†„ƒŒ°ąĄpX†YXf“Ž…ZX‚YXH#:w†…‡hˆg‡Œ…q­ŧŗ“€ ŒĘŌĪYDCDDˆ€C OÂōđÔÁĀĀŋŧĨ‡„ƒŒ¯° nU†VUc‘˛›jT€VWR;V‡…‡hˆg‡Œ…qŽŊŗ“ŒŒĘŌÎYDCDDˆ€C OÂōīÔĀĀŋŋŧĨ‡…ƒŒ­ŽžlR†T Sa¯ĒƒVSTUK. €0v‡€…‡hˆg‡Œ…qŽŊ´“€ŒĘŌÎYDCDDˆ€COÂņīÔĀ€ŋģĨˆ†ƒŒŦ­jQ†R Q_ŽŽŽ™hQP? T†€…‡hˆg‡Œ…qŽž´“€Œ ‹ĘŅÎYDCDDˆ€C OÂņīĶĀŋžžēĨ‰†ƒŒŦ­jQ†R Q_ŽŽŦǃM1„/u‡……‡hˆg‡Œ…q¯ž´’ŒŒ‹ŠŽÉŅÎYDCDDˆ€C OÁņîŌŋžžŊēĨ‰‡ƒŒŦ­jQ†RQ_ŽŽŦ­@… S†……‡hˆg‡Œ…r¯ŋ´’‹‹ŠŠŽÉŅÍXDCDDˆ€C OÁđîŅŋžŊŊēĨЇƒŒŦ­jQ†RQ_ŽŽ¨œ‹I†-t‡…‡hˆg‡Œ…r°ŋĩ’‹‹ŠŠŽÉĐÍXDCDDˆ€COÁđîŅž€ŊēĨŠˆ„ŒŦ­jQ†RQaĸ“ˆ‡l$† R……‡hˆg‡Œ…r°Āĩ‘€Š ‰ČĐÍXDCDDˆ€C OĀđîŅžŊŧŧšĨ‹ˆ„ŒŦ­jQ…RSPRtŽ€…ƒJ†+r‡‡hˆg‡Œ…rąĀļ‘ŠŠ‰‰ČĐĖXDCDDˆ€COĀđíĐŊ€ŧšĨ‹‰…ŒŦ­jQƒRSRF-([‡€…‡n%† P…‡hˆg‡Œ…rąÁ‰ ˆŒČĪĖXDCDDˆ€C OĀīíĐŊŧŧģš¤Œ‰…ŒŦ­jQRSSI2Yˆ…„L †*q‰hˆg‡Œ…r˛Á‰ˆˆŒĮĪĖXDCDDˆ€C OĀīíĐŧŧģģ¸ĨŒŠ…ŒŦ­jQRRSSI2Yˆ…‡p'† O†hˆg‡Œ…r˛Âˆˆ‡‹ĮĪĖXDCDDˆ€C OŋīíĪŧģģēˇĨŠ…ŒŦ ­jQTRF1Yˆƒ…M †(qjˆg‡Œ…rŗÃˇˆˆ‡‡‹ĮĪËXDCDDˆ€C OŋīėĪŧģēšˇĨŠ…ŒŦ­žlO@,ƒYˆ‚…‡q)†Ofˆg‡Œ…rŗÃˇ€‡ †ŠÆÎËXDCDDˆ€C OŋîėÎģēēšˇĨŽ‹…‹Ŧ­Ž—Q"…Yˆ„…O †(Q ˆg‡Œ…rŗÄ¸Ž‡‡††ŠÆÎËXDCDDˆ€C OŋîėÎģēššļĨŽ‹…‡Ŧ€­¨ š},ˆYˆƒ…‡r+†/ˆg‡Œ…r´Ä¸Ž‡‡††ŠÆÎËXDCDDˆ€C OŋîëÎēšš¸ļ¤Œ…ƒŦ€­ĒŖ—’‹ˆn#ˆYˆ„…†P † ˆg‡Œ…r´ÅšŽ€† …‰ÆÎĘXDCDDˆ€COŋíëÍēš¸¸ĩ¤Œ…ŦĢĢǧŖŸ›–‘Œ‡ƒ‚‚„m#ˆW†„ƒ…r+“e…Œƒp´ÅšŽ††……‰ÅÍĘXDCDDˆ€COŋíëÍš¸¸ˇĩ¤ŽŽˇļ´ŗąŽĒ§¤ĸ‚ŖĨˆ,ˆm§…ŖĨf’~όŖŽēÅš€… „ˆÅÍĘXDCDDˆ€COžíëÍš¸¸ˇĩĨŸæååää‡åæĀ>ˆ"™į…åįÎR’ ˛į‹åæŧÁÆš…€„ˆÅÍĘXDCDDˆ€C Ožíę͸¸ˇļ´Ĩ‘‘Ĩ˙×Eˆ&̈˙ ‘$Į˙ĘÄÆē…„ƒƒ‡ÅÍÉXDCDDˆ€C Ožėęˏˇļļ´¤‘‘Ĩ˙ÕEˆ&Lj˙ä[‘#ō˙ĘÄĮē„„ƒƒ‡ÄĖÉXDCDDˆ€C OŊėę˸ļļĩŗĨ’’Ĩ˙ÕEˆ&lj˙ĸ#ō˙ĘÅĮ猃ƒ‚‚†ÄĖÉXDCDDˆ€C OŊėéˡļļĩŗĨ“’Ĩ˙ÕEˆ&lj˙į^#ō˙ĘÅČģ‹ƒƒ‚‚†ÄĖČXDCDDˆ€C OŊëéˡļĩĩŗĨ“’Ĩ˙ÕEˆ&NJ˙Ļ #ō˙ĘÅČģ‹ƒ€‚†ÃËČXDCDDˆ€C OŊëčĘļĩĩ´˛Ĩ”“Ĩ˙ÕEˆ&NJ˙ęa#ō˙ĘÆÉŧ‹‚‚…ÃËČXDCDDˆ€C OŊëčĘļĩ´´˛Ĩ”“Ļ˙ÕEˆ&Ē‹˙Š#Ž#ō˙ĘÆÉŧ‹‚€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗą¤•”Ļ˙ÕEˆ&Ē‹˙ėdŽ#ō˙ĘĮĘŧЁ€€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗąĨ••§˙ÕEˆ&nj˙Ŧ%#ō˙ĘĮĘŧЁ€€„ÂĘĮXDCDDˆ€C OŊęįÉ´´ŗ˛ąĨ–•§˙ÕEˆ&nj˙ífŒ#ō˙ĘĮËŊЁ€„ÂĘĮXDCDDˆ€C OŧęįČ´ŗ˛˛°Ĩ–•Ļ˙ÕEˆ&Ē˙°(Œ#ō˙ĘČËŊŠ€~ƒÂĘĮXDCDDˆ€C OŧęįČ´˛˛ą°Ĩ—–§˙ÕEˆ&Ē˙īi‹#ō˙ÉČĖŊ‰~~ƒÂĘÆXDCDDˆ€C OŧéæĮŗ˛˛ą°Ĩ—–§˙ÕCˆ#ŠŽ˙ŗ*‹!ō˙ÄĮÍžˆ~~ƒÁÉÆXDCDDˆ€CNģéæĮŗ˛ąą¯Ĩ˜—¨úŒ˙Ūl4†64SŧŽ˙ņ‚55ˆ65RŅ˙žĮÍžˆ~~}‚ĀÉÆXDCDDˆ€CNģéæĮ˛ąą°¯Ļ˜˜ĄÖŒ˙÷Û͆ÎÍÕī˙å͉ÎÍÕôŒ˙ũČĘÎē‡~~}|ĀÉÅXDCDDˆ€CNģéåĮ˛ą°°¯¨™™šąÍ˙ĶĘÍÎŗ…~}||ĀČÅXDCDDˆ€C NģčåÆą°°¯¯Ē€š ĐË˙îĮĘÎΤ‚}}||ŋČÅXDCDDˆ€CNēčåÆą°°¯ŽŦŸ››œ§ØÉ˙ņËĘÍÎˑ~}||{€ŋČÅXDCDDˆ€CNēč寰°¯ŽŽ­§›œœ¨ČųÆ˙ÜĖČÍÎΰ…}||{{€ŋČÄXDCDDˆ€CNēįåʰ¯ŽŽ­­ĢĸœžĸŗŋÖčô¤õ—ôėŪËČÉÉÍÎΐ~||{{z„ŋĮÄXDCDDˆ€CN¸įæÎ°ŽŽ€­ŦŠĸžŸŸĄĒąĩļ¸€šēģģ€ŧŊŊžžŋŋĀĀ€ÁÂÂÃÀÄÅÅÆÆĮĮČȀÉʀËĖĖÍ̀ÎĪĪĐЀŅŌŌĶŅĖÆÆĖÎÎĐØ€||{{zzŠĀĮÄXDCDDˆ€CM¯įįØ°ŽŽ­­ŦĢ̍¤ŸŸ  €ĄMĸĸŖ¤¤ĨĨĻϧ¨¨ŠŠĒĢĢŦ­­ŽŽ¯°°ąą˛ŗŗ´´ĩĩ¸šēēģŧŧŊžžŋŋĀÁÁÂÂÃÃÄÅÅÆÆĮČČÉÉĘËĖÍÎĪΎ”€||€{ zzšÂÆÁSCCDDˆ€CeK æįāˇŽ­­ŦĢĢĒǍϤĸĄĄĸŖŖ¤ĨĨĻϧ§¨ŠŠĒĢĢŦ­­Ž¯¯°ąą˛˛ŗ´ĩĩļ¸ššēēģŧŧŊžžŋĀÁÁÂÃÃÄÄÅÅÆĮĮČČÉÉĘËËĖÍÎÎĘÃŗ›Š~}||{{€z¯ÅÆšOCCDDˆ€C I‰åįåÍ­­ŦĢĢ€ĒŠŠ§ĻĻ€Ĩ‚¤ŖŖ€ĸĄ‚ ŸŸž‚œ››‚š€™‚˜—–‚•€”“”“’ˆ€}}||{{zzyŒžÆÅĻMCCBAˆ-BBCDcâįæŪēŦŦĢĢĒĒŠŠ¨§§ĻĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœ››šš™€˜+——–••””““’’‘ŽŒŒ‹‹ŠŠ‰‰ˆˆ‡††……„„ƒ‚‚€€€~}~}||{{zzy{ŽÄÆÅ…I€CDˆD€CPŧææåÖ˛ĢĢĒĒŠŠ¨§§ĻĻ€Ĩ¤ŖŖĸĸĄ  €Ÿ,žžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹‹ŠŠ‰‰ˆ‡††€…„ƒƒ‚‚€€~~}||{{zzyy ÁÆÆĀ`E€CYˆC GwãææãŌŗĒĒŠŠ¨€§2ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹€Š%‰‰ˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy{žÆÆÅ”LC‰AB€CPŠ€å'ãÖģŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–€• ””““’‘‘ŽŽ€%ŒŒ‹ŠŠ‰ˆˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy†¨ŋ€Æ´^DCCBBŠBBCCD^ŊååäãÜĐŧŽ€ĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœ€›-šš™™˜——––••””““’’‘ŽŽŒŒ‹‹Š‰‰ˆˆ‡‡††……„„ƒ‚‚€€€~~}}|{{zz}ŖšÃ€ÆžnG€CBAŠAB€CEcˇäâŨŲÔ΁ĖËĘʀɀȀĮƀŀÄÃÁ€ÁĀĀŋ€žŊ‚ŧģ€ē€š¸¸ˇƒļĩĩ´´ŗŗ€˛ąą°¯€Ž­ŽŗšŊÁÆˇrICD‹6‚CD[šŲåãâáß€Ū€Ũ€Ü€Û€Ú€Ų€Ø×€Ö€Õ€ÔĶŌԀҁĐĪĪ€Î€ÍĖ€Ë€Ę€ÉČ€Į ÆÆĮČČĮÆÆÅ eH€C€DŒED‚COižĮâ€ãâ€á€ā€ß€Ū€ŨÜÜ€ÛÚ€Ų€Ø€×Ö€Õ€Ô€Ķ€Ō€Ņ€ĐĪ€Î€Í€ĖË€Ę€É€ČĮģœpSECDFŽD?A€CBDOYhwƒ„І‰„ˆ€‡†††…„„ƒ…‚†…€€~|reXPGCB@@+=B€CBBDGI—K„JK¯JIGDBBC@2’-;ABÜCA>4% ‘  &,57:;:9„:€;<€=€>?@AA€B CEEFGHHIJJKLNMLKJIIGFFEEDCBBA@@?>?>€=<;<€;‚:9€: ;:9964)" ’j   !"#$%&'')+,../1335579:<=?@ABDEGHIJKLMNOPQQPOONMKJJIGECBB@?=<:98654220/.-+*)'&%$""!  “1 !$%&()+,./12345799;==?@ABCDFGGHHI2HHGGEDDCBA?>=<;98643210.-+*)'%$#!  • € #!"$%&'(*,-./1123457789:€<=>‡?>€=$;;98976542100.,,**'%$#"  €  — € !"$%&%'()*+,-.01€2345656€7656€54432201/..-,+)('&$#" € €  ›%  "##%%&'())*+€,-,-†.€-,,++*(('&&%%$"!! €  Ÿ €!!""#$##$€%$%$#$€"!!€ €€  §  €ƒ€€ ą  €€ƒ€€  ŋ €€ € ‹ €  €  Ö€ ‹ € ˙˙˙˙˙˙˙˙ĸ˙˙˙˙˙˙˙˙˙ųA€C×DCBCDi—A€B€C×DCBCDFCB”D€BãCH9CCB‚CGNPSTTŸSŸRŒQOMID„CADBCDL[}§ēÉĪ‚Î€ÍĖƒË€ĘÉ€ČĮÆ‚ŀāÀÁĀ‚ŋ€ž€ŊƒŧģēšŽ ƒ`OEƒCFGDB€CG_Ÿæ€úû€ú€ų€ø€÷€ö€õ€ôķ€ō€ņđ€ī€î€í€ė€ë€ę€éč€į€æ€åä€ã€â€á€āßŪŪ׊iLC€D‹DCB€CLyŨûü€ûų÷÷ö€õ€ô€ķ€ō€ņ€đīîî€íė€ë€ę€éčįææåæå€ä€ãâ€áāāß€Ū€Ũ€Ü ÛÛÜÜŪßŪŪŨՏSDCB‹DCKˆėüüûú÷ķéâŪÜÛÛÚڀŲØØÖÕÔĶ€ŌŅŅ€Đ΁ÎÍÍĖËËĘĘɁČĮĮ€ÆÅÄÀÁĀŋž€Ŋ€ŧģēēšš¸ŧÂĪ×ÜŪŨŨÛ TDCCBB?‰HD€CGyėüüûøđŪŅÎÍÍĖĖËËĘĘÉČČĮĮ€ÆÅÄÄÃÃÂÂÁÁĀĀŋŋžŊŊŧŧģģēēš¸¸ˇļļĩĩ€´ŗ˛˛ąą°°¯ŽŽ­­ŦŦĢĢĒĒŠŠ¨€§ĻĻĨĨ¤ŖŖĸĸĄĄ  ­Æ×€Ũ۔N€CBA‰ DDCCD\Ûûüû÷įŌ€ÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄÄÃÃÂÂÁÁĀĀŋž€Ŋŧ€ģ ēšš¸¸ˇļļĩĩ€´ŗ˛˛ąą°°¯€Ž­­ŦĢĢĒĒŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  €ŸŗŅ€ÜØwGC‰7BBCCKœúüü÷äĐĪÎÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄÄÃÃÂÂÁÁĀĀŋŋžŊŊŧŧģģēšš¸¸ˇ€ļ4ĩĩ´´ŗ˛˛ąą°°¯¯ŽŽ­­ŦĢĢĒĒŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžĒĐÜÜÛšTC‰BBCCWâûüųčĐĪÎÎÍÍĖËËĘĘÉÉČČĮĮÆÆÅÄĀÃÂÂÁĀĀŋŋžžŊŊŧŧģģēšš¸¸ˇˇļļĩĩ´ŗ˛˛€ą °°¯ŽŽ­­ŦĢĢĒĒ€Š!¨§§ĻĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœŽÔÜÛŲuGCCB9ˆ€C FvųüúņŌĪÎÎÍÍĖ€Ë ĘÉÅÃÃÂÂÁÁĀĀ€ŋžžŊ€ŧģģēē€š¸¸ˇļļ€ĩ´´€ŗ˛˛ą€°€¯ŽŽ­­ŦŦĢ€ĒŠŠ€¨§€ĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœœ›ŧØÛڟLCCA>ˆ€CKŸųûøāĪÎÎÍÍĖĖËËÅŊŗĻĄ  ĄĄ  „Ą €Ąĸ€ĄĸĸĄĸĄĄĸŖĸ„ŖĸˆŖ¤€Ŗ€¤Ŗƒ¤Ŗĸ Ÿ€žœœ››ĄĐÛÚŋPCCDDˆ€CqMŗųûõĶÎÎÍÍĖĖËÉŧĸ…zz{||}~~€‚‚ƒƒ„……†‡ˆˆ‰ŠŠ‹ŒŒŽŽ‘‘’““””•––—˜˜™™š›œœžŸŸ  ĄĄĸŖŖ¤ĨĨĻϧ§¨¨ĻĸŸžœœ››ššĀØÚĪRCCDDˆ€COĀúúėÎ΀Í"ĖËČŗ‰yz{|{xwwxxyyzz{{||}}~~€€‚‚ƒ€„……†‡‡€ˆ‰ŠŠ‹‹Œ€ Ž‘‘’’“€”••–——€˜™š›žĨ¨ŠŠĒФžœœ››šš™¯ÕÚÖWCCDDˆ€CPĮúųäÎÎÍÍĖËÉ´ƒyzzyw{smk†lƒm‚n…oppq€pq„r‚s„t„u vx™œĄŠĒĢĢŖ€› 𙙤ĶŲÖZDCDDˆ€CPĮú÷áÎÍÍĖËĘŊŠyzzwvq‹Ã™“{‡™ŠĢŦŦĸœ›š™™˜žŌŲÖZDCDDˆ€CPĮų÷ŪÍÍĖËËÆ¤yzzut‹™Å˜™–{•ĒŦ­Šš™™˜˜›ŌŲÖZDCDDˆ€CPĮų÷ŪÍĖĖËËŋ‡zzxt’ɘ™z˜Ģ­Ž š™€˜›ŌŲÕZDCDDˆ€COÆų÷ŪĖĖËËɡ{zyuˆ–Ë•}¤ŽŽĻ›™˜˜—›ŌŲÕZDCDDˆ€COÆųöŨĖËËĘĮŦz{wl“’Ë“­¯Š›˜˜——šŅØÕZDCDDˆ€COÆøöŨËËĘĘÅĻ{{zÍ‘€Ļ°Ģ›˜——–™ĐØÕZDCDDˆ€COÆøöÛËĘĘÉŤ{yqŽÍŽmŖąŦ›——––šŅØÔZDCDDˆ€C OÆøõÛĘĘÉÉÄĨ|xi΋ŒlŖ˛Ŧš—––•™Đ×ÔZDCDDˆ€COÆ÷õÛĘĘÉČÄĨ|xg‰Íˆ‰l¤˛­š—–••˜Ī×ÔZDCDDˆ€C OÆ÷õÛĘÉČČÃĨ}xfΆj¤ŗ­š–••”˜Ī×ĶZDCDDˆ€COÅ÷õÛÉČČĮÃĨ~ye„̓„i¤ŗŽ™••””˜Ī×ĶZDCDDˆ€COÅ÷ôڀČĮÃĨ~ye΁‚e¤´Ž™•€”˜ĪÖĶZDCDDˆ€COÅöôÚČČĮĮÂĨydÅ~€€zMŸ´Ž™•””“—ÎÖĶZDCDDˆ€C OÄöôÚČĮĮÆÂ¤zdĀ|}}~{wtplhd]G"™ĩޘ””““–ÎÖŌZDCDDˆ€COÄöôŲĮĮÆÆÂĨ€{dzģy$z{|zskd]XSLD;4*# ™ļޘ”““’–ÎÕŌZDCDDˆ€C OÄõķØĮÆÆÅÁĨ€{dšw xywodYM=-‡™ļ¯˜““’’•ÍÕŌZDCDDˆ€C OÄõķØÆÆÅÅÁĨ{cļtuvtj\L8" šˇ¯—“’’‘•ÍÕŌYCCDDˆ€C OÄõķØÆÆÅÄĀĨ{b´rstl\K2šˇ°—“’‘‘•ÍÕŅYDCDDˆ€C OÄõōׯÅÄÄĀĨ‚|a˛oppeS:“›¸ą—’‘‘•ÍÔŅYDCDDˆ€C OÄôō×ÅÄÄÃĀĻ‚|`°mnnbL/–›šą–‘‘”ĖÔŅYDCDDˆ€COÄôō׀ÄÃĀσ}`ŽjklaK, ˜œš°–‘€”ĖÔŅYDCDDˆ€C OÃôņÖÄÄÃÃŋĨƒ}`­hjdO. šœē°–€ ’ĖÔĐYDCDDˆ€C OÃķņÕÄÃÃÂŋĨ„~`ĢefeV8œœēą•Ž’ĖĶĐYDCDDˆ€C OÃķņÕÃÞĨ…`Ēce\Cēą•ŽŽ’ËĶĐYDCDDˆ€C OÂķđÕÃÂÂÁžĨ…_¨`a`Q/ Ÿžģ˛•ŽŽ’ËĶĪYDCDDˆ€C OÂōđÕÂÂÁÁŊĨ†]§^_ZD ŧŗ”€Ž ‘ËŌĪYDCDDˆ€C OÂōđÕÂÁÁĀŊĨ†€]Ļ[]R4ĸžŧŗ”ŽŽ‘ĘŌĪYDCDDˆ€COÂōđÔ€ĀŧĨ†€\ĨYXH%ŖŸŊŗ“€ ŒĘŌĪYDCDDˆ€C OÂōđÔÁĀĀŋŧĨ‡€]ŖVWR;¤ŸŊŗ“ŒŒĘŌÎYDCDDˆ€C OÂōīÔĀĀŋŋŧĨ‡\ĸTUK. Ļ ž´“€ŒĘŌÎYDCDDˆ€COÂņīÔĀ€ŋģĨˆ[ RSP? § ž´“€Œ ‹ĘŅÎYDCDDˆ€C OÂņīĶĀŋžžēĨ‰‚[ŸRSK1ŠĄŋ´’ŒŒ‹ŠŽÉŅÎYDCDDˆ€C OÁņîŌŋžžŊēĨ‰‚\RSQ@"ĒĄŋ´’‹‹ŠŠŽÉŅÍXDCDDˆ€C OÁđîŅŋžŊŊēĨŠƒ\œRSI0ĢĄĀĩ’‹‹ŠŠŽÉĐÍXDCDDˆ€COÁđîŅž€ŊēĨŠƒ\šRTO:­ĸĀĩ‘€Š ‰ČĐÍXDCDDˆ€C OĀđîŅžŊŧŧšĨ‹„]˜RSQB)¯ĸÁļ‘ŠŠ‰‰ČĐĖXDCDDˆ€COĀđíĐŊ€ŧšĨ‹„]–RSRF/°ŖÁ‰ ˆŒČĪĖXDCDDˆ€C OĀīíĐŊŧŧģš¤Œ…]”RSSI2˛ŖÂ‰ˆˆŒĮĪĖXDCDDˆ€C OĀīíĐŧŧģģ¸ĨŒ…]’RSSI2´ŖÃˆˆ‡‹ĮĪĖXDCDDˆ€C OŋīíĪŧģģēˇĨ†]RTRF1ļ¤Ãˇˆˆ‡‡‹ĮĪËXDCDDˆ€C OŋīėĪŧģēšˇĨ†]RSTP@,¸Ĩġ€‡ †ŠÆÎËXDCDDˆ€C OŋîėÎģēēšˇĨކ]ŠRSTSH5$ēĨĸއ‡††ŠÆÎËXDCDDˆ€C OŋîėÎģēššļĨއ^†RSSTSI9) ŊĨŸŽ‡‡††ŠÆÎËXDCDDˆ€C OŋîëÎēšš¸ļ¤ˆ^RSS€TM?2& ŋĻÅšŽ€† …‰ÆÎĘXDCDDˆ€COŋíëÍēš¸¸ĩ¤ˆ_TUTRLC;2(ÃĻÅšŽ††……‰ÅÍĘXDCDDˆ€COŋíëÍš¸¸ˇĩ¤ˆT,)%!Ƨƚ€… „ˆÅÍĘXDCDDˆ€COžíëÍš¸¸ˇĩĨ‘ˆHͧĮš…€„ˆÅÍĘXDCDDˆ€C Ožíę͸¸ˇļ´Ĩ‘ˆG΍Įē…„ƒƒ‡ÅÍÉXDCDDˆ€C Ožėęˏˇļļ´¤’‰G΍Čē„„ƒƒ‡ÄĖÉXDCDDˆ€C OŊėę˸ļļĩŗĨ’‰GĪŠČ猃ƒ‚‚†ÄĖÉXDCDDˆ€C OŊėéˡļļĩŗĨ“ŠGĪŠÉģ‹ƒƒ‚‚†ÄĖČXDCDDˆ€C OŊëéˡļĩĩŗĨ”ŠGĪĒÉģ‹ƒ€‚†ÃËČXDCDDˆ€C OŊëčĘļĩĩ´˛Ĩ”ŠGĪĒĘŧ‹‚‚…ÃËČXDCDDˆ€C OŊëčĘļĩ´´˛Ĩ•‹HĪĒĘŧ‹‚€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗą¤•‹HĪĢËŧЁ€€…ÃËĮXDCDDˆ€C OŊëčÉĩ´´ŗąĨ–ŒHĪĢËŧЁ€€„ÂĘĮXDCDDˆ€C OŊęįÉ´´ŗ˛ąĨ–ŒHĪŦĖŊЁ€„ÂĘĮXDCDDˆ€C OŧęįČ´ŗ˛˛°Ĩ—HĪŦĖŊŠ€~ƒÂĘĮXDCDDˆ€C OŧęįČ´˛˛ą°Ĩ—IĪ­ÍŊ‰~~ƒÂĘÆXDCDDˆ€C OŧéæĮŗ˛˛ą°Ĩ—UĪ°Îžˆ~~ƒÁÉÆXDCDDˆ€C NģéæĮŗ˛ąą¯Ĩ˜–oĪ9ļΈ~~}‚ĀÉÆXDCDDˆ€CNģéæĮ˛ąą°¯Ļ˜šÎ|ÅÎē‡~~}|ĀÉÅXDCDDˆ€CNģéåĮ˛ą°°¯¨™š=ÍŠÎÎŗ…~}||ĀČÅXDCDDˆ€CNģčåÆą°°¯¯Ēšš™wĖrÃÎΤ‚}}||ŋČÅXDCDDˆ€CNēčåÆą°°¯ŽŦŸ›œ—cĘUšÎÎˑ~}||{€ŋČÅXDCDDˆ€CNēč寰°¯ŽŽ­§›œ˜gĮf´ĖÎΰ…}||{{€ŋČÄXDCDDˆ€CNēįåʰ¯ŽŽ­­ĢĸœŸ›}VÁQ‘ŋÎÎΐ~||{{z„ŋĮÄXDCDDˆ€CN¸įæÎ°ŽŽ€­ ŦŠĸžŸ —ƒ{zyzz€{||}}€~€€‚‚ƒƒ€„……††€‡ˆˆ‰‰Š€‹ŒŒ€Ž€€‘€’“”•––—˜šĄˇĖĪÎĐØ€||{{zzŠĀĮÄXDCDDˆ€C?M¯įįØ°ŽŽ­­ŦĢ̍¤ŸŸ ĄŖ¤ĨĨϧ§¨¨ŠĒĒĢŦŦ­­Ž¯¯°ąą˛˛ŗ´´ĩĩ¸¸ššēģģŧŊŊžŋ€Ā ÁÂÃÃÄÅÅÆÆĮĮČÉÉĘĘËĖĖÍÍÎĪĪÎÎĪΎ”€||€{ zzšÂÆÁSCCDDˆ€CeK æįāˇŽ­­ŦĢĢĒǍϤĸĄĄĸŖŖ¤ĨĨĻϧ§¨ŠŠĒĢĢŦ­­Ž¯¯°ąą˛˛ŗ´ĩĩļ¸ššēēģŧŧŊžžŋĀÁÁÂÃÃÄÄÅÅÆĮĮČČÉÉĘËËĖÍÎÎĘÃŗ›Š~}||{{€z¯ÅÆšOCCDDˆ€C I‰åįåÍ­­ŦĢĢ€ĒŠŠ§ĻĻ€Ĩ‚¤ŖŖ€ĸĄ‚ ŸŸž‚œ››‚š€™‚˜—–‚•€”“”“’ˆ€}}||{{zzyŒžÆÅĻMCCBAˆ-BBCDcâįæŪēŦŦĢĢĒĒŠŠ¨§§ĻĻĨĨ¤¤ŖŖĸĸĄ  ŸŸžžœ››šš™€˜+——–••””““’’‘ŽŒŒ‹‹ŠŠ‰‰ˆˆ‡††……„„ƒ‚‚€€€~}~}||{{zzy{ŽÄÆÅ…I€CDˆD€CPŧææåÖ˛ĢĢĒĒŠŠ¨§§ĻĻ€Ĩ¤ŖŖĸĸĄ  €Ÿ,žžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹‹ŠŠ‰‰ˆ‡††€…„ƒƒ‚‚€€~~}||{{zzyy ÁÆÆĀ`E€CYˆC GwãææãŌŗĒĒŠŠ¨€§2ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–••””““’’‘‘ŽŽŒŒ‹€Š%‰‰ˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy{žÆÆÅ”LC‰AB€CPŠ€å'ãÖģŠŠ¨¨§§ĻĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœœ››šš™™˜˜——–€• ””““’‘‘ŽŽ€%ŒŒ‹ŠŠ‰ˆˆ‡‡††……„ƒƒ‚‚€€~~}||{{zzy†¨ŋ€Æ´^DCCBBŠBBCCD^ŊååäãÜĐŧŽ€ĻĨĨ¤ŖŖĸĸĄĄ  ŸŸžžœ€›-šš™™˜——––••””““’’‘ŽŽŒŒ‹‹Š‰‰ˆˆ‡‡††……„„ƒ‚‚€€€~~}}|{{zz}ŖšÃ€ÆžnG€CBAŠAB€CEcˇäâŨŲÔ΁ĖËĘʀɀȀĮƀŀÄÃÁ€ÁĀĀŋ€žŊ‚ŧģ€ē€š¸¸ˇƒļĩĩ´´ŗŗ€˛ąą°¯€Ž­ŽŗšŊÁÆˇrICD‹6‚CD[šŲåãâáß€Ū€Ũ€Ü€Û€Ú€Ų€Ø×€Ö€Õ€ÔĶŌԀҁĐĪĪ€Î€ÍĖ€Ë€Ę€ÉČ€Į ÆÆĮČČĮÆÆÅ eH€C€DŒED‚COižĮâ€ãâ€á€ā€ß€Ū€ŨÜÜ€ÛÚ€Ų€Ø€×Ö€Õ€Ô€Ķ€Ō€Ņ€ĐĪ€Î€Í€ĖË€Ę€É€ČĮģœpSECDFŽD?A€CBDOYhwƒ„І‰„ˆ€‡†††…„„ƒ…‚†…€€~|reXPGCB@@+=B€CBBDGI—K„JK¯JIGDBBC@2’-;ABÜCA>4% ‘  &,68:;:9:;€:;;:;;<€=+>>?@@AABBCDDEFGHHIJKLMNNONMLKJJIHGFEEDCCBAA@€?>€=<;<€;‚:9€: ;:9964)" ’2   !"#$%&'()+,../1335679:<=?@ABEEGHJKLNNOPQ3PONMLKJIHFECBA?>><:8764430/.-+*)'&%$""!  “0  #$%&()+,./1245679;;=>@ABCDEFGHHƒI1HHGGEDCBA@@>=<:8654310.-+*)(%$#!  • € (!"$%&')*,-./113556789:;<==>>…@?@?>==<;:9976544310.,,**)&$#" €  — !"$%&'€) *+--.012344€566†765'43201/..-,+)('&$#"  ›  !!€#%%&')*)*+€,€-†.€-,,++*))''&%%$"!! € Ÿ €  €!""#$##$€%$%$#$$#"€! €  §  €ƒ€€  €ą  €€€€  ŋ  € € ‚  ‚ €  €  Ö€ ‹ € ˙˙˙˙˙˙˙˙ĸt8mk@ =~ĢŋÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎÎĮ˛”V! YÆúũũūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūūũüäƒ&,Ģúū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙üØQ:Ėũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūîj*Ëũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ō[ Ģü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūã6Sø˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙üĸÁũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ķ;4ö˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙û‡ uú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũÉ žü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ņ´ũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø)Âũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø;Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>Áũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø>žũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø6Ŧü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ø! ”û˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūč^ú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũ˛#ë˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ûm ›ü˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūŪ%3ę˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙üw qú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ū¸’ũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Ķ3Šø˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūÁ< "lØ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ņ˜4  >„Éų˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūŪŖR*  +=Tuˆ˜›ž ĸĨ§ĒŦް˛ĩˇšŧžĀÂÄĮÉËÎĐĶÕ×ŲÛŪāâåįéëíđōô÷ųüũūüúøõķņīíęįåãáßÜÚØÕĶŅĪĖĘČÆÄÁžŧē¸ļŗą¯ŦǍϪĄŸš|aD3!  #)2:AEIMQVZ^bfkosw{€„ˆŒ”™œ ¤Š­°ĩšŊÁÅČĖĐÔ×ÚŨāãåįččįæäáŪÛØÕŅÍÉÆÂžēĒĻĸžš–’‰…}xtplhd_\WSOKGB=6,%    $(,159>BFJOSW\`dimquz~‚†ŠŽ’–šžĸĨŠ­°´ˇēŊÁÃÆČĘĖÍÎÎÎÎÍĖĘÉĮÄžģ¸ĩ˛ŽĢ§ŖŸœ—“‹‡ƒ{wsokfb]YUPLHD?;72.*%!  !%*.26:>CGKOSW[^bfjorvz~…ˆŒ’•˜›žĄ¤Ļ¨Ģ­¯°ąŗ´´´´´´ŗ˛ą¯ŽŦЧĨĸ œ™–“‰†‚{wtplhd`\XTPLHD@<840+'#  #'+/37;>BFJNQUX\`cgjmpsvy|‚„‡‰ŒŽ‘“•–—˜™šš›››šš™˜—–”’ŽŒŠˆ…ƒ€}zxuqnkhea]ZVSOKGD@<841-)%!  !%),037:>ADHKORUXZ]`cehjloqsuvxz{|}~€€€~}|zywusromkifda^[YVSPMIFB?<851-*&#  "&),/268;>ADFIKMPRTVXZ[]_`acddeffggggggffeedcba_^\ZYWUSPNLJGEB?<9740-*'$   !$&),.13579;=>@BCEFHIJJKLLMMMMMMMMLLKKJIHGEDBA?=<:8641/-*'%"   !#%'()+,-./011233444444332220/.-,+*('&$"    ic08–Ņ jP ‡ ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2c˙O˙Q2˙d#Creator: JasPer Version 1.900.1˙R ˙\@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙ •˙“ß‚@ Ö}Vqëw¤’™ô1žˇŲąąŽ}R•âiÅ­Ŋ…!x !lel…W8Čâpw)Μ<› Ų‘…ë“›xą[Ÿ$ŧÍ?›?߂@ ֟tĨŪÕwÃĢļ —t=äŖ’\fŨW¸ũ`<đ:ĸâéŗmķ+Ú.ŽÃđĮ>ß*@™tōØn uņĻ ˆÉĢĨęœéÛ.¸o߂@ ֟Z\‰‹Wz{8SŪåŨm]Įo$Ųfũ ¨zŖ‘Oĩ †đ¸yŅōISÎtSq6đ|„ŗe,˙ī0N„X~U×6Ë߂ ŽV0 ;ā`•!*N%žÂEšĩŋ"ĢܡĘq×Įŗ æ}ũirxŋ6AP`ˆb‘aš\_Zúî…؜ģߙėüáö›€Kd!žÂã(˙ze(\?ɀ­Kæė{1Õ;šp˜;Vs>ÄËG Ŧ¤6ĸüÅ@Ĩq^ģÅȒĮŖ4ÜãD°‘Vë–â’; ü° }x3ĐŌ¸pBĄÄY/PBaĪ”xâvÚâ`Ė2ĨGƒwO‘vߑˆÚ{SvEõK`‰Xbm…BĘĪã$[uVa Ցü9E2wˆŊæ_RėøF@xļáˇ$&$ Ø˛y… ´KÜ Ų”báhÚĮPr%{*ęR&ŗwmÔxY*āĖŲÆH+ĨŸĪĀ’~ąø€ _愀ĢÖŖÍ7ĩ`FkÂĘͤn§ĩÎdĮĨÛ°¸ŅB,ēh¨Zst':hĻŧ,A„õ˙U$SŪST\YXŅhYĩdož›…,wz&ų”@U'ƒyū~c)“îâ6ë} ČK˛;ŋĖ~Ŧ6×…ŲŽ›ŽHĻëÖļߡMã/ē“sĘ{DHåNqD4åąŅû‰ĨŲ¯VûxŦ÷ú[Ę|‘;vhÃŦŋp#z `uäįĀÜÉLA>Iū ]xœõ~üßöã7$#›yÁ-û˙ScA(įķÍøZūõãœh?x• uÛ)ÁN–k˜iΘā]¸2üۛ)š–Éžž@ĸˇtĸ‘ƒ‹dčj hT-ÛRMdc&ûŦģ‚ÅŽoβļšÂ•äāŨˇ;đē°čäob+}“ĨM.ã/á&–įræ‹=Ų’:•ķíë"čq›Pŗ5n}!11ō€TŒg“O¤KúŊTŦîŌqLDeîX§D&ũ9†iv”Og†ĒRß[$<ÅJĄģT;†h3…}ízé8l9ģ˜ąKōy ÚT 7­JUiĘ"ƒĒūļ@ŅØ’Íd=ŽÉsä ī[ÅÍ÷ 8ĪĀédœ=đ¨ÚĖ -K `õ<ō Ŗāî<]˛ÎËoęõīīÕMęÜ;Ød?Ņ/ģŌ(Ü6VÅfl&3¸SæŌŨÃâ ŊÔŧŲ= Ø;e41ˇ2€•1rVzLcWĶĮnßoTĩUU–€ĪØfp‡ĶziŖĸ-L›/Ĩ¨¤÷IßRŦrÃ)8ä9t)n›qԆšô‚m5:Ū>dL†Qâ5đ”CāĖØ si_1×ZˆV¤pZß_$ŠAë'!ŧČČT”"TKuŌi zˇč1k=[ö…UŋčõõoåV"47 Ē6™•ŅŅ8­jGhd}ŗ0¯"w¤sŋyËÁi@bÚ!R¤â˜ ‘ĮËŧpģŖŲ­á 'ŠrĻßW„A2¯jåŦ„Ί:> Ļ H¸lN[ƒ"¨ˇԄd/yeA *Ĩ‹¯ëĪ“Ÿ…Iƒ°å“U:¨‘Qũ} ī÷ģFÄaƒŌUמ–T+-‘Ē,0Zæo‰ŠúûD‡2ķZĄ ĄĻŽOëǘZ8āŸ\ ĻĄ\Ûđŋ뀈ꕧŨ=•Ķ„č†å¤¤ÜXqėQ뜴ļ]œo ށˆ´›Ī2CĢIĄ™XNœüĖl7÷ĮŅ ë×YrŒŒåp`ūŨÄē.Œ.i*| .KÄfÖ õ„ZĢÎv×h¤ķrOâč)ā6ßåÍéĨAÖ-U,ßz†Ą÷˙@öĝļûÅvZë—gxl€ˇĘc}1üēčū0 ī…Akģ¸M=„ĻN{pÍ&ōwW_€æ¯úC=uįáLC8‡Áę;™d•ę܏69 ƒ>aP`]uąƒÆF*úŊ66 Mā¨Öâ:ĻoÖEöįÂz7<ų4&ËNŖ´â8'ûõŒ¨˙ Ø4ac˙%?Đû4ų|7ÔĩÖŨÃĸ×đ:ŠW֎V>†ī*‹īTà lšvœö•ūŲv"Đ<ʇdĪÁJ~ qø`ŸH(|üá#ŸŠŠ>’­Ņk(|3Llš@‹Š[ZG-k÷a´wBD)kf՝Ė@dåÕÂuqĖõ3¨jI¨¤֛ÚÃЉMY Ū h1Jā@Ÿ‡<öǏë EšÉ‰kÍZ´×ҝKØ:¤ūX!=`Wí_ÉFŨcö+îáŊĮŲ|†=ÂĖ>ŨoŦņŠ%ø‚ŽģÎ^. ˜.wX°Ķ+ēšũ ¯ĘK“’Ĩִ拓Åõ=—˙4‚Ō”âa~æ"î0ųÚĖŦžĄKe=*×ĪÃ{'áŧQøkhÁB…˙ ķCæ€öcèūš}žŨoä[|§1k€bšŠŽAÃmīüáú‡#kā\zšq™ëh{īPĪU“âÄfžØWÖ ‡˜­ī`gžwŸƒƒĪSÔ>´ ɄĪg–Ēmƒ-Ũ ˙ÍNá5š†ÖŠ4ĩĻ•`=Õå› Ũ¤ūH š{ qĕQ×!dt*I aÅšŊų9gˇMŸđę%5ōėŽ÷øĄĸėgnĐ)õ,ģȸŲ&Ãũm֚ŽWá̘xČö¤ˇĸqP”ķ…ˆKOyúčįËëSŦĮŗ=ã4]|h͎`āYā>iŖĒJ,ËZŧ@BXØ8ÔÕh°ëČļ/ĨDšģ*qˏhBßmCwÚC@Kôå;÷Š >ƒīîiˇyQxØ R‹ažk:8IeąéáėčEÕ[h,Å€žgÅ2b„pË{;˛[ĸËš›'Kf~,¤h؜Íb‘•káŒč×\ąˆ›n—ęåŌ\*eoĪ D }ô€2,{´ÅfŖˇČŋiAB™˛ųŌú‘,ééĄ7ÃĘ­>ų‰Šp…Į§6bTŽ80NkĒ pHõ[ʒ '/ÚĩjJ43‘Ā˙ ž;ŠÜ\Ģz@w¸yŽ>púƒ|âsl÷ĄcŽ"ß÷tūÁķSH¨,lŨî ĀDģÎĀn„Ŧũ–#Ÿnäįl†Q3[đÔé†bN.‚īđôÛõzĶŗļA—îcxËėK’’9KC' …kŖb4dYŖ-:†MŽŽ-ˆŗbZi~i‡>~”6h›2ŧC„´r&ĖÍŦC.…Įĩö¨ÅŖ*ß62Č/ę…ß’‹ö#7ôˆ Si"1mĩ(ß‘ücmwö'?ĐĮĄæ”ˇ›Z„ꊂöWī[ĒA ëņŸûnĶ}oˇŅØŦ×<Žđfá9ŦéĖö‡|3ĀÍ4jzžõ§ gė6cÍMeEiĖÚh#Æs¯\ę¨>"U”áŒhcĶ”œa€Øãlš´u×RâĢcōøĨl–˜é/_6Ē—{m’CovH}  ŽŋS\Âķ嗧˛ öaė/ŗü Ė›5SƔ8š^]ģŠĨC›ũĮņdōÄb­ŋČMr&ĄZáēĪŅWÖP0ž†čģu_āPšcŪ=ÆĮņˆMšË%˙*Õž¯ÄŽĶdBü$­ m$×: ŊiW?bÚŨŠËîB[§ąrá2S•šZ—4Ų¯!Ą;ųUQÁ‹*åžô”Í$k ` ĩâ~귖k‘ 8[~NīéúÜHxëRŠžģ•PÜ{ŠŌt”aōfœĶhUqaQVaŊQVœ­iœøÎ˛ˇfPģ:Ø%ž+‰6ô2>yTƒ{Ÿ^Å?Ã:š†+ô`×čÕOŋĨ@9‘#Há•øĸČŖ{ĐĢ5R/eËY„dĸ%|/x mɊļ÷ąî6œ&Š ÔLŅYŗČhčü×ŧĮk>RŠĮaa\z* Îi`@L ģ6D÷h‹=”Õ7ņŠÆ¤ÜÆų‰1A9ÃdíZ͗Hč3ž‡Š>_oę ŧ’qmąđ8.dŽJņρ¯Ō•/CŽöČæO ÅV^7.īĄ}ūq¸ōÃ/ꗮņ#rA;iunģĢãšČĄ†ˆ¯@‰å9^gĮ66Ōkiąũ0Yģĸ? „(‹ßgĮyĶi<Ō4a*XŽĪÃr'áŧŅøkËY…+rĢs]ãn§VÂUåęôĨ´Î3f{Ŧw†TȅÂåpJ:o*d3˜÷āuÚ%,„ŗĶ´×4j=5ß@~“ačmÕ1ᙞũPĨ'¸îÅt:|۔Ōî ū\bŗę“6h Q–ön-ôUm„YfŌgĖnT=Ž˙Z^Gŧ¨ė4įĶtŗ¤n•.ģšJÎhD…&@ M1kßęļ&#ŊQŦ#†a¨5QȒ2—p2Čr>åČ1ņikBÂ˛7g.Ōü+Lji’F‡ę˜ėL31­Į-– îK*Æ˛CšgŖ­Đ@¨wŪ8‹z4 ŽI+?†î¸ŧƒ7ÍZÖŠH ?ĖĨųP/PÜ t:SĨ´ų ŖYÜõøÄnõ~ᐆ‚ō]Š6w¯2{@žĘD:‡wJ:VۏˇT×3ķšékhš„˜ĢjŖc֔ë×/žŌ8Ä)ûĐgÚXn}^ū?vôĸ_N!ŒÔŨ§œ´bø‚c~´ĩŠģô<ûl–‘ Ŋâ‹+9á›|-M›û™™ÆŠ}•!Ž•Q˜Ŋ6t鉠€įpßN&^ų‰ÆašyKø`ŅQ…q-ō ėL5 ČíāÍíl¸•Bū?/˜Nn˟;ŪųNîFŸîĮ"ú6į ĻĪÃOxЅĢjf)ã¯c9DĨūTL2:EˇÚ"ēĘËKNŖ!VR č&ÕĐ8˛P¯­ŌÆmŲs4d-rØŌę|Å×AČŌ[SëE°PötĪžĻ‹ķ”Ųä5nÖYk1aŠI0ō*IˆŖˆļÆ&ĖO+ Õ}œ|nú~`đ0dŊã§ŲĘ+ˆü3RĮŪÕīÚØ,äøÍaã§FhŽ-"~ē\žk5ŠS‰D8eJsFbvāę´ ú‘ŧPí+ˆIPô™(ŌŦē†@1‰a /Š‹ÛĄ4?ãJÍúWW͆tŧWÎ>2˰ĐMhC]b/š;.+A¸”Ųä3hh‘¤0•“Ā+ĨŸ´2ę”ËĀæÔA‡å—ŖHīc1`X(ũŧ´hTmę§ÁüÜ|]cXlĻ–đ@ĸe˜‚ul#˜ąlĸíʘ€SG͝&đ¯öEu t~i‚\0íŨūųHY,õŧĩ> $°č6¯ȜîÆ×ÚŨWËĢũf×ۘr¯öB§¸’'Ų͑¨`tŽąl6ŊgŖJŗĢ%ŒĩZĩqEŋčvÖŌL@ gˇņ=øœüö5H‚ģŲ¯!Ą;i…ũ)ŊŽ0*h°SÖ Dw8>+—hĻ$bĸ4ĀęSKŠņĄ'Œ\Ž”‡ąXŦ¨‚i7í}"›Mķ…Ķ@;iT/EĢ­ÖLüõ ē°āO2­—\õ`¸Í“ļ’ä>w—åPģbĀ™Ŗ¤'Ž›üÕŌ*Qė;€ €īÜĸˆ×úžmkØĨŸoÂsŖíãƒëˆēOTF:lļÕšė™^ԎqĒ֛m.Í<œÚ6.ew:ŋÆ ē]@¤ŧ5ĒîĻęō°Lī1 FžÔä§Š}ĒG$Yt#/â2íD‘ˆsÎ{@Į#nvČpĸlt„BĖú*!5ƟŖ.“S9˙AÎÃ/ Q5Oš8Îš†Ú›?z‚œœ)W™8 ĩčjjßS ĒDŽ#kĀ#°ÆĒGAŦķÅ4ã;#šsôŒn xZūÉ ‹:ą2bTŦ°c“"oĒzÖuŗĄ¯ĪÃYŖíŲhü4\ËŪųdJƒ¯¨°ø/îũ.-(q)~úŽlQ]ŠF7ažøåD)Ļ.´$ī­Vĩ ŲŠú˛Ŗ5ā —ēN`ĩÜ[0 ~Đü4Jˆ°ØaėÍS§ ŨtԚ”Iė$vöė†:ąZÛ ššôđŲËgÜ1/Ā˙5āŸbi>…ߍĢ/Ē ¯Qnh(•û_*dCĒŪėÖF¯°uŲđyQ ŽC¯°ø'lĄVđ€ÂOŧĮķúANÍîˆ}~åb87dĖSS‘÷į\[îyhHŦ Õ˙{a‘—KæĨƤąÅœë¸Û/e°ŽĐÂŋ ą@[Ōđ…pĻ×Û&ANĩ•dė,„]å~8øÕįKå6­*ĒŋĒ‚(Ës:$˞Ų@ÄEt=M›û™™Æ¨†\ øb‰ļFi å-wü˜.ųj`õ ąÔyļ8ā{déû°E}ļgŽíą˜Gú*†jĒÕ’Ė”ĩņķáŊDų, f9čxÚR…’¯ęŗ˜“ĸ´Ĩâ* ?īŦ3„)/Y63_¨í€Dd¯4x€7Dņ~›ŗĀ5 %¸Zģúbō]fdŧ|Ôr+áūEįßß]h–Įƒš& ”ž7ɕvŗD&ƒŠļ! ŗyĒ6Zũ_U¯Ēü؏LX‘š[Š,ü§ nĻÛʈIĢPōųgcfŲļ\ŅŨ¸bk„˛ŽT*Ōu§Ę˧PÃâĻ„:xņ°šŅĨQūÍŽzÕĘjņn"Ŗ‡ĘŖoÅē°Ũî-q6 >ėœŽũ%˛ÎŸ aA{ĶÁ‡å—ŖHīc1`X(ũ2Š{H4“úūm,ĀŦ\Ž Zb,āß$*ĻÖéîāûÂLÛŊ-`"CŒ Ϛ=ws 6 ķų^´BĮPØ´LN˙6iĀíâΕŋzKínĢãW–Ąĩ÷Ŋ÷iûąŌ0ĮeŗÍH Ÿe+¨…ŪMėę˙sš§ÜQŨ”ąĮf|7ʄÍĀg”Ŗëwõ˙‡ÛøA#÷GOĢeũÍNI ķũ™44 JĘĪYÔ(Äâ]qœsš4\ķęTŧ3¤É „…hˇūJ3569]ĻĐ;¯Ét!îÂŨƒ đ!'îčø‘ Œƒ˜+‡Âîč&îÁk<Á3ˆūYÍÍՀ:*D_Kâüļ@ĘöÎĀÎãpÍm2)ÚÅų¸.-<ÚĀ w"™ŪaŒÔI„pûŽ]H.G,¸}ī&lsĢ—æÚÄpyk{d2טë ėŧ™_6˛ ZŌTû™]/ÔFx k5ũ$“`˛Îg  ģ+Ī•ßõĻn:i$Iįō!ã5žx—Áĩ .ĸĘ&nčFëŒX¤Šˇ3Û@Šsß÷ĮƒĪŸ…cƒHĢ$Û!|û“å|Ĩŧ[â) 9ûžũ b<%ĘÚ Ëf2éX‘ÍM*ęī`ŸHœš”]O‰ô‰9e{KŪÎKĀSĶ&ũ[‘jb5¸.h§OũĖKĨ•íī-T×Ūú×EįĀ j\Ž0Ÿ nxĶņu‚´ļķ>Ņˆf”'f­kĐŝ å ŲÄķ Å7+ž)Ë'ŠIā”Twė8¸m#Ŧ-ßnžîA) ‘ ™Zy/’˜^}pļ3WŸžŸZô›īÛ÷ÅÔXbĨhĶŦsJÃËî…B,™Nô§@Ą_ŧ~čŌߟ¤}ž0û{QņØü@]ŲˇķY=ENŸ%( Ķs!œq›Փyp.…2áäøgąŠ.ä/ūíīŠ ŧ– Cĸ0D.YŒ&Ē闤oŗ›Pã­īģčĩ•ŋCŽŊÔ9”oš6“ģÖ`čūŖMw´20Ō{ž$…ęyCé­^ņ#ãģˆ*•\ŲeA÷ĘÚā}j ŨDŧ}|/‚ !ƒÚČíš ÖÂgØxH;E…%„Ö¨ū_jŅF8š:wė‰\*…˜˙ Zļ[Ŧu$‰2ôÃ<+8{Īų/÷ŋ9ßī˛x,ėĢ"do& ÛūW7øČ€ŸööŊŦIRkęvKkĩ\žŸhũWõĩ¯qâˆLžcŗžs{Ę:L~´dŠåQŦAųåm(ĶŸJúT¨õoę˜PZMŖ2Ö#×Y˛ęy‰ļI–zĒĒO p”ĻtOΌRĀŲ2Dg"p1ŌŗÜ˜…mĒ‚ē‹PšāËųØ40ŋø*J†bû›oúxS ĒÅlŗŗu 8 ģ˛A0e'Ƅ]"Îļü‚žT HEe¨ōœsÄĸã{û Ē`ŠęûnAÎp&V’9„#­FGĢĐOF €{ōڂ@e Ô-Ķā ÍĮąŽļ]v—%”s|rCWŗ$‚ۋƒpŸ}ŖXÖûįVPĨG˜™áüũ›Õ;ųīÄKFéB0Eã\ÄsŠöאń—™_zÜ"Ģj#G/(Ψ×ãÁ%ŽB>Ė ĮßaąÉ¤jũ˙Zā>äËhčņQ*Ü ÖĒAĸ˛ŋ)G{%JÖž„Å<Ė2SdČ†Ēh€)¤˜ā#čføW#Y“GĮ#—ģPŋ‚ķ:ņĐän„˛Ę1mB`“Žĩ.äÖŋĀeÎâĀãø‡VØĨQšį^í˙eDD Ãøĸwr?‰xÔw°ų‡îųLD8`ģōĐ̎#'.ôOG>´/ôŗj:I×ZāQá,Āĸā;ëRo´žpīđdNÆ*ēNļ*C4ĩŋŽÔ YFĶųŖÁĪf-›ÄRķ6o?aīŠßģÔør÷ŧ靋ŧ|čÁIų‰ŲY¤2uÄŽiĻwāāP/Ģ`ÎOia:2E\ĘĒlTØŠŌ…*kf qŠĒ8l § 9:rí˙ĄÜÚQËĖ]&ĐSčŧ¨?aÄg+"€dIIŲB? 'wŧÍöų ėJ$ÛBuž\ÚFĪ_æf1-1ŪŸ•Ž€#LÔĖPĨ@¯Ė2>ŋ:ʅhæˇ}L„ļaPlz´Mž;Dek5Ŧũr5wÖÄBü×éÄŋîšiŸš<ņ´/S ÔĘĸų‚ėáJ-‡‚õŖ¸š2.a¯‡\/×Z‘]:S'íŽqĪĄIЏÅéŽ"%mŪ%¨T3ē Õĩß"›­wUsšd ‡sĶ]3đ˙gëÛGĻØO8s´ŸOMRuY1œÎôÃâûą€%yЃ‰?îŒĻīļLhŧ˛ÔĩĘ^ķ+EöŅȜˇ|Š$JÕ"#Ģ‚T‰”˛ŅžŌT#]ŗE5ʍÃAüBÕøAîI |ÎDˆāyaÄ~8™ ¨ŒGŽ+4÷Ëĩ˜$`ģjīGOäŊ+V.ÛlPՆ cßbáŨÍŋæLõÄ j?Ļĩ6QHíž•H;§ŋÖ$yƒ]čĐ}Æũ¨ĮVę=I6ÛmļÛmļÛmļÛjšw3b š¨Sl}‰¨ĻŞ*y´˜*ĀSŊŨÂ5ô\až’acîĪ”Įæ9TÂĻ6įWЙg×đúœl_ųĶ*z‰$§Ž“ãnûÚg:LéŦ´z¸ÃŨ^‹ĩË.2YĪÔīiij#Î*+åŊøŧŖZ/åÁ˛4rI$’I$’I ŠKé+Ŧŋ‡œ…đ[đo•22jF9Ʌ¨{Ųę[/úŗĶ†+f*/FYŅ ōBˇ%ÆH:íŋ2ęÂA×ÅÂŧ&‹t|ԎĝņĮļ¤zŸŠUú[ ņE[bO!­ŠĀ–4V‚cŪĄiĄũųĄĀČ-ũė= ĸÄæŅīNb°$SøéŨiÎĒén8Đ›´z¯¸ÚgHPmÉ RâĻEĪC5,܈2§—ŪVŊįYâ¯Lcũf7OKnIÜccT˜īpg‘šŪŸC@éŧd$į¸}\AdUa€ų,ŖžyāãÖø›)?Ei˙\I¤Tc{!•™jG ¤0ânAô'ŖųŅlĒŠÍmr\æ€ë4ș+yšo‹ģH&ŨšAđ‹ ŋĀ3õ1Vsū>ôclŗK]5r(ŲĐūʛ)!Yuî7š÷…ˇåÂ/õ8„SˆÔÕ"ŅÎĩämĪ$‚šwm•āØ% .U5ÄĜÂ_7 5+ŗĀ›fšĮķĖ$-AãI›ŽD8R^’d8Ģå"<[´ķ;ˇŋĨ\V\˜|å3Đ4V€ŧÜõƒ/ämâOIMJŗē-hpyĩ™s™Aąß*Ģ˙÷ ­•qÁ–XÁ+é[uÁdø“ģ>ß&ōūŌFd û ÕĒXđÖiĄXk¯;"6’Üģ_‚œ|-4ģeJî<…`ŽRŋyZ”"K€ 7%ąMZVRsãÍÚß Ú &lsž &CÜ!C]†õ>Ė0-Ēôiå€Öj‘Šé\jO¤pt €čėÚĒßR fžÎmx>ŒáąFŲ'gæ'•7›…æ¨Ė­‰÷īģ3ÃZÁˇ’ŨÜŧAtŦ÷•=/}6žSg(KÖZfŒė2KaŅē?°†aW.:7ĩχOšé^é¨ūš%šš{ŧY.Ãˇsi MĄÎa@4ĩYjáN5ŦėŌ7W Bk”ÎOД4š˙Fā´ŨĄ or_ Uøg×j 2 Îõ›É)ų 'Æl•ŪԜ1l›„)ÂyĪ@q~ôOšš&LIÖTM4& ĪiˇƒF 1œ‰¸ãësŋ`vg‰‡RaƒoLH)?Tækû‹~ ™-ÛׯnpũƒW4‹ÅpČžđ§ÍŒĩ/LŋĖnĐˆÛ˜­*ķj˜5†<@k Nv8‡ž…Ņķ7ŗÍitŨĮŦŽ™o÷ŽEéŪ /Γ¤ųēš)ÅėŖŽ`Y´‚{šFģiYΠrį5C›&‹•{ãAËXĪ-|§é,fįöÛīÔũ­˛ãt˜ NŧŦPpĸfáX&xČŅJ-V.ƒfšCü’te¸|á îY­ĩ ‚npgnq ŗĒī?‰į&§2'ãsā §“z¯Ų_ e`&˯x Ã˸eQætHškāā1 ËTMBĻR$JĄÍ%“‡ūW‚ÕnöÔ5ē“ųŒŦįĪūmN–ôVŋ8‚&;ßŋSƅa# ÂrIäđVC‚Ŗö>/ՇŒ6ˇ"ÕË'îĖ`EŌK÷QÚR¸Ēˇâũ`Ė‘(m›‰|/Vר%Ŋi{íđbĨÁV|ÁSlbc™îÎä°ŸÁaŨÅĩŪGj ÁiŸôgĐkWf¤ 6ÎĻĻˆīS’R`8kûøÚ{ĮlZÄíĪ'—Ąlų†'WĀÃäæJœdŊ"üŠ‚Ķã´ÖÎ"SĻét˙UežēyƒëE4žÖM3ĩHĀIkjØ4\û -KíņVíkĐüüļÉęÁ\ģë=īũ$Š™Õ͆ >=,įĸęŠ<äūõˇsvļä{B*¸ˆj$ļ˛šËĪÃĐú>ßIx}ŊĻņØôzeΊj} GE„1pz6ûƒ‚ˇqnķķrģmųm“˛ŗå_UmcDhC|ėAČ ĨÂXH~$ųģķGkH÷bā\˛ysģVu,Z•É×ęÜ0ŖŋĘm‹Zô[ų¤Å,“Īp/žrÃŊXėۂÁ/Ō 1Rņķ¨Äp%pÉ…@]šČw"yE+“Ö¸:^đ‡$ēĶ_S„ōËyUj,f&P’NXŲ2ä€FâP mŽĻũp4Ožæūüķīję„_ˆŊN˜ Šķ˙BAŦ ˙+9Ģ΃Ôéī f(„&ÍHŖšįÔ*>Z(3Ãn—h >t&ĐĄēåcŽ!ŪčEčŠ<îî9)g@vSŒËXd]fËŠæ$ …+h‰Ržœfų?÷PĪZPbĩL†dļ)Ģ­öÛú×čaų2‡˙euÜß"xcäüšƒÛ ĪYuE&†ų°›P~3hAāĐ)Ë ŒĢ{! Ä'ø…Ÿôya?TNv ?žSõ¤åTuw•Æ ÷öBqtã@ælõã§YĒzę;Ņ™"_‰)k:í'B{šĄu‹Ąč0ÉY°ˆÅÄW§ ”‚qigo’ōL}œÔŗžƒåƒ¸ÂE00NÜį"!ã‹iŗãú&éđëyØJū!RQũHŌÄN¯‚,N„Bæ4Ą6˜÷ŗë+˙o͍ų•NVj F2T҉ŠH}ę€Ōĸ“oR …Ĩ Økŋp lMøAĶĄ3­w_~8RŽöJ•­såvLZžŋLļOaˆzų¤ĸŽÉ“YĮųY, $` >—lÁ <Ũ„Ąbį+–Ž>:GįéRû([šŽøúčôLB?Š'w÷#÷ņX—G{—øqžī‘”IJ A¸Ožf#ëeĄ'MĨjŧāÛFá? “ Y{wũœßŠ’GšãPA2읚yļ }KĀ<ĩ(áhŧß-ĩ‰ôĢsxT=į:Aså Ø‡‡ÄE]k•éyøaâÆÁ :ôq˙5  m„cĄŨÛĘŊĻ^‰`8âžMYŪmį‰ųˇP´Į’NĄ|ũzęņžš¤ŧL‹ †ĢĒBײrQ?|ĢO—į GŽ,Ø2ô˜zXÔũY„ŨĘ%ĒT]3K¨įšņˇ_Ĩ4‡Ø;ņǰLnH@ÛøĩŠAdāƒW%šY…EŨž[\)ŽŪŲe•†/g•âũ7­ųQĸüŊ‹÷̜ËŋhûQŋąwWŧGø›Wã…M>įđOy€,zˇcŋëC‹ÜÎAXÔ¨>8…ũ0áZ#6‹ųØŖbüÍío-vbû˜âj9OŽ8%ČÜČŌXķŋAjŌ$z’mļÛmļÛmļÛmļÕuč)ĸ"8ŦÍÉCŪ#ãKō*9ņîF­ôķS§Ķ + F+ZrÅĪá‘ŗ)…&ĐÄsĀÂSĒTqčž×įgy<—”E8 3ō~Ė1¨‰Ķ´ûŨûWí<˛ Îrk<—Ĩ¯ûu˜:Ü•:­æ6FƒnI$’I$’I#aRIsŅeʋšEHŽ/ųúąíXĢķ?‡˙QŖxc͆!g¯˙uz”fĮÜA}ęčĨĸ@˙>øūņ“^í~l¯úOįž‘ wyBŽMā' WėfōûŸŅP-ĘÆ>líņŒŧĨúmF ōdîĸ?LŒŠųūü[– Q‡Ú–ÕZ֌˛ųM0ŋŸŨõÜ"nŅęžãi!Aˇ$l,Xsˆ™= đÄŲ÷$ †¨Ô8ŪËÆÉė§tBŋ;ėļÕ&;ÜäEfˇ§ĐĐ:oIyî{pdDŲ{ˇ›JœÍ†f4U i‘Õ  ’(LO~š¤?ēMä@ŋš‚æ§?‘å…ĒččH%ŊCŦí/ŌđOėŠJ‹ßÜ™ƒs11ÛÂd ÂxQØeÕ–åéĀĘ9XƒtSĮ˛Åēå_ËCCÆ5ĻO°™įKG­f„u䉠@‘͍—˛nŋnz…ŠOXq÷ĩ§u ęÆxu2˛a“ˆ>ņl/]›îrJåÜe´Wƒ`” €šT×s |Ü0ÔŽĪm‘°•zo2qũļ7*fā…ˇ•#q\Âa;žŲĘF ÁÉ¨˛™č,ÁZ*ĶĢ ųx“ŌSO<3k}Ę-"¸ĘYƒ‘Ĩ[ĩ•bŨĖ“§/OšČ°C¯ÍΙuÉÔf (x2ßüĶĮĩŦ¤Ąđ B}JËO˙s­āBėü„-F8•e2++˙wûOĸUN°Pô2 Ÿ |Fp˜jëIRYZ>íōaûQŠķ4˛S™)OĶĪUg[#ūP(į@ė7J{Eå<3¯ų͍{[Ē ’S‹€..Mkũ­ÔtĐV7yŊ¸¤ŦEÅØ>rÂŪüä2J̈SkužÄV`øüģíŌĘkļô™&Ūeņ)ÕÔË÷C°G)ßŧ­J%Ā’ØĻ­+)9ņæŒío…m“69ĪŠ“ ˆĄîĄŽČ‚LEŪw[ąuUMA!”.ĩ8Šø€@…–Ō~ī‚dķ…š7^#Äŗaã'įĄī]–c–x%Ĩ(žTŪnšŖ2>ļ'ßŧ^ėĪ kŪKwrņŌŗŪU!9ļ5*ä*e}^xæˆZ˛V„-„’œ‰¯yTĄPú+ĸdí}Ú7ŦŠfchN>+X'Ŧ72Ņu 2{ÎP™ëū­S]¤-•gøB Ėë¯gí„\Ņa,Ŧ+‰&ĨnBßbČ†‘Îųu‰vf›Wę6īÜkŋ]C?>V‡]Ļļáw^ÔĢō žĶP)At†Tä"æCŊ* kČ]ÍãĩØWnīČ5ķÅ%Ū§j,Įđ!û§ ånžëÅx­˜ųfÅ.×.M­Đ Ēifƒũ Tbė°övŧ%GOJ‰…<ĨįēZÅ.Č;‹ Kîęf5Ž\+?¯…dŊÔĪîYŗpŪ"uYÃ'ÛĘe~ aBŋÄÍS ē`-ÛēŧTYøŖL~æũJ) B¯&ąę3$“9CØøoz!.Šž !ųuáõūTd8ĀNtžƒŽ^ũÕ'ē]ģčL͏ĨÉŅ…“ÂĢ'ø*§kĮĮüŸpÜŗ>”ĒqË_YHėÍäã-ÄkŸĄe.ču˙&?ã~ö'm \ájÍ YdaākŦ,kÃã6Ā-ļv§2'Û¸_eāΓÔIĢ!Ë÷vˆÛ8ötQČ3žaĮ1ßÁĀĻFļO¸RęAéĐnĖAx@{Á‡ŋĘûBäwū—˛Û°FÐŋKߡ&5”Õ5(”ÜOŒKC_E'`A”F¨ÕzņĨúŖwŎ3ÕnįĻē?ĸ#euAŠÖÄ kÅ2”Ôøzõ8­!A„ēķU~ ļt:/¤We5ÍąeÃbš„-‡\Hžé-Ws‹7yŽÄ´ļ™ĒWŊh:Q…Ės"üYÚCÖ&  –4ĀOéę|bļÅķâ8VHęßf{|äšyo`(§I–}Đ:ĐōY§Œ|ž\Ü pm"O Š‚ aõ#ö‘š!8ņ1YÚ ūëãԁy A†Ŗ’Ö$‚ŽÄĸįĻ#š$Ī ¤Įny†í<ēÚTI˛sn;Ø­å›4@ķnXŦ$âõĩkˇ<ž^…• &}ú °ėmõÛKw>ÄH{áåO'ŪÃ~í\ū¨Ķ~ŠkSl¸‹—›d¸‹Vk-æÁáîį8ĩ!‹šÚsƒüI],™Ķ]ãøæ~qņ–ĒÃĖáĀΏĸ“ē›RŠÔđĻŲ”‚ŖG´0Ŋ‘f!—ŠÍ¸ŖZÆyņOĮÛÜd}ŊúÃíëˆÍČ{¯đ†;ˆÄÁÛšA)r[Sā(Úy‹äoZÅPņRŌŪąž†1æŲ´6‰Sƒ‘÷P˜#!Ģ}lŅÉw†Ĩ¸Jē2ûBHĒ'aœh”Â=E—ĢaĐešÍ>–ž´‚Ų f›, ęīp}ōdčĐfZ!›ÕčmnԅIáĢsš3ŗí*ûG #8Lõ {¨øšcûX–č'˛ĩŧvíŗëŪ‚ĄOh >s)eÎĐ) âGDũT(ŸUM’D*A‰ęYzāŒíTÆĐ0‡](Sqô+ÉÄĘ‘™ŧĖņčāÔíą°J+…đ¨iÆę‹rWĩ[Mžø´&XYÛų[˙uîzŦ.‹'SxRp˛–~Ē ‚|č×Ķ/,VÄĮFČa\`¯d'N4fÁQQŪ:qĻ Ĩ7¸0ĩļT`<5mžŊbČy§üĻôæ ŽžĩŖ ¨3­T950Ŧ_‘DÄß9ˆn@ō{h Ÿ˙b•ūz’\ëĮîž(` ą+Ä9W-"ƒBŖČz'gĸëÄANĮO¨ˆũǸųvĄÃ†ōšogŋV1:ÆréĄõßėīĮ QŪÉRĩŽ|ŽÉ‹W×éƒ6Éė1ī_4”QŲ8ŧéŠí?‘đop…Ö°0˜ŲPāģŸbkr×2Ø ûĒõč^öĐ¯:^/Hi8vWō‹‘úK° ģđ‚0¸< ƒ”žË Ézp}ŸQ3šíaU.&Fõk˙x‡Ö~zWœĻ•Ž„ƒCąŽ3ÛY$kā.$X.ę#1ŠÉ<&øŗ,›_Õũgŗœ ĀĮ܄Q6ŗÂĄđ_ĖHĨĶ-āí]ÎF(ÖŠQv@Í.ŖžkÆŨ~”Ō`īÆĒÁ1š ˙‡ļ9ņk ‚ÉžéX\2ˆ2/¸DLõ=Šō0ā(ąOŽáC­ąĒw.ķsxvmZßŋĘ ƒĪ’%›/Ŗü}ëÜŽÄÍ`ų3;Ņû^Å[‰āŊâëKÕ7ŽÜĖ͏7~9ô¨‘“&Ä;wS×üGĘÕØlÚŲ5û•BÅÍÕÂŗū ÷ķÄŲk6h(÷/ ļā˜ßå0Mę VPļ3vƒ…ö3-NKú7–a¤ĨŠĀʏjFĀŊiLøE|äļLhŧ˛ÔĩÔ¨Å#ĩžiņ€i*˛§X”ŋŋôĩílÄ}ՆāŌÅĨ gŲMĄHĨĢP€{~Éšü=Âúöķ@ķ|^47ę7B:× å,ĸN;ü'WĶ8*ī9Ŗˆ/0+#ĩ" Q,eķİ)đž+‚Ņ0TjÉĸH'Ž_&ęKĸðX,m|ŅS~RmļÛmļÛmļÛU×  Ĩ‰ GZH—įÔzˇSŌˇU­Ŋ bGc×Áė,~¨]0z.‡_|):ŪcüˆEá‰č+Ī\•XRŠ?P,vx›‰“UîlSķ?D_U2 Û’I$’I$’HØTŒĀ ’hō úWĒĀÅ Ã3Ū–y‚eĩâ {ēŠUé—ŋ€9$5ŅėÄ1“ģg-ßX;>Ĩš!vFŨ;ˎƒ Ŧ[Č"k^œK§Zįu?Ŧ“|W#ŧ° —§––ShˇÉ Ž]Æ[@ex6 @¨ •Mq1'0—Íà Jėđ&Ų+ WĻķ!×Û`ƒrv ‡6Éz)Ų UQ–vi;Ën(ČÛĞ’šxס~īšE{7ŨGĸ×ų7€ßiJÀMÁ€c“Ä‹Âā öL2¯ 6ã!ĩĢŽž<’´˙5)-0Đ]I›Đ˙nŅ⇯QõÂvļØ4…‰—íß˙o gé ôķ X}Ŗ4tŋ˙rU m‰¨ō@f>>įûhduxšú›ŦĐô'Ũ}ËÖ@Úü-30¤4ŲnŪ#ŗâ&xŽä÷íâ`V;0§ÎĨr%`-…\un‚•MāFy5ü?dMãŠÔ_ōôĸHå!Û÷•ŠB$¸Ŗr[ÕĨe'><Ņ -đ­  ’fĮ9āŅRaq=Â5ŲIˆģÎëv.ĒŠĸH$2…Ö§LÉa]†Mˈuãyˇ_ĩÁ\Ũ­Â(ŧÍ÷ĩb*€o%ģšx‚éYī*‹įNŒ|}kˇA‹{Žwʂ !Ģķ?[úĩÉ>ųÛEļ•Œ7ė=T…*ǞLJc|Oø~+]ŧXbģR2`™Žô§=ī _sš†gŨęLh`–Õ0ƒĻ7X$gÍÅØDY§OŖ'ŅŨéu™\!4ŋæ3ŦŗycÚu *¨°“žY ˙‘&Ķŋ˜Ę2ÉoEÉûdúwũųÜĩ ˙Há/ŪPŅqÍáPFAÖ Ál܁ XrĨĶxZ>9I•‘ã IæYŦ7ĖwZdMyʁ ž{˛88g| ĸúđoMCa°9–ã4A“æųņ´^'4Āũíéļ ĪR ŧ`ī…‡}W°M¨aޚWŪé7jƒęļq8‘ˇßŸ›Ŧ˙)JĐÁd2ˇŧ¯Š1å_ۙ>7¸_b’Æ-„(ÂŪŋÂØĄé0šz?~ĻÎŧ3Ķl횂‡ˇ´îņFÚ/ęÁ8Zģ3Ĩ~ĸDEXō–EÛqm$†ÍNޝöã/öäQÂ˙íV=—Ž0~ŌKäsĶ9Öč>,1ęņĪ}Î—ŋ`ũܑöRîA-ŲŒâö!ŦšąÎoöņ#~ĶȆ´@–ĢKˋm ] {Ū*—ŽP'8˙Yw‘W@ĸÛöĮŗā JøĒ`Ē#<ŅÍ~īĨ"yuÚˇĀĖfÆß2éį:x˛öfĮCfoėãmTŸ´kÚD˜IÅ´˙ĄåQ`ãî>ŌĮ%ô,¨a3īŅ [īöo>žyŊÕnŧ+zO?ŗ(„đÎŖžŠe<įRP^YBĢ,^r¯Ë@lš2‹Āž{Ų{ũ´ŅŲš&CÅgÅ ‘j™Å0ūõW~' íŠķĖŅá˙ v˛Ûé ß"íø×{™ĪÃN'áĒûlĐ͖{´ đa1 GđįÔ?˙AŠĨÄŊĐåĄ$AUí÷é4ÔŽGĀėūŋjVų´ū͡Ņ*îŽÄ ×A2`ŠdģY眍ÉEE$ĀøÆB$æúŅ›ëHYįûÄã.‚Ņ20ųKĘ^Rō—”ŧĨ㙃oPFyöM ã+7ŊÂÃxö&Ėʧ%/)yKĘ^Rō—”ŧž×ČąĶäwĩ^ķcąsB6YŒTôæ-‰ÖúÚ][KĢium.­ĨÕ´ÍU<ŲíéžŅŅŅq̞Ōmī“%ęÖk“sÔÚjIb€ģīzčƒR,>Ûī a AļŽā !:ˇ×/ĒøėA†ö2“3–×ĻįžRō—”ŧĨå/)yKÄU…15āāÛÂŦĮŗšųsáĩ'ņSĪęöËåt‰h(‘:pIĩGŗ‡Ã.,ϧí TDŏß$ QÖÅGģfĐQũ€ķåoȞ_˙~šĻE€Uq*­=ס˜–+ÆįĖ9iŒá– ĩ‹%ĘPhõđšvŗĐ9fā[x^_‚ʏįTåļaz§MzÁZ‰9c\oÁŽķZläoÁ„•…ĮF6Ėå°?(1áÕÖIk,ļŌžNVûˇĶ<Ė&=Ôę Œõ‡-™ÍžÍ‘÷x¯„>î@Vh§ŠŠš`jØŖ?3ƒü؎čĩĒ?ŋ%ž‚`ۃ­wų)$’I$’I$’I$’I0ã+ģ’ĄW3/•ĄvMW:æŸ{8Ã&ŌbA&īÆgp˛âūĢÔ~yQK‹1˜ôeT˙ZŠŊԌ9Ŋnē’$’I$’I$’I$’I$ŗ`ŌĻCšrƒ˙md•Ģ 0r:Cą¯ú}´Lƒ­¸l´%*žfØKõO´ˆ&=ƒÂ3eš Ļ ÷fģs¨m‚ãhŒ>”‰öAĐĨõ?ôĒ ‡š—S=Uķ bĢ2žq֍LÅ¸ÖY:‡čj[Ųx؊ ˛0ØĮmŸŠ&nی~¤ŨŽ,÷HĶ-ĶÖâ^3ĢAöpđË!úé–ä˙ D_šAĶL[/ō•œ]zwŨÁkmĢI ōs˛RgHhTžFÂTː™č˛";ëiųËmâm– >åöõ;}Ŋ5ßoU˙aÔæ?oN×ÕŅEõuG}][`ũ[;úĩËõnęŨāė„œŖ},¸}¤ûhëį1ÃŊ/ĘE=´ë]ŨiúöūĒÔéĩö‹ķĖŌQĻŸ0Ē˙(C¸ŅMģ˛_‘āų  <:$éyģĸ(ÍjčķĄ„q‰JÁâ&Ę4;GŒŊ Dab“”įÃôēv1ČwķU}÷†bv%3Ņ9J~Îė‚úYåũTčã<>ĐwbP•]Ķo†˜ojŖ)Ú"ŨĒ’ä éé5ĸ…Į‰"žY…´&nCĪ‘Ё‚zT` SQZĮēËIüW䱀†Bb&É ¨8(Aˇ?]°GŠeé-…72T-ÔDā é}D6Oģ„oJâWgIí7H‘wNÁTßԈĻNF+üH˜Ž4Â×/”*/D—ŸĖų8§ōĘ$gÔĀf›?eyZ“…%H…9Đ'0*4ŨĮaÕŌbEĒŠ„A7îj׃ZšiĨÍĄžœûâ}uũ—æŖX”'Ö[Ė.~úÄ|¨ōŪˇ?ĘqÛŊåõÄQ~Đ)ŗŗ›ņ- Xö’áĐÔ+vÎĘR—įą ĀkrŲy0œađ5–ŠŠšĐŊ9Pܖ=—Yę“bāu¨8‚$iid9B´ĸI :˛=SëĢč×CžÕą7ŠHGĮ>ĢGüƉŒÅ€g™‹ŊĒÂCm’ŊĢJĘK=x¯ãSõZ˙bi–šãæĻpģJĢ‹‹ųiČÆˆEîaŌ6Š­˙ˇ.Ļáã7‰;ŒA×Đ|ÆLDĶēķu’S~;}ū-r­ÛŅ õįPIQ낰’ĖH Šö]Č@&‹(\<ņŽ İ;a*¸aa‹i| H ÷˛g>ĘdAI%dĀĸ°ë°‡ â~Į*:eœ$ĖC#¸kŲ[FŊ/Ę_†uåh›ƒ:‡ģNUOûǍõėDPvēl”rã¨öÅĘʐ€Ņƒ1œH;níréŽ ×`ÚãOģŦžLŨGCÅ:÷ë†ā &­Åųxŧh‡FˆLŸė2Zq毅|”R#,5ŠtŠļāŌæü &ŌÛt.ŅÁ‹Äú¯ƒöŠb[É$]ü˜8yé´ĪāMėÚIq7C Ŋ‘ ƒ.†Vũö[™ČæÃāūZĶP @Ä$ĐëƒvÆJļ÷e7ęĶ6F1Ē!´¨Öœ*ī˙ZZya `Įžžī0ŒīŒ~`Âiõ;“Ķ,”>E˛ôíd|ÜNôRÅyŽNJq패öø%ÃĨė8ŧĄ_đ&t0y. ŌžÂAÁ Æõą"ޝĖŋ āÃ=ū@ĀōŊ ‡Œęꁀ@=Y<0 aį%\_=n{GđčįæQAXūēūtÃ+čĸÅ>Í"ˇjF‰ÆøÕōĶ&×ܐæ4ÅÅņÄæŲŊy‰ˆ8B YŒÎōé+‹púp ŠM}ŧÎ} ‰ĮG1GÁjˆ­ @žždåzÖĸæūĩģÉļâķv&:Ą¤ %§q$ánŧnL­Š^;jcÄKRs2šŧĸMtÅ"Œ#oJ’ąÃëĢ=S訠uíŗ ÷dĐČ7)mä ūî×QސϤÚ7ā\UÛF Ī:¯‘ä/8Ú˙8 s€šej?Ļ.[FŌ°tčŗ&dėÔ ‰—¯Ô!NbZ.ˆ¤HE c¤HÄŪ˙N"QpņOÔõĢ`˙üđĮ yīÔõS˙ƒuÔ÷‡X!†aũ?yŒ¨\OWūb˙ĸˇĀ ąTŸÍšÍŖč¸|ĀĮün$ų Œĸ'čË´7]ˇė~.ßĪ&č]ĄBAHĪø@}û‹~Ay`¯#˜GxMĄVÆČ÷í7w´æiÖˆŠS\ ĨZ@(Ė ņu†;šųîĪÖˇ^ÃŦ!o‰™â'ņļ"OŠpef ˛4PʎÜ-ô”*JuįîŅ-†žZ5ŠG0 1:5ÛžĒ°ž0;įŧ6ĩÄ/ÉėĐŦĨQ§ÕƒB’!ŒÃA_įĀÖlō×ܭܨf Ž1TKCÔéŖD^Ŋ€aZ͊[UpS„Ė1F´†5Ō…ļ´äe˜˜gösË\kԚÃcÜpûĀBdÎŌŦ€CŅ ažōÚ ũ°›`ĸNd{2}Zd/b‡tzīŪˇ¯Č‰ÕM'c‡đČŋ/ Ŋ0l´K2k)ŒŠZĶe渟N(9¨×~?Tåaõm6’?ÔWš;{¤cP;‰Ôn$ šAČа#e+ˆˆ@ĄŌ™)‚ĄÎŸˇĖcbā­™ä„χ{ßŅƒ35\DÚhWĢj:ėL—02œwoÄļvŸ%ĻEŒ{ą%oŽŪ Â(ef˜¤’€1ŖqčÅãnĩJãá[4Ķ< 6+×QZottëÚš Į÷ŪōJ ģ'=a!7izŗ™zk$᛭ųÄū5 ä‹4e’T€l•¤éЏ,™›B#.™zō”_ņĩĪžīĢ6’ GÚÁŲkbNã¨eĘ•ÕŲ‹˜ ÉzX]%‘+æÚęũÉga;ŧŌRâ8äOåÅۛƒš~”â'ņ3kl*{…šØ>  …n?JÉÍ‚Ú ķ‚ũl›áŸ°6y…!Ge?ü1a¤Ģęp‹FÆÜ‘r‚ĒG îvŽÎXŦŧ‡‘j ‰´Ån&āM øˇ**5Ŧ˜‘ |Ā•Fs—+:ä.u`W§äĐËÜÁ ™yQĨ’<Ÿr§Ü#snGbĩj ŧãđcđ'ķÔÄXQĄUĨ9č ĘáĮHgĄf?ãĐļäRĮVf¯7„ŋËÁfא Ōží!&™ŦBIiOHXŠ+hĢĩũv&‘ĩPÄQ(xÅaO\Ļ×kjxČŠ,‘ÕT¸UJV(EĪSYīŋ/ëBW‚kVŌ D„œvíDĪōųØŅ<ĮŨË{*ŠîVë Ķn×Åā“) ŗiĀ."<=Št+ēhŒ(ēRßHPãŪ0“ŋą†æ ŧčļ ’5¸`…ŠOX‹kW6˙H?GŠÜáŅf/{™AÂą.Ģyũ}{Û%ĨDŦ鿉šNIũÅđG­mÛŊ´<5Én e|N€ŧĄėo\M$¯ŗëŗæ’į[ȔŖiÉ$^éĸöqēĀką!÷Äįåäļbö?—˙ā¨Höœũ%ī"ūéŨ͡3ûĻģ‘°ËŪœš ”ôn_mÃv{>ÎĐb "îø_ėUÁ°ũČ_ũ‰ŊOOƒ'H“žc:wŒŪ)ĻĖOĢ#ÆBŋ˙UĄ" åNŊ\’™/; “8§á˙b’Ŧbw͓°/q‘…œeëŅ ŅyYAų¤BŒĻ€(KBËwSŽÁŽCoO%ëÁtąBĄüķ|yü ‡ųCĸnūŲąA:™Ū2ūž‚ģwÛP¯ĻöĀ÷yŽSŨĖá2™Âj*Ų?ßĖŖ%@ßĒ 9ļoôx˙gŒą~J‹\ĨÚŅ,–9}!9GŌjŗ‘3‡Mđ.›Ë&!(Ž R÷€Ī4äStŠeÕpo!äĖ{õŦ]Ģ—XrīvËūgĖZ jNĀæ™ŲīÍØl>@7cņŨĢYUô™úäŧŽZįōZČæ bQ23 ByŸ|gŽŅNUÃdâÕlüŗ<–ŋĪD:‡8JØhÃŲlx¤LāG5QsđNItĶJø“ĸ›Œ2Đ_–ŖųO!tŒv&jnzā62UėķJtæö;ž .úĐ͍dĘĻĄ’\¨qeŌĪĢËJŖČ‡Ģ”n˙WÛîÉpOŠFŲ ęgai„TŦŽ<ÂÄk€´Ua;Ķ÷Ā0Мíu?äÄ'N9 Į^=G!2Ĩ-Ž™íz‘K´u˜5­ÅlyĢčČ2Yqšô˙jĪõ"%ÁiÄîÉiD^Đė_"ųĩ(D~G4˜˜Yį•ë+sŨf–beÂŖv]_íÜ8úZ…^ņÖTHŧ2xMČ—j^ žZ˙O1*Ččāá†æåôĻÛCåxĶ/ˇŨŠŽŸ+|ÅąÆëŖ”ĒD, —†ĮņœâŠžË}D8)\Ú HLė¯Ę…%CŒˇ'ôLĢë8Ëaī|´Ô`BÄAĘ,ņÉ^))ڏ¸´īë@[­\k÷į4č%ę*­ųÉ3ŨãēˆÎN3Ú˙ …0Q×9…+؈ø# •ĢëŋũīV7í´ÚÖ$,į?Ũ;JņxX=ũ€hAØYąDp .ä¸ŲĐo§rš-Ų韭2&Í<}Yŋ‰Xk[‰Q÷ōÚŌđ׃ßJ€ēd:ŌËÆ÷Ũרâ‚ã˛T vFo„¯l̂Ҏđ˛Đ'njfnâ8*@] " &ėˆ5yãųjä]~ē`˛F˛]Ŧč´ÚđĀû$­Į!…œi­5ŠúZkr¤ZģËHņ›7!btgķéßŊd—_‚É\nŠĮö§Č0ĶēáLü†EģŠ eZ'ži§3íīc”0ž\%Pf×ĀĸH,S¸H6ךA† U@<¯LôFŸcŧ+~&hĸŊ uūŲŌ"ŊgĮĮ-Į܅ryÃM-a(ļlĀ äÚā / õĒ(;‹Æqĸ\1R€L–™ĨnŲ']ƒ 96įÂūIh!Xˇ’Ī)ŌR ×öķŸvÂ8”…ü( ĢĸÛÛsԈųŗÍ¨}atéŽē[=šh'˙˙ųķBel“ŠŧAH‚tÆ}8ęaCņķyĨ&÷8PINo+>g@&ū˙—€>V–učŪã§âšÄáņ,˜`Ąšl2PÕ/Ũ{C—yôTŽØrÆH;!ōāčšę÷ÃĮB''‘ĩ‰Į§I e!îēÕ|6oĒÂd*ĮNÚÜë…™#ҐÖ×åŨ6W¨jīüØŅśƒ06`€æ3†‡ŲÁ¯~J€§v­Yßōūīčĸ!hô”‰ôyVæ7į&ÍĖv .YȝJ˙JsŋÚ)Ėn?Ō‹MÍRdÖđ=åÂŌ’&Ķ0´ÂX˙/‘6•7ŦĒ–ļDˇ…ĒxK‡Ŧžģ;cËâ…ŨÂ´lžŨ:Qa?ģå×ÅĪœ„>QÕ÷@+ƒq;E`OŽhĨp$ēE ɔōÅhdāD¨ŠûŽ\/Ŗ“önƒÜ§UßLs"?YžëôlF•Į š–îōN˛ŧĻYÃãiABKƒŗĘ*œAģõ-ĖĖĸ)ˆ°>›‡YqÄML&wy‹­¸×ąßļcs~#õBe˙HílŽzT$Ģå›Wúķœ-xIõjŋ^gŧ”ų*ņķ_‰"d8UWZÛįíŧ;ꗗ3ëm-„D-#¤šzy/BŠoūd-/b` ŠĐÜģqiwŌÍ9ÎfzPœo[—!Ŧ9 žyˇÚ!,æÛqœÜPûũä`^o@T•‰ė…đjŦĢ!ОVŪ"?˙u,į̀ON^ Ņ'" hÃXÊ*ß&ąOņ3ŦvŦŽŽf ļX†øˆE>zzŌ߁åLmR°•žōP&b4ĀtqŋÔö˙!Ú[ÁwĨJ)Ķ" ŖũˆŽ,˙~[=ĪN!—×üÉyQ&YyÁB2`á6ڃĮ}ĢņÔS(Ĩ’r\¨×ÍĮ0üÉGš5‘rē~UOõ÷,$„&Ąūq{Ô(ą˛ķ!AUŸÔėĶ%|)Ģ6zÕüÂÖčŨF,XM6°Ŧ^ŗZā0Nņ;š=R€ĄeKD‰0,ÚTüœĄŌķF°žūŖg<¤äĄfc%¤ÂÄ×-7Yāč)ū×øÚ‘ōa(ŨöļpNâ‚ņĒؙAœĮ'‰y´ÂĀÁ÷™ˆ€‡ōP%fŌ•LÂgm;ę_ŖwD&5:ۊ=ũ4ãöœˇŧG†~ģøWŨJrhd­UČ*ųlŨ<ū˜(ÃŨ|đ;ŽŽHŧÅq0¨ŠtņDō]ž˙O–!MM=īĢĒzar&÷ģā Ō9VyüÂ#Úôj§Ėq˛˜÷1Ēs c—Ú2ĸ†M6 W|í!Z/cVĢŲ Hũ.×hÄn‡õÁחü ÎôG%Uãä=`E!Ā/¯2Ši¯ú^'âķĒĢ2S.ÖĨ‚ĐŠŽ"šn|ØVgUī.ü¨LÛ~ Ũ`:â°âˆ›{ũŖ!`m#ęwŽĀ۔$.}Ļö@ĢcåncJ§jbeøö+f­Ī×!]ˇsy/2dNŅ’5C˞ļDLz8ƒí‹(|ą&7Â+ŨiŽ(,bīĪbÃ°ÃĄikVũ›ą5ːe“ ģdVH‡QUdé~ΐuãsoŨHäTŦ}ĮJĄ~yčŠS˛ū*œväĘįÖŧđہs_ĨȈĖĢ]! ,Å*dß Ŗ~v/JR1ŖnŠ™Æ8: Ô'zŠĶ+`†™+ˆž˜ß¨AâŸ.š)(ĢUyËHoŌbŅ_ôŧlđ9jp"ßÄ]Uô¤—Iƒ!T‰i"Šėˆ¯døÁīöšĀqy}­ .VŠĮą†…“B™SāGųâŊōtÄûˆđŠIŨĘPô5K\K¤RVĘ8ą˙yî}°ÄXZ'šyģÖw{§6Š}¤JŠ/Ō.én[háxMrX ”ĨVŦŌ}r¯r K,@ę%WÕ.|TKƒš\A˜§û>Õ÷ēuv}4h–ÕS<ę†Öė[ÍąŖ#¸ŖNøõ×>Ž;ĢŠážayZÂ1bFMnîß_´œ{ŠHz#ēĐķŧu‹Šķ_RˇOŦ€č‹RYč÷˛ŧ•’{Ŗæ5N´/תÕ#ũá9äZ“Q!ėĪ éŠ?/œ=H…Æ5F~€™ˆø;Lb&ĐA€ÂˆméŠđ;9ÔúĩënĘ—Ŋû‚ŗ¯ägjˇĩįÉOÁĨ‡H…Čĸ¸ú˜ôØíš+)…™<˙V@ŅĢĀA4}Ë*ĩ-tĢ9Uw{ĨÃ)ÚUķū˙`›ô4*ļ—sĸ¸Äį ÅV:Å;ä?ąNQôũČæÉŠ›Õ¤U’G9Ŋd—ü÷CŽ+ŖßV˙Z] ŅŊˆ…od+ņ +$ē*?8fOōŨ|/|ĸtŪ—ˆ‰p’q‘åQSÅŖéû[ÃŽj B˙k58WĀ§Ë€Į0ôM†éõ@ŪÚ{`Ī<ŅuċIPiãF‚ĩÍ~ŦZMVŽM÷ʨ"¨ĸ&]×ŪŽpr&j3˛V(ųJÕE:ėõ…íŋĩÍĒę!ėŌmSđœ÷93Õ]÷ĘČ ZÔ~M`?ÎųŸŸŦšm<^ĘģiøŠĒDČJ2,iB¨ß•Ņ~kNŠ;@o›;ôÚ 9čUå 1Œ„oĢĘ™p|ũą!Ĩõ´ĩ‡vĩ‘e4\‚âqéāÅ*#ž’ôAûæOęoBBŒ2N)åy*ú¤˙Ksõéđ‚‰ŸÅU]æƒôsÜäe“¨Qĸy^ôč~*.qÅŽæ ÅIw]šE‰?‚/`ļë€ęĄô,Äa¯eúËŅ'Ô;F†Ģšė˛˜ÎˇŲŊ1zcō-ĮLQŨ7TÜÉ@ ĻŪōü‹ ĘMI—qč‘ü;ŧōĒ;Tõ"0âH˙) +HâMĮ“`ŸëFiÞŅŧĨĖ<GÆC9^Ü[Á¨† á~ŠdÎŦ€Y[0.•úŨ:Ŋ˜f%?ŌG|%Wėօ­ ūãģ$Äûmš‰Q°š'ēDƒ‡Ų"ŖÚGOxũ€Ä—×å˙=rŧ1ŧ›ƒąĪŒúŖëĩ@˙ãöô…}]-~Ū°?oRøũŊ,~۟oĢĒËęęĪę×oÕ˛Ģu˙+wė„ĸ˙HMhę—-+QDžgåvâ(ęĐßŅ >wæųņĐEgœ,ƒJEu—Ąņ˜Bˇ9d†ëûŠē!§b8ôŌé-5˛ęēŧ ¨€ŋôĶÛ´Ú˛‡;É3.xdPDf<ģKöxŋ÷ļ rē…–ŽONõ7ʰTW%Z"į^,˜ÕP?~iÃz°pG)G͆JĩŽ™Û͏Ėo‰Ã*ĒRti•ą\„ŽŅS‘ŌÄ0ώˆ{r)v!ŗĘōxõ1åÅH>ųė)^Õ܌ã_ФŸĻuôõŊÛIrĘRTzgöĨy[Įlz}Î+.9‚ˆ!ųŋ>d‚Ü÷ŋÃAAÉČ.PĻ^$ŗIšŌĪ"ȡDGf-™$ú˙\Djē K0JÁR˜fDŅ‘oĶÄu:%ŊŗÖōĪ$+ĩ_ŨƒˆdUũĘÄ´‹L1R€•tĩ‹NķĖVÃĒâá¨#…××ĢßČĘūJęVĘūčU›‘EŒ‡9+oķW§ ėIĄųæIŖ Ųē؃žŊ²&8ü´Qxؑ å˛đ{ņ3ŋ lTbķT­5úHÚU{Ķ>:RˆY›ÆÎöÉK†­ķEį#°) ¸:,v`&:*]Ÿëeb+s,RV Ģíx„˙”s<׹’õsyŠ ūuâįë9žbĀŅĮŒYyÆqgžz#ĖÁw$˜N)ú˙6wí_‰‹ēŗ"8Ä6ƒŸ+ƒ;ˆŪ‘n&rˇĐˆ¸ĪsÂ/?ûÁ4ÜŨ¯˙ņ)æā¤–×Jë*ÁžŖƒ˙gS‡Ŋ‚Šķ °z)ÍJ¯‰4Ēŋ_įĘ$Ząŗ^9hŧŧŧŧĮ[mM(ĀrË|häßq&^8Ü1Û T7ÕO-ÜõC¯Đ#ŗČ åiũG[?i‚cž“ĩžr8Ŗ"Ôŗmô€Ūņ<Äûn5x›éî>ŽÍ2øĄÅÂå˙<õHčŪ‚ŋ:ĩĖđ)¯îY6—&—!5wlt˙ ^Y_ZŨ9â8ߗÚ~5vũ/Ųßå/ôU¤TõÎjģæ[ÂĒõ\*u“ÅĄ]oc˜•:ŖTpØĢųQ>mĻkaēČŦÔũĸKfšŋ=qŠ-Ūc%ėsƒ7>Z"r^&_QJ@#ĶĸŖ˙R&ķá ˙_Üeä–0éŲĨcEå=‡Ũ‹ņ`e,9÷ŠOô"*—įĩ]X/m=‚vŗĖ"* ÷՜ÖōëŲ6]Ȥ9SĖõ3¸“uąŽ…pɸā1Ŗđŋ+p9>m(ZP:1/Ė‹¤Œ&ō&ë7Ũ„­3_ ģåĀÃ/é(- ęŪđw‰Ģ_Ņ>¤ÄáU´ hã-ėž™i^^~Î0@C™y,Ÿ˜ā˜|ōiĮZrlgg4ÂC;Ų7_;ŧd™[.qJŸôÕqÔ#Ō‹ÖŊŖ+ĘapŗŊįĄfūŌØĪ] €ģdë(串Ë˙Pâ/ęAŨIˇŠ=åŽ'Vȝ씛ÜÔ_IKj–4āæEE›Í}¤…—`tFĄqi‘Hģ†ˆ(ü34&‘¨øĐRq›€eK|ĢQ&QƒĮ™•HôķVŊ&‘ŋŅ´ĘgŪÅ,0…ÅZ*•lÔąĄ Áę|qÜĸЄ!šį¸n2jÜRI0šõė-RÛũuEzaŋU$¤Įõ’f*äĩo×´_>NėŽÖ ŗEb)Ã}ûÍw›rf™j/Äw^­p ),gmQōąĪ^@c†Ķ%uõĢw÷ŖĘ˙a9šhģ[ļûĮĄNĢ׿–•°&Qu—" @%$÷Âųõ|ŠŲÚŧOÉīÔ&tÜÅūņhEũ}#îŖ´_æ˛ĀĻ"˜˙FâO‘Đ˜Āę"~ŒģCuÛ~Á÷âíûh™ö(tjŌ&–Žļw÷áŗ‡é×yq>Ų…–d •ũž×ƒķ˛Â!ĩąÅĪ*Ėi‚Nа¸ô˜é’9j*Ā=tĢͅõ§‰mŽ‘=­„„xŨ0jĐãm}ē"vî€wÉ|âFÕ‘H´JĖēĒ)ßw ũŒKoŒ ˙UŊ:(eę˜2įVKmWŪîâ:ņķ*L3}Ļ%šŠˆöSŠØDėŽãž:ė2y—­‰ŌezƒÁ÷׍ÖĮTÛ x(ęÔ'Qš,EšąÎuā_q”­ŸƒûŲ+0-0ėø>q¯Jk p`EÃīU “;J˛D1†ûËjQ5Ÿ%VėēO)BúŽü ~g5[Özŋ¨o¸á§RŒÛ–4YgmrŽŪGmŧ‡ä×BÆE#}c—s˜ŗŠ&ÖKØ-ķZ>šše o~ĢÄķģVi ]ĀŦŦˇ­bAFęĩSŗ)ŧa˙ˇËofÆ´ēÜ𓭇ę åFĪ0@AyüļßōĮ’c!á)ÉīG¤đáŋ—ûÉ XŗĻ˛N׆<œāã˛6ŋIG6Ėôw%e¡pÜū1ĻLž|ĪãĒÉF2íÉ_Ōۈ÷D_Zy̎ĢkēĻ pŅ7 oˆĀXÁ‰žkûMé§Í}dŋ_u>ÃØ˙8]=¤īljhĐķÃksîØfegAƒ§0nŽte}øÉŸ‘WlšŒĩ–˙JN"Ō¯ūQß1Ë ę¸ _‰Ī•Ą,lUâĐxC7æÂ#RœDū"Ļmm†&§ŒZļî2rkÁ:Nö"~Ô8ŠmPgå?z0ŨfŠ'S€gOž„”:Ōi˜3fŗæ¸ūBŊĒĖ-Æ:Ŋ.Ėnи‹rĸŖZɉĖ Tg1ÉrŗŽBáˇVq pM ÉØ Ŧ˙V@î2“ÕčķÖ_pŊd ˇQ‚kOh*\îã2îkƞôæ™)åCņ°Ņ9vš3ģÜžĪ5ĩCėQČĨŲŨ˜ķzáW­ˇæ~4­?<ų4*k [Ž0$ÚE.x[BG.ŒŨh“üÎ%Ô{*Ā_ĸŗ]đŌ¨˛Ŋas āÖ ĖaŒk Ėęíī¨jĸ­ĻĮÛV‹Üw,ž÷ų ãŠFးŽĶ&‡ _éUgĢ&¨Pî;sŸ."<>ƒÔĻĘhŒ(đæÍŌĀ[˙FĸYÜúDZôi‰ŽÜ0BŽTŖ Q@ö&ŽY+Ļeą°—/åßÚ~‚+1 D”î/ôeÁĸ8Žß2Ūq\`EāÍEĢéUüŌ¸Ųē:3žbĢšį×ŋ‡aĩ‡é¯‡Öœ‰‡°ōÉí<‚i„båšuÛĀîûem|„ā¨Höœũ%ī"ūéŨ͡4Đx9ŒŊ/`Œ€T Ęx၊–iÁg:L €—‰]'ŧ>ũ€fŪ}$°ųŗĪOūÉ!ŧÄā­Ļf, ™˙~v\ Ī7s@æ{Q Š5 r†ī›'`^ã# +WĮŌäĀuRLvšxw?#›ūų2˙gč‡&ė -q‘¨LņZ==+m;Ö@ß({7üXTFÍTŌYJ…É;K ¯7Â^ߖ;pg ”ÎPÉPũ“ĄÜJ€;õABĮ6Íū€NĩÍ^Ÿ XŦĢa˜”ķmwppĸF˜S2Q§Ë€)ßįíū—ë’‚høĨs§ ÚÕbÛY6N ũį‘:ĄÍ2ŗŪ š°ĐÂôYÖæ˙¯YUô™û,ŊŽZįōZČæ:ī­…N´”aQ™ úrē.D*ēzXĪ*†ÆŠâ˛ĄÛRđâ7o![ŌJ|ĸGoä˙ ģ„ƒ}Mšfß{z|ŽzĖ&5)¯8AP_÷mü*ųƒyķvܗ›ąŖŲã§˙s•ëæ4ėÂsÛ÷$%õ+"Ŧî`|ëƒÛHobc÷ŪTęĸÜiFŲ ęgaj-í.$),,Fžęˇø lÃuލĐĐhNvēŸė3Yú'íŊ!›?dr€ôAV‚ĻVhļ]]'átöYÂŧ§ŠØF ēląNļą&WHķķ„qđ՗R))˙wLxįSđ [ ×˙w^rö0AKīrĄÃÚĨÜ|‘`˙yëë+äˆģ ‘ų÷M1ëáRƒ´W—õārYb­?¸J3%”)Též×b".õž1Ę!ÜtҰX4JE#y.+šÆčōÖ°kØģĄi eWĶ&OŅÃĶwˆĐ0Õ Zĸ6ˆ†H ŦŠ3õŗÛ*Ƃ‚—Ōuĩë{'GÔČÚâu¸ŗZû€”Yj*=PsãĪ‘&Ą ČĖÍŽ[e^O¯Úĸ3ë{™8ųúøĻs=x¤ƒŨų¯ųÁ;à w‘ÃxáŠ]Ëúvšl m<ĖKGtXá `ə)jTü‘†J€5X]˙xz×AJ€ū='Ž)d”…ß,ÂūDGŸ@”Vņ͸ņtžÂ7Ŏŋ Ęۍ•ŒĻę8*ž6~Dâ`%BsBžÜ̚yĒ'bĨá­zĻ…üÁūEëK/|[ßr1Û°\vJ€NČĨī 2ãĒ(ÚąæĮŪl"3R~BFõ؈Ģę‚z”@?ŦÃMŦ¤ÕŪĻjxeŨ —^Íč>qģÛܰã4Ėž2đĢķƒ{M [ 1ą:a!˜äeb÷đęķŨ #€LbP÷œķĩÆMpČ´žZ°XHÔĘ0ŠÆŒwŧ™Ē WC™1 ë72ˆN+ÍŠD…10•ĶÂøg”0žK ˇ Vb,S¸X åá­å‡•™Ĩöŧ˛ÖwãüY_3ŗ")}w´8‰Le7Õ<PM|G~…T0äĖdĒõp÷ÎveŅ”Â„°būá‰H€ėŋV¸ũ˙TžßĶ2Iiöōņ ú'Uô"´đHĒwDPŽ} ŠljQ›u€. –đŧvkã°Ņ—­˙TVūlĶϟö˙xíÂØ€&V˙qFTq”ītõā§”§¯@5āû´adFHJ3{.°˙ŞĪQ§yud“ŧ{âÛšĮYWđbsCāĻØ™Gī€Ÿ~āVUŅ1q[†2T0&UuĨäžlŊ\đP š÷;# •אäįŲk›GĀģX$ĻáîØsô€ļC­Š@:Ī Fk sãˆj‰’!„ElŠz"¸ ‡ÆÄÁ"xrĻQį1–’@'”ŗ€ÅcėĒ反õ–axŋAēbwiEŦuėLÍØ8ûQõޤ‰ œÁ)”BdD"CcI„åúGār ŽØ‘Å›ƒ06`€æ3†‡ŲÁ¯~J?čŠF{ī‘rĢüŖ“Ö•ĸęQ4„9eyũĖ–/¤õVQŗĶˆŖ@"ĸm'WoKpcdÉ ËtĮŗCĨ\ Ļļ1,rjŽaëĩüjãŨļ~pŸVæ ’}7å˙˛™EĀ181ąaų]D?pōœ›>F˛ÍŠ üaSôĐ÷˙Ö7)Œ6Q€VQK–1Î;[Í˛ûÕäs'ŅÎŗxzlú1Đšā–¸hP¨VēiņT>™\ÜF3Åw x7IúÜWë‰âNœ™(aûJ ^°-1ģ.8‚I°)„Ĩ›­1uˇö;âÖÃ,noĀ„cŪ¨L ßäd;[#ž• *ųfē…#‘ŊHßAė1æ[– ų\”2B ^{¸…ü‚¤g…žpđ:xc¸#}.#üGā­?TuË?7‘÷Ú]üŽ5ûģ—< ×Ķy›Ũ‹Pœo[—;„Ų(õ‡(-,<ĄĪdlzõŸ[kaŖū˛—`ÎķáDžŸâÆ™ˆ#˙í)ā€ļz­û`ūÅFķš_ WCw8ŦhĻûk4ēVĪ<ŠčQhúh›úΊËX‘ĸĨŊœ‰p6—€Ā ™ÉI>ÂTÅĪP=/.EËĨÎ! ōLdUŲŨ'§1 ‰Ÿ˙}¨N2#Ę$#ŇBöh˜’”đk‚4XĪ8ŗdĮ|ăi A(gƒF˛QyWXaíy­;.nvÆĀJPĄ U.€6æ ($â yLĄm‚IŅ@É(ŲŦb‚f=PîŲ'ų{GOė<×õëT+ą2 ‚ iœ’&AæĶ đöT0Ŗë¨4 8E4TŦĮjr÷ 4Ŗšãcģˇ8đáŒÛ^VäÖzA–Z ļ5õŋ{°h Ųœ—‚ėúōū°eFcŖ/+í}}l7\ss.m@䱕åyx!`,,vEõ5?ŸĐ¨ļĄŧ†ũ\ū†Ëá­ũ†DŖgŅúƆëŪęĶqĶŽz˜hB‚”;õŊ>Gw ĻL“G:rb‡´ÛŲ Hũ.×høƒM Î+… ĖŊûb Ŗ!}@G€Ŗųåæ;Ō§Q§ ír§… NîęÛõööŒöŅ_k[+6sržĸā~q $ˇÖĢ>ŖdQy•ôĘŨZ+Ę*ˇ‡Ŧ~ģd⎏õŅN•ĸ¨‹ÎĄŦeõ5ækoŅžš5Å0ȧŦíâŋÅ;’ˇĖ2DÉĀl$Ë|ë9šÛ‚ÅKîf#ØíÎÁUŠāUązÚûĢ“ž!Ã:‹Bīŧ]ÃYt:ø”qt&bø`áF’ØģÉ~‚é |xę(ãė;Ĩ ŋ ,2:x/ÛwŪʃRņŖbD#÷ŪÂu~ļ'fá–ûŒāĖÍTO¸Ęcļ˜īÎX/øV#5ą’“[áôģē{¤Ô÷‘ÁtQēķ‰ĐĶbd/#ø’1†ƒ•”ˇŊ§úüכ8p„ŋū2gŅūWĸ?v:ާ’´ÚƒĐ'2Œ6ˆŦJ _iŸLâ—au(š  ŠFŲ†[ąšĘuŊ¯>UD­ uđËÖd&Ūüķ­féŖĀް„Ā}CŸ$įG‹8f&¯c†đJF.ømĢķ Cč†ä|õĘđRAĻQŅA%wšƒ å ŦoŨ”q Tüđ%†ŧĸĶ@v~Ŗ‘˛å (Õ­€6€~Š™ô7/ĖūƒŌ&îۋ†“m įę¤ĸÁÍŽĩkŽoFŖ&pO5 wh“ûē÷WË`+ įüpļčíĒ#Ö=Ŋ2Đø#Ø ĩ‚ūČŖKˇĮŗœJMá´uũ€dˆH—7Š9Ö?v˛%ÍėlƒŖŋų ĖĀĻpš'-īOįąėžŦÄŋŠÜ°}‘nŋeú4&WoVû؊­Úļ¨^fČÁäč‚.iԇĨÉFˆ”ģ˜"‚û$ü²¨{yŧ­2púÉdúųßĻ,×č1}iioøÁ;ø:fŠūXЌ"ĖZW_­ÅāA4Ū™j,mŽq^ŲB;߉Н S}%õđ9üŊÚ~ŗšSÕ`jŪIĄÖ›ąfÆø=ZaŋÅé}ƒZ"”|˛qv13Å\{Ę™eŋ’núž'Cv:WŗG¯fęė3}A7öÚÔtfę›'1âõBÄ1Ŋm ÃUdÍúī‡@¯åĒļE™Ûĸ[8>›K¤‹gWDŊĄZų°!õKM’ķĪeŸ‹, HĄį!Ŗƒ[oq?2ŽĻ˛8 ¯ §N+ĪĐ?ĢÚÛ˜ŨÆŽÚ2Ė#›ˆ.‰¯&Ņž¯y>E­cĮôÃĪ]W˙ãöô}] _WI×ÕŅ?oDwÕŅÕõu}]-āũZúĩģõmęŲ`ė„ĸ˙HMhę—-+QDžgåvâ) $6h ¤é•Ø—ęėŅ—Ąņ˜Bˇ9d†맓¸Z;ęÕ!5T0uOA"ŪS/˙DčÅzIXæxOÃūåŗŦã?LžCfü¯øŊßw†õĄüÚ5‰Z]´nˇ'o†?0aĩ­‘š‰áŒŖ ‰ aėgú>XŽÅĶyÖR˙4ŌŌđ2 wëŅ2˜hAŒ:ūr?‚œ“÷Ž9JJ‚@ÃÍ$>|â ķ:ÄÍĸėŸJ(×5€§Á¨×Ģ6ü”ĨqVŠUEķTĄ˜*āŅŽöã`jMŅDsĻØ`oĶÄu:%ŊŗÖōĪ$+ĩ_Ũƒˆ/žģ—40ô^mŒ'ž¸ 8r;#ãíÍžŽ•Ĩ¨KˆîA1\ųÉė(E0miãŸķĻÄ2R +-7=eũ¤&<&('@ČWNn˙5ë‰tnÛ`ŧ¸Ãđ“îku§õätÚ$ÉĒpĨÚĮs”uĖ]ËCfģ1i›mŌá́´-ģ„vG%æŊŌ; 0.Īõ˛Ž1¸ čl%šāŊLzú'š—ŧĘČúžl.N%ũ´Xír†‰—äĖâob˛ĘŪr {…ķxŖdōúJfŽįr(~ÖŋM%€DÄfÚcYvģvfâžE?‘ŠĖ÷˙Ôæā¤–×Jë*ÁžŖƒ˙§AFC¸ĒDYâģ…ĸōōōōōōōī_8ļĻÉFH¸Ú2\¨Ÿ4 …ãŒyéUęÎáR~¸P]ÂūŪ ŧ’ŲõŽ+HäČąÕĪ8đÜļãW‰žžƒ$Ÿ=Īam\bīmˆ_Ywv¯.B(lv†xV™c–Ô1–ĖtÎecÎOŋœ‹A„ “A‹ŽnIË2Ļā™_Ôyé0dœēÜŨˇ"Šmúë÷‡Ŧe(­IBv ŖŽŌĩ¨Ųb^•-ūî3Ÿe&ŠSä"[įŦã*÷¨ ☚k`R@Vš1ŒD āûŠ[KUôm IķU§åCGTäĖūŒúzuqÔC˙mlbāËÒę=õ<Ŋíá3†"ëĨâw •Œō‚ø#˙Īe\—VČī)Ų^= Ōô†Õj_qlyÜųģ õÔŗˇĸ KŌņČ ‹đ§øÖæė„sDņŋæ‡Õ3‚Z˙U&‘ŪôsÎĶ­>đà J¨Eũ‰‰Ä˙q*a%#ŌŪ’ÕW ­x‚đ[ŧ3׈Ģ3> D&æ˜r„üy<ēMyáPЇĻ>čŌ4 †x{Š`‹hķĩ&Á‹Įj`@ĩc_‰Xž4yŦí%å°Pč=iúĐ ķ>¸īĀÖi.âKÂ- Ęp*kØq+ .œŒ2ū ļĢą<2Ŗz´^B”Pŋcqy2‰ĐŨ6_ĨēĩíŌpaé1rŗ &ŨšOôbč ošKq#Ô-Ō#9ūzÜĻ`éÂJ/1$_oV”ä9ôūÍŪīŪØbõ<LšØ<Ÿ Ŧ' ž”f†ĖÜåῆ¯XŨ‹!EhÂđƒŦUS‡?›]×lw# \Ÿkŧ}ʝtŅ ‡ŖŒĸI\–§3n;Ē7ãõÅû%Ič,ë6ãÚf2ÆHÕFcE>ԜƒÕÂitN¨ŦXø28v˙[ÚåĖÄē?ĄēæÁņ"R!!Čü wôƒÅ[ŋđSbCŅҌ€Ãš~ˇ_7ĸ‘š€>ĶīV˛`RyęĖ[kœ„EįB‡T2‹[Õ¯æÂS0 fu´0:ŽŽ"} ‰ÁWx§ųŸŋ .ßdƒĸAŋÕ4c2{„Šcūrŗ:eč‹Á’ á\NxDĖŌĸ–įĸŠ?aüPwDŊ$ģí8ˇ”XéĒ÷Â0š+%Sū¸ČÕā˛%NƒÕ›øB¤čĉ›ÖU“øAÜs>$đs÷!‹u:M†a.ūmŽ_MęĀÕ`W§äĐĖ|l=—ņë@ņ”ÔCĨiũA.p•ÅŊĮļųjÃGqoĖ8>đđ ĩlŌmž Ö}><€vãžofƒî!_äÄaČÇÜ}—Ä[3Đx•SŨėį°)ķĢņūÖ9ÚÆļ…'L3zEȕGeĀ2â­2*vņ!HŽGXų6ŊTōkÆ4ЈĮŦü¨ôo6@ ˛ Š%| Æo‘Pí‘úåÃܗ ôčí†āl¯ōG\šp §ÚŖ*_Áûâ`HŠČ‡˙vŖÂ‰™-öŽžÄĮgí¤Ĩ[w͓°/q‘…Žū ŪË,nsĒ‹ÎWCēåÔ[J É÷§ ĪÜe~üq<ˆžo R—ā5q!=îģnĐõīÕŊ‰83„Ęg ¨d¨ ˜Ã¸Gd¨ũPPąÍŗ ­sW§Ā(M`×@Ųē´q҇ā‘ņm“)_ĄĶˆģ¯p€ûĸöŽgäė°Ū°bL‹SŧsLė÷ƒf‡ė6_ļ&7§ŅFĢYUô™úäŧŽZįōZČæ:ī­…N´”5TÃ.’ü¯’RŠZ5Ž‚ib3ķŪĶ‹’ĶėČ䒤¸?[o6#ŠnĻ wØHÍõĘS Ĩ@odP'Š…ĻT B0s ¯ē†mūÛōY w* Î×Sūävų۔É˙AåB˜+ô5äĒķ ŠĘHŠÕD“Ļ 0¤%×lØnâ+ËF3ë|!(b’_ū2€‘Ûs -Š˙jzUđîōī™eoL„§*Âu„Íōģą˙|ˆvUŽ ¨ęY˙~nĢSDĮø‘û´ ŽßtøĖČáģˆs -ŗwi¸N`>TG_‰(įØũŽĐˆDJ„+'EöTu ´…ĀƚàÛÂJ­LËÄ"˜Ģ׊4.p-åėæM;Hŗå&KŠĨ,vR;†ŠÕ÷s˜.Ū¸+ÎWoA^j øŠ]Ÿ>†5ĻŖ"7ĩ ˆûËßí gÛģēGšbv8‡C„ߤxëƒ=WdĒ+”=…ILז a°cZ¨ŦĩčlŒ2TŦ?Ž˙|LC ~Öą!g;Ä~§ūX!…üˆ>(Ž?T‘};úZx°­šO;Æwįb i@ĶÎŋŅž;õaÃģf´*CË[Z+cņK€ēd:ŌËÆ÷Ũרâ‚ã˛T vFo„¯ląļ{”襨{\É×aĀ+sWՁS[üXøĒĐ?IԘh”Ŗ{ŨĄ]8Č%W”­€1 wÂ?Û$Ü9>Â?žbf; ‚žĸä ÁĘxÜ`NcČĨŋžSņ4Ksĸ4ĩ„e4î>ŧėâ[0€[)ßÎimd3°€w4æ}Ąė`˛†ÉbŪ ĘøI‚CJw ›ŒŽaˆ•™Ĩöŧ˛ÖwãüY_3ŗ")}w´8‰2ÕÍ2÷îPN~c_ŖŅ΁ éŦ´TėŅī˙õˆjmĢĄÍ(Ô˙gl)•Ą.ķ$/i_qßIU GVoŪŗ+ ėĀDĄŧ]VŨËäų§Ôyø%ÜąGOGß°ä\ŗ}z%/ĶT-q‘íÂPwü­&Ļ<ÉĐ9 Žļz"e'û™âē•T­ũ^ro0•ƒcKaüƒƒ[ā´˜kųē9bH.š4D1ŪČX­Č:­/úAPî)ÅÆPnß˙y&hnģwÕéė“—dŅ Æ¨ ˜˙%î=\ŧLĢ•ũׯ]?; §R!ûtĖŗžhtĶāPĘ€mßü€Įî°5W"āLMš„lā4qe.zfĪÔ [# •ė0s/\nH‰jĐeđ[¸æ€o&YôXŽyk ͜p3§øēS˛ eÁÛŦY:ĶF—_“‚ļˇs`KëĔ/)2QÚĒ“,+uW—2%řHKŠų9;Ę6=įåˆaúGār ŽØ‘Å›ƒ06`€æ3†‡ŲÁ¯~JFŠyĻpC鹇KphJZņvĀē¨î€ĘzRīŲJÔé6ŗ=`Ræ…T”}ßĸŠÕ-M™xlYąĶ†§4Ô맯ÆN[į‚@×ņŒЇ‘ļ)ĐÚy°ãB•\Á< ĀéųŊå ˇV6Ā“ũiÎ5žW5ǧ‚˙%zÅKYqßxōYā.~ŽŲZr^1hŋ""õVܝãō%jKļËĮ|R“m2›Íô<Ą 3Ž2ģąO=Îu­1‰ž$€Āq]IY?Û[Ņkã_ŨDÚ<% ŒŊ'Wnå1dŠaš¯Ļģ•ÍøŒ{Õ ”üŒ‚:ž‘šéP¯–kŌ†kx¨]ÖC(pS™†bné'2qI%ˇĘŖūˆË&/~qŧŽ)2 âĮ!Ō1ŗĄEl’ëöF Šlĸ7ŪtmÁđ į9U†uv¤€Sņ՟á k=€Hr@~+<`O‡ åĢ5ĒÃlôaæ¯ëÛú;m ķÁđ׉æ};C¸rú—ėūČ]Å×ėÃeú•Ķ'ģŸW6 O‹ÄÃxø€ ÎļŠü:sė%@(ßÅ C¸ô\Üđ‡„čøs`J$pÚģ™$-&™$;÷‰b>ŸyФo6IŌŠI|C4učÎ.Fd(]ĐRą @JÕa@%—™ ûÖ>ČŨĨģi–ļ! nõ.¯]ŊcģãJļ'~jį 'ģĮææāYŌgõ>¸$$\¤Ôe¯ÄË%É*˛ GiĖéRBŗĨÍ§rZ4F%/š—`ĨÎôSm_F&”÷\(}yRĘmŽÄČ2(<ķ JLƒÍĻ ĩ×H`G×Pi@p ”ņ0žF;°ā;kĄ3P6ĘüĀx“¨Įô|I‚G‡{ąŪ^9‚ŌF‰4Âđ믋ejV¤Ü‹ŊĸuŗWØB…lÕˇsņ›3`NųÍg1đĒŪ˙8ŧ(…Swt'˙ĖKąN‚$lĖíÔíÚE ¸L:(gW%ĮS“Å đ­"8x­5ÄõŌ7”$NŠMĢ”ÉÔTP>öįL”wGõĸäā ū F€D(ŖĘŦąĨĖxJ´`ˇ¸ƒ—€čų„&˜peÜ+&ŋŸÛâÔ§ €ĀaiÃ5ņd6­pDɟ5&=˛C˙CĨu°kåĘI8ĻŋlHĩ'ûÍ.Œk9™Ø,?ŒĀĮÜŪ;ādú&ˇŲ°ŧ5 7h>˛ûī‡ÚQü‚˜OĐÉ• îčŨņY œØgšh?–@^úōÁgW(%Õ^‰č___ˇ4KŽ\R@0v…*_ŖLē"9A7ޞ˛ mO"#|…Jōa‘ 2ÔDĪFb”’āžø5”Ķ^ĘļŦŗå"áŗãŅá~ҎÅ;ō:q1Ntdë;ĐYR0ņ_r9HĮ‚ĸ’DŽ–ôĘr Jž€īâī<ÂŦ‰ú¯øŽx+á{š˙# ™ÁĀ Ī6W@Žf˜Č0ŧ™†á/Ŧ’Č B$ˇ9×ŖÂŸö:šģS3QÉŠDdbÉUĨDČšļŌIž?ÉevãNŲĐđyņ×lå‹QĩŠ.ą˙NŦ\ŧ˜ÖĶs:ģ8Z%^LJ¯Ų´bsJũ0%cøn`~Šsũ´ģpšŋTŪ>‘t+Џ}âuĖ 0'ĒæsAĖ3ūĻĖņ"Ģ­ųQõ%ûÕą §Ų Hũ.×høƒM Î+… ĖŊûb Ŗ!}@G4‚gEŅšųž-Œs°v(§.ËŪÔÍņÅ:=VÕLeBãuˆa …ĐJŦ +~īAú ol”(Téč­œėæø¨úP´~‰ļ ŊņÅŲN,NÆ3ĸē.û Z€.Œ)üé}ģžÆ‹VZQ¤ŲÜ9hrõZ=›*ķ!B7@`ŠárJˇÍ…R Ü@vœW”\*˛CĄs“ōæú‡3ū¸xsU&uĨŠ×A6Ēųi=MUž`{â$Ą˜0 š”¨ÅSh(3l”¯āaĪ•Ž0ŗĻãā6aÉ1ÏuŌĶS› Ãō.uëh í†^xģtrwÂ1Ėa‹™Ėd€ãīĢu Îņô"|fē:šôm4$ē)#ãøÂ"ČĄFŨ"0ņzãGY{ĒI¨Ō‘ŋĐqČ8íKkĩqÛXĢûGepM_ Ļ@f ĪŨ$ë8čAŧžo^ÕĸˇōtÄûˆđŠIŨĘPõ<뤞æIvŠjžzB4”¯iŗÆK[Œ„qJōędT?'=nû͈÷æjĘ1¤ŸHæ~‹!g#¨¸¯FŅz|ķĶĩ^Ø9ÚåũqÂx0¤7´õ…-̉%1›â/ÁYák_ÚüDđąS[ęŦ˜Ĩ…Âø%^]|ęt ?đĢ„¯ü‰áJŽI ØN–îÄzpI^sŠGšÕ 9 U&ĀŅz?û Ų)ŲËb°ąŠR¤HąŧˇŲ”8ēöŗäņnß;o6—‚÷ˇdᨘ‡ôļģ•´ž;Ë2ęũ¯1†‚ÅH~åҟāÕ@Áú)MÄDķž„÷öG°]Ge*Ŗj-cÖč\ÁEx§"4LĄëI˙!ôãÉÛéqįjKĢ  ÅK?Âŧ4ūÚyv;Ã:ąß-:•ŽĪåi‰ÜŖ ÄI ĸgĨ|ĸh^Ą ÎhđqsoyyXú–˛@ĄĒĄĀ˜KG9Á ’Ã˛Oķŗm=¸ŗãkȚĩÎ[¸FA )%­Ķ: Wß}Žwō_Âgr¯øū< Ë7ŸŽøŲ`Ũa¤T¤|Ŧũ“âKcõ“DöÅlĸ§îiKūÖkŽÎw9j4UGīĄ•YnŨR´9J īnĪķŧë¨â;ÃĄ vp~õjÚ! ŠéĢĪʛ<Ģ…@Mž%:Œb ŧs¤3ŨņT(ošaĢ#….‘Æû¤y [õžkäOâĸ’€ÉˇSÅ`g–r•g¯sŦDĒäŧ(czˆĘ–kb#ŲDōV<­¸HŌö„öŠwEĘŨ.ĩĩ`ųž Ū>5[ē%sŋŨĨ‰Zí4ZãHĄÍŖũ m†”}9Z‚UæzČ;5šQœ4R’ Á͒Ō.OuvO‘ĻģģÎuBŠ„âGŲ,Äg;˙<Į­kĶ›g)Zí ēZ@‚üøz„V<€ā’Š”‡>\nt÷>ãą EņōN0ŒXˇ1Ž"E'&íĪb@Z›%îP ŗ[wi­O|ĮÖsŒŦčÖĶ"ĖämÅjūŸá7bŠÍdî]ĢE_h°õT )ĐĀĪqļ.ZˆÕųŅÕĒļŽLī)nąD#W˜:_ETßŧčÕøšø?˙ãö°ū×Û^~Ú÷ĩ›öąūÛwöÛ?Qę2ũT¯ęĨĀäA°=āĩ›Ûŋ˙Ģæ’Pņ‚2\q; ŠÄôæ…m×Íķ|ß7ÍķyĀ!Š#…“‹{,ã!i{¸Zŋ!?3-b€vššš´Ėîi‡ ëyōIíQāš–Ö!('Ŋh)*îŋį&yņd2ŋŊFü&—?:ėUņL6^ Öõ œÕŸ š xŌŲTĢĩēųžo›æųžo$/b†.`ád–[{âüđ…#‹Zļœ“qWĢ~ëæųžo›æųžrĒĀšã7§ã<6r<ŧŧŧŧŧŊņOn;;Н,ãøõ|ídx NūCĄōmÉT!uĨa[Z//////‡• >Gģ™lÁ`y˛ĸ °¸’ĀXUéžúSjõ†C€hVȈN~Ũ+ÃN@'Dm`߯ŌšŦŧ.Rčmîxä\jÖúŋ˙ĐŊbŅ1+sĶ*BØ2ā,øyé ąÚÕVú^âšá5ļā{Įä4Ņët<ŊŽāŧáßĐā–r{Ÿí ÅĒ7´v¨(†Āžˇ‹Ú ŅšB¨ÉFfĀDĖÅq-˛Xû/qԟú˙>'0ÚŨ’Įĩ7s*å6éØd;;ÔčT?‹ūuĪW˜Ë‘eäÉRŒÜ“).Žp˜Ëė¤29ķæMˁ]ūÄtSôãØ‘ˆt˙}4ĢØ:ŗ!(íķü¯)Kdu1lɋƒ¨âÕ%ĢxŊūPiu,2XēēŒ!Ø`-\ĨJĻM!퀑{Ž=ŦĖÃúÅÃO^4¯¯ĖW“3ЎRWâu-9s÷>;HÚ;JPāÆËēŠŋ°šb|īV6cč™čė€ Ápá1l ũ¯! •2ÖÄ@ũG:–‹Qķ[ƒîˇŋ˛ōâ1`Ž’jŽĒ€˛Ũ–‚ã5ڑČ:eKáhŖĀ-6ŖõĐ˜ˇąĢKk~AY=$Yĩ´ —Đbí\W)æíGõuג 4s†ÂĸKŽüãî×ĸęzŠíåĄĀËY—؆Õâ>P7ė(ŠÃzYS$g‚ēŒŦįÄâđ‘ÆŦZÉ ^ ëĪÎĸÕ×įõŖpMX;čā–įI<ŋx~uĻEâŅąuŸ~2Ŗ uÆgüÜ{9báÕŲÖ ,ĸŗ°_—_ãÚp1‚Ė-€ÅĖë'nF×ŧ%žY;izK2å'*ŒÁĶڝž íš^j€iĻN"Ä+ÜfBx€Ģ—×*ä÷g[ĻkĮØŋßęë\ž@šÃ ĐL9„öŲ]A•ˇęû>p}Ã4$čĶäxj87ëEēī>RsÛ?âĨÄí˙ĶēŪEíہ¸•ˆSLÍũ ^. •=/MEVĀrXúĒNėHNqpZXa vļėŅ–ŖvËé(ŽŲų—eĀS,ČĪ/™BBœ$LĪŖ¯­.˜‡4sŸOÎuˇxD|ßeŽ&OæY %Xn!ēÁ ÃX;&âŖ÷g 5¨ę¤U‰šƒÃ,Į Iˆ4.Rt2…Û˙˙Ųic09T¸ jP ‡ ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2c˙O˙Q2˙d#Creator: JasPer Version 1.900.1˙R ˙\@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙]@@HHPHHPHHPHHPHHP˙ Sw˙“ß…ö|XōģŠڝ´Ž77öSYŽ$dtûmDÄ*į) ö_KŸGO2<…Ôî÷‹võÃ&wŧřųö{-‹ģ•ÂąVCY“ĐTfŋ…'škf=rĒō‡ą¸Į÷¤ô–[„fÆP†XT ܅PShl|V÷LozÂO‘X”/䯑Ĩŧ؃&ė—ŖhÁJ \Ō •ĸ6ŸPdË=2˜?tļļ‡HY¯)ŽĻ‘ æA2âū*sôP l‘ÖõÍRŅí ÖæpaæŖÅ ”w€ķņOumĢAWđ}Ÿ ”& ĖmZ×Ûvī ņ˜û–f}/Á‹7ŗ]Á`lP>Q‰voTŽā6ɀĀ~iŧfffįšųrqô`yS ”Н-7ŲqIëŖJīæfaĩ $ąDžžØjāÅŽÖ9#˙?ĪÂēļŗÂ”ãn;CŅ6ŖXjbŨלŪeՙ S™Nķš×)Üé nXũ„vk`0ļMušŖëĖĄ0KG|ÚdAĶ&:ũĸĶærGīÛY\!åāځâŠĒAŅ×n°ę ČāÍie‚YķxƒßŖ ƔĻrc |Tõ­Á­cÖswŅ; ZŊ ‘úĀŪXÕiŦ$jNmĀîH ĶGZ@=AĨũĐŊP˙-ēĪbĻŅŨ3žÜäĸō 5ƒŠßj͕ÛʒH'šę[ÅáÄe7ī&qØ9|ŧ‘/|F„’{0ŲąAæÃDĸUዺB°%Jg,áhŋsŅtŨå8!L-ˇüŖÉ.ĀŠXYŊŨąî.DĨŅûŽ Å’Î: Ņ?}˛šÚŌuŧĢöëÔ‘LVtŖÃâŧÛÕéuW™‰3 éw%âySxPa›z&byÅØės,˙tģY‡^=ŅņH`Š‚ũ!@Ķ t āĮļ73„B€9gb<éj8á)¸ ŗIHĀ‰ëIfËæėjŗ.övßg5 W.!ĀoįMmą  ŋÄqL˜üDFfڌ›āXžĨ˜Ę )8ĘáÚÉļãÆ3’ÅIįT&+tHCų'RßĢÜz•x˜p ļš9#¸ ŧØ|ī¨ÛoĪ­ļšÂ’äcĻĖ-đRŖ[’ (P(åâëUÚƝsąJOždņ ąlŊ‘7Ęôfvõ݁ō°Å3øH؝KĘÁ[{÷Ą”!)ļ[Īđ-# XĢÜ[wö!ę‘ÅĨ?úhs%%øja ģW¤~v8ņvvIƒMč0ŪIŽxcî¯GLĘÉôŌÔŪŧÚŊŽ"EAjpÉŅyĖF6Ô=6Š “Vi9 ŋũwB ‚ü,Ō™[ū¸øÁ•$ô X4E}_§ãM׉ĸJ{% Č›ƒ*âöa?f0=ü‹‡?-¯ÍŌUŠĻY“į:„Á¤CŅĸÉåË5–š×`…ÕY1R!’Æ-(qXQīžŨzĢ;‘Âđ×AÁ-Ff5›bđąŅī¯ŧ‰Žš?zũ.ĢžÖđŦJ€"zcKåų5L ŒŊFKJ°ãˆø Œ=¨DÉĒŦę ū<›”bqŦÄđĒI&6áw’Ę9đŨÁ ąđ(ķ+°<3AßŅyÄJ“šģPÁī°Z(lguΔĄ˙rásŨ§­Ĩü'Āåˆ-¯/šâĊáÖ0fÄ=cZĘ'p6Š'w=Ô˜ĸŗo;ŋĨJԊėÆ0ĖĄöĀpv7pŖž¸ğF(ÕtĂh>ŽũJ°éšSΉŸ…Iƒ¨ädŽ:ąŠ âüĮˇ_efš_‚Š{÷|CDžaņJ­io%XŒ*†l°QÖG&Há|ĢRPr@ Đ]\nÚ3$‘_U܇ŊƒcCŊølØ-ætHJæŲsÜO­Úg”?+Ÿoū™XN÷%š÷Ÿe_ĨÞ¯ŊŠhŽ>Ģ×#û›¸CéäœõŦÕ7B:¸}}"Ö Äģeu°˛_ĻlŗnäҰ„ĮÁEÚ ˇÔedĶüÎ`Ŧ‹ĘˇArë0Q„á]úõÃŦUöįš)¯Ą>7gk6ŋz>3„še9´ÃÉ ņå;ĩ:<„ŦNąŠWÃ3dŨØå8Ôfb%‹p@EšpĘEDb{)AMo™Y4č9K­(ą\Žhgø`˙-Ķ;3›Įâ}=ØĨéî!kĮåˎĨzå†$Hjū’DŌF Q.ö_o^š^ĖĐ)R Ö4 ńęÂ˙…ˇq›9Pežå }U–FE‚H&¨WžĀīũĐ¯;ŗęĪÁN~ qø€`ŸD˛Ō@ũlģ“Ti.<ƒd°dvp¯ž€Hf`ĖŠv€l ­iu†uĮüDoC€4†kĐ4ĄŽ‚ä؟ Ė&Fíļæé‡"ãŸúŅ%&ØĄĘsT<ü{Eu‚žĪ:6ėÕCøv¨‹Ü´x9šë÷ŧסČAŲI>!BsMˇNž. 6ãDđ•vuÅžo—ĪôˆŽĀ'”ŲÁš:obpPŸá.ü iy$Ž Ŋŗ;wLükyĖÍNâ…zī_æ)ŽTŽ­>_  ŧڎo8/•SŖ'ō`Փ_õ\Į<^Œ4Îķ<†ōĪÃx'áŧøk@ÁB…˙|-˙D1ÉĶAPĘÛĀoīÛY 7˜Œ [3äąøŲx’cA‰įxđ^pų+ųS5šQCŌ2h,°€ÆėpĮ×u × /*Ĩ3´yū*’$́Ōėî2ˆ43Æôa†Ô:Ô ČŽŌœÅ”WËŊ†1ĪöÂo‘ö S>øi,Á¨Īp:ŠĨĮī5đ$ãD“ođmģ—+¨IŋųPk9Ōwm+ûũΐÛæOWąõúpnc.sޚ"Y'rƒ™ICíæÔp˙VĨîĒzÉ×dĮ2CU‚ŠoĨ ŠĖŖ7™ ŒˇCĩđjFãž[ņâŒ¤ÅÆ!Ļô.%ΰ #˜nØĻž ŦΞŖZö„Sß/>’ā”Âہ!'ô ĄžžáûAû€Ŗ:īr/<56"ø"î×(P"îęĘÉGrŲn –đf`ũŠjŽãq9ÄĒŦjœĪ=Yą¨ļ՘˙Fŧ}ÕŒ˜Õü[a>äü­(Ē”4Џo¸ļmjįŒĩ(“z^AŠŪąž¨_äm\JKė÷…!´„0ųJüaŖ›@'ŖØSÂZP ÖPÍ Ķđm*)LWŦ8Ę>z§ļäŪÄäb+UpC\Ļ´ŖË`âAe_ްęÜ<À‰ÆÃŋ*āĸBë6ž€ķūĒیbTgŸŅQ•Š—kū9]ŽšÍێ0ž30åâžö2.ĩëžY1îžhŨŠĘudB<~NPģ†#˙JØ+Œ×¯ƒTTõ‹ģŪ$*dŠ!ŲäėOhÖų0_䟧@ҚŅēĢd÷ŗ"~ˆbœ]9â:ķõ]i<Ĩ-FÎôčŌ˛ÃÄMåĘAfOiĄųZ9ŧë)@S_ã×Q-ōR˜ŽažTͤŅxÕSŧšéeLŧ§tO]ƒ‰ķ§ļ(Õ-§Veĩß(OŲIęW5ÚÃ$‚võņ§ƒ‹a*ŽÍįúé~rŦià†'Ræˆh´Įx8ėâģĖŦF˛¯›r‘Ū(u˙K#ÉŗXđ—PØ@Ež\˜:ĖļæėŸëįš°ų0â™o_66Č?ēí‡4HÁĘ*^-ßž–yāōGī(‹ oņ˜:ßÛäßāĸ”HôĨ&Q]á`‘Ŧnpœ”÷$Ĩ3÷ŠŦžä#ķČíøC0˜t›wŽh„zmėpôr{܏ˇs]éIHOˇÜ”kgŸ+hÍuŖ/‰õû ÃĨĩR}9ŒĻ5ŨAWŲîöIÂâ23šws"ąl튔*wl÷"MI¨¸FˆŽ×FŦŠÚœÄíē:Ā?Ëäƒ*5ŽĮëMX0§ŌĖÁšÔš™o.ŽÅ!„ŗšąr“ā°?ŸYč/|įX{ö…dZ7Ēf˙3¨ęS}"‰ú*gƒ›ŒÍ–R3B|ŋëôe#"d,é­L‘€…b8Ŋɇ¤į‚ÆėĶP /ļÎ.m卿˙dFl$1į‹ėåeļûkcw˛āü’Ã9‚y?ö+ļĮ]ĖÚm+*ā ‰LĪ}ėШĩĄôū“ŽÛ’X;_ mŸāÚ §5qúņ5,|īy¤PS”ž<7ŦF„žg™0COũ#V3Pü–u^ÛõŸbŗSŲĮĮö-ō’đ“ū‘yá§´5nÎY7x`ķčp[œÄŊ‘īŗ DÍwĪ)č{¨3T„ƒš*w”îã˛ôdŋCíbĸÄū!ė !^ÁÉîW§˙~Ž~3\Į•\"wžÛ.u~ČŖĪÃo§áŧ‘økH¤|dGŊž‡SēvßđU)`cEz.×ųȐTÜIÛ˙…&›uD†0KBÂ4 ģĢĘAMãv"yy I=Î?gJˆeŦ¸ČīØvĪÎۖEVįÕĨŠ•ÃO:šc\M8M4÷'›œBËčü ¯Ĩ{Ú€ĻŠ…Æ=. *¤3åۖ´‚}$˛ĩ[\ÍG0ą%bö:ûĨvúäۙ54”ŽĐææ†š[ s9—Ą¯ [Ж”•Q˜&„„;°+9{š˙p¨-ÍX.w⍨Rūˆ}鍚,oöHhŊZ„EēuēĮQū 7šiĄÆō‡+w(ú@˜[p$$ū˜=p‚}’(Š8UĪPfž´u e2r|ay—üŠ;CYŦ×.qļéBĢL)CJģ.ŒöŧgVĖwū3‚ŽÆöĀö0~¯Ģô¯nŗs —= Žhé‘ōĶ}ŗL¯B€Ÿy•§Ko^Įw80[˜Z°é;¯G`îģđē”Œ¨ 90‡<øĮŅ‹#ƒ h.jvĶeI9Îõ(Mŗ "Y°ŨōÅ\×)­(ōØ8(#2Ār*Ūļ׿<Ŗōė,‚ ‡bĨJ īhŋ˜*°†UĄŲD”Æ)´ęíØ2“žC{č¯ Ė?ŽąĖŪäĒͤ0dķ;0ÆČcŋAŋ$˜¤ 4´č/TųƒéÅčÎ\¤­īmĶŨ‘rVČĪūØûS.q Ä+f´lÎ'm°tYŋ^Oõ*kIjE—Āø Žjŧ¤ķCi=ęH€€šôjĒ_§´Öîk´ +˛RÜéƒôĪFAƒ<]32”Õ ÂĀØPŗN(&|­×$ĩĻÉdnrZī‘2Ŋaˇū ŽZ {l†Í$ĻkžĐ*Ķ\ņņÅl&ĶŧŽŦˇZœ[.øhvŋ#M2€ Ā^FÁ3,b=fÅgįfL€†d´ cÛKm6(B#Ži„ĩË'ŪIBž žnˇ"@`Œ“ggC_¸ë_~wp“§ã-S7YŲ'jˇba ņ‡bV rŨ8ßē\Ļėc:žŽLŦHO€Č v?šÆŽY.¤KGĖģŊ ”€ė'SÚ÷@’vT0ÔĮ¸8S0äãoœj)é=Eī¯%6Á!šc¯ŠĪ ÂvÚ#i#xyį™U\Úßææ:U•qûÔÕᲒ#¸ŋÛÁ/” 0.í uŽŖ×͝5 vHIø<¯ļˆ&!I m+ßQÅB0ëė**ā-C€“Ÿl…$^‘†0ËŨīWŖO 9oj?p!¤uĢHk‚mŲ‘sŸú !a×Oî”, Փ<'“‚Õ˙c¨ëE$–¨Ūq)wž?e¨ô)āi)æu‰ŌIO—ClÔĸô E˛A.¯qiƑĪėEļ¸G4vÆúō7F?GîYKÁÕ(Rījv‰|VæĀūCtŖģJđĶļ%ou”)Ū3'Úåģ™õ´ tŠÉ?35És˙3įUīÆ@ŅzãYŧ7Ļ-ĘG ˆô ƒZ[ˇeˇ!_kḃš7V˧_#.u›Mģžž ęļ“?*ęĻõ‹¸)ŲįōŨ8ߨ<}ĪY9ÛfÎWeHGrĖy­ŠįˇÃՍ\ÖVīĢmGęëJŊĀ!ÜōօZ—ē2äeøœÁâËąįŠīšžÆ~,NíŲpÂZ==îGöĖč6'öÅã)ĸb^_"ņômō­x’éāp•ŽD€’b”$plįߛˉVČė‹E]c¯ė"{š¤ķ4ĩ8š{š‘ĖúX،G­d†‹ØT§ë̚pŲKŒÄģä=Ŋ+Ú-(,§öJ(üuĖ^0ž˜õz‹2,ŗN3Ėīúõ?eÃŋž›KË’øv1›>o Sey2:ąqŦŅp={†‹œ9BđBŸ:ī¸ŲÍäE–ąũûŅç ę4‹#-ŊÁŋŅ.e:CEC`vęņKĖ‹ãĻКŏ‡‰AÉđQ]Ĩ;N7ŋj€˜Ŋķ|”<ž˛Čoé52ŖÂ"§@Å:š@˙ë~‰Ŗ{ųDÛ(:ËŅ@’ö(:’ CԜĄ‘K؜ÕŸ.|Üŗũ˜N–•˃ĨĄû/5ÕoZ.i¸‹RįL=LĨŲTĖe@ŗ]ŧq1ƒ2<ČZ 4y(Ĩ†‚ßžo<+Õ l°SaíēSŗz†üūÅ­ĮîK5č9°ŋģđ[ŅúžÚɚ^đ˙ČîŖŠâs>čËVņU‹ ”"y‡H”4ģ?ĩU‘Ÿ7åĸ1?KæČą˜ŒI­|pēqrŗČkîô°ŖšãœV; ZĄâ"_ąeĀ.ąĸžE-åĪÃĶ.~–qøwíĨõ`ŨY Ÿ´-úâÛÕnsvy"DŸú€RoļĸēÍX‡høF÷tĄ"ęĸ×+Īú0§¤ŽÜ…d$„Ę9Ō¯á#YĄæ“]°+ē=ŧä]Ot ā†Ö-ų= CĶJpŸņrēõäŨÛDŧ!ß鏊ĸ>„ąŪãI“aLH1Y4O Ŗkínã/'Œ’d‘ąp˛@>žfHTm°õj÷ģ;æÉĩ.˙/nŊ|s‚ōģ˜ž†!ō‚‰¯因m›´ÕĻí"֊5Õ -%NHÆzŠ˙LĀkŧĄÔĀ÷%Ēí˙n,îēKqR.ļĄ8ĮcœįšBļgúų F†ÜšņEiŒ;9Až•‹ČŌČĪp߯|Ą%Æšîē´ÍĮÂŊū•ĮŒ\”_K'Ŋ›9GG$’ ¨hûØķ¤îĖ'ØaŖqŠšĄe œé§‰¨‚RΈ5š˙Î,9œnņČ@š÷-8šlURdM&8;ëÜŊčĮI-Đ2ūPŽÕčō}"z.4‘ 3Îuŧā2šW3ØéŊļ‡$myq^¯×đ—ŌãíW?žYëē^\MĨ[ķ\•ę—÷5?34­Bß>Å?Â8Û§Ģ‹Vô"ˆ™{âņJUôĩŪ ĶwyžSÜ "ßŗÅU´:N%ާ…CÕXíōÔú™ëŲ1(ŒäVXĘĐ`›Ōəvé€<6‡o§ ČCĐĄŗGų´ô— ãÎÍŋ.xŋ—Ģëą"+fæx ĮĶ/ÕЉÛÜî ÕsN`ĨiŖĀõ;Ų5‡S~īģÛPÆ5ĐūëĄõ/!„üûÃ!ĢúūüNÎ%ŊÄā¸:ˇŠŨl넟_š8|nŖ:ß*/rŅ9¤Ē(~832eáo˜˛°ū#kûm^ÖĖEV Ōw¯Lø Xßztü™ĖMo Q‰ûŦ!ĢĶ"O.`(ķ’5ĀSX‡Ŧ8Áj|7Ģ$tŦŠŗB™ÜÚŌ=ÜFq k+WŅeƒ’‚ņČöÂPã&\1sĶ* Šųãāz‚sŽŗ´ôß°ęŒR 6wYņt6Žƒ•ÛT—Rf:HɟĢÔxmķŊŗ$“XtŖ’I%ė^ķ/ˆ•饸ėęn=‘XSZ­ #uŒŲؒĨ×Â)™æwÍéĻ< Ž˜aėbē lJHäkIaL|.ĒŽ÷ō÷"^kÔNÜ(Ģԟ†Ŋës‹Ü ’hŽ…c7i_§0wų ҞžÕҏEŸ^ą“ßēæx:‰č”/8Ã9Î3hņĩ^_”š¸+rĢĄZ ’Gåô!Ų”#ū[Dœ#ĶžHmøRįđ™ÕæõLĀZ{yTũ': ‘KôlGŋovŲIë˙‡3ž›‡đM6ūžíā9 }ĩßa ôÚZ^é(7^B JwĀņ~ÆsÜ6ĪØ‹1ũļ{cŲΌ•Ŗy%XÜk æ8üqÁL…ŨcÜ2BÜwũG*kRŲėÆÅ„đĩRq›šūÃA˙NîÎĻ ü6J5ü2?aÅíkWRžŠé+ß{9‘šn *xgpCjmíėB“SwúäĶûĮuķÔcÜ} NĀūŪšĢ¤zË'ßE˛ŠûHŽėŠŋ…¯?ÝÍ[Ü$°W—×6SC^Š6ĢJ˛‡Ú¸Ø0ˇ5-f‰jë¨LÕ°^ gÔõ 'åÖtÄŸáÖčÔPgî¯eÔc)éū FŗÄô-wŠŽ-đfG0éŌ=[¤ļtBÃ՚kFpNOÖâŸėųáŠĮÂlœŅŨŠÚįAĖînģū@f@ŒŲŒš))ļúPų O}üûl‚ÜYÖÕ\–˙wã(JIėQĮģR†ƒęfē%OŌŖ[PÃL$]ÎĶ â÷)0Ģ˙ÖËq:öøj‹o2lmF-¯}ĘâžcHđņSé°ÔÊŦĸ AMģK¨RęŖJŧmžķĪæļŲ:āɀvĨd€Ša,LĻ–PŪȐ5VŗžĸŦ-qÜqMIđ°Ĩ…9[i€SÚÄĪ[ųīmŖoŨ¸‰OVÜĸ9ģGÍč×Zd†uûĢB ¤ûÕĮÜû§áZÜOo׀Îŋ …$vYĘ#ģ§g ēŧ×ŪuGƒĸ¸¯Üu T˛tfĄOŌ%ĐĘÂ)­3mÄĒ›\Ė­´C–qKī^/ną×ĢėŸ‘4‘GģĘ,Ī÷Ąd3`ßŋeíjFĒ֕&îûĖ­úamX/ë îËÂĨ´J1xŖč=}"†—Û4ęswˆsĘē]¸<]–%îĨž˜ĪĄĀw-`Ņeö‚gí“hƒÎ8¤UqŦŨcz8Ģr›77ĮtųZW•$#‰~Ū=x&œĐpRđâv æëKîÜ|mhĄô,L,{ČÁĖxA=āąūũŽÁ6“¸ožķ ƒĐZ3\#õVyŽ[kŖ]DTŧëÃZNāByÁ%4™„Å&a™ŧwNlâ¨âˇ¸ĩķíˇĸöՍ…Ã+UN4›ŧ¨įiúåŨ˙GĒģoęšNéͤ “ÕŦ1î—uÁöwÕâįl0‡ņhō│„7Ö¯ĢQ[€ĀĢ5XžĄ}°PØŠĪŖAsâœä8ˇ5“;¨™6_YãķûE# ųš=Q›ĢŪ žn?<^J ŋųų„ĩ\žyū5 •Ęģd#TÄ}Į"r3 žË› 6ģ§‚$ØgzĮ2s},ˆ]JĨ¸¯œbėj!ŠrŦΆz).ëķ bԊ!A°Š´e˜ąß:fDĀũÔÂQ4Ŗ“ÆŸ`æÍ™…J‡˜Š>šŦzœsĖĒVėŋ×ÄjûŨzúâ9Đ.ÛŖ÷ü¨ėņ9&ɘ÷ķHC6œ ˙ąҊsĖe¨Á†Uã ÄfWąŨœ a°Öüå:!rŌ"ģšÖŌ –Čhž€AØš!NxRĄÚGŽöÜ. ō&„u$ūîŧ jg ĄĢGŖrS¯kĮ=ö5?ûI;’ÄUņņļc<—Ņm`™OCd>fŽU%™ĐÖzˇr(8ĒvÁŨæõÆÂ烁c䒆z:€ŪŦ…Wp!{™‘!`ĻĸŽŌGpÚÄŦVũÉČũ•}! Ąn9ŋščÁí5Aā×zqˎDĘĘEB5-,=}ƒå ĄÚÖÂz+u'ķæŅđåâUn5Š‘)HúļéūN˜¤@Ņ05…Į'ët0u•Ú]ë\ ¨ö˛ĒŠĪÃŌF~™qøwōĖ~GpøéÔQ˙2é€{5˙žæ ÉՌÂļYYã§.€wSĩRŽ*NšPŸ3˛šíb/kęߗIËcšÅzLYŸKÅ,'3Ä(cšĩ-h;[qŠD;ž†2nB¤>sŒ4 @ØÕ&ƒ¯g‡„Ę`™ČTÉb˙!ÁOĄ¯C MNŦöÄōČ@f0Š0€] ¯EgvßKśí~´ÚŨ$OYÕIQÉšõĐP ]‘ ČԐnô—*+QȓeßÅĩĀSĀ‹V›XÁP_oļē´sXåcM!^1ô8éáķÛ÷ôí÷RØöx} œ.-°æ×ĘĩĐą­E ķė& ŸrAD5ŒSŒÍų‘õŗuX3Eõô\×!—–GņŦž&7‹Ęđuw¯…8čū˜ëC^Ô&,á×ÅŽō‡+ÜÉ/\Æ;WĩtôlĐ%Sæņ‚/@ėĖīŖŖ_ŽZXō˛¯‡Ķm´#žRЕÆšîē´ÍĮÃ0ũWũleŪ„÷Qˇnø×ëč̏ ¨Ŗ°ü1žŨlĒķqOÎŪ[ޞɺ|”†fd:˙û3ũG­î”5 ĘuP•Iãۆ@:­lm $ųŧŒ’ī´ˆķĶ_ĘÚŖ]˙#O‘=ĪâéÎ'íĘÍķĩtĀĒwä2Į;Xä`OcŦ'>…÷mļL’ÕĩĀ y‰=U1^Fô3ˆīÆ/ ÆĐ ií[N”pâėūŗ´Áâāu yਠY˙Ę&Žl*aÅŧ(RBŧ—ᆸæ}ļ;MđÕÍ} Cdõ¨îPĪiÚĮŋ)6s‚BƒnI1Z¨Ũa\@n5 S×uä˙ā&ÅÔÁSļ~ä?čõ…ĶĀÎĒXM1iz-üw°?J,ĩJÁ°É(éLoųHŪ¸ôXôq–­AQëvAc¨o$ššÎÖ~ âdų-›;üD0įŦßB ÎęV—ޝzC˜‚QŨĘ@XV>M‹2žúĖ„9Ú0XĨp?wē”ˆõu„ŋ9ËväšÃĨ’I(Gb÷™> üX‚Ū[}ÖŽ#*GNWXœ;§V\ “m8:zëĸW—ŧ>ŌLSÍᐠ}tÃcĐcbRG#ZK cáuœ+ƯJįrôë•ęS!īˆ‹L/újaÃÍR, Üe˜JčCaŠBeĩU˙rôŦ§;aĘČTĸ${ĮFĸ“ÆŧžilD đW8ŒlBûyʋWčŧĀ+ædŋû_E;7ûžÃŲ“BĄVšŪÉũ÷ƐgüR°_ũútÜS‰]‹…@!.ž€DiøåņâØžKÎ9րtr2ERtCî%ōü| į¸l?ŸŽ8/yžU{ēC§*âČ]ņåÆ7”}ĩ/ևrsëĘĶ÷dØE„z–†Gu‘ˇ_ԃ“Gc763]bí9õØ`í‹ÕģŸė4P¡YO^𠞂q1ÛŲ.ō€QqŊΌ ÁĒ.lũ,áÂ~ū)îdÃÁ+ņĪĨ‹H”Jf3C@˙jSAí§K ƒō˛Bo”“ą­ĮaōpŸĀw~:žąM(Ãe‹­ŖæÁЎÎ3™žtsX;žh•—]ë{÷ĩ|žS`b ’"Šy2úÜĀįtŗ˛eÚabC‹‹w)ŨęY„īËĒ ŽáˆĨ Ўî_÷‚Īā0ā ž9ųŨ;%8:ƒˇĖSgˆ¨ŲIZÂëTN=ūV=<ūWÂÃHëD/ņlA^R(1Í×Č Č›1ƒ÷%%6ßN.ŠpÎmŊĪL; îŠÚŅÕ€Į¸_”ˇ­cÍ ¨ũ˙ Ō‰§´so3_Ō# °3AĨĻģK6löHķāÛC:ÃˇRņ’­Ty– P‡ĩËļ߯ēęũŸ`߈°älûyÕ9æUXžސ?ĖĀšĨß§Ģâ†gX›z7<ā~oX/čNCÛšĶ Ž8néĘ‘Cx(€ž3T…R ŠgX4E‰wâ9_˙Íč×Zd†uûĢB D Đ4‡ęõ/߉O6˚)ė­æ}8ž0 ›į¨Š8đôöv˜NîØ"€åæ_Ĩ{iËN2•¯&/>ĒI‹jâĢé=îō˛›â5![6n+hė{­ØĨ˜ cc@‹5ĩĒvX–â 2ĀšĘ*€Y€ęŖ3¯ßČ×(HŲųĐ´- ™›ĩ-G[}ĩį×6#†;vŗ5ŊŅŌVa‡xFŲnĻūÕ˙'Ũ ãIįĩŽËaâ†dėŨŌô¯Æ›BūžũH˜” ĖãĪŗ,&˘H…îŖüÚBW É a| ‹*)ÅĀ9ŽpÕöÄÜčRAaĖešˇĒuAAô$•2acĸ˙B#ž5œ d˛zë ,1øc@đBŲ‹iĩˆ¤‹'Ž_ÂÆ–´퇋ā–ČMĻT—Ÿ+G{âļ´…ĐŽîūŅKbˎYؐ\Û읉ŖĶūņá˛ŖŊC‡z€ d=>ūép#Īxžm%Ôŗ6lN˜”æžōŪÛ/É$˛‹“ˆēËÔ|ĮÍjLi€íBû`ĄąSŸFya Iõ)6ĩÖú _G2Fĸv„b،„×§‡ 5:zŖ%ÃéĀ/ė’ËBN8ˆŲûãŠāÉēŽ¨Ÿ!ËŠNĶ16õ“Ų€Hųķ…uÛvĸÖbgétĻr:ôˇgudŨDfĒ–5ęų;œš•‘䞨ÄAÁ7"ĨEŌm[8ž€¤Š0ĸœŦä׉ÁˇYŲÎ$`K(´X>qsAĨbLŧ38öašģJ+ã¨ŧ—sĻĨsP2HxĨę ”éLŊē¤āÁ×vB? H?Rķ%íuˆ˛ũļHė‚}eŖfļŸ>īƒĨv*ŊP°|°Š&ĨJÚ}U+W/_Bģ>Ŗĸü\‰Čŗ>ŗš,fĢháųĒŋÅĶ8s ŗĶ•åĩjÕáö•5—,„I.-ī.™bt884ĘÅbįįœÖ“• \ŠáĖ?C<y&>īĨ) =ŲIEeũ‡­Œ§6ôg̉n 0\č‘ģxN“Ē8ꏋųĮf¤âå_ Q”õCújŠ_DZv:)†Įā]=ĻŖ#H#ŧƒēĒԍë6û=ÕË+ōĶrām}ôûKĨLü~R›Ēw¯Ķ²w<ęD¨:TŅą‚”šVĸ&„¸Äuߎ°úÕF%ņœ´NߛôFĪÃŧ‰øz ĮáØXõŒ§ŪöcŖáv 5ĩĄēŸÃ˙o§Ú4ķzŽíƊs'ĀĮÛ|sŨw˙{ ŖĐ*´=™ķC ]_ëöKŲcTĐĄsē=û/T*ßw7PpúąÉĮ°ÅëÕwĮ^ qëũnM_ŗ–5Eˆû–^%Ļ„~õ—ÍTÅÔÔČUx uâ¤/•ĄCŋÕ}TŅHĸ /FZ2ē+“†@UŽoË4°Îã랃nõ˛æ–Ôõ?\:‰2•mcFåëđ0¤Áæ~žW7Ųä˙˛'Leáps~§ŋTÎÅÂF2J —]’-w”9XæIzæ1ØQ¤=y–í‰gŦ tŋ‚_v([¯œ^DȌ™ģdN~‘0ī_Đ3,øV…YŽc>ÁL˛=}}ūs]“éhdŖl#†ØÎ-u}Šæ¯Nœ˛ą[R¯Ũ— †Ė—șãÍeÕwŽëģĩá°di°2ūPŽÕīú4ųŅŧū.œâ~ÜŦß;WKŲîED0XžņĘhČ>Îrōĸv Đ{Įra:ĒČš—ķĖŠŖ@[˃ˆ."m3ŌčҞRōõގ1{*Œok];gđ| ãčbU;gÕY>įž~Ũ‚ÚσøōŅ[ļ 0›éuN›ŖŒÉĀęĀŧ„Ȥ ž•ĮOWļ]Qģ6üšâúd-.‚ŗ&g×ūÛ(tĢÅÁlH›ģ2'Ũ¯ĀÍŲĸÅį´ņ°Š¸ØוčMŖ;! (ˆv%YšxOģŌ™|Gî›ŨZƒ¯JfOƒ~vŗX)…Ũîķ W…ā˙^EūŽMėöRĩ¤~}Ž`KųPüˇéQæ_RpÍ`Ld`~”`¯J5¨Á§Ä ô˜UéĩōdvÅqvˆ,ÁG´Ûc—6‹:ĸ}(ÁŌ,šCm—ē`IÃ+/ŧ rči$2†ĸ‡aŧ‰wéß:!ĻGĄ]žÂötß/ôöŨ…äFåŠeŒ^î)éÂΝ­é@fŌĀt‹J7"Œ1īŽÖGiîã–Gv-û¨œ7%™ėL;"Ԋ¸8ēB˜-˙ŲÑ7äËÔôČQÖcŒäYæy~Ķ~Tį`ņoēP˙¸N sŊ?z?"¨T’Ö>ņaGōÃ,ͨ€ß61›ŗgíˇŨ{šEŋRU?û×|"Øøŗ¤S3Ē+ éĐ; Íž7ö…T,6=LĄá_#Úv~uܧxā˙s,뤚`ž)‹ūeąBĨ/ûJ–I­Æ˜ é>6˜ XôwoSp;k}Ž&PUbK€Ūŧ4Ëõ}ŋdōŦ”‰—‘’.™É|DÎû*“Ō<á{ō“m´â/LŪ{V°?$–S‡× &:žtĻ*6Ōķ.tF¨ëfV§ü„œnčŦŨ'ņUqcũŸ3{ÚŖ4nœ¤ČNBÜNˇ×jN•Ō‘ä¸pÛĨ€]\*žŽp*ÄŲĀ#IÔ [–=ųIŗœrIŠËÛhÖuŠč5ā+”8ßm'á0B…ĒjC"-XhŦą_äÄ 7Ė_Ž ĸūKôWĤzjŦô¨*Ã˛’äz¤ˆŨ[¸ŦGpøîbéįؚS‡%ÖÖ:ļr vãąÅ¯UÁë1(/ËQē=}Ŧ|ˇnIŦ:QÉ$’„v/y“āŋň-åˇŨjâ2¤tåu‰ÃēueČöÂzlsä @c‚)*kRúŋŅ9ĸĀËękŅÔ˛R ÉëũÍĢAq•%†šž|%Ŋ<šdÃ&Y‰Ë‰™ų0úĮ+ęÕ`ņ‰%Z<ß9>•5ö\Ú]‚w ‘ō'đkŖĘžû–ÚŸPšlˆPmû ˜›m“ßŖ.ž$Ąz,+ŋA”ül_1bĘVĩ’GŽÁãŸØŽUfŽû ø_ æŌŌ÷IAēōPŌS¸–‹đî3žá°ū~¸āŊæųUîéœĢ‹!Ģa…‹)€ä$\žo VēԐ8žH$Û*´į×aƒļ/Vî°Đ}üáČ.# äéY&ųöĻ}¸ë\ i3qôĢŨņá+†!@;ąŪŖvi‚r˜ūéÁ)gÎU;ĮÔđ÷H˙ų•‚ûĪ<Üæá4'ĀI˟īë I’ö…ú_˙…h“Ųf§“*œ7M'G6Î P ņK•iâ˜>ŧRs’f*¨’ l‘’ų:R"y.­q˜æUGgČsĶ#‹˛EŠRŊ—ųeQ`ÖFíČ÷,ŠišÕ+˜vūC‰{ņđŨ‚ŧ¤Pc;›Ž˙H Č›1ƒ÷%%6ßNa0áœÛ{ž˜ vAŨĩĸ)Ē pŋ)nZĮšQûūh7J'ũŪ¸™ yC&iføgƒš'h×RfĶ Š…OûPúeVæ‡UXßļ8ėúd—‚ūĮgéģPÚŌĶ{ŲGôŦFųeúŠķgŠjœ§˙rq ›eŅ_U>Ō›˛d—F+ !áÕŋŠeV1)­Aēg“•u…ˆ@ũÍč×Zy fPžõŠĻĒŪ‹!×2đw¸Ā•…ŸxĖú[ ˜Žv`dUļ¸÷]p”+$1 ÉWũ‡/Á: īį *ˆ“¨|dôV¸ûã!eÅũ‹˛ī; Uė SːÕģe2čŦ\tŸ—xŒ™MâöĶˆĐ´7î$†Ûƒs°¯¯¯ē?õN‹iĀwÄ9zäS)’į‡mƒb׌Ч ˙'ÉĻCŒ )sb{á–Z*…)äM ŧ-s?ēũƒ<ø\9&‘ņˆ ĩĶØYەîĶĮ~,š‡Ss}ÖPž"—ø9ö×TH­OĨ‰1_u1X IO ŋ…‹ŋMņ?sÎ}ĐĶu\īÚ ~ #cšélŌĮ?ų"ČĘūŌãOgīAkīįĻgöAõ•Žte€„°AÆėĪäeŪŽÕ9V4§|‡Ũ–ßÂ6ō+´GAc?KŖ4‹Ÿ-ôĢĩ*Ē˙oxŌÖâŠL_ēˆLVŋ€ dõķ~Ā.ŠÚĒīY>\Ņ”įÄ0YNāQc>U4’Đ7_đ ŊŽB.‰FôŖ2ÕĖŨÉYG‘3`.r•åRŠo8ŗYYsēO˜ŊØ+öŪļhéíĶÎ0|´âĪĨZöÎy_ZīœtĐå>ãt'ƒŖLˇ(†vâŨĸ÷$)ƒœõûy–M°ã`^ÚĶ”7 "íu ĸNZÉŗ¨bCKu]Ä<4XÛ¸/l@PÍpß)đ1yĮ,HĀōxT=s—/!ģOĪžJuŧC]„™ˆ8ZÁ|å€âĮƒtŅ3ō Vč€N—MÚãŲÂŗ ZĢu ĀŪ°˜>čĖÚ˛)9RšÚĶnÍĖņ0„„Ŗ.Å*€ŊU‘Š€[ų›ŖŽā÷˜…ڏ=Î;‰Ņ¤™1§­øXīœO*׸ĩ ÜÁīž~ƒŅīéú}“)ØFsXę1'ĄŖ‰S=˒b(ū–ŧ0ą(ƒe¯ģT|RIŌsŽ"ųJ OZœ ļ“ũ›˙)D3pOč‹@6hDüiãŧÁņŌĀ%!ĸeŅ@}„Ē1\”ŧĨå/)yKĘ^8/ÁSüÅ;˛­|íÛ}‚]híŸkLĘ%kŠĪŗĨîX‹Į3øUĸéĘ!‹tŸhĸņ¯CŋĩFáK9 NōK+:ŪÁŌģöĻFMūjØí÷ÆZIFq'įĒŠ|øė‹sZ–BiĩņĄ) ķĢ•;*f*ø|GYŖÄ8ûā&;˙˙ĀtĶī%{R‹>Cĸm2[¨!҉æSį YgŊÛ7 ļ$`âåØ Û’I$’I$’I$’I$qíÄŪŊūŗįu˛č.åSĶŗÅŌËē…öĸŒE5GÁ9“[¸ ˇŋãœgqr=‰ŽhĀĸøŌEĻZ-ė€6ä’I$’I$’I$’I%ˆ3ƒe!ʧƒĄėt•x´¸\)›Kđd…č^Ų—VÎ8%§˛ėELH$æ†8j*rJeķ[đíĄS)¯JCŸNOFoĖ+ž&á*=7i?+@ežŦ!!wIĮ ĶæĒĄŗK3¸HõŽ…n#öŋi›åˇ Xķsę{C$ šŖ4–pSöp÷ō‘Õ2B]›ü/Røū\nŠøĨ /Š YuÃ$Ԍj,JFĒÍ>ūÔ,d­Sé÷¨% ØwzÉQx¯T+'6€Ž ´Ĩ#Ą¯L4m{=;Aqâųp >b5˛;D™‹O'XiéˇZņ:’įđëkøugü:ęūkåöõe}ŊC´īˇŽ\?n÷~ŨŠũŊūۊöąUΉCúėFõ+ūY.5ėvLR…QO=ņVíHtkžm’y~iË*‚ÅЌ‹Đ0ūØ*uŲߨ3ÕY `”ņ‚ŠC|€=Åę˙­ŗ ~Xĩ>E$úGųđ%WûĀp`8 „Å…Ė—.ūMJkŠ†ĀžZ Û‹Y‹QJw•xîÎ6¤zŖ [oîûĘw9Ī‚úÆYƒŲø•Õ:˙úųÚÁYßB‰ëé–ĩÛ6U™¸.zs.¤ūFé_ĄwnRJ&ģĪEÂá }Ô&ŸQÅyŸ&W*ņ>*ē‹fÅ[hxqQĶ]į€Î{E6˛õuâũJÁˆt¯]ÄCĩ/^Y$ÍĀ<žøōmĩ4Ô2/ži˛)8˛ ‰h%ŨžC. BŪ%ũį!Dq?jO3W ›<ašŽīÛŧĨļĨÆGī œąĄ\9œoôEô{c}Ĩ~ŸĢ 0üNAüvŊqiŗVff{?`bĶ|‰˙wėJū.…"§$u1Čú!× ģU։ēf”ŠĪĸ ũ˜ëkɈé-ŧĀ^ûŋ[ËDtzë¸1}ˈęĨ[ŗHQŪ’ũæĢ”Y%Ëø8S|Á¸ŽˇOŅ€čŪ#یU”ŧÚ¤ÃbúH @Ēwų>b "å`&ūNÜ­ĶîķhY‡qC>^Ī0ęč7ŪËūZhN;ˇ|’`Œ-rnaö+Ŧšv/+eÖķ1.VAŖ#Ψ[G@—~Wš˜É¨åßo5ˁvĩsôuLž5Ŧsĸ† É3ŧ`ˆíqŌ/JĪØjãeęÛaŒ*a§*AvWŲØxņļ:7-ˆĨz#!ĀŦģ˙@;\ZԞF{åŦ¤˙„;6HĒ|x­ŽĨņnŲBĒĒđ†ņŦ9LBŧE#„­ËcÃŨĸsl€ƒÜ…™ēŊĸN)÷säQuŠ> röĢ¸Ú Ë$čåÎO€Ë‡¯ũ+KËō{æ;õ%‡fX‚`č“IKõd Ī<Ž}€öØá"e}‚Ú5Ąt{P‡?ƒW` Đœæ¨žēJą…FĘZį˙öKkįØô ßßŗČ~ūŨ„›b‘ĶQĒ^dąT,ënüGĄOÁį^$tÜY…1sXŊ Ž‹™‚F:s9ߎNģ9ÆÂįk5]MÁQÎhéôoĀ›ĖŲØ5ÔUZIԈŧĻIā] †Qjxm§šÍumŦs‡č7Æęz†[ę xK`äōžˆ~Ķ9ä‰áÅĢÁmÔeāāÖčitPû‰0ģüe#ãĀîW ÜmoņŊ6SÛ`ŗ[ø.āų —˙+AjũoeŸyCuEå,nˇŋWe†¤+4N]U~é,‹ČLŠMön#įãågeo#Õŗįį™Í€hāv Íû^úō4°q~ôˆ,íâ’vÎVbgSî)-ˆeŋ˜"# G^ˆ+Ķ‡Íz\“Ř€ÍsnČŠõáŦ°’’䏯aŌq6îHŲ4–\aüŽééM0_í_¨ˆo$2[ũ˙y(/X|yHSzāūÄÄs3•:5‚, éĨÉ^īŠ ÂÃīOZ°:ŗxøžžč.fLY#qŠƒR,hė^¸ClvŠĄHbÄ´,Ū:ŗŅQ‘xē­_ž0ōŪm—ëcTõĖĩĨ]B+(­Y/ėÛ(ƒÄv0Ģ‘ d—2ŧŒ‘t ā%Ēęb ÉuLÛĪo-`ō`)îÉâ.÷ņŠáô=åsGh°1fYŊA =ÎĨ7N–‰ŋøËv-Ė@ũļˇ˙>ŽgMK¤kT&JŠļĨ˙QđYŸâ_jštą Wôˇwxē,qÖ+HmũÜœũyÁÜđã÷á|YŒ0žlâˇúĐ/’ą:'ėœJĒĮ…5î˜?Ė W÷ÛėAĪņ;Eã°Ģ ˙ĐŦëõôšœœ'Ä×یģtŗ÷qŦIŋ‰Eā?˙{oĒ*ĩ_ËĖ>ŗķS˜DmĻŲĄ&oŊ]НG2e ¤žLæāötcØ=.Ī_„k“ƒIäPßÜhG¤.ũPƸ]^˛ŧöV8×YĪ&Y:;#–ā_ž¸0Hp*E"„Uģ7™fĻ}ĘčCmÖ ŌG8YöДį}TĪ˙Lāø'ÔRĻ°,iõ:ÜŊ!‚f|vuŲ.ÅJÜûo8°Š€ųМx¸]Ųx-PUđ‚‹Œ—?d†ü ›¯_Ė„&ĻĶ#"YUPh‹Ž•SŪĸį b(¸e‰SC1|%Îü‘UÖt´9‰‘‰xquîøĶPķnĩMĒōæ2Cs@&ÜK„\(ör–õ<|6¸˙ņ8`-Īžŗ˙ėœéˆ×į6ĪÔĖ4¯˙kØH?{Üb ĻiÉ%&n,ĄŦcÃņ¤fröŨc€Ũŗõ…ŨĶŽCøļGÛN-OO_œņČ:0HķÔ+7'Fdi™DšŠ•äˆ!tÎÃßM‚nAnmæŦÔqĩnzŧē.ĸōŪLaÔÉÅ8㙠Cđ¯ļFąČ흤AN, ‹™ĻYJīË$’Ž%ø>aÔĘwž&ßlÜQĀÚqķž˛z<@IKå7ņf5ũŸ ũ J<ų˙MũT3;ĖŖīĐīYëä%);ŽØV9!õõE‚­-bÕĐG~q™ėE–ˆ!VNG—WČiĖėYoæN ”čôWBגĻ>zĪrŦT:`˛›‰§ęŪÖ˙ \Ęšx•”é͐/0đ=tąŸŠÛúVuČ Ö˛5lįˆ)œ¯jؤđ)B8^Éx âŽr‡Ëā˜vN¯ė¸ A§shNz°Ũp CEĖČ\‹7Bî/…Âõ͇6įērFœđ9Ú+Äļ•S?oöƒVÛ´8"Æ´ˇüÚEŠ:¯Ŧi_äˆō|z˙ í´FŅ;Ŗ[ĢõL*ŨIb™Åind("ŧģ‡k÷^9ĸ)ģÉ.“Q*įūTSŠųæÎ•<ø›>.Bßę}q0đLI—Šļ‚*!ô ĒN°Z ~§R[ pEÕ[‚č~څŒØE6[ :šáųd$CˇoY Û;Ü ÚÉšbĸžJSLD"bĖ€™~¨¯ŌoØ´Ö-ĀäųVįœēnĘP;iČ+Â,ÂÅļ|[„ēČJã7+}ŋ_3đũxōąņ+ūī—ͰâZŋ(yÚ¯¸&ŗ­˜t˖ūT$3Å}ŲŊB3Qp–Ô=g^Ī€35&ÔpŗĮķߔۘSÃŋYėE€š{!ÔQ .É%Úđአ6/) ›ī?8Wā*ˇ/´ØēŋO´Ũ;•ŽkŽæ“íã€ôzÖ%¸BvģÂÎęq!„‹`nx˜´÷.>~9l]Ē.-ļžĀø+ԃ`€ÅN‡ãÄ8k-ˆŅęœŦøÍrĐ'_兔ˇ>™aqĢ*5_¯ŧ3ËäËCŽk1č; Ádš (ōk’.Ŗ¯D° 3Ŗ{AJšTÄkwsā‚Oß1v­.RíģÎ°“îðüb¨"X5 ×{ë¸uƒ&ŧî!^æˆĄ~Ĩ†uč0FäÜøâo6Vč*ĖÉ9ƒÜ´ž'Ú¯­ņR-Ķ.âū€Æ°øØŨÆ$ §ģGQ)[:Ī*Ûĸ\•ą_6…˜vM•ĸEaīiŸUĪfZšAŒx*áë— $ؘ˙Vz*Üį?ąmŠÖĢ]ĪH¤qIgm OTiˇgŋ;/ĐŦdŸāƒŅ€GܐŸ˙SĨÔiåmu,.`Ÿęū.­qv„÷Ųöíŋ†ļT )ÖUÛčIf:ŋĐQNöĻxĻMŸ¯ ōqâ‚A¸„0(žQdŅūö#Յ˙‡˙lŊķ6+Ūę—ĩÖ¯ĐéøČ•ŧ=ŗŸÃŋ´ÉN˜Ūgˆ‡0LP%ŪÎĐ/S¨=RÕÚ ¨âXH59š í*AŒƒķ:ĀōŖã…Pí,õcËāf¸)-[\æ†âŊīdũ§7Ī:#ņZÔFî5X (AĖ CRÔ­ĸķOíîž|JCWq _ÔUk…ôņÔ*ŧÁ#Ŗ3čYNdŽT— ­ĖD!ĸÖ?æ7"šYûõéŦĨ›aT"´ëá[•âxô™n,5†ž_ oōԊš‘-žÛ˙0„ļŸV-ãRôĶ-Čđ&Y˛!¸Pņb*ĻÄ}ÃáÄĄėĄ˛v}mß°<ĸ;÷]Vb>ÃŦĸ4uÚ{‘0“b&į…•xg=7Ņ˙MøÁ•­ö›<ųäleʃUųë¸ãc6&‰Í´‰ß¯3 rAēpĨ2[õn<ũĘģŧ ˇIeĐų^uˆÚkJĶõŗŌ(´–F°ĸ0E×čM 7×čŅ3 ĪiTXWĪžíŦTžžxXčvS›~ëįLHUš•ú‘yn‹„g—ęßĒĨ‡yĒ¯mĒ!/\ $ë. °3åĸC.Ëą|Ž™?Ypåœ†Ë˛hՄŽÅ6Šã“Ø ĄĄHiDįv¸vâgįl/°bY[įČÖ^I[ßĨŪ!bŖŧöˆ˜ļcZTQ5ķ'WnĐ/äáTÛøŽL-ûXtLr)åˇBãlsĐ}ŊŌ7QŲ°å’CŸ˙%Ō{ †3zģë鰁ŧī+ũÆ,´xāÂgę~ÅZiO’hQƒx‡ĶFûÎ4a’*GÉôÂå¨GTÃÍī—ž1„ Å×|Íšę2™vHÁj*Õ`'L.ĨéîYLbĪųA*ū…gŦ>oOŧĐír;Uũ§XķÁīüĶâęĐ#ÎraFŧzŅXâ6]{Ēŧ—=ÛË\yã>’&“ĐũĖē!Ģ™ÜDŒeZΑ¯.#5“Ū ,+ŗ$AŽâ‚'¨ŽD8hß멌‘ŽĻĀĨˇĩ—g’; Ū .ę‘ļÁļØČÅņōhčĒ9 ”Š9ž—s;ūãú#gņ×Tœl}—}—ráljëĩ^ž3ą“×]Ři&=đ%Q‘Öád€õ<čØĢ)ŋPĀŽšãņhžŊŠHū@.Dņ IūdņF!:ú<.Åš!V{AŨ5üIvåKn:Œ\+ ĐV‰ČMŠĨX8{ß-†.8îuāRĩ,‘+Ä&1pĩ‡§°J¤é´DĖæšÄčąw¸ÉV9€|ÉėÄj”ghčļą°VCíÅužw+3X˙zVížö.Î@|’3Žõ)5Žf+įs…Ч†‹Í¨Ž0Ä ¨VØŠøFĮ)ß< rœŖČÉÛ˙eæ”'Pt? œÖž…ogG <]‹vũŦQo )ēĀļÖ&U—ęíŒä2Iķn­˛žHŦäZsŸ=õ;Ę8Ũ°×ëI)“j€đĀXQȊĩĩž–Ž}Ÿiíĸ(XÕ*ōĒ×Ö§ž÷Ņ7”5øY kųÔhAģH~Ŧēēŧ§ĸąįä¯ņŪ•XN(ß$Ü$;wÚÂ>ič6dģrëüģ^ė.Ŧք_sINsO!4‘ŨAGĩ|X´ĒrŽr:ë2šĩÜQŽK ģ(oS4õYÄ`'Ē‹™´Ē§Åč„úTūŽ b‹y~Ā)ģOc;׈Ql&včAŖ ní›YįÚēĄYb\{A?ˇĖĀæŌ/–Â+‘eŦ15įJ*•9ß`§'™f%â¸qúÖÄvÕ cÕäÆ=L¤ũČŊĄhØ QoƒÛÛ<čzŅ* |tđü˛ÖŽËˆ(čĖXƒ$”H„;ĻeYīÜx*Qã]ÛU`§Ô|čFО*×Ī ęÅÂK+j}}Zī$SōėôTeʞJšá%-ú?Ãö't+œDnöŋāGtŖüúYŒĻ1w_ 㝞‘ē\uņŒwąnPžÔyĨĩŋķ´ųoĪw)ˆ´{ē&d1ŽiëyBR͇cšû>Žjõ¯ÍĖ×īÚ˙wåaMˆFŗ¤˛įV7“nÔ[Öa„+˜ė%Ū}0”õÕysĄJ ŌÂ' §7ÖąŸƒ2đKæŊ%eé>áÖméAKx} Ē& Ÿ5B f­ûŦ4:™ėą‰°-‹<ņ’ƒDAŧD×c…yģįéuj ÆĮåˇFwašjōûØS…Ņx×ä–A­âû- …(āį­ ØxÆ˙r†šl´_ļL 8o+āįļ•ī q…ßgÕŊ āüãÂUôI(3ú7ˆûŸ?áŸ^ƒ./Tū.u­;)‰GŌÜÖĸy Pɲ"NĖđcãŌāÚ9ā)fzˇ›ÔēU…ŒžAH3?-ęfäGîțĪüXwš\jįl‡Ēŧ*FyߊŨ°ÄŌæ~¨įYĻü€č†jw[c“$…›ģũ˛bČąŽû_Ļ4YxĀ øĻũ:38ą–QzŸ6č Ãy9ā ‡š-j†TøDĢ~‘ĢzŦÜÔ0Ė`ÆCʋŠŪŒOéį8įŋôX›Ā¤­rŦ|¤Ö›ˆéqBŊŧ#6Ąĸ gA\<Ģč&.į¯ŋč> Håáõ„˙Ŗnđ ’LןŅ1˛iīܛôŗ­æŽ=6X;8ĩbmöEÉ>ŨAģŋ‹ Û߆҇ˇĖ‡xû-ũË{3…#vŗu†ŗÄë#ŋ„ÄذŽxq~ĘûC Ō0ĸĀ ›ÕFsfsļŲ¯ŨCõī‘ ëü6_ā…PąkØm4lŋōWŠŲ‚&&JŌ[”4Kcļkîæ’ŊŦÔō|z•MdvƒÆKˇĒVP!üĻVmüQđ…‡ ÍÖôŪڏ 5Â˙$Äã>ęgĪA{J VÍØø–Wa•§ŽlĨđǍˆėėÜÛxÃøÜŲį>T°yVįŌ(°Ŧ?VÂérÄļ #īÛ˛wîdąüėkˇtI §[!+ėŲŅī0*1YsđTėbŠŅ'ú­+ßķIĒõ(`ËD:¤$ōø+GĨj÷Sāäeã2žę…Fy§bŽÉ•sīt\×Ô/ĘåÕÄS,| š@w/gzÖ]#Įļ z ”˜åZĘK㏠,\Šļ$—XŨ…ĐÅ^;ßÖϝņĻYė:‹1™I;;fĪ'kJ^5L˛ëifœ°¨Į\%°=ŗņļĩđĩ&Œ`&įM7ˆeŽĘ*›ŧ7$gĶ`/…€Åš‹˙ëu€Öx>XŽ“Ą4vpdâG?ĖJƒĐx{c)[*Š_îŊš{*4!ķZÄĮ%HŽû÷O+ îDWåöô×}ŊQŊŋ‡[9}ŊE_oUßï+íëÃÛ¸ˇxoFŋˇĸĐč`G# ÜČōlō$!īJW/÷dG˙`NįÁé‚* ~đ[5,\ŗQėd¨¨0JĮÖĸĶ&šĪ…t–Ķ}x˙#==E­×Í÷.˜+‹Ü˙^âb §í Īīž$ÕeôŪ/äfql•ĶÜ´—o:ōōˆŌg&xyŌņaįôė­¯&#¤ŗøá'ˆõŧt ƒtPJD.$sC‘õQ ŽÃ•1–PVŦÖt=ĐĢĒ͚˛{ã.aö@]9äŅ[ܝt.ž+°]Ņ›üX‘>ÅQPVÍU8Ģ$ūa4‘ã‡)2yŪĖ–Ž9žÔ—Ūö„Ų—¯2Ķōëø>yĮ¨ō G_¯o˜ŦĸĮhŦ@%$…(3ĨK˛6X“ë_ē'oʆ€uŨĸ%]?˜Å‚JĪØjãeęŲâ_i’䝁yüF‘gb*S{æ€yęRĮŠ|1áí“ŗáļĘģ¨}qū ŊČu’N…ĘájöËm˙2Û ü|j9ļvJ§–‘Võa㍤8^ā4ąB•3ĨFų ĶôîæÁķ™jôëFŗôâ aë”Ō@"âZîachI@ĢÕ˛ƒ,Fq*m!!83ĒjUŒU-°%ųÚžŸ‘ënYēAÅ'a–b} vÛĻ‘+ŸˇlÁ ũÍyļPÛkpt8œāšŦą­‘W<¸ Œ>it…LŨCÚáGČĄjé×rgQ‘Īƒđ~†āx<”KP‰Į—C´b ,ų¨‘ 0ÂEļ‰\΋ŽũĻŖų2ĪÜĻwšĮZĢî¯ûžZāAm;ķîe…;ļB*]…ÛÖ6ƒôüÆËúΟÍ=l$—ÁSXˇøq],~N> šVzâd÷ŋ`æÆ$‘ũF<#æJMÛĮÂ?íø "Œ…žßú›¯,­}<:“LbvŠo‹ų§žDv§2–&ȟœf‚ͨ†!z/f÷ZnŨķöš8Dįâ÷ŸŨwĨrgËô&îīúmÄÄ4öÄoŪátõŌNé‡Âõĩ†ČC‹ Q÷ģģEų}~Ö¤č{d¨esƒ ÚāŪ düvŗԁÆ$h͖Ūū‰F%œđ)Į¤c.ûe…Āötø7ģƒ?˙~‹ęP­O1Ąķ\“+ꛀÜeüèXŊB94cķSũ§ÄQ>X/Û{ĀrIs(ņøí2ŗB!ÁŊŋëÂWØB •ŧ˙SbõĒ’–u^NÃæœbšAáĸ>G¯O-úÂÆ §öČT°ŋ5lŊ3J!å: ķ?ÎZ,ŨĻ]ņzšæÚķščQ8i.čĶ)ėsnj$č“â}†O)Ŧķ€Éåi0Ž*˙]ƒĨ°C,=RaŸK<ÚÜÃpí€Wėļ|´‡ŽĖÖ¸ã9Ÿ—tāa>ÜåÖ3°éO%œC{ÆXĐŦ™öíŌ\ũ’đ*nŌįWD‘™S˙aSˆI„3­…îY5ŠÖôë-¤öt ļÉŖ>DQŊSoÆ- Č9Šî§Ü›ôd€x™ōķ5ĶļáįŖ|pŋH’ÆÄœUF;žž¯Ņú3ôm]‰/ÕnŅēFáfėǰ:øāĘ_öéĄĢŪØŸI9 Šģ—jČ<1?Fg/mÖ8 āą†´‹:tƒK$ũž/8ŗīŦ¤ŌūŲ¨ėÖ įЇ°ņNąÁŋS~XŊEÉ(ŧ”\e`k’ŋ†_ŦÁ ;|¤;B.Ĩ§LQ w[Ē&fé}˜Đ2uē’}ÎN¤ŸÔT}KĶÆšˆdHMŅÉ3`Qƒ:*ĩ›ŋn¤r˛ęˆĄa›Šīmŋ<6Iõu‡ĐāZÄŪ Ž3Į  N‹į6k3ׯL†*˜‘UKNøėŋ͐÷õŖs×´ÆŨŠĪNūjUķøūāŒ×Ĩë1žIzņÆMŠ1Z~Ģŧ čĘŊYdŌpŠP ^}Ŗ_ W͌C÷Í3#ZO+ŧŗqGiĮÎzČHč<ņ%/”ßŘ×ö|+ô5(ķįūoꡙŪ`Ŋ~‡omą2ž„HˈËL’ņä°oĀ@‰{/Mj^EPũōęWîÂZŨ¨ķ—ãŠķ5­Q^—JLXãĀÄeé!ĮÕ8ʆ›Ļym em~’ Ą¯‡m¨0ɯ‹aúA˙Ė“ŒËG°ķ§`§ęL<ėM9ū×l¸‰ņ*Ö?Ÿmzö‹ ˇívÃû¸„¤’s,¸ŽrƒxĪZ¨ >ʀ@Ö̌$_˙,Ÿ„G:“|¸ŌY ķc_`@ž¤0×âë†L‚XPö!”é9{ųVĀų6Ÿg9K¸ĸNŠAhĒŅ)wWõ˙ĖqøŠßk1iķÕĩe3ĄøJĩN”5× ;dkÁ‘öûĩüX"<ÔI?>šį(Č}‰oNcÁĨãŠ;gŽ~ŋ™~?lj*@`čSŠí'˛¯HS RY3~cōč)u;jI‚čBü•=PčíocĸÁ?WÄvÜ(eŪXŸÍtūĀŗ՗ÂhøöMЅ]įˆQT%5Nę֐ĸáY Æ`očĻGVJPy- 1—M!ˇôš |õ‰Õæĩõ13M¯ŽŠĪš*ÍdĸP*’Wní­ÚęŦJRđĸÔWĨŗ XÖ´-ũD‰ĩ,ņü÷ÅĨ6æđīͲr˛ÜÁ\V\/Ŧ6 WØ3ĢÃE0 āŲ`ĘíĪhŸ‚+W^`%A)bûĀĒŨ´xú¨)š\ä č[b@õd"ąJPÔ*ũtoĢBC̚~R?IĪÚú˙Cũ,iJYeÛkôΈŽ¯5Č+ō |§›=Ã8õēīNGÎ/w”V—ãa R,ÉĢk¯Š†@÷Wx˙,M“ŖSC^ÅžøWÃĮŲOŽ˜lÃVˆ†ĻšIŒyt5æV¯ö3Ĩ†~BŽĸ7tŅ„ÍV´Ų*€‚ūuƒ&ŧî!^æˆĄ~Ĩ†uč0FäÜøâo6Vč*ĖÉ9ƒÜ´ž'Ú¯­ņR-Ų¸jĀŦĮ°]b|úŽŲuzîM4)ߞîZ÷ūqZĶͧž ΤQ”"7U×ŧ¤×ąœƒPĖ$• r‹ē^„3ÔíEüāš’…ūĢi/iOmÍP#īßELŋ$Žâņ›&DvāƒŅ€GܐŸ˙SĨÔiåmu,.`Ÿęū.­qv„ƒņālŽkiY_ũŒ'÷Ŧ 1ģj$ŠŽc“‹åĨfĨqŲ°2ˇøŽĐR6´čkmGšķ-ŧ’ĨąëøaMd[Ļ$sHēžØôöĶ‘xˆįÉ<ë)Â,uØ%„ƒS™ ÎŅ’ $Č?3Ą*@įŧ•ą8(DVđEq ĸÅ>ۊ;gú×i&ø°ûᲤl8ycKs‘ÄárŽ w­§ŧ^LØxŠōÅVę¸åŪ9 ^€ÆĀâ‹v.:Ôøgg×@ցF›Åŗ ¨˛ōԍDË>ŽnWĄ¯@0§ë˛V˙|zŊ›Žo*ãƒZärˆøHˇŦsūĮķūoųrŨĮÆzđO¸a|ú/Yq/<9S&gī˛<.ĸ$;\jŲܧPkßĢ“@¨ļ)Ã:“äĖž&K|ÉzsØÛbŨsD§a°Q7%¨á”˜Ęú!ī@S0'ĨÆWŠķŗĄ¸Ę|ôn/ÂķW*CļŠ;˙÷åFJĒJ'MūŦÜ˙;Ú~-Č77`sŊP˙%.„‚ƒS7ũ™Ō!b Áņ¯™õ[IŨØāī€õ_įöæĸõÄÃ@ˆŦĖ€[ΰJ?ōÖĶs.-´÷ī´^ŨĒãËŋĻn^ ûĨķoļ°]ˆĘ~ŖŸ^“‹ĄQæG9įÕ(Ô´ĩQ YŽYWŨs •ÃŽÃßüO+U›Eŧfí7y—Ü´ŌŲVķÉaˆĖc“Ŧ­ĸ1ŦʡiĪûMwķĖ }'„ž­—YvqæĖ~xWžh_T°ÉPdƒâ!û04ũāũ¤/;čęëŅpUm•2ƒĪÜ<(EI§DĨÄD…ėΆת(üę1[;\ČĶ9×āĻ& û4ø'ũšJôAP™jėUÎÂô)¤ūTC $%¤aËîŌá ˇƒ#; ŧÔĩb5 ¯„ÚúfāÍ[×une˙ĸ—ĸ?âˇTčžöp$,äWė°jÖg aÖŋ\ĀŲÖ˙mŦ-ūq‘™=pœĖÙŅmL˛”“˙2/ `IjŪa(Y:Å%m%O]Đ^>…Pí,õcËāg{ú])ū ë]<ōt5–¸ˆ>˙cņRÆ'U”øāfŧ°Ii¯“KRŅĪÔīĶŧj™ß4}Z'•“Ęũú'•ˆ• ­üÎoŒûô™zëjÛ×Å΄ķŠĻO9^Z¯­ÕƍŦūr_~TܧcĘ|4ĐLێˡEc.WtlĖÁ‰x5 Ę%[g…€Ņ‹0†ˆSËļoĘwlš{xāMÔgĨųmå8Ir‘.ÜŪ(fû7)e[‡ŅC¤Ŋ‘Æ_Y‘ÖöÚ[öIúj$ Tįņŗ]"pëø51jF|ķŌ Lš97ļÎwiSÚːŲĨ[=Ä­ô×2mŽ æĒąŲ5ąééūGŗÆ×ЛĐâ uėGKŲ/Á‹úVęaM^ÉígļqÎęá”Sŧ:ßū(ĸĸÃ! €~įĄ%ô ĨĢđF¯‹Í?Ö#ĘX0Ģlĩą?„, ˙xãKŅV}ĐģFĐØÜf oÛlqÚø„‚Ũt CW".GģãD˛zq†č!/\›KS^ro`Ŧõ¨)Wm€{Ú•˛ˇļ°~@ęąM­îų€ËrĐn9&„zbķ˙qā äS’%e”i, šg|_KíĘpڙÄVÎ7Ŗ†k­ŸŋÍ^0Y´ĀT0å)Ŧö‹NņÚīVėãŨ×)”`Įû7.î ÔīŸe2ĒáĄ;‹{NŽpbRȂSĸĸÜÔjÛæÚe!ĶįFw{¤û&ŌœČ‰zKĒëĨ(ĸSĖ}îņ9 3ū9ôVĒž7õTZzė}Q ?RŲ?Ãywˆ-ĀoŦ,jDoaJæąēĒH?ĸg ÆĨzøÔrĨp¨°Jß­bt#<Ôaëo`7ĻO6Ņ Ŗ•ƒÔä]eĻ NŪM?Æ J׸6‚nØ%vœâ¨ƒ,fÍõã \īm’2Ę(ƒl#ę9…÷[‘ĮA?l΅:’+i[ŧí!ƒX´˛d›Ŋƒå`šíŽ”FĶŖ0ņšÉ§Z;厊¤že7~ßčØx•ūA{E-؊§ k丹DŌÔ8Ølw ë\˛ŗ§åKöÚöíā᝜Ą\âm¯tē+ę4 ˇ’|$u…ų¨Ęâ-Î1wī7ŗ]&Hŧ'îŋÖĻÎy)qËGØ[gģ@-xŲžŋ‚næ˙đũ_ĸX@ bžéQÎĘņp›×@•ĪrEAØŌŸ˛ā´žq BŌ×ÄĀL.§CĘ xŋuÕf#ė:Ę#G]§š 6(nxYW†sĶAĒÎZÆ ­o´Ü é!&âyŪL°ŖMDK˛äî Á܂ ¨Ģ,ņ˙?ƅ˜\ĩ¨ę˜yĸũōķÆ0_AĒÄV'5íd;Ōļƒšg>ũŲô]ĩĮį(Āwâ‰Ēs•Ž•)ë S]l3#“ĮpƒM§5>`æ“ëŸČØ,đEŊP¨2žRú0"EŖ“:?ö߆4[bč¨ŋ­'Ëu š)BßÂ?…ømūŖü¯Ē‘”•ø¯ÄƒŽĪYœ4oõÔÆHĮĶ`R[ÚËŗÉ…o—uHÛ`ÛĐį>aW)QTk?‚ˆéôÍjŋĶGčģĮoo%-$CØ*Ēŋĸ8âlw z˙oŲh†ÜWž˛WĄ\Á"Ėūœ‘\î6ÛĐrôš.mØJ\Ą%‰,tM`•ėĘ : ZnƘL]cDF[‡äŗT…(1ÎXĩdīqŽ%FÍ ÷íŖoÕ6*Ÿ#‰ķæ*zËŠQČŧ܇o¨ ¯†Ã—ÉîMÆLáLIúɄƒúŲ`'äf{Ā>T҃EŊŅü˜‹ēji6Ū+ûE"Ũ8.č‚ļjEųkfß{į >IČGz”šĮ3ķšÂčSÃEæŒTGâPT +l E|–Ũm(ũIsyĐb qÕĻr°ĮđË5Sh)ö`ščˆF’‰{‡ũ…/Čc)Ÿŧ䔟[e´´THPføxîˇå#äZsÁoRH|ŧ‡’‚ߐfÆŧöâ`w¯ģ–bÂtÍ2‘æļž˜€É_ŊJÆIĸ˙7Į*̓ø ZgÂL]hēäĨxب&Ŧxt"s¯ąøļ:H¯ksŋ_Aė6ę[ĢD›Œ;ŗO•ŦC)ēîĒ>Ė™tÆe8ˇn‡ åÉ;†$í•U>/ÆČŅFjú™ŸtŖ†u¸ItÃRÄ÷" Ģ+B~H§҈\søŽ8 'Ō[ęÔB# !1ˇA‰ÚŋŲd˙i|J\ĩOŔxtŧ+Tm…Ā<Č{5T$CžIĩĸ0 ĩ^2p=*͜•ˇlĨĻÛ Ę‡h‹”¤€¨¨TķH(ũSÛļŽ§TFâėŸmK›VŗßĶA '*EįÃ÷­¸ēv,ˆ–Aūå0u!ŧ{ëÛŠ,bˇVčCu+Õjg˙ fųĪCča#k¸äAmĢ5‚ßßawxu¯ ƒtŖ—EøIã’YĶw9–I/ņÍŲõАBŠ ŲVh‰øDĸ¸ ´'•1Ķ÷=XėžšŽ†”ĀūÖ5€uđa •1dvoåŖˇž~|}”kaį–*ëfé;I„ÍÁæŊ%eé>áÖméAKx}3ŽuŠÍęs_önĒĪá:Œĸ*gÚMŠūmĄIÎ5Ԇû,$oŠbFjjq€dãXwŠ×ķÛ(ācXqÂÅDĒęZ:w<‰Ķ“•Îywm@CfnÄėxâŒNNžžU”ŧ´?ãíHcIÄ˙$×q!m đé °]/u.ļĶč9BLxÚÃ'SĶC0/'KÉ3dŌGļ@ÉÚn֋*ģė 0œüŒū` ĮÁ=°“ôŲ#ÉÍˍLĩP§Nf„Ŧ\sŽ‘2.ų…T¨rf†ˆãv âĸú›™Ŧ€ŪŽQЍ{ĸpnjķ\˜×hKk‚ak"IĘ2ųEŗW'ÖįCۖŦƒą—KDęĘ)w\a=cFf>B\›ˆî ŨęēeDcęÎ:‹saĸ„ņ3ž Œį qĨ(k"1Oq–X(Å­‘DāŌRÆŽÂVžRËÛa˛dŅKvW7ôĶæüŠËqRƒO[ŖĀMSÎ °j"s‹e§R{GˇÅāę5Ë > g Î}ŗ]]ŊDUHؒá‹čU¸…ĒehĮÄķī4㛸°'•Į &  ŨΊ˛ŠĘ­øqĩJ^ÁAī–ĩ8oĮ'ˆÍf?ŒEň§•ˆˆŽsôÎŨí€ŦSĮ× ¨R' jDôĩŖ“䌛y¤¯Ärŗo9ôĩÍ §—dNmĶÉ-ĒžéP(¯KKyŊĮ…ė/2yĀį„–€2_āŨÂhŧ÷1xŨ[ėėĢ?1"MĢģ:iô@äŋĒ‚ĘâW1}q;@N ž}ԈGTO xĩƒvĪųŪŽúųsgا¯ėuŒOÁ‚ü]˜§ļį‰Îy}ŲØ:7 Ŋh9T°LąŽVđRđĐM"&ËēbŖ>šPzYzŊRÂĐĮ’ļ¨qč뀍‰(ž9w­ĒŖyz^ˇĒû[ĨPTø‘!dÜUJĪ┍9;éMÃ:Æ} ˙Ŋ‡x¤—+5{õdZ%įj3.¤Ŗøä÷Û56 n’č/zV Ÿ<ë‘o}•ĩēŧõ˜Œoĸn“øÁãŦ÷`/;_]üĒ&kÄp íö6â¤wæŒõ¤”á°ÖŒ wŠfˇ˛TøÍˆjĨUcŲôq@s ¯-2ßé\EyÁßœŨ šÜ™An‚tēߟX2čŖ–z[ōĶerŲf™iFŸÅĄĪQ(ũŅļ†ĩ BJĻUvâŋ€Ÿ"›c` ėVīŒ{Ž7Š“\ŲqdXĮŪ#č]úØæ[Š”&äķūd ‘¯'īxM: ÚÍ-a¨đƒī]8Ü@TYaŧž}0ך’`Û>Ų€”!Yô kI}?EÍŗŠŠ+ÔėCG°WŠiJī9Õ%Č:å)Ž7Ž}ûJü6Aũ˙7?č> HîÚKqFgŪ€3a‹[d†xėĐÜsäÆFŽmIB;R. ,åqx“°Õ + ØvŲ’ŪQ 6âÂV‘Ŋōyŧ͚fĒ҈ŋôōŧkũ’/5ŨžŠlī°wXĢ0ĐMĢ€ÃF´ĸTüÚ4z—9]ŅŪH’ãÍĐ 7g¤fÁ Ąb×°ÚhŲŧÜĄB¯ûVĻPÖįoėq™gŗ$Vņi?{Ēe )ûAŦé厕„m^đŋ*X8LÅÖá; ,vAeWŖhW/| ëYË͎Ú VÍØø–ĻĪWBņ2Ņû"PˇrŦ›}$Ú^Úû‚§rÍ˙ ā5ÁZģÉkžXÆÍ;8œÖ•˜¯üŨĖAV­ú0&Î#.ĸ•q{Ŗlĸ đĐúÔ9K+Ÿŋ9ÍÃą.•¨ĀBˆŊÛÕgĢĄŗĩ°(p/ F:ô†%wT?Ų™°•š^Ļ,uÕhZŲAŗl3ĄdęeŠyEKš¨ ší¯ŨŸūFŋ2 L&ėF^šd›¯"Éhb¯īęv›BX&°ũHd~ĩfĨŋbą~Ô)XÃgāŅį'ŽĢ+Ŧ a$đŒÕŲāģ‡ UR"įĩčå ėƒ5 9ŧiŽT û}¨k NXļö\Õw3G2}yŠ'–ĩ‡ōŲû<­€´›âé?Ûë:nāŠ8î`îEã‘nŖQųĖūąņŸãöôOûz`ũŊDūۚņûzAũŊ7~Ū¨oO¸~Ũ•ũģoûz ũģ¸č`G# ÜČōlō$!īJW/÷dG˙`Nį˙QĻāå†ÖzO¯ú3ĶÔZŨ|ß7ÍķtûEĶĢčIŌâ5ddë_@žx §á)ˆąŪ7jŦhžuŅ ūR;nwķ¯zÜĩēĐKĪŪF’k]\40šw ’æL*ríÚy+pFĶQÕHŊSđÛÑ%ëŌlÛ3„ -dęģm˛[ÍÍâå]† ŊœÚŧ yšåߗíā–RÛd%˜ņ˙Hø†/ędi˜´daø Ķ7ÅĮü?j#y”æĩ˙c!/ĖsĒđ2÷C’/ĄĖÎĻ_û?‹v–ģ<…O„ĢÚÜį=vR­S›@Ę,¨§>#2GۜœķH!“!õ_[ēžoį<›~>•CŠ+gƒPPM@ )¸oÖD˛yĩ÷(h†~Ųēüž7îŠ59ÜÜäg|—(2˜Pŧúâ˜Ô¸íÃŌ„!č—å&Į:€ū–ÕēåúÅ\ŧ§^•Ė^ŖëŠî8o]˙^m7Z@#V$Ąđ;ļÃÅĶe_˛ÂTå߀Ã:ë”Ō@"â`‰‚Ikúī!ēšŠĢ<}K>žËęûdĨŽėßÉ\>*\Müř%š'Ŋß~Ŗî…IeËb°—ũ?—ĘÅ@p~4eö‡ŦŪÁPūXCĢ˙AUßEŲ{OTí–ŧ1ęãzHš”đ‡ķƒĒ=Ųĸåĩ')›A¯d$ČA%%ĨėĖė5ŗßĀE‡“ŸÜĒ3Eę2.OU•ŦD•Q‘Ü—š€0XM´õ!°¯Ė×ĨąN@ģÔŦ;¯ļXAâėvE+@æ™é/õ2"XĘÔWŧäƒÛ'_SŠĀQ(‰6ĒÃož– âJ‘5 d ¯?89H`,čãc)6f&T‚žCU°ũ{*w=P¤æ gąØm›Ģž-ŽIųŌĖ?e‰8Ж~lŒ‚šåĀhČaķK ´*fęcÅāÄ­'M‰Û}Ē/ŠÁą$wׇÛaž÷++Mô§æšÁ:ĨRę˛ģîŗuå…ąųIœ Ĩ%!ÎÆûSÄĢ՜žˇÉ8ĮļMØ:ŠŋyJŠ øÄs|…/¨ĘŨ¯9ë¯ß ÷Ôg\~&—ą¤8\LéîV5. ̝‘œīôw ‹yæūÄό­æ1˛ˆo8x’CČŽÔæRÄŲ탌ĐYĩÄ/EėŪķŖ+@ Ûž~Ÿ§ž­Ę_VCՊÍ0ÖRĸB4P`+Č&ĨÔ¯ëĒqíuŒoWQSŖ2øOB¨i™Ĩ {ÛxSB"|ŦgaõR⚠`'ĒČč„Íiĸŧ] Ž.—q˜āŽ|jEæ>QĪnIîEƒUcm˙@dŋŌ5 "ŪS0i'Šî4eŸUyŗÅm#Aūvė˙dWíuņ’9Šœí”Ąę'ĀgĻéųŲY¨úįKĨˆoŌŖäF-~ r~ž\ÍBēņ‚WāĸpŦkōöô™Â¸ė4áe7—,°Ön’(qCXøƒ&-đ’R+xÃ4qéœ8…B˜´čdâām?šĢF:ņ:/ø‰Õũ’}đ)@;th/ę9;†ˆO=AøŽ# ʁĪڋŌX´Ų'PyûĶ?^tdDlŲŠ_éÛ¤C"šBjÛC[žīßQ8‹Ö=p—žÖiŨôŒ‘gÎSАÕ,R*ˇW:qĘœûŦ„ŨˇaG㧞}Ũt•z~§Āļ?kū˜’UŪĶž“d-H֛v)×5ž–÷…54† €ŸË”ewb9Ô×Īĸ2ĀØt­ĮŪF‘\/ö_KÔi Ņ'32šīZ¯đН[›˙RōLKR‚öĢĘ&VdlßZŒ‡|„žļäé™viāUß0fC…Fáĸųrė7ék‰cûv=P{mWŲœX`ņáGÁš‘xúÉ2w‚|Ųr)Oa›´ET$`mwûI>ĄsÛ72S^Ú­°´˜ĮP‰pf ôaĐFųšē¤*–ŖAÃOœ'xFžRŗ¯ûa5§EŲî[įDjąUĐĒč_@Dâav¸fõæ˛’7Eģ(ä­Xš#Ö *%å!á ŦĪĀ/GߥÛÛ)͛°B¤É°įT– ô{le$&˜:\cT Ån =<¯ö8"¨— ëĒZ7˙cÅ(Lß÷zĨA+‚ęžž¸Õ5cA—×ČÄ6Đs˕ŠuŽŗ…h>X¤ĢČôL˙^9¨,<~ÃAoæ.âÛ°)Ü502bë¨ˆåhŨZæ‹íK„…7Í˙ĒɒŋQ}JĮ_Ū¤‡šaŸŒč4CGŗ‘LHpísŖ’[Y-į-9Yn$Ī?e–ī#ęZǁ‰Š•Qų<ž Dß4ÕW\:˙ufđC)ōĀĒ _õ^ŗ‰áš‡´Q¯€FĩĩžŪ†’Œ‡k[;“Âį?š€v-ŠpΤų3'‰’ŲÔœö6ZFĩ孀2ĸ8'ä˜÷iö<œ:2, Õîö{4(ũKNie Į§×æ.Ķå—*†pFOšĒˆœämĢĩtĄš†Ļ#˙™>ˆļüīé`JW˙˙zōF5ŊgūX*qëŅW‰×V.īŨØāī€õ_įöæĸ÷;ēâ+3  )­H/‰õž +*;¯Ål;H/ZcøŠ3ķ§ŽƒŅn…˙ ū¨eš?ÅļŊėÁ|gûāūYĻŲ2c˜ŧôučÆ;™Ž–"$ļž–ŋߎŋXsM°Uųô/ĒXd¨Rd7“•B_Ē<ÔMļÖŨķ|”Á)Žą_Šä@jĻSD^4TķPŲĒ]OÔĸW­zĐ2ũ‚3ŸØéäU†‰ųOĒÍje1†)< ™}Ú\$âŧUÛE5-Xh+á6ž™¸3VõŨ[™roŧ$ qá0_ûÍugĩvyŨÄ$x=Ŗ´ņà PMjėbâ3Ž;NŠ–X{˙6‡:UĮc  ÂW KVķ @ éąģá*zî‚ņô*•čT<ŋVSģĨԟâĩĶĪ'CYkˆ€uģËŧĻ ”—,%Ÿé֒kž‡uŋSlqŪYIO[sXŧŸüeH›õĢ*uq*F°Ÿ¯ ÷– ŠŌ‹•ƒ^P͞ Z>^xåéČŒ”fūŦÔj:Įj[ü~`(/=ŅÔ&S´Ü­C"\ĩ(Z3ā–LļoĘwlŧĒS` ›¨ĪJ1ōŪJƝ6Ŗ1Ú8pĐ!+ËIJ­Ã‹hBčéc/ŦČë{m-ûęīú€ä Į<1ĸX Ū¯ąü|ŽŨíeFŧ>)ŽŊĄ&Õâų?ŲæˇėÛ įFC’÷Āxdē?(0B@örĀvF͜\‘™´rčPQđņÅĨ|pj§ \Ό/˙F §ŧĢa:ŌEÅ^VÅyJŠZ— PB D?‡YÅ_}­Ęħ6fÄwÜG€ęŒœ5Su8ÄVĸĘTee%p˜H-ë÷ē`Ū7`> cUZī**ŅÆķ,ožŧ=JX īLPîÍĐđŧА ƌŌ:M? ä“8×8äčČ`Čđ4C˛B~šˇ[yĀČՊ æ\ņ°å"ÎBîáz#ôƒÕD;>´§6ēÅw+ō,9S’‹`ZŊę`׎ĖÖu-ŋđųŅÎmë …xí˛úė›Js"%é*įDdŲ—õį'`aŨ‘ķ¤Ųߞs1OJŸ0; “ގ5Ŗ‡īnIx:he8‹}fŠĶJšĮątųvHØŽiŒ‘ŖfĀtz9 Õu›Ev%/B>Ø×…ōL´Ģöá&8y„ęÎSA ÂŽ…å+/†W”OęÅúÃ-=–HË(ĸ°¨hæUõķfl<Ŗ°¸wr$“Đ,ĶKÖõ°"¤GۓZ+€ū Ņ GvÍ[­¯ŽŧփŽ[ˆ&R0Nâö^æū3äēJŨn'_Čašúgją‹^V,–œžŧžūČVˇ>?ÃB<…_Kcûæ{=~į6ā #Ō1A)šy8dÚ_Ü'×8W=\Ę \Ē-ƒĪŌu"ņŖ7‹ûĩę&PĪ÷p_ĒîžĮ;}#tŖ—EøMcĖ+¸Î†Š¨xĪúI°˛Z4ANēĢCø’ėŽd lwįŠ"Į¨'z`–ØœØ §ÜŌÁšŨ͕ÛÅŗzāO”BâŌ˜ĒÔ˙h]tpŨŋGO‚æŊ%eé>áÖméAKx}3ŽuŠÎĢŋŋ}NŠ`ņ\8ąā€fŪDm%äĘ9fĒ-WĐⓔĩēås2ķiWËūģqÎųô 6Ÿ) h }Ną"€sU#ÎÍŲaĮU7ĄęvķseÂnĩ4ķm}Uü]Ô-lJ+LöÄn’uęcîŲ+n>䙲i €lK2tx ‰ÄVl&ÅöRÖMfä≜öE)4ãÁz–‚áũœâ¸pĀcQŽđŲwa¸y䯀M^'Æ5Yëv/íSåƒBRž™ƒT†G^8€QWfđŠŸe&_]HĮЈ°ÍQÁÂY‚ ą+ 4ĨœT{mđ×ā{Æ Æ\‹ķčœ)ļđÃČÔd?žßš"‘Ú<‘$ĸ›M•˛æėËũŨĢy(9/^ŪŽoé§ÍøS–âĨžˇG€žŧWĀú tûcxŒ5ėY5]æ\Ÿí(S_+̇ŋAëÚ6€[ģŒŋúû]XX5næ×NĨ~Ęvõ‡dJ͝ÅĨ‘r[yÖãŲļ\š†­F~ôuÎī’Ļŋ/ņÕOd~p+n&īŽ€…]G{Ãíqƒƒ”LˇãbS‚æMn4᨜ī@+85agŦ퐐ōH"?Õí#O{šX‚UÆģļøB%&"bü‘œ™ĮVI@Yœhržr!’ã=čĐt˜ŋ$8KÛæ|8ũL3nŊÖÎLŦJÛ b}Ģ‹ˇ3ũi0žœ“ķKu‹Á™ešøĀôĘZ‚ø*lElŊķ5Ę:ŽvI)“ĖÂqfǝ Ē×,sÁģMšX‹Û]šDKŠŒķãÂŅôe›0ČJ pũü%ú˛ķcxø†īô÷Yg/gÁ´Ļ.TSīöFĸÍĒėEtø=—TŠŧî(œÚAŪÆ'4)*^“Š#÷å˛;âlõ*ĀJĻRWf\rœÚúĀ[•5ŠPāīØ×ō5 U ÷ L`ˆˆ˛Y&TlQJÉ÷ŖŨjŋÍ T  ¤šŠ ÅKTäM×fPS‹ęÆ ĖdBŽ… ¸%ފÜčAœō0§ÄŪŗYfĘ k€Ę?ĨšJōŒV”›ÂĶq¸É}?Ŋ@ÕˆĸrøË+(– fa;ŸW˜Î­§ŧŌ^P/ƒ"2 9mU2ÂØ“Ņ ­Zí2Õīy4Ā4‡¯4§>ŲÁVSPÖU0ęŪy9„9”eõ™"˜AšŦĘ *ZÃ'e.Ôv\ûŽ!„p`ytŊ¨îjfĮÄũ`…gūbŠŊ`đ‰ô “-€ŅSņæo*z‰Kú@#  ø,>Ķ%-¤:˜=Bō˛úŗddkŸ“ČBRÖÁ \C† ŊÔāYęōīLęČíįÁœ¨lˆu5ĻÜ#Ņ^ZÆ2ŗR§ŠŌDĮ„vŸuŌ´ôĪû ņ3Û Č‹æ‰!÷03!˙2aoŌoŗãlŪŌŖkPĐ*ū(ņŅŅH%` ŨíZÎX•˜PŠTôQj@+žē™û}‹XĶ6X¸,lBÁĸCXˇÎ—AmMcd˙'ë—ôsqKš9€§ãlėŏÃø+BĘ`eƒ+[%íÅ'6DBõã {ŖX¸Özō߯:đßâĨfŠ`•ČvÍÚ5ũ-°¤Cî™˙qfûƒŧzxķá=é’Īg8eŌ2õc n”`1d_›(ąˇFÁîŖ!y3)€åž3†žsd)îø•uō˜xŨōäc kÂՂGq(ē‡+Ņ_ū´,Nôā›0šĀ_åöŗ˙'đĩ…ĄŸÁŪû]īļëžÛ¯Úoü ˙B˛ūhäs–!ė:ĸ/ül&õI3ŲĖn[æŋ(ƒJĻ‹}Q>įÉHČK 6FŽŸxĖËP•ˇwR‰`ƒÄĄ@ôv Šâ]4‡ø ĨAˇˆ\'üŧ—AŪ¤ãĪōxŦĨF ~†ī.˙xĨãŧē'Q Ûz™F÷UB"gUßõŒQdéŦ‘kĶŨŒ0üy*”Æ˙/ŪfŦ—ä/×šŌƒ?ŨŽõÖ[úšB 0-–­Ũ$ĮXœē2÷i“Ā?ÉBxerŊŸ÷• gĩúÆė/ÕOۅ|lĒė™lūėOē WÆîW-G]ԕWƒa°Øl6 9¤%˜I&uû-ˆpœ:mÜ Ī(ŗlĄņ_HTÎ05ŅŖ<‰7Ŋúd:gDĻH´]¤[œŊ3Öū īŋi˙ō30Ũœ…†ŗ,5Ģ,Q‚ŋôŪŪ´ŠėųÎΞhЁā!úSBâ°Ä3¤9nŌQ€ß§TöĢ8ÃqĘgÆQ7ú”Üũ7Āujbø˙1kĪUsŌĪę&Üâ.ŠŊ,˛€ųaē ÔĸŌŖ÷s“O˜Ļæ=301í8•fËI°_ī#5˙萏ĢzėÃK1_ß$Ļ—^2ÁáJÃ=ø á}eD×ā´8é°QåΤ6dņ‡˜Z>|H5ųûBîŒww˙ĐtmtŋŒô`‡čm“‰˜“Ŋ˛áÃ#Ô*C¯I%ŠĀzįĪÆ&!_ôčŧĄ+]+@•EšBįjYx§šī'€‹ėÃÃä6Áey†Ã°sĪpę :ĐÛtޝ}ÄI85(XžŽˆw'Ā8 īē;QČæx¯`2ņČ÷vÆíOc­kCā8t-bRdÍm ã+0ĸc–ųKdāõdŧ6x ķŸb ÜŊö¸įŽE﨔ļ36 ØÂ† ¯ŧnž™×îËGLJ@›´Ų;—*˘&'†HÉž•‡jĒ´ŲTâ,Ā´] ¨vqģč Zž9VŊPë7o˛ˆwnˇņöŗ÷"ĀX0ŨÄØÖ Ø°4;cpūƒ7ėXŠQÂkô($ŖÆĩXmŪ¤vģԆŌ8ŽäCŪŽNä͛.E¸ē‚ŨFĶ^Đ †‡€bûį¤VķTņIŊŋôjOÎŽtíÎäõ?,´¨pW!ƒ:oâŋÚmäô>^ (†ŸV3omJ0kˀ˙$tŪĶĐĢō\)’Ĩ€ÁÃHÕö…j_?!„‘Ü`7ŪņSŦN3ûLɎø@/ŠYŠSĮ3R[ tš‚ĐōMė֋ äž§§Eoi .X‘ÃÔŲ(ũVŧ t`=‘‰ ā…M'ßUãûooöß×ÕwßVöOŸģŗįīŊõo7íÖHŌz čÕ¨QōhËę¸UO>M ŋU›ú¨úŠ˙jˇŋVŧÞ¨Cž¨ū­díbɊaŠÃ+“+Eļ\SaģaÕû^quŖˆ+~ūHo?VŸßD€§¸Ÿ‘_ē °Åˇšΐï’úAŦÛ~ˆ& ŧíUđLŸ°?ÕYVéÜqDiŋn=§Í@QfšˆiÆû\č8¸^—ŪI4ĩҜ;qT ģL#N°üU%XųÅęŠÅYŽ"ÚŊV ôFķĒ…§gŦ¤‹boŗ™I8 đNŧfQÛ+Xtä“Îi›aÄ<ڈg˙4'\pļ`Ņ}ĨĒā,'Ër˙ķ'8eŖ\—üÂŪ/>˙u*ƒΘâü6HQÛ~\(+ņ­­]œÃõJiR°—(žöƝ¸N…ƒ_÷B ̜Րqa8|%Á@›Í:epHÄŗđĮÍ|8ÍIJ$œ O N Wį éņ|íiIĀ:˙đØ|Ņmæ1;;ËŊãW*Á|¤ÛŪgŗ´ĻúÚ¨{ Ļ)ž4Õ#¤{įķÅĻė´yâ lôÜč^ãø’ÆÛđß*(ļ„@Kˆƒ*ÍnReë+Oeü:ˇUÆÁ+9ŧ„‘< Y~˙™đh_'hĶ"*m  7ŧčpV”5ë!ŖŠbŽ Ī•÷ļh%ļH(ãŽQŃĨÔwŽKN\BfļēēT'­*zøyĨŌĢä‡Â™ąQt˜‘„ŲšGŽŊ´‚ Zė -öCV\\‹ÅÄ'9¯˛I߆6 Ÿ—€¸nZĖĻ@ŒÃ.:>ôzĸēƒģŧ§lGĮ˜åī‰qE˙.t$œ4yĄ'SŲe(~œ˜Öåf0ŠtąaDēĩ‚Mžŧ9Īt¨âēBÄl$2ĩO@Ø*BöŲj•`ûŗ.žŠ?ņ]A;Ląĩđ>ôģTõĐpËØ sÄÉJËÆ”vĐÖŗĸ ¤Z.ˆ]r^Ļ}`Ĩ>0ĸŌ2Ûqūã'*Sü#lvĐ ū#@æÄ^\h헒jW5čJtãëļN˙ũŠYã0˙rœ1]PYH b=W ’¸$@—ö5cXÕÎ@x9ĸlÍtڝã+A‡ŗ6dėâeņÔX\gV¤‡Âd$§›“ŦëfĖ0šo* Æ ƒ{ĪICũĪu|F‚īg*'IÔˇ´ ɐpŗö’ŽJ¸lpčájԇ„C'B™Ē¤JŽī Á\;ŪÉŪŽ 7°ø›č4ĻĻí™ĢŦ˜Fh}aŊ_0•W€—uvSā‚;?å'Ās”c)„">Ąh˛–WõÉcCĄˇ˙7š™b˜įŪ{y“yŪÆFAT—nĢ—øÉûāmŲ"ŽØ7ņļęFĪļQĻĪ$ķÁ„?5uėŧ ¨a—aØ´¯‚T1íËą­6nFņ áŲß‚k"›öĖy9žeŦ˜Z•*Iēā¸J`Ø]˙+Tf§ąŋ5K’;Úw vlčĩŌųąöęõwcĒöP’ĸHZV æWø?åKŸŅŊEA<@)â\D+ ´N{…dŊXÚІĘc0š[Nįĩ J°đČęąįô§ã/ŧéĮ8;ō;Ćü ĀЍƒø°Ü?ø’häŊDŽ5#…CĩíɌĪ)īx¯¸Ŧ0¯ų÷ĻÁyĶbZĻ ’:3‘|>1‚sņ  ĒjÚS,ƒsŦž‹9@<ī1ãžĻž8,UaOÎŊ¤Göī)™y||ũÜ:–_ŌaīZāvT`ĀĄ“§Ĩ<ãbZŸ™¨Bj~Ĩ¯Če‰ŅV>ĪnĐ?ĻCÕâĪú/U˜ ãˆ$ÎŲIN?k)RæđڗRcfօāCLÚÂdˆÄ"ĖWũMß4>G< áŌŖ×qøķiYųÅwīĸō AŖ`ôɃۤžAŧŒ‚ÜVx ÜVÁ“œ¯šãëU_=B!Ų/ø öá fN dsäÍĩĪ1‹4ž? ˛k[Ô.Ú\ą€| ]ū<ę°ÚESŖGž‡B°e—¯JtäGj4“r{ū~Lũë\‹Ņ…PN –hväĐ ģ„°1đ˛M R;M˜ę€ds˙xĩŒŖ§ƒ€^īœÖĸ35‹sŲ^5ꄝûĀnާ`X2ˇËHR#qŒ;`ë˙ĩŅú&ûŽN|ob„0ėÕL1˜ c03äAƒ—V‹ŋGâĸBŽVîj'lÛC‚€›*‚I- t›ĄB ^ j˜į#8‡VŠ5ÃŗëŦjÁ‹ø˜ĄYÍĘų”Áž1Ö˙ÛzøŪųc5Ö5­īP_a°NyÉ×N#…Yäé|žäÃc5TÁųx5)Ÿ@}ÎQpOG´Q™Œ´0qáÕû%ûF¨C†v3AŽ— %ÖkQũåÛ ‡Ęə~lƒŠ×d>,°Qm |8€H¤‚Ž“ŅQmeŋ2’v}šÆ0čŧG ZōÃÃĨ;Ãė!ežv H܁ŧf“ĻI8 ŧ]fę˙„úFáāi:ŗč÷#8*­+đņ_\ÛVßa7˜Ÿ ŽjzdaÔĶ”ņ@ J|¸6,ĸ ĐÎîÕũôöĖ4įXKaz<ń$ĮeH˜b¯¤ÉĪũo} ˙"bä’mšŸŪB§ΘŽ|Ī Ģ>Õ)ÜxZam¨Hŋø¸ŧ7,‡˛āf.>L9ô(Īô&°Ŋ…ø_…ø_ˆAä‰;c>{ã’_ɖĻŨkŒ2ũŽâpēšØZņãnœ_ڂu u7Ԕ/"…ĖäĄE=í摯YútŧÍ›°@á5xDvÛrēPSŋ8Ÿâ‹@y„˙#7ũ¸7O\ė!ô6ÆĖĩI{ƒüīzŪkČÄžâ9ÜČ ]H9FS’îķ¤ĮŖÖųžo›æųžo›æųžo›įyÔxĨå×$ŌŌŽĀŌëžt˜VŨSäiŽveãoYOáŠŪ0ĐĄ>;îĻ+„ū[ÚX1°¯´‹†ØvËpE.Ô Ų4ÅûĨ"Ø{„F{ņ…@äoĮ÷<ũۀūĩ`%.5zrWõŨôęÃYGĀŲ~^į‹šĖV’ TŖ¨3@m ‹S2…W)Ą¨ņŠ]9ÖčOĮÚ8†zf–Ū˜5‹š‹K*ĩËhúw†čdÖq…@…čå­ĶŌZ›öogû…(.“9b'K9”Øt‹2AŊfDeUũ@6ā.+ RĒ0ˇŧ|u4ÃĘöfY;ĻãöYŸ.—‡t8ãŲoļ™˜ļ‹š `°ŸŒ C}æSž_1O˙zš$ ōZŧTUíÅ'/ØįŪĩį}øŲEE&•Õé˙Hōŗ(é"Y‰ûǧQŊ˜-‚ ˜˙GË1jƒŧ[_ĸ>˛EĀhH˛sĐ]ZšaūÖ^"u§ŌXxUųīd‘9<+rģŸ&vž^R2m˜ôzÎL% ¤k>—üNöP‹(*™}ķūÆû˙D+Dv•^-\ž¤ā•$€ŋēõûŊĨĸHŲŌĸZFЁ[Ë8į~  uü{zũnĸKī÷žC†ĩ熄ŒŅ"FÁXŖŸ{ŠrVb{}Âlīy—ŦIß.r'ģÍāhnI…[21‚é“V]>Y¤ŽŌžÃ2Ü8 îšv†eLHģĢúo‹0āÉ2÷žČĶh¯gd*ŖU†¸a¨]<õįƒëÚéÉŎŖ‚Ú&ė Sžķ7FÍđ;Ũ¯˙4ŲUTXjušÛ30!uæBEušJÜãâ͸˒ÔŨÔ}ÁKKåōčzeiJ—åAvŒÉ’"vŠÆ&šY^ŧ˛Ŧ9úĒņú_4˜¨GÚ\ 3wĩūe­[H^S‹;)7.A…Wßī˜^ŠļÍķ¸dSR‘ŗ/ĩUŗé¯Ŗ’õLŒ–ňJuŖŅŽ\ĐL4lŸPDŋPį˜ŧ 잀Š ņŅë^w6‰ ĄēT„nĻy äú–ĘMÚ9ņqP^W¯øÍô‚4„šÆĢSčpDž@ŋEâ'a“⎴Ĩ¯§˙nŖGkXdųÍWx‚ĪGéEō×§­¯#. ŧ÷žî=č?QH¨ĪŽãvxûâ聰}€%î‡c VێũÜũ:¸Ô{ŨëŠ=Đȓ¯(à E`–ĀsŊ@%;NōŠŠpq+JÛ¸Xm@Vy–ÂémMl¸TíÛb6ĻÎP)—ø´˙F\ī—ĻÂĪ儨§Nʒ ę”ĘÚ#7 m-–ƒô´ŋVJ×]*Fā`tĮ˛9Ũ=!tZ~+Û(îÜĢęg\ø7 Ŧ„ûáŋĻW@?ė îĶ``‡@%ŋāT(ÛjĮxÖ`›~žģ6žÅļ?ę°īoØåo7Į“Ãh]ēœsĄÆ S›Å|Žá?wyāĢĪęSá-ŗHxėå+ķüdM>”˜(^ îmī’C1Š1õ>Ķ„Â5= BötÍnHmËä=fíV$ö—¨ŪÅFú] a->H/ū؇ ÔMFČ퀭;ÄâįâP†evčŠw•Ũ‰ÁÜŦtŽî7úė ģÎ3Đ@°Â\đģØņB7OĩõĻ|zēõæČdnߊļXŽmžÖĩg„΍ĩšM&ZōawĄ÷Ŗ˜úPQr÷åāˇjzw7D’ķs؁é(đė -i­\Ôü ÚLŸd†ąˇ@\ø–( Ųo;\ěŌ(ņîõÎ7ÚŅV$á÷´i÷ą.GŽ×ô˯javņ4ĘĨxęIBš;Ū–G’ˆb Ŗ^œâĐ"˧‹í¸dÖę8ZāŌ€“¯ūė<ĶNÃŗO!˙PĶTŽ€\ĮĄ/”# s×ŧŊM&ã/ šøCDŪ‘ŗI”d´Ž€ĩá5ĘP˛/7°Y"pf’@,•SņšoĻuīøÎ2Ŗ´'9d`Ū9Žą÷ͤhž5‘m!>ĻF•ˇ¯ÕŖ=ŪqĢ T ZŋB)XãRl'˜$—ūNEĨJî(‚!H†–ÚĄ9ŧ†ČÎ{Œ1íį´ëĩ‡2V9Õ:NG.!šČφ"nScsĻQ>Ē1gŸ%b„™12¤"3€.goU ÷šičP]Tøéæ2(80,Ĩd‹OZqGd†X­° ‘÷}pį‡Q­‡ÕX€ã/‚MäúTRÛŠ‡î_‘ĩčĮHúgäDö—~tÚË˙–9Ŧ՘Ėō‘˛ {pAoJY”0Wģ—îpÖWž”“iāÔës›tūDu ÂeJ _N‹UĪ™ÕĮ&ÆåWĄäaēĻÛE~š„'–USüūå읛ˇĨ÷˙h1N0ʐŋįÁHNö …đYšxΞĢĸ:$ˆ¤ŋėŨ9ø$ ø›ĸœ$ šy‹r'_%8{ÉōĒ/ŧ[&(ĻŨĪz™‹ēܑÂ7Ŧ$%ÃÕĀ ˇÖJ0”¯(cIģ’›˜pWK°ĄkøAã5öÎ;!´W!Ë%" ĀiŠ/‹ŗpmÅâ%ŒYA™Į2Ã>ŅëAÉŠ¸ ŌaÔUwZiԋI–sëXrAFĄĶôQgŽp_ķ ģ…@ įgHāCÍFۆ&Q=1põ”û+˜Áza˛øŠĀ˛Q˙cEWüF<÷›âĞŪ5ôGezČøޝ؛rąYį.‘ÅâC—ƝGC-'ČĐ ë0˛$!Ĩ[ļžįéV Nž‹$ŋ+a3åęLĒām6UäŖYw HšjÅQЉģ9–|^Ї4Ī 4ū×'ųōx{ƒãqÍÅ ‰3âĸŲ;ŗT(ŋq~jŨL“î/• Tbgv‹ņK<āĒÆŊ*ÚeM9é;_X ˙!y8kēKdųhhđ–ņaĢo XŨ¤N4úØŠāؐ!ÚĢ&Q¸†üfÚ6 čÚ˛OŲd2ėž:¨@bįųÁŨđŸAs4H{įŗĮ#°đÖõĢĨîėÁętë((åzŅMΆ!öđ×pĀąĘ—BMĀ،ķđŊbÉ5˙XI¯Åå>aĶQCŗē5Œ2ž•öįU’ õŨ×ēˆu?9ģœVPذjj…[ZWŖnŨ - !{žČŅNF+mĒdoøĢÄáBÜöōX;`”ĻĀ0~œžáR"åXŠŒ ĮžČđŖR†ÄüŊ[ëĶR3Vžė0ÁŗHdņ¸ņŒ:ŠƒގF(ŗcJÚæũņqũRyŦV:üŋb_ĘcGū ĐęŸKékhÃˇâo˛ŋŽåj™CÎixÁɚw—Čײ$vJ=ET#ŠN1…‰ĶéyˆFĻ—sdÖo0Ũp­€Š˙sĸUyÃ7ė5}$¤ Y‰„TĪ­i´’%bˇÅxC#¤ÁūĨPuÄĒUVÃJ_đSČ2s‡ Č&HZRBxBšöxáUí)÷qā‘Y_ū¨u?dĪõ }û|˜Ãø0@PŌS*¨ķ₤2§WhoeûN›ØB1ĻĀ&HM?*†2Ķôčǧ˙nqÆ|P&üLđÍДOđ5Áėˆ3GHl‘}5!ÆbŠCzAtŗéäf€Â;äÎWrLč!EÕYwvrŽ)ō–q—w]ÁQw~Ž‹d­Qî"Y3Wi2NI“Ņq’K8Јļ>îhHÂ5‡ØúÁ!/ųĩD§z_?øĖ´ą´ÕLîŪÎõ`5zīF¨Ÿbō}D*Gwķîß~˜‚ĮëbAhĐČ+˛dÚfPp‚Ũ‰Ûũ}ÛZ¸× Ōį¸Õŋ"o'L'ˇŊK„@WŌĘī/`g˔ĒG|Ļ÷)ĸ@Ļāl7ėõŧaŸ7qö*ˆßøŽtWWņ9co’zQßŨ¨@Ũ*ß€†2ßōĘU¯šÆéĩ˜ %§0ŗc\˜Má7ĶĖbEƒ‡ ×+/ģ‚=iîŸkĮ×fŅHMĪÃŗ-n˛˙kˇ´´üjN§öŒ§Ÿ‚Îj\Iˆc†ÉŌ^^´ÅˇÎčŸ<és@VoÃŨ˜|šũ…Ã8XäWŊīŠ Íūēqsš'+ŋæŽĪąëߎËč7=aÁၕæ+ĨaĨv%Đ1/=ļéŌ`Ãp`H8|ĮE-qZ9ÅÃ!ÁæAāô{¸ë´–‚Cö6ōßå8~áÔ3‡Í'’ŅāzÕ˙(Wœ6Pâ×.ôĒyiÍé ī‘úŒ “tR Ër"0ákāB?sŌ~äŧ“M4ī›Ėú‡Ĩ°°Ŧ:ŅîCyëđĪüÁĸüķžéjŸūq­ļų¤ÕWņQFØO Rģ˟˛ŧøĢ­Mg+ڐ7^å 5Qg!\ļE.âF‰=ģ‘#ą*ęu&…Í“Ö@†z9+öŠūT<Åūãā]īƒ^ũ@ŖëfnēÜ;$xDMËa2˛1ˇėŋp‚Äģ¯į!˜ÔsËqdhJÕå3{šŽą­G@OČ‚û(8.(÷ŽĪĐ<Wšūƒxj aÉĸJ!JĘVž‰øúŒĖtŲ|.  3 e8°XĶ.āÍšĨôA  ,dŠ¸ÆŽŊ€fÚ>€_j鋸{⁈dÖ|ŗd#ĐŋEˇL\Ô&ģŒ]œ2|¤í¸ŽxONtŖ6ĸk°%>YŊĶŧ‹úrÆæ)m*ĸR¯^c—¯ æf€(šũÆīI!īk :}|JQ+G˛r6ŧ]U¨W,ŽäAet+īåĀC'ŪodĄ‹ĸ1Eaė¨sņΧ ō€ŠĄõĨb߯éĖ7ÚĘ]ˆLËöũ"’:ßĘŪlmn?/­Į‰•Oá›ĸTø‚ãů3/°-ģl„ ÚĀÜE'Š (NX {†–T'ėąé†3‡ÄȊĢHhĸˇyČÛUĢ^[ \ŌO_Ŋp=œų@ī˜<Ú¸ųË Đũ÷? į;¯úXK\Ŗ…Œļlv‡S(žpiŪÔ1:82 u¨“jô”įb1lĐAlÕ§9øšSioc§įÛW†ÚLaQVšj{ÜäWQdyaJ"›`,šVgä/Ę͇{ŋŽ4 U‹š”Ũ°íCģB˙"ĶĢé…Ižë:ą.•uēˉīJ'­:5Ų>ŠëŸš<Ã*x X3 JŽm˛e’•ßąo,¨H›¯Ļõ’é oĸJ¤dŊŸõ\ûöī"¸ˇč…˙0C­M×[&Vĩh°¨ōųΔĩ×Íîcu„gĮĘD"Æ ŗÃüe­ŠMæ1 ūRVyf(ĮcrgÉ X;wœw*˙iķu'Ha~J,÷”ŧ( ‹%vxŅ`›’×ūŲG.VÜ< ASÕ];"+õŲ|k›œx×Kwč€ré CW0Ā_&Î>ŦˇėĨ+č€đ] ËęŦiŦo(?2™ų­eā‚ČLšĢ´Š&ŖļÃÚÄØ˙e–O˙øš˜ 2SĸY“ $%žŊ.ÜXe_ć2øj\0Žú܂åášomoa…‚ü:ĩŲBŽrņēâāĻŧ‡ČŌMzæ—pm6ģ_ķ1žŌœA6ÅējΈ7ę ™{úĄ—jÛ44liIH~Ž)š ķpŨ)Ņ[æ’Ļų‚€ˇöŲdSÍŽš§Ūą!+Jŋ…Õũ´dô”ž[˙}bˆãâC¤á[† ô‰î‚HX ļp*ŦrģŗFëÆëIØfJNôÆ_Â3r Ûry#"­0aēVZģŊ­2ˇ%uxcuælhŒbžÖ˜°!Ęŋüđ´<‘ž‡-|˒@ˇ.ŪÜQØT"õĮč֍V^É ôĀö‘ŒˆŽ$<Ė__Ä'ųu ķÛÃT “‹å+ÄĶāåTĐÂh!LpvĀÖpöڗÎ>ÚĢōrÍ;Õ{ÖljŸėŖąŸúpW‚'¸•GŽbžĄ[U\gē}œˆOC•ø.áØdaãĨG4Û*]G›{˙ZКl$p>øE^B&&r'ųb5ô’l-ē†Ú Ą+û0ųäõB‚¨Ģ‡jã°GîĢ•“JtIšIØĐ’ ‘|ķ™˙T ģärMbcų'aŲŖ2É˙- Ė`¸M‚,ˇ7Yŧ¨—2ē¨!raQ=EšīŖŖī&gļoÉÖTyúōW“ZXüŒ3ņō“¤÷FiǝĄˆ_aC@8bIėy Œú͈>Ym*›lĻJZņ†]ŋÉĐuF;üķŦ‹ū¨įÚŖ GsĒŨ›ßhKæé?räÕx…!dbčļ Wvęaíâä)ÂR§4Ķ‘ŸÅ†Š-B`ČYēān­Ü…ū‚p÷ā˙#e"í  ‘iØUēRLæØ`Ct|—ô}9?>žÚļeđųDR3r›WÅﰜ™ŌõhTØ 0˙Û_0ķ‚#L,Y,‹ĘÛŲkI ŠÎÄéû/0TŌYgÛŪĐá:™)Æ‹ˆG }SēĪRÛį”(h´ĪÎâdâ+­'&ÛšIũė{yĖRŌČŌ.Ĩ1č–ÔŲ  HĖI .Ķ”äđܒl ßzxûĖ%` í ē÷ü\Û;OÞI$CË_ž0:OÎđGBĐčøO¯ņÍykdŅk›ÂØm‘q>ðũƒöÎŊ‹“Ņņ[ĄÄô-w€Fƒzž•‹Ų ôÕā}Ė”GųvļSMĐÕ8÷ øzĪ]EŲ‰ÖTĮÖēV⚹8څßZædĖWīė4gíSvŦl;Ļdā–eÅXŊ1,79oŊ}Į ĄßkxVų‚c酂6û4Gs×ErLžũ(pFcŲ(΍ŌžĶu9 .mZÅcÖ°‰a°Æ?ƒŠŖ{jiyļĐķįę†ķXįšfbś˛„uf$‘y ‚=4•1ķģĖH§*ąō4ÉU(oܧĒÖ8ˆĩōÛ V‘R}ŧ‚Ô¸ŊŋŽ"6zŠEkĮxŽ ā1ø6!č•2ũQ(€¸Í jąI“yŅŅv¯ōb–KūĪāŌmń6ēWHß—ÆŖ´ÍâHĩxŨFÎc:â­Õë‚đųŠÅ’{ŋws˙O'­ dwĻáíheô ŦüĢ…‘ŗUĶj8Ā™„¨’ēöéG/;Ĩ4nÄ@Ų`Č[áųÉôߓĪ^īėēNJ–€x§™Ŧ ˙}}0 B+’q…į2đVÍČđ&AĻļ{•1oŦˇŽ…=we/&Yē˙AlÎz"@Ëf|úŗą]ĻĐaŠŋ!íl#Ž k„øĨ'įƒģW,Z+¤äf&LöT4\‡ČˆŒŽâ˛Ģę÷0­ķōmŽ2íîøNŒŠŽ`jšQŽÍ6[yč,ã‹hTf‰ĸL‹ŠIŅCiŲuŖ<¨?kÁJĄDáŦ+^¸îX”Î̏ģE!īõeđŠŪę}Ũeq1ĄlTŸ6 n zshŖŧeŒ’œŅĖnķ<ÅՊĶJčpĨ9é|5ú–^/Á–_˙ā#ēoQá&ŧ\ƒūnIfâ(ļŧ'ĨÖ8ˇ nÍXŪRĮ‹ÄAÍæĩm+ßáHŋ*5B•Č´ƒth`œŲŋéq‡)Āú-Ø>Āûp¸Û‡#(‹„áˆi ›ƒš)ĸāCÎÍĒéž×œu¨ŅOØĩˆŊ2Kyl$'Ocę÷–Äֆ3]a?2>Tk%p”FXĀn~˙Ķ”ˆDōŦ’øâãũMžbĐ1‡gŽ)XęޒŅ÷ãÂ1ŖJö`Ņ8ūĒÔį›ôĮʕƒO§˙…`˙7 tme­ũõ IÕē@ą'Íŧ†gÂŌvÛWŠaķ6%zĒöŌ0HĻ#øtŦBPĮÍâ0ÕœžãäDfô(ŽíēŊ,!%#Ęęožxí(œ4˜Ž-÷Ō‰ û 9ĮÎ=Š–JĨōŋ†Š"īņåB†¤ úvëM}E}š)D“,3EU$žī!VÕ(~ß<ŗW'Y ÚZ\ 3‡ĄVŠ.¤īæŽ%J­p“t/}ũŋ—˛d$oJzŧФ9jõÁˇūCĘÕ¨†XÄi°ŧV”3ÉņąÆĐėžL/oØ@1ięĪ Sô‡îzD#ŖÃ­Ŧg'ÍŊ\ÅE‘|+1đ¤0§ž'§BŽČe$…Ęŧ% ļåč jū›&ÖXĸx[į¸ŧPéß$U'ˇv˜…$û-ÎŽ ^ų„¯÷Ė.ÖR‰0ˆ >"ī/Ō%,šŠ×jXpm4Âv;'vpÂâ˛üÔ:÷ëXFH›¨ŪgmgLĘQKRĩÄé:į’ĘũŪBhû;k_ûˆö˜ä–ôę°O X@ZōaĪîåÉ u- Lœ62ŧS\×āíƒ ,ōíwQŽrMVã8GpŪÚĻÍ }šÃVaé{Ėv’Tč]\÷Š ŠēēeŽu LzĻ6ļŨģ8]S˙`ģņ§gĻW "qŗÜɲŗņĩĘô'ڀ„ŋxœ€ûédĘŖŠĄŖ™ĪļY¨V–žķ‰ģįL’Ä~†BŠ(ßu…#ŨAüØ?¯tŅ—îĐĒōŌŋu¨ ~“:S–XAĢk`ų.’æú!ë,ˇ}%qšiÛ/æ8Ģ…ŽãĶ‹ŊŪâxh†ûĨ?Ā]ōīÂŌ.8 Ĩ"MKnĢņĶũ[ôŽķē$4fŦ 5œĪW žÛĪ”ĸ!‚Äh[Y_{ŲhÆ U)+î6õ^åũH.s0íj[=ÃŨi–Ė;æ G™ -ōūĪ‚ŦėTÎsĘ(Í3Ņ×-,Lö°é†V{%‹Ŋ'ĮįFŠ2œBđr:…ĸrËj§}ÆQßM“¨2ž¸\đmd—=é­y—MPúÔ \ô!ÄãĐĖԘú€Ë ŦcMŒ¯l'¸¯ĸĐ\K×iÁTÉ;ͤ#DO÷ŋlûų $XîŌ1 ŦĪÔRÍø°ßũj|\Ô–Kå2wNUõ‡¯oŗâV‚œĘ‘ŽĒ‘6#YNã–(¸DĘĖR 6"tYí†Ūŗŗ|b{§Å ŗŒ¯U¯|\!剗€<ŨÂŨˆ ŲÎ:Ëļ-FĘ6@C™ĩįøžXŲŧSÔJíL•kuꚩ=PĐŦŽÁiYčk%HŨØą\ē’úČTōūc>R?B„ŧÔyHQövJ‚ex—°&h˛KŦ0Õ-õn+XfVh‡¤¯Û°t :Đėîh‡˜ôŨHW‹)åˎ9@5Ęˆã‘ 7=×WėĨwövS¸ĩŨ9zü™uŦ*ąģ}oū37ŋ×ŗ•ˇ Š'@ę@˛Ŧ@Uú6đōŖ[ņ›?Ņã"˜’Đ)Ë ^^šĒČėŊ‡JlHzy؆úŽŦ<Î˙snļØŌõK.‚$WŊNžįŪAŽģ‚oˇB¸÷Nq ĢŊŪWäŪZč’G*ĨŊôŪ€V9p)롃¨d)ÅÃ9RąR3†nĪ_XTH;ũ<ķÕ˛ÚīķdD´Črû/?Kŋ<ÆģŠcd|äiž8ØųūŲ†~ØņL9Žc’r˜ŦuPTÉL‡ÆÃÄā Ž?;ÉK`âNõX•.nœ•fēŠ‹Â:ę˛å÷%3ãĶo> ψqüM˜OöQifüpoB&[Ö4‚'8ͰĀDzT˜ͨ¨ :ûÂöv؃’Ë‚<*Ū 9Ž÷äĨ+I -Ō'[Sžĸu‰üļ-*@ŋ•T‡ūą.LJčx…w1ÚN”ĐĪ¯‡ŨĨ5āŅ9„ËŽØ ^ŽhüĐsŸjPĐ]W–ˇKŗK1œ=Ŋ%hwšoŽrÃxÕ M–ĸ]fjnŌÉZø_ŖOm÷û.m–Ą–đŽ=ÂĻÜ|›Āųc\‘ŲÎKZ×(Ų‡Ü'ךrÎ÷Š'ĪŨ’~1cwŽÅÁq?5 ŠÆđĶ2‰d’qH\fėĒ—”PIŠYDĻlČčŪj¤Ģå°ád~œ˛QhÁ=‘œ6ˇæqߕ†ą.bI͜?¸Ŧ*ĢĀÅĐPt˛Ā ãE ’ŖęIÅę`øíĒŨ)Ū˜ø+zĒŦpāθ¨ļDy˛ŌŅĢîB%Q˙]¯Y{ĩĘÉ.×LĪ„r‰+Rˆ)ĨhR@ąîM‹>íЧö s°{ōâ…SŨšŠô‘R?{†ĻĢÄs–Ÿ|ŗ ˇâbđܒŒrŊ߲°Ģ[ÍâˆũĒ.T\Iĸޝ1ÅüĸęErö‘nÎ`üųØŊwįFæ‚1ļ3ąÕ8dPž4h8›aú1­ßIĖ%RrÆôtøƒĪā;ƒA%ƒˆ-ŰXYãzÚø°¯žĐŠTø ØŠ¯’ĸ÷x^ŒŋY'r>Õä@—‹˜ų…ŦÎÎū^™Í§ŨĘāļÃbûĨė&3aĀB˙ÉÕöšˇËbĄÛōC Ŧ¯zԒl8ė›ˆ:Ėcü°{öËŖ)^īx—ܝ"Í;äŦē?  _ČØīļâ#é!:Čę×@žF4PߓYS7•k7‹šíõgi%Q¨‹q§âžöáo|‡ë­!.9ŋW:X§Ļ§E¸ŋ>Ũ†4Ęa‹ЁZ÷,ØV´/~ë~+čÕãIpŅüËEšœu,AÃG ”bHQwJĶ‹´!%ņûĢkĨąčYQŊ ÖpiÕ!m*$6ä)%ÚĪũ?˜-îđåĮŽŠŨGp}Ą-mídˇÜPšē_̤ķtWîĖnžü Æ|Ķ|b_ü C&Ķķ͌"ڕ/Ã!üi)ÔēNŠ ą÷׹åđ:͇öÕQ•ĚJ„nÂŦ[KĤkˆˆ€ũ$6’ō>›ĢëĘ3˙ œĖGô? üōú ØeX}Ÿ5ø@Ú˙đIHĘī(jČæŧØLIļ"ŽÉ¨ž‚ŊP`<ģ•Ņ\˜dĩ}zšĀT'2Ô%Ԙä˜×÷č¤xIœ]ޜ—9eC|Ēķ JsÎbũã ūė<9¤ūŲą3›ŽUÕ¨Ũa¤9§Sî+drĐ#â>1œ˙s;Z^ÉĀ+hĒņ Ōūˍ„ēwi&xáÚĐG1XDŸÜķč_`° m SŸk.€˜;Q=cî3ĩÅ‘_1Ûō*ŋ†ßöŧVFãāŊŲ#}ÅŽ3ÍVI{ēōžÛ\?íĮ–t!˜Ũ˜‹Ž!›ŗė3iŠŖ:īRWG}:š žžĐŅ;ŋđíƒÂYLk˛˛="e ]C¸ˇgĶwíécœÁī&”\3w"Q•MëW‡ĨÂĨ"{…Œ9t›ĸÕĖsŨ‹/ŅpĒĩ†Ęŋ ęąÕō ĘûŨF†M ŒJŗ_˙ŲD“ōߝoEõđĒzäā¤(SÔ\sæÁ=aĢ€$ĩIæŠ#’Iœ[g&Öũ1Ų{ņVķbST”Vƒ^ķ>ętĸ]Ÿ"1 gą<ŗ5¤ŋŠ }Ņš؂zN{!|”§”„IΟö7ęoēW°ŧžn4¨ˇ”ü,ŠÚgĂĐûAÉÆĸî—oĢķ"Æ J´Æ9ŸĨ.=8OžŖĩ‡IˇA/Ļé˛FCŊĩšŽözâ ŠGžáY”$S´${0Vΰ‰Kœü‘Ąä@Œl‚UĢŽĩ é8žl™ĻSģ&ģLõīA3y§)íw(pÖm˙ō‘IŨfåĄŅåŦ‘l<™GšĐŽō¤;nûێŊÚ"afÅДH L‰S üœĖŧVÜZ/\FW ņΟ>Q@hšŸ Į´ä?ŗüWõæg¤¨æK4ŽMų5Į>bÔHō ŌpH¯Ņ„ŪCyõßķü7z!Ũ×õmúĩŠ}VWęŊŋVÄįęÜŋÕĄ[öܗÕģŋĢgcáõbĢWÕ˛\_Ģr˙+J÷ęŅoÕĩwĪļoŸxū­0ũWW}WįíПöߗÕxßVûΟģŗįīõõooíÕøŌZ čÕŠPøzf|ú"ųõŊķęú­›įŌ›õR˙Uo~­†‡}H‡}PũZÚíbɊaŠÃ+“+Eļ\SaģaÕû^quŖˆ+~ūJÅs̘°3ypÂÁކqš0ÜËąÔÁį(a9ڏãžԚs§į#ę.֚2(j‚}BéîįŪy'™^$kāĩöšĐ&qpÇßëŋœgæmđ#4,A8Ŧ(ÄëßzÔ;ÁLĮ÷örŧģ(ĐëĮ6ĩÁwUȉ’X0X5î40y}ę HEWģ‹w[b/rĻhBtĮa’˙ÆĒ5DļĄ¸ˇz0˛Ĩšt .ßzÉĻŅuŊvCKõ4ŧbgqđBÄËŨ4¤U’76Á@6ˇŨí– æ_­8ĒģŲ°Ģ*ƔžĒŦŪēØī”žģsu ×dv_ËáĖ>^Š `6ÁSíÚG0õĄõė+“6Ås>~_ô֛æVˇą7ŸI¸ĘFļ떁2ˆ/­ ë˙2īdœ“Ī7ũ]Z"îsŪôܐu• ŒūFj‚m]đ‘ūŸöį‰[Ä΁§IÎMŪ0lŽšHūvĒĩģZ´ę?S܋%`/zÜÔ¨ŲúhpĀ Z$WiâĮœ¤ōާ-ëwEŪč8,RŊ´ "ÜëÆë4āØ`q+ēVō{×6}_ŧņ 3ŧ \Œˇø=C§kpC9…‹kmĨL}K5Ŧū“o›Ø2fģæũĀËA˛ĩ/ũä4};ĒõIÚA¸ĸīsX€O)ûđ˜âĀŨ>I8 đNŧfQÛ+Xtä“Îi›aÄ<ڈg˙4'\qŽõ ‹mך`˙ķ'8ķ=F„5*Ę(ŨĄ4†vÁÅx0U%/é§ÔÅOUé¤b‰¸Í:ĪöyŖã’*L/ÅāŽô o_”&,H;ŋW‡ĄžŲä˛ĸcŠwÕ!ļ íƒ[íՊ õaČĪųį€Â,,!¯Dæ°ãųOûįj/PŸđØ|Ņmæ1;;ËŊãW*Á|¤ÛŪgŗ´ĻúÚ¨{ Ļ)ž4Õ#¤{įķÅĻė´yâŠĶ øy$öÚÕd•2,į6&ŽJÍü¯„sjܤËÖV In‘ČšĶa˜āōŊF͖Ķēt­ëíŋåFށ…Ņ]ŦV“&ŌÚw5z܊.™Å#öG=Ž Ō€FŊd4i+šųTĢT|š›ĶÆCĶŅŗ6ŊJ<cpڂ‡Ön„íP‡uŗ3‚$ ¨Hđ~Ī’‹° ?Oō͆céĩ5ļ;ŊĐ<{M"ņq Îk.ōBŽD°]ĖtÂßų÷ĖÚÕŖH¨„iŠéZL@xa=ę'¤ÖĒ÷J-oˆė%™ZíõŪ7ŸS…Į^.ˇž0Äŧē4CŲ­âĄPFDÃa[mņĪÅt…ˆØHejž°T„'í˛Õ*Áš˜B,Ũi‰üē ŽĘčƒĒœ[D‚f"TQO*ŋYS"qĻT4ˇŁÎ!kƒÛjđq’°:KõUž/žŌ\šŌ¯ÉRÛč;û bƒ Ė@Ŧ…ÍAĪJtäGjßh J¸ >âP*`É \;k€bŦ´^&ʘÎkŨkfUÉ xĀ’{ĒĩŸĢ2nJŋ/MÃOĸjU(Sį 9äĐ(oĩ˙$TĨÛj cOyÜđëėÂ*ŧ–|FwÄ ä÷ iã éž –/Nȏ€ÜÄ áCęšÕ"Iɚõē4´n֓>į@Ũ˛ūeŨa|–ásÍë>/Ī.‚ÅéĮėÃ(×Rgüâ"?ĀĩŅåŅčâį˛@=h⧏ Ę3ŲjŊöįÂūę—D‰ĩžÍŒũÎŦr›,tk#Œv_IKE)ŋ§ģfé3Áô­Ķ.éKc7›G8S`Œ*îg$ļnË\q¸Ãf0Ķ’|Ō|„Øjų1Áŋ^Aŋ´ZÔgëT 2!{Ū.šQu|ƒįģKôԇP¸''žņA~ø?åKŸĘūįŪå]b °ÛExN—yM/ģę‘īø”ŊTAÕŦÃĶ&ԈyĸĨxėĨ xīllF@ԓOŖ.wĀČuŗĄĻETHžŸ§^ņš“~ØäīGáÄFEúÍ('¤ˇÁM–õŠ8ßģŠBolÆîJįeĮ3Kˆ–ƒŽũZkwÉdܛ5ĨAļ#ãˇ`ö]yäD{ũW€įÖk˙\ãÅdCJ+Ļ—“Ô]6ÖmÉ KŊƒK|S D*u蝄đ˛4Ēō˛íķ0ܡ˜ųNŽNXĖX/ ižGö8išÜnHæđ7vú{ĪJ‹eҝ—°\Í\A˛^!Z:Yk<Š ķWėADI>Ævø8ŽžŪ-ĶĻ\Ą͜w[åh2,Vģ1U`´žĐTž2ÖeК&Āaՙ4–!•R,āz5ŨŊõ"ÍÂ6ËåĀ…Ė´…&Sũė”ōėÖ ųmO¯š ĪFrOC~ €ėũ‘ĄeNw37€3ŗ›c#V†…†šx:Ų˙F%íÕEĶ Ō °=s͸‰,ÃĨdŽŠ™ ĀҍčŦƒd¯ˆ2PŎfëztÉž_0¯•(č2 6']šÍ7œF`_…øZPĉéŗm7ƒā\Ą/x}ģK@ˆUˆÆ-ļģ/ôosí1˙zq–/D>¯įR´6?ųąĩWÅmÛ÷?E˛õrˇTŋC‰õ1ÅÆ;æØP1Ÿŋ"đt-3ŸJ9„ËĄŒš130!"ĩXģF¸Ų<Ķ)t‹vsŧŨ2Č=šĪmá:D‚ ĐčČî‰QwŰe0š+ĒĢ5õWdŪG'¯bŪ™Öđd6ĘÍī-`<ØīļŦüāÉĨ†•ÕTg Ĩ¸É) Ū“‰ëÃEš Ļ4—J¨˜€XŪj-&vËŨ ŊũčáĨeA@ģOdšœ,Ō(ڞzšml#ˇÔf–ã÷-|×pM"ß @FWíĢgžlŌÛķt$j°§Ÿ‹LrA[ Ī`›[›ãëU_=cö&Æ ;bR&K)ʞôŨ?ËÅĨQœÅá 7ŌÁŽėøČ ‰í`ãz,Ų¯ãO ›ĒnqZDŠŧä'JtäGj4“r{ū~Lũë\‹Ņ…PVE"ļâI,1‘˜:ŪŽœģdĖIčü̓ĄÖ!>ģ,l?EĨ×5Đ+ūōÖ\ßK~î)õ ŨûCÁ ŌWZ9‰ņŸ­ķįsRŽ7†‚üP†šŠ€Ļ3Lff|ˆ0ręŅwĪRiĖeÚÕĻĖ6$€âÂŌ|¸ĻŪuU_;{6Jéo†`2FΡęŅ![fW]›–\fQ+äŖĢØKÅA¤F˙YfG8.}׋uSķ­W¯°8§<áÄëĻG¨SŊ¯°^SœÛ/m_{)ņ+Ŋ‡Ķ~CūÖm+ØČSŊŸņKKāk ažS˞îŖxĸå ¯ąŨ\ŋ#`sŗpĄ5ö›zPŒ0#7É ÍŖb{ę;ĘŖm‡ãƒú€(ڂoa0Ŋ‹ œÕ@,ˆßÔg‡áíÎ/fÄKŨĢä…a 9|ΊYk]x7GÁoķq§¯cēÍÖŦ8H‡ģO’“č÷#8*­+đņ_\ÛVŅšSpž´G Õjæ6(3}āšFÉŖL܀bKޞޕo—YÜŊ>´_Tu¨¸šéƒ?ÔDfađUZ~"Ŗ`h5o¤ÉĪũo} ˙"bä’mšŸĩ|Ņ@įv3^ķ‘ų4Z*Ú_ü ãæ›Â82Ø׍B^j؁{ đŋ đ,%Myr}™féJį… bN_ŠŨėÆ8bŦäK~D÷Áņn–ŧ…UĪšĪŲØ/Awĩæē AT?mŸwņŪ?wņی“įĖžŠy˙1žãâĸB+7%LMoi&‡øūŋŖ‘ÉÆũQ|c`X‘Ķ䃠Ōö4+ đĄ’ČĨąíæG5Ŗ“Nū>J+tĩ„3CŠ7`ØÛŲ4-)Á÷ 3¤x]ÁüšČ$—ÂõĶäUŗ#-Wūėžæą<æß&>N ɒ{kŚÍųd‰î’đ¨ŅķŒüÕxl–Æ5;¯†W|;ĻÕÉJ `Ŋæ ŋeL;*ėÁŲæØÉÔF†xâ¯õoå>`Zœ÷™ē6h§ƒ§_õn k×°—ØÁĨ·ŊQR!âgō×8õŌaž;ĪÄ ÁKLōäcÅ8ô3Ģ•/ōŖlȝ.ËĘSrŊ7ÀN8ā°SŦzf›J$ÂŲzŦ/dé<Č`(ŗ#Č9øĪB Œ×•cX‹sé­čĨŽjŧæš÷š­4NŗČ˜ąPĩ/JuŖŅŽ\ĐL4lŸPDŋPį˜ŧ 잀Š ņŅë^w6‰ ĄēT„nĻy äú–ĘMÚ9ņqP^W¯ ‚äĩGA_v'ë¨bķ%ōh¸onnÕ)ké˙{¨ŅËŦîķ:3ÂJSFQ#‰ČʈĖN<“–¸Ž¯œôe47gž.ˆØ^ča„|+1F'YF ŧyŸĩņfp‘Ģ—„ö1PPÎ2ą_Üå¤ÔI0ŪHs˙üŸpÃjŠ ĐYŖxIS}MŦhņ$ú% ,_ģ~Æ ŋęÖ(Ö%ŽfŋĘÛŨh7E Â&¯ˆ=øŪ)ŗi1™k ;ü‹ręžMĻžčyút[œ×Í@¨ö2ßČ6Ņۗ¨ŗu[m?…h¯´„†I•iŋũ2͜c5˜Ōä€L ü(eÛ˛9ÜEvNž›Įę#NsAtßT#¤Î֟…}*/$éƒWŪy{įšá€Ž ĀĄü7 ē˙[Àtļ,N@EnŠ.ˆUĮĄ˛ā °éõg•"‹æõ9p+@‡ķŠo,Ä=eP(ÁŨÕbd=2´NVvÚíp7š ˘zŊDöpõJ'KmßZ?šëœf##7Āʨ4h[ĻnĄĐvĩž´-ö¯pMĨĸ´Ū+Đ@´ą({aG éЃŒĮ–Ę­ ¨ĮKl‹/?āÅN~ŧ ´ˇø’hš¯iāŽ"ž-Ŧ+;ĄLã]Ķ>Û/⾇Ũ˙Aũ`)gHųi˜íP‹VÆā ÁŨOäéŋkÕŗnčËšæîŲE/tÂõeoe+G‡Ūãá†@U„Ÿ%J=Ÿ‡q0“hmŒpuÔFē%–āÄî€Ųs4ŧ*¯ã—ŌWŒXĪąx…˛YƒœcĐyŒ69ÎmĘä3MĖŖ-pŅĀvŒŖ`'ąŠßŽũ˙šr-ÕŗšGMę ß#˜ļ+5Nėhû9šđUÄÍxI^7 Öøjŗ‡Wõà a~Ę˙ô€Î뙪¸3é71ōUüN„ŅVĻ?ŒŊ`@ą¯Û*¨ų‰ā÷b’“Œy4'ĸlŋąÍei4uŗĐ\‘nÜĀ[YP8”éŜëÅVąuXY>)OéĐ\rBaĩ3pIįīpŨ+lņáRP(6E0#>E”qĄ”ŋæ\ˇ|<ŽI áË."“ĩ÷}›hb€Á—–Į~Ē9öđ…CÚ;Õ¨‚Q_ęä•Gø%°FÄįk˙€y^G‡ė÷ÕŽ1ēąíw‰—(+w/xęļ›gƒíâįŦ xļŊFi=ûÍŽ´č—›sũ˛S˜í°īBđøhL5ŧ#î`¯Š€ +;ĩŸŖ"QBžEŪ‹‚áXÉß[ē‚f8–~Ν'šä!~äzURÉĘ$ü¨ЏÛļ3åŖC,ž•Ū8ŌÖļSEC™nķķoTxÕ5iķōqŲŪqœ ›0—˙–9Ŧ՘Ėō‘˛ {pAoJY”0WX0ŲČH?č+ǘ–Í—$õLũ9JDČ +eî0s‰CŖ°¸1Ž|‘ą!—ÍB‚‡ōŊÔ4Œø4RË@ú/đe$NíPKŌu7ŅvŽ[ˆčĒķĻčœîą  ÃôŪ“])E°ŲSíęw8›hö™Ūŗ ØkĒĄ¯s¨ˆw†Ũ,¨ EáûîžsģŸÜŠÂhûĸ)ĢũBFŸô÷❅ÕŅ„/`õžžû†âcž¤_Ėa8.ĮúÅķ+ą+h“ ŅÚôÍ; RXC ū˜‰ëīú‘‚œSčø9ũ&Ũ Ž2Rnĩ…Ņ<ÄköŖŦâyġÆģĨíâ”BĸęlüslžĘĀÉ[~Ø2žĮvĶIįČęōĻsČ8(ˇjˆA1,€ļ)¨­ \đ°Đųz ŊnˇĻʸgŊįÁHNö …đÄßæŠN›ŖĀv h˜ ‰")/§|üÜ|MŅH˜¯úīˇ)ÉßņײS ŋčō 0pÁ}âŲ1Dž˜CNÆKˇ¯˙Lŗ@„Æāŋõ˰Â@cmõ¤ÂW!…j š7ԀĩzÚ!ā¤ĄŖŠsv‚Ņ×x ļtņ<¯pm Ãgˆ=…Ƥž.ÍÈĮÜTËH38įeÛ¸:xā˞kryJķ w4Ré^čÁë—ŖiPÔ+œ ÔE0Ų)ŠaH°#=/zLząQŒ5AūĒé™TŲãOFöĪÅÄ5ĘW)ę›õ{­Å¨ˆŒĖĐ <6ĘëH)ļž€l:?>fũ¯4áy܌ä:1æʝš/Eâ<¤l`lIB€ũ)â9ĮųÜ˙7ņK $%ß[lŦĪ ųЏaĸœ#čSSŲCž =.§Laj˞„ŸæŖ„`ĨB:m"į ĐË:äâAL6ŲŽĀŒ¤Ķ—ĮVUq—w9ĖXUŒAžOĻúˇđa-]9tSĒdæ+rčĀĪ"ö>FzMŌü…@LŲĒ™ŗZŠ‚Ä9Üēą ˛Ų_™ é€4\ČL{Œ"Y6¤kŦxųfđ思î!,ō߯øf~¸āĨ–Ȩ ąPj(ÍčcŸÔÃŨÚæˆu𯎞īŗ'šŸ˙įÁHMÎõ3œÕŲ*XzRH9ãO'YíR[g#Qĸ#ô(‡áëŨĖÕøLAiķ× 0Ž> 鈸:Č?ˆ†vĶ÷vĘ7^ēnCžķu8mâŗM€mØ&€`ķøCŒzĨ˙É9Rąņ¯íĩ—üũ›éĪúBG]69+ÜÚ;UĨų¤X^v-Š/į¯ļȄ™œčŒÛ8Å}aņŖĘ%’ŪuįŠ•/Į*ú,–¨Xšs7•ÅũV/`wđåÉģŪpd@YõzÂĀtË0]Ą{ĘŦ2Ōrš5‚üÁũ€ũgÔđ%įė#Õ5ØĶŠŦ.Îą;,W< `? U'Sg…ŽĢ›¤TM†pkWŗė”Q}öIz¸3~ŪÔ)$g ‹\RŌz_W˛ũņ[嗇ˆŦõŠūŌĖ~ō'ūŦÁK .^‹O*¸M•y¨Ö]ÂŌ&šąTjbnÎeŸ …ČUUrH,0™›œqí§G>\k˛" ÷JĶ9jö’œŲl.õĄbē\ãåĐdŌI~ģøäOŠŅšĻ -lķ_ķ§9Uz­ą$‹ ĨbÄ˙Ĩwļe‘ąYeŽ.RĶĸ‚WÂH-}ûמįļUá%cB7§ ‘1Á˛ˇņ@ĘžöOSHԏž‹*>`ęŅŽhå­¸‰™Šh[l‡ŋNŅD '°úų÷2āųŧQtgņÄÄ6HoÃ@@ų´ā§˙*9nO§ÛŖUy[ĐLė ŗéJ~tÚ ÃĐĒ%šÃ}$‹Ĩ¤ã܋ ĩãįʈnŧ)?ļmR¯ø˛0Ŋf3‰ZČËhÉíŦŠ6ÄvcķCEōTį9Rëe˙€+…ŋâŽĘáÛęzęRXÖ9ˆ_€Đ8LŌíÎĨ3ȀūŒAŋxcV‘čČpM§đĘyUg‰žĘämØRr‰â.’UÔ1*ĒxúĖįu)?0T˙VDeæc7ƒ5YæČ4’b˜Čq:>ņ’0âÅp˙}B ēüČĀÍ0i6ķJŪaUFĮ0¤ëĘ…Ņ ĮöŁ’kiæ^HáŽe%°: {gøĀ&Ûˇ9žZerÉĀŗīN˙‘sšÜĪŋ˙}Ā@EIųˇŌÆn;_ãüâJë"@+čš>ĘķfģGāÎ1ņ-§žJį–Ų÷Äms"ĢČŠ^€[Ncā•Õ1~šcÍkž!C‰‡7v‰m2ī[Vö-§”3ôŨ*0IHđœQJm—Ā‘fU’ĨŊ_út%–ČžîP†×DÉVŠŧūøûF; CĶåE,4`īž{ˇÕĸ3ŗÄÁâŽ~*Ų×q’ĖÎuIīŌēÕs¸Å žĐ${Xž?DØĸŠiÍ_ ܀ĒÜģ¯Į#MYv˙nđĪ•\lŠRs#D~ÃVĮŦfĐŋˇw˛ mŊāļ FŠ áą‚Ã^ŠD•ÜxĻūÕî0ixÛ*߇ĘHRĖŗ‡™Hß4=˛Ë9˛`j˙€ ’î>MöæßÃ_— ĢßÔ­ŗOhōfŪm kĢ$ęÎ]8˛ĪŠؚz¨Ŋ?ä(!Âč Œ‹ŠĐ,M`—;ģg;blāÂĘxR„ФāFĻĩ;õUšdđQŁV` é Đ}Ķ5k ×DNâ@gMÁ9<đ÷ÖI¨î˜„Ŗín:Ņ‘(‚đÛx˕4Ģ”ĄWYÍ-aõ$MCÖ´zņwĢUHrũėļ3Céöú Ŋsĸĩ+3ēU‘ĩĸzlčjˆVxģāL:r øH˙PHÅédßCAhØÃ‰ŦT‚{Ë+%ŠC Ø˙#ę4_hTyáųŲírLeņ"RLŖVIŗÔ ĀKɨ4Ų;õ¨C.Ĩ`ú8:ģßætČN™S¸3Ĩ ZÂH°} ؙĶ5ÛJt“χé&iŽ[‹Ū6H”ķ0&‡÷äíĻĶ`˙; Qa?(WôČp‰;3 ^‰—Ŧ’Éœ×iĻîhĻŋm¤‹†kU#VĀ"ПgĮƒ“ļȉ%äYz1 M’Ž>G÷ë'šKØíú¸pŽ‹N¨GĩRæķ¯’ãŋWqāŋ<Ø-§M1cMüÅdÆúÔę+€ŅÄYąĢɑ9n]ZīáĻéÁ5ė¯ē‡Ž'$ÉđHUGx&Íũ‘īĢl(îAˇbģt…b¤U›Ņž#ŖŠD(†RŋC ,ë€å4G´¨2K}“õĸ:ņŨĨņŠ9Ģ–<Â"ĪXė"ž8Y\wQ÷ ÔÄrsˆûč` Ît×&W¤HFĩ $%ä`ā˞ŧŖ´Ŗ!Τ6mˆ¯D˙Q$§¯ē°žuwÂOĄŠkŌžĻ ¯W{^\ĨPИÂ;å0šMu7e”Ę~đŦ nŋ‘÷Ņ÷'ã™HI”ĸ=ũĩ"ę& d¤[ĩt|ĪfÆ%1‘_zíy/{ÅdZ××1‡Ģ(.X}ÖAĐõÃda’Ø0$ŒÜv_ņ<ĩ1uđĀÛ9Ŋ,d;ywÉ/” Û-É­pĶ)*åųšŖLņŠq.gÉ;“UŠKl_C2Ž„ŧŊPļsÃP|w}N‹‘:ƒ<Ģ88.Î$]˜˙!Ø(%ÆIę” âãØ“Üij~ãtrĘúsšĀwÉH%;ã$¯Zc‰¨Áoi‰1Ūܜä5ōM†L7ß"ķ‘u?tĖÉ8žD,kzĄę0C‹˜Ä˜¤™oŊ3ŋå8~áÔ3‡Í'’ŅāzÕ˙(Wœ6Pâ×.ôĒyiÍé ņ%Ļ*NόöMuT’ŲĻõˆ,ÂōWo-[ø<)ŸīD­SĸHP%š‰šŠČ‹ŸNåDiąß ĶÕ˙ Ŗŋū•é´Ļ­šh¯ü:§áz1ˇå€Û 6%-̀ĶK–Õv€‡|īpæĐ\J sÕŗåë΍åī-eÜS”Ã%pn4§Žhˆ\ZęĻ3~kO'ĮŗÚķnہæåšĩ_đnŠyë€ÖĘę’rlV¯ú6' /_ļ% ĮRŧ˒mC3‡w—÷ĩGkß)Á°žÜ j ПĮˆAF‰˜´8jf_ŌĢ*ˆéöÍ…æCm†| $ŗō:ˇÄ.öÄĩčЎrˆÛ1U{ŋ8+ö›´ˆŠ„AĒ0ŅÕ¯Ú4îØK €ķG5×ųŲj7Έ×Õé”­ē"wQŒÛ00æĩ=¨§eĩˇÚŋ×k”kōžąŌ3;BLÅ2n'Åĸ°ÜÖP‡jOā]Ę"ļ’Æ<ãõÑ˙ōTū¸ž(ę˛/ ąQ{(€•ŖŲ9^.ĒÔ+–Gr ˛ē÷ōā!ŖæQJS2!/æĘūIŽąž)N{yŨ|ŅgĒĐÚæčøņÍ÷*•ÄFŠĮ•Ƥ@áÑ3­Į‰•Oá›ĸTø‚ãů20-ä|öéî"“Ô„Ž”',ŊÃJĄPũ™´°ūBß,k‡ÉP‰VŋĶΔßÍ6vëâ#,“ũ|8¸ģ˜†˜ŊNpķ–AĄú3î~Î<$w_ô°–¸)”Nå&{ũ•‹ņS—E&ŲŅJžkĄč1įę‹č;*ŋŖ6ëÂō@fpyJō` V(ētÉh?ū֒ŋ;sic‰Čæ[IŦYËU |Ŧ*Ū4%ĖņÔ •‹!j] īÛM—›-ģ­OC:îŒ-Nčâe‚Ŗ9Â\\Ė?MBmĢR’=UĪ‚˙,{4—ĸnâÆI}÷ė™&Š'>ʡb6‚M~a‡rtXļ \įë^ {™Đz€;ë˛2T_ WĢ: ž2':Åy'${IŽ ĘTTEÎ#GōŖûĪ^Pšž4đ¨ŧž0ō‚B–ŨįĘŋô‰ų瓤0ŋ%{ĘGŪĪŧæ•ÄY'{ūbņÔäôĐ™ ėGÖ6›´c„ÁVŒŽtŗœCęÉzI $QŽeęîų*˛ąŽņ/p{ļ~ëˆ(;VŌÚ36íK^$Ī0—MFķĩ¤…ō”ŠņžDč^˙NŠ…—˙e–O˙øš˜ 2SĸY“ $%žŊ/Ū(;l”]Ö0æC? K€ß[\Oˉúë‡÷vėa–?ĩéOĄy]™|P&­ÁL¤g"ņTÆrF7ŅēSˆ&ØˇMYņũA!S/` \ã“vxˇūĮ#4ĩ|Eō6õsĄŠž•ÁtЎK“/,ô4)ĩüˆ˗ŗßĨÆŋū†aI)ü\x"HîŪŋ×õ cžcęy'[öņAØ{NŽ)ŅŖå^]ÄmŊÅ;¸ÅgS#WĪ"í‚KîR;ž ’2DŦûķÂČgMŽúYÃī’3ČŦ Xƒ€š2ę •4Ųž–FâÄ͎ŪMq [k RäL"=>8ËGĨ0˙Ō5z;—™ÔP¯2‘ąFúūš=›?‹küˆĢĢū¨m&ũŦĪLžą@ŗô'ō¸ÔūĖ˜ōŒE9*}\7ÕŦ°VÕüą˛o+Đėíā ˙OC•ø.áØdaãĨG4ŋ§hxN“ΰ9û \Ôuiũ’HĄÂ9O&“ß›>BãW‡rļd “T "@ŗÍđĻėP ,4¯Û aĩÕŦ-usWƒļ˙b3ĻîåÁNqmĩÚ˜;<ĸjÚ„.MÅüîõõb,æß˙- Ė`¸M‚,ˇ7Yŧ¨—2ē¨!raQ=EšīŖŖī&gļoÉÖTyúōW“ZXüŒ3ņ÷´¤v] @š]D-+,q==h„|H‘[˜ąŠûx@ԞпJZņ†]ŋĶ‚ Ģđļ߁umŨၠœŸ —iŌ<Ëí4ë—í˙ž_MĩĖjü„Ö(QÕ„ČCMĶ[ŗ$ũ?°â(q 's­û¨Ķ;d(ų‚ë ÅLåû'kÂÛföÅ)åšÆ—qtÎŗĢÍE°’qõ*^ĀÅJĄĐę€}\ž˛ŧ}ĢZŨ„ģõxÛq%´…Nö˛•ˇ˛Ö’š7ËõŠ™ø‰ĩ¯&SU`žęh-ƒČ+¤ĶĄqNíõÖĩ)ŦÄ!Aėjô(‘ņ˙:QšKäfǐá?ŨA '~¨Î-øöÎÄ ?˜ÕZÕel¤œDōVõÕģKHC5Ēí¨7͘€,Ūh°…$šc(¨oö(hiFëšķˆŊš§GËV¸cDžI.MÂ|„Ûá”ūZŧŦÚčÛ&I Žhėļuė\žŠŨ'Ąkŧ04ÔđŦŦ^l„*õđčŲs˛[ŧäuˆđ×w-â'Ę 0vÎÔ0’&ÚDޞ=↏z™Įi f˙/ąßíSvŦl;Ļdā–eÅXŊ1,79oŊ}Į ĄßkxVų‚c酂6û4Gs×EqútÎíKåšüXuްbƉŲpž78 ęËY"YĻßš2žû< 3âˇŗFb=Éá-Žs-)Ŗë-L‡įÆōĐ‘餋4˜ĩ6¯ w)Ŋ–Ē16\´kP ~ũ(ŽčŪMģšGŗf3w‘“Lđãd īnjČlW€Į⨇öũč)qh €B†biŲNČTTl^wŨÄ7nTks]ø¤l/܃›ŊúŨ}gßî7Āƒ™üuĩŠčŌŗ:”‘Ėd?ú×Ķ%ëøåîD8ÍhS#Ŋ0įkûC/ `åVU‚žģKĘ˂gšĄ`]EgÎ_ģæës¯SjŋÔÔĄ:ļĸT\m?؁ē­YVn+96ų×-ŕ<ēFI÷mæYãŖČYJČŪŽ%¸ŠĄį¯đ&AĻļ{•1oŦˇŽ…=we/&Yē˙AlÎz"@Ëf|úŗą]ĻĐaŠŋ!íl#Ž k„øĨ'įƒģW,Z+¤äf&LöT4\‡ČˆŒŽâ˛Ģę÷0­ķōmŽ2íîøNŒŠŽ`jšQŽÍ6[yč,ã‹hTf‰ĸL‹ŠIŅCiŲuŖ<¨?kÁJTųņ6%îX”Î̏ģE!īõeđŠŪę}Ũeq1ĄlTŸ6 n zshŖŧeŒ’œŅĖnķ<ÅՊĶJčpĨ9é|5ú–^/Á–_˙ā#ēoQá&ŧ\ƒūnIfâ(ļŧ'ĨÖ8ˇ nÍXŪRĮ‹ÄAÍæĩm+ßáHŋ*5B•Č´hÂŨĒ ˙éq‡)Āú-Ø>Āûp¸ÛzÁYF8œ%ųÚW<õ)@#Æ~‚yķ(āQ÷Düa|ĘšŠDĖĄa+1ēeúŧ<…Û¯]¨Å­%Ÿádëņh˜nŽqõҘožŅ[Ą ĸxĄŠh#Õō5œk&gHŌŖ â ŗ­^ę7?ĸĶÆ]į@Ūw`G2…ÆÄ!Ŋš0`¤}o…HīO§˙…`˙0ĶÂžÛøņ€w÷×?PúkĮl ´1ƒ1ėŸ’ZzZM=¨HK ûŅĄŊy߲`ĸ\ų4ņÁ3Y؎m)Ũ'HM´(<–M÷õĀĮ6ŪH€E-hŧP$Nö7Gž]ÅаßįĀ‹õąĀĶž\FSÎt°R׿ö4D­F"-§Ņdũ5pā"ĸŽo^i„^įYn@|åZL6ØH°ĒGŸįhĪžg1`0°cõÍ6€ bƒ&ķ™Rw‘}R˛Ũ&oéSĢ%ėjˇœė90 Õ5 8ZũzL€j>á3ĮŦU¸ •Ķ™8žH|jRųyOķ‰ļÕĩ€6dFõĩĒrÛHƒß…Ųūã9ū×ęņXįÍBũâF@súņ ģIČēb0ĘÚâVWĐ&Xû×įaVÛb$„ũ§H‰ ĨĪAÜöX­úžÍœP59i“īá´ˇ/ĢYëHšūy‚ÛáF“A‡Å˜æŲNŨĒ[Ũ2.n,ŘĘŦ}~FâBÚWĘ=Ęå,FŪfģߎãüėۚÅ×ųbw[šá˛BŠƒŖß¯´Ōŋā’ÔžũÄāÂԎx3jÍŋ… ;ŒŠü%”"O‚“hI›Ú ĸÂäWæ6“ũŨIjĒį=˙V%ĻĒ=Ēd’˜ŋđ¯ÍRĨoUwnU…­ÚĒ6ú7ÎWEí=)ä¸k˜Ē•8—¯`¸ÄK‹ĸļÔŌ'dtŌķA`áĩŌN´ƒ(‘j`?âXpŠ->vëšÔsŲÜāä gšęĮJĢ%üë ÷ŋlûų $XËūxá €d÷8ēģOęÛäà_ƒ_ƒÆuڂh7X_^™Ę‚āΞ–†ÅĖp×/×i.3ĻrĢøŊRĶCæJßš3ö֞A‘ĢāDn‡‹[›åˇJË` >Ņ(œf%Ļz ĩ9#í‹îpŠ jŪČŊ āÔ æ? 0˛A6ĩ‡Û+L´ ˛[Õ ¸9ŠnÚ÷WšL/WceoGÂŪĖÕ$´M^ ĨŋÉfhāū 4ĸĻ ¨˛ ûMˆyÖ‚UÔĸ•2#”JuM)Mĩė"¤¯Û°s@øĸDĸđÆsļNž˜€ôš–.~’Qƒ ‚hÖĨëŽ%€UĄyúLVŠ~~ž(ĸÝŅ<T°á×Į†Ô=@‚ĐŦ%ÆŪvÄ3%‚÷üeŊIAĶ&“0GEĖr—øđč2véļ›ŅCé˛ų߇10ļ ψqüM˜OöQifüoˇ7Ėí8ÚÉÆûnģö&r;ĘxäËBeãĢZb]ļ˜LFéŌĨp}@ÜrŠęĩJņŖŅi-ŋ/Û3A]”JÎ*/ņˆđīR‹†ĖÛ ĨÂhāV7ÍUÄĩvÖÅÔȑXƒ_§Đ„ë̏ĩöz.ŅĨRx §Ú7Ø=†°*{I"ļ oS ‹˛šļĨ&T´`P˙5s™Ū áyĄĐ­“‰JÄhî)×ü¯ˇôÕxׄʛœzuÜ.A˜ōŦĪŨ’~1cw}GēUL<-Âq‡aq•uRÖķÃuš5Îe/¸g×ąå°—ĩĖôā#IŗâÕãVGoã'B&ÄLM•‰uģĀf›OtĘģw ÚĀ[h—Å„)˛VÂBŋO)N#ëåGe3ukųĨü.+åŅų+ģ;X¨ļDy¯Ql.ˇīh<į{^Ɔ*ŗ—ĶšēkßĀ0ķ–¤ÕéÎu4€*Ā]K2Đõ’ķ tIP2œ–ˇpÚH->͛ũ騖ÕŽq”`’Á;:ė$×ĮRQD>­ŦЊ)o4fˆT€6ŖYē0VĖ!ėũÂnãF†5yŌ­Œ›+ã[ ÷zķĆ <ŧÃQÁ*qæu÷ö:iĸįvūN¯´ZßÖE(ķō–#?´‚ūRĪÖ×Ņ|đ…\šu ë-ŧAûę1cĩ–AHrj‹5ŨXÔëģķ RĖ@Žš|åâYV÷Ž—ąÛ ŒĮhA¸Sčdčj„Ž ­zԐ7ˊlE.†ˆ‡x5<6Ëŧ ØĀ9—äQŽ€Ã+åÛ †w´¯Æw7aÄÕ6î8Õũžwŧ•˛ ˛íwęYmy87” }Ã#)°fô‡].™_\Z_Žn–Ž?Qéŋ”ÚĢĩE×8՚ üH¯S[ØíIEęTV%L ŒĪ‘ndFģ ⊞ÂÛCÎ@ ŧXĨ_éyYÉs=AĮäV‚Ō)Ū k.ĻrÆN}•äØdžî• . íčąÍÜŖVÃÁ8Ī Ÿˇ)ŗ¨ĄšKt|KxŲõ¤>qS~—û€Q3 ņ—kPŽŒĪ&NT01ž~7ċ^>ŨvöĢIåQ­sņIæé؊$>ž’T’)ŋ>ÃĸđĨšÆhûÆķWhųی› \TArIã¨(Āîīæ Tĩle+øDÛŲ˙īa-­ô­GŽeĐŌ1A=¤K™‹?LCA­‡ĮŒbL}ue(ŖĘ'qŋs]ēŗ´CYî_õ gyú/Elxūe2ũYāí<ĶmĮ kÛ¨’•ôgîˆč—_cãOmf.¨T!2Vōäņ#@ëũËüČq´f­ÂŸ…h3ãŌ9¤â˜ ņō‰hļÖ.‰#×ö§žû†`šĪ4n*œ'ĻEĮU$_ûkX‡RŖw­Ė_ŗÃĖ âšĖŒĶ;/§N0p^­#ڙ@„TõƒN ¤v gÍJqËáwú?ž •jŌJjDđ 9ĘÎ 0ŧTņj…ƒē„Û=Z&1ưYÆ@H$Ŋ¤Šs\ ,fJ†ĻąŧS4ÛÖA´0‚ŗKÛĒAmč}^˙á€Ø˜R# ō×í¯H?Ē]Žsm†MĄ% äQAU‰ ˌ‹˙ŲD“ōߝoEõđĒzäā¤(SÔ\sæÁ=aĢ€$ĩIæŠ#’Iœ[g&Öũ1Ų{ņVķbST”Vƒ^ķ>ętĸ]Ÿ"1 gą<ŗ5¤ŋŠ }Ņĩô9Ÿ='=°˛ŠcŌåÉ× Tŋ=Ą¤Š1P,ũۚļmmlŽ$î(ßáņ‚Da4p!đ ŠC¤ąø“…QáHî\†zŽûIŌopˇ:I?÷  $ˏ, o|„ÔįôŪ&ŊĩōPä¸ÁČ.Áķ$b$aüXī€GdœĢ™Ũ§šë†XT÷“üũ6“1ĪUž[PÛ!ĒÖnž0Âl0ĖôįÃ5ję‡sÕ/8âÚB•xF~Ķ噞`OŌJûAkŧé*š—ī•Dļk]yo2â(Ų¨ õ¸ĶŠEváûo;õW”Ņ˜Œnņ#‚ S_ ÅŗüœŪ ø“›Į?’P'˞kŸũ-“Ĩ@‹&ŧáPģÐ.ũ6ēķü7z!Ũ×õmúĩ’#ųō\?VÆįęŪhøuØũŊū­‡Ôč|>ĻßVÉņ~­ËũZWŋV‹~­ĢŸ&Čž{Įę°˙*Ī˙6˙_U?ūÕĪÛtßVüOŸŧsįīŊõoíÕøŌZ čÕŠPøzZ|ôkįž_>ŖßĒæŸ%Ao~ĢŖõl;ęŒ;ęęրíbɊaŠÃ+“+Eļ\SaģaÕû^quŖˆ+~ūJÅs̘°3ypÂÁކqš0ÜËąÔÁį(a9ڏãžԚs§į#ę.֚2(j‚}BéîįŪy'™^$kāĩöšĐ&qpÇßëŋœgæmđ#4,A8Ŧ(ÄëßzÔ;ÁLĮ÷örŧģ(ĐëĮ6ĩÁwUȉ’X0X5î40y}ę HEWģ‹w[b/rĻhBtĮa’˙ÆĒ5DļĄ¸ˇz0˛Ĩšt .ßzÉĻŅuŊvCKõ4ŧbgqđBÄËŨ4¤U’76Á@6ˇŨí– æ_­8ĒģŲ°Ģ*ƔžĒŦŪēØī”žģsu ×dv_ËáĖ>^Š `6ÁSíÚG0õĄõė+“6Ås>~_ô֛æVˇą7ŸI¸ĘFļ떁2ˆ/­ ë˙2īdœ“Ī7ũ]Z"îsŪôܐu• ŒūFj‚m]đ‘ūŸöį‰[Ä΁§IÎMŪ0lŽšHūvĒĩģZ´ę?S܋%`/zÜÔ¨ŲúhpĀ Z$WiâĮœ¤ōާ-ëwEŪč8,RŊ´ "ÜëÆë4āØ`q+ēVō{×6}_ŧņ 3ŧ \Œˇø=C§kpC9…‹kmĨL}K5Ŧū“o›Ø2fģæũĀËA˛ĩ/ũä4};ĒõIÚA¸ĸīsX€O)ûđ˜âĀŨ>I8 đNŧfQÛ+Xtä“Îi›aÄ<ڈg˙4'\qŽõ ‹mך`˙ķ'8ķ=F„5*Ę(ŨĄ4†vÁÅx0U%/é§ÔÅOUé¤b‰¸Í:ĪöyŖã’*L/ÅāŽô o_”&,H;ŋW‡ĄžŲä˛ĸcŠwÕ!ļ íƒ[íՊ õaČĪųį€Â,,!¯Dæ°ãųOûįj/PŸđØ|Ņmæ1;;ËŊãW*Á|¤ÛŪgŗ´ĻúÚ¨{ Ļ)ž4Õ#¤{įķÅĻė´yâŠĶ øy$öÚÕd•2,į6&ŽJÍü¯„sjܤËÖV In‘ČšĶa˜āōŊF͖Ķēt­ëíŋåFށ…Ņ]ŦV“&ŌÚw5z܊.™Å#öG=Ž Ō€FŊd4i+šųTĢT|š›ĶÆCĶŅŗ6ŊJ<cpڂ‡Ön„íP‡uŗ3‚$ ¨Hđ~Ī’‹° ?Oō͆céĩ5ļ;ŊĐ<{M"ņq Îk.ōBŽD°]ĖtÂßų÷ĖÚÕŖH¨„iŠéZL@xa=ę'¤ÖĒ÷J-oˆė%™ZíõŪ7ŸS…Į^.ˇž0Äŧē4CŲ­âĄPFDÃa[mņĪÅt…ˆØHejž°T„'í˛Õ*Áš˜B,Ũi‰üē ŽĘčƒĒœ[D‚f"TQO*ŋYS"qĻT4ˇŁÎ!kƒÛjđq’°:KõUž/žŌ\šŌ¯ÉRÛč;û bƒ Ė@Ŧ…ÍAĪJtäGjßh J¸ >âP*`É \;k‚Ĩ@ÅSá&Ôķúŋ@Āጠ'ąęĢYúŗ&á¤ĢōôÜ4ú&¤Ž`ũšãă˙%šF}iš+›YmD Xc ƒ’0ÎĖ"ĢÉgÄg|8Jąž@/pļm0Ū“2¨ėą1ZüG~F‘“Ģž3Ҟ">9ˇŦw ‚ūWÛ| wāZąŪxÎĄRįH=&Úˇ×ėaĶá˙4=`"yäV×éĶ-­ˆ×L¤ūQéđí‚âkŠa¸ NŦ¨Ō¯ßLĩ(Jģˆš*¨‡dh@ā뤗‡JĮˆk¤=ͧsãũtĄÄŲ›zwhUë†9™q‰ÚĐ?ĪĸõļuČŨœŗ˜ļ0ąfĢ ˙Kˇēqį!%ŧ3 @ō,&˜ķhnY-]Fk6wUÎ#`Õ~ŲĻ*-0ĨZúwÞīņ%W\„M”læGVâĄHš3qšč;w˙$]Ã9oüĀ„ qTWh÷1Nö"î?Đpīi`ũAnÜĄuFëI´ķFzŨ¨å61D |ÛŊƒ6îŖ+ đšyY…đ^CXŅ‘3ĻĮ#2Š':ÉÉyž_0¯•(č2 ˙õpŲÄfø_…ø_…ø_…ø_…øM ņ-*äā2jn­ė+eäŋĮí*/ĪĒįGJĢl߇ū Ŗ.ą¯ŌaöŦõePķ'še.‘nÎw›ĄÆYˇ9íŧ'H€0B;gx!¯ĘQ8DōžKæ!ŗŋۅÄy?$¨~@øe™J=5Ûuõ āOåožÆ#O6AüŨ}dܐwŒCFEGŒiXŲõÁãŌ÷˜–Úh܃$\ū°đģvGĀ™ž†ŸœĖō+}ėÎöĢŲņžãŪj7MöEÍ_-ßæ<Āŧ´Ųgš,*Áđčv€—6–y$aRđ—ņRퟙ›?—~$s猜‡;Jé1‡KAęZc.‹Đ[ĖZĘę|Ëí:ˇJtäGj4~ä1D/ú ŌBŲ ë[Ę5C˛˛'Œž–ˆ€ CЊÂH‘‘Ėą÷ å,ŗEęŅFDEŒ”8R.´pÕû{ÕąÖšm-c/E‚ŽöÁ“˙P˛ĨŗĨNá ŖD_KíBƒŗ™ M…'ˇđē˜"›äėP†šŠ€Ļ3LfĄŧ6Ŧx{i P3–Ŗ9ÕĖU;B"l,īԜ39m¤ĪMvõB’ņNNÄžēŦÁIІ;Ō‚´†ûĘŦ‹īŌŅÚëSbßYsĶĄë•Ûzøû¨œ1p%Û6™û؏,P‡yōO‚:ž.•îĖŲS@ĻĻ§vãfåŠ0č-Ų$&ƒtHz’+æčÅŖîäūÔËØŊ¤ŠōŪ§„zŅxzUi6øk:ŦǴNį7ŒÎ MĘ ĨĨŠŌÛĀŲmÄW7{7tFĀĀē`øqūHĪSƒ–LŨ;A[ÔÆãĩH—‡¯f °kÛ™ŠŪDQCQ ĩe0Íí}ÕLJLN˜ Ī"3Ž3Si=zG˜Č‡§RŠ^-â Įcæ1Ĩ^õãļø\æjÚ÷Æ,{ÃŖe xĢ´lč|”Y‹ĪÎ)‘˜’{åqģĢļŌģÎŦ>ĐgĪtˇøIŨhcYt…r‡UŸˆ`qBkÎōƒļEŸ8eņßâ„ÜiNÕŌŽI|ĶyŽÄîz#īŨÃīčŲ—›BŧöIc’ÄÂŗ°U?“™ĄĖâĐĒáËÁФņpën9478ņÍn¸ßøQuĮ3'ø÷TäâA…™ZūcĀ/á­3ö `x"„Ow¤á˙ũs:7wV×í–Ŗ§ļČüįœëHcu…ė/Âü/Âü/Âü/Ãūƒ|ŽĄíz•JĻ7•sHKv×{3Å%Ø?+¯8S+ŦTVŽ92ĩˆ6YtNF‰ĄGORÅSWvZABqЕ˺ÖīŨRœ!˙~cĪĖĨžB@l†oA?Â,yIā*.‰ySŧ4‹cą8łŽīFŗXĐ˙QD頑BEŸF ;”ŒœsˆJČKƒ[|žR=ūš<7ĘŽØŗˆŽ•įx‰ú>˙}’˜Ųeãkᰀ3ļįėåsũų´ TÕ¨ę2Ņō.R?@ŅÍ€åĨŋ ãÛK¤¤‡+{Šᰅ$ƒ§HÅ^dÜßčėRYˆ8¨ā7Qfü8ųĐÄVVB‡A;#Ŋ’"‹ē\V[°õĒé˙Büĸ3q4öå šQ‘8å_.,°ú7'lúĮZRé`4ŗRSrxĄ’ĨdÆkYģ§<䁊ĶŪPA>!‹‚ëo%(¨E6UUŽã…KŸ ōš˛ü…’¨œ*" ‹aŧüA`ÜQ¤ĶúøÍLčwZė,Ŗä+ߕ:hē„ŲƒÍž‘ČLëĀ+#fŨū5âb4 Ö“É Ãkļ÷čc…°ķ“>hŒ įášXŨ˙VÁļsõöĸ“”ūcá÷JuŖŅŽ\ĐL4lŸPDŋPį˜ŧ 잀Š ņŅë^w6‰ ĄēT„nĻy äú–ĘMÚ9ņqP^W¯ ‚äĩGA_v'ë¨bķ%ōh¸onĒ m)ké˙{¨ŅËgšjŽÜŋ€ËģŌõŪãØ7YĨ×Ųņ¤–ę KؐHėė7gž.ˆØ^ča~Š„ÎhƒĨÖjå׸đ,:™ĶZ’ (Ų(ƒVCZOößä›TWåŋ’eƒ*a}>ĪųøaS°7ņUvoēØ8_íOŒšR•Ė…!HãTi°æĻ#˙-ī8m™0˜ģ¯d×ūéäg_Čō Miŗw`ÔĐĐ­Ä&ųŒĄcĨķJ§,’(ëB6kŗUSț¨¯]…Ž„Ü(ū Ž÷Oīæáyāx¸ŖĐ¸}:ËO¨$mËĐ6x¨#dOn#Z|°QHjëąį+ĀBˆĨ}1ŧEéKø§šî@Ežˇņ9ņjūā[ųž%’"nÖđ8cíĐ%J Ô~KöŽH-6™Īq•;ķŠo,Ä=eP,oSĪ ÎĸÚéĄĮßô“8ņŒ¤ØiũvJŗ<ķī#5ˆ‚ ×ãåē+Ųl‘œÄļô䇃åÅ.ī8áĖ•åžˇCUå"‰ĘFĀKvŋN-}žŌ0]"X$Dô<˜K0ĀNu^ôĄ•ˇ˛s˛Ču-ļÜA|ÆŠKk1ān.+™RķcŌ“‰SnūHBĀ÷Ė7›hũW@øązũ@—õ1ã āzæo_FˆĀ“lqÖĐŅúékƒ‹ŒĶü^™”œ/ߒA”!‹Q…’,ûUß),‚_­ë5X ×Z/ėŖĐFž)đD”n’‘'ā?ņZ˛ V0æŅ"#ī-ƒP’ˇ‹ĀÅÜ ÅĮģé `ŧ÷Å Qg7˙c-ŨüœķЁDŸ‰Nnĸ— ž‘\žĶôöÕ͒´ŅĻe`nû%IđŠĒôG'ô€Îīū­ŒĘRLŠk‰2Ōgē™;)ĩčø‘EÅŅ{Y› Ŗ%oC“AÖŊÆ]aŨ•Ņä¯4ŽŌ-ĒOš9üJő+~Čd{–oščE (rDKčhæąÕ:Įbp7xĩRF­rė,ûĀsĐŗ:‚WąŽ$>9œĻõįņĀFŨ}éŊΈĖ+Ũ I0–f˜Ž0õ€ē9Ą1a0*AąĐą´Hšēt5 Š`}q­đ•PéĖââR^ޜÜ+cŠd"ī\§!F‘fa3YB{OųÄ^âALęú[ŨĄ-ŅH9}îDAWõŖTŠ)'cŒĸÚSō<䞈ĶK+Oœ3˙Pá:Ö|@qáPÔß/°Ŗ……(Ã1ķ6(Tš§TŖ/ļ(Z7ΰDDsĩŽôŗßehĻĢwžōļēß˙–9Ŧ՘Ėō‘˛ {pAoJY”0W÷­ƒųî>DĨ&1)Īž­Nˇ1âSU9úmũ˜}(iŸņŪj:ņBĖŌŠÁoˆ—ø‹›,äå}šĩ÷÷ŌŽ•ZŠūš‰ é užˆ eę&ė-Ÿr„Ė AÂÄ"˙H/Ŋ‘'éx;Üo@q›2꟱ķĻÅøwІÕĶ{fų~Q“Ā-âŋqÆÁ–ĨŽ›%Ŗ¸Āa OhØwũZŽx*$Ē=¸öhYėŒi°ŧØjĸ¸dH­uیûV< Xߐ]Ŗ0ŠÄƒ FąĒ5ķNgúˆŸÁ™Š ¤Ņ6ÅaT>X0ŲČH?č+ǘ–Í—$õLũ9JDČ +eî0s‰CŖ°¸1Ž|‘ą!—ÍB‚‡ōŊÔ4Œø4RË@ú/đe$NíPKŌu7ŅvŽ[ˆčĒķĻčœîą  ÃôŪ“])E°ŲSíęw8›hö™Ūŗ ØkĒĄ¯s¨ˆw†Ũ,¨ EáûîžsģŸÜŠÂhûĸ)ĢũBFŸô÷❅ÕŅ„/`õžžû†âcž¤_Ėa8.ĮúÅķ+ą+h“ ŅÚôÍ; RXC ū˜‰ëīú‘‚œSčø9ũ&Ũ Ž2Rnĩ…Ņ<ÄköŖŦâyġÆģĨíâ”BĸęlüslžĘĀÉ[~Ø2žĮvĶIįČęōĻsČ8(ˇjˆA1,€ļ)¨­ \đ°Đųz ŊnˇĻʸgŊįÁHNö …đÄßæŠN›ŖĀv h˜ ‰")/§|üÜ|MŅH˜¯úīˇ)ÉßņײS ŋčō 0pÁ}âŲ1Dž˜CNÆKˇ¯˙Lŗ@„Æāŋõ˰Â@cmõ¤ÂW!…j š7ԀĩzÚ!ā¤ĄŖŠsv‚Ņ×x ļtņ<¯pm Ãgˆ=…Ƥž.ÍÈĮÜTËH38įeÛ¸:xā˞kryJķ w4Ré^čÁë—ŖiPÔ+œ ÔE0Ų)ŠaH°#=/zLząQŒ5AūĒé™TŲãOFöĪÅÄ5ĘW)ę›õ{­Å¨ˆŒĖĐ <6ĘëH)ļž€l:?>fũ¯4áy܌ä:1æʝš/Eâ<¤l`lIB€ũ)â9ĮųÜ˙7ņK $%ß[lŦĪ ųЏaĸœ#čSSŲCž =.§Laj˞„ŸæŖ„`ĨB:m"į ĐË:äâAL6ŲŽĀŒ¤Ķ—ĮVUq—w9ĖXUŒAžOĻúˇđa-]9tSĒdæ+rčĀĪ"ö>FzMŌü…@LŲĒ™ŗZŠ‚Ä9Üēą ˛Ų_™ é€4\ČL{Œ"Y6¤kŦxųfđ思î!,ō߯øf~¸āĨ–Ȩ ąPj(ÍčcŸÔÃŨÚæˆu𯎞īŗ'šŸ˙įÁHMÎõ3œÕŲ*XzRH9ãO'YíR[g#Qĸ#ô(‡áëŨĖÕøLAiķ× 0Ž> 鈸:Č?ˆ†vĶ÷vĘ7^ēnCžķu8mâŗM€mØ&€`ķøCŒzĨ˙É9Rąņ¯íĩ—üũ›éĪúBG]69+ÜÚ;UĨų¤X^v-Š/į¯ļȄ™œčŒÛ8Å}aņŖĘ%’ŪuįŠ•/Į*ú,–¨Xšs7•ÅũV/`wđåÉģŪpd@YõzÂĀtË0]Ą{ĘŦ2Ōrš5‚üÁũ€ũgÔđ%įė#Õ5ØĶŠŦ.Îą;,W< `? U'Sg…ŽĢ›¤TM†pkWŗė”Q}öIz¸3~ŪÔ)$g ‹\RŌz_W˛ũņ[嗇ˆŦõŠūŌĖ~ō'ūŦÁK .^‹O*¸M•y¨Ö]ÂŌ&šąTjbnÎeŸ …ČUUrH,0™›œqí§G>\k˛" ÷JĶ9jö’œŲl.õĄbē\ãåĐdŌI~ģøäOŠŅšĻ -lķ_˜íãBļ#đUÁÃÕ΍oEĻnåĄĘč1ÛęÔK^ŊPā­Üׁųszĸ|]o%ØîĻ ¨°€ßSŋęŌˇ[ņŪ7/Ōæ§õrė\’KüLĐÅģrņģoú~2W`Ąš”y^h9˙p<|zŅuØū‡ąĪŽÜ™$īįX\Šx“üûIĻ]ŽD&ŖŲûHÍ´ã\ 1lĄÛā2Ǫφ\mũŅo×^W3q6C~$rūØŅŠOxÃŨ”ô-Œ¨ pƒOßõģ¯)ŧOÚ—V|Ha°Ég' }„Ņąo>ÂząĐōoR—ÖķšDd`ßN„Væ€4v7ČQAu"X­ĪĖ6(\hĻå×)L˙č…ŋOÚĘælÉŨ×ųˇ˙cL‡A^SŨ9rÃKųõÔ=1@=îC\ŠÎį–Ų÷Äms"ŽÂĢäz_îŖŧ1jÎĀYCȨņw@^Ú*V—}&XĮA›&ęrÛŌ>Đę珧ëÎUîļuz„7;JÁØ4Ų Š‘„îæ>30ŊS…*°ĪĶ™ŽĖĨÁ#;?+ĒT‹ĸše餁 L…[žš(įôZŠØ|ęQ)Û:52Øä‹Į}6 ™ ;9Ą€€@‹‰ŗ\SŠÕĪg‰đɟžāĀÔû˙zræf¤ZF€;Ø`Zũä(!Âęí@Žé@mļėĻd‘ Ā›n%ęKÍĘmB§4…Úū`Ŗx ģûšŒäĐGŖЇŋs—ŧË6–3$F ~ŠĻ:!°yÕÂō lec(KÜģĀĸė.ÍēJ‰Áaë­iä=yØO:6ŊÎ|0nÄđ™ž¯^ö:Á1kŖQ1™5ƒˇę%Û}´‰‡ŋąâē€īÃ%oE¤ŽyžŒ”ÁL“ īĢlā5v˛*|E÷Ą+”Fî~ŨsŠ)ķ™°L]œõ.ãĶaw_…—ˇ…øH,}Lnņ˜lČŅ.yXģ&~„TĀ:qbRŠVÜM˛ŦH~DEŊ~$/×l”,v$WxãŧdúŅåŅ į¸yÆč)›Đ@y#Gm͌æßˆ”'] €" |ˇģ'Ÿ3:?6˙ƒķ?ũ~.9ŊũąPФ”ŌĀ돍tæ;ˆfjVʕfāĻe¯Ūƒ{žq<’2ŋšŨđ˙(åxOŖdßߨ’™wVwÜ {ŗ ÄcE;3îSĢK A: CMí#™ƒ‰Oƒ~Ä9š•õē÷0ŋ6‘}n'ôĘ'ļd%SÖįæ…8‚īâ”ĪøPę/ҜĀߗģãh×ÍGą´ã[Éųõ[c€3sœ­ô=ņT‘CcƒŦAÅ\]o 79e;ųWŅlŦ‰zŠķAŒ Ä\ŠÔĐĪ´*0 _RŽĄ+[s8ŠüÎ0ŅN Nôü’ŗKׯæ5ŧ"zŠšč+nōļ_8{!xUŅ-ĀL$ Ķjt=¤)8&2ėCņNÄhEN°;ÔKø_'꓉ˇŸ61-ĐŪ™’hob˛á î(^Čb!-6 •hÕ)¸PaúË´‡~ZU˜XēUŌ ŠÖ k0+ĄIÅõ6ė8ž›åÄaĨËũúrÜø Čô’ęéÚāĨŊAžōíģH–ĩ•Qc(ĩCÉ)vúĩ\˜|-a"‡b8†ëLúYØŅ )Úņ“šŊ qŠ͆ÖxLĢ8&íšāo¤z-ŧbXÜ1û …õʇŋqЋ ›_…{Ŧ%}øGČãFŨD‹b͝v!x9 ͅģå֐Œ UYĪį°j|iČųøAI=ef˞­0jœđZüĐĨ^øB¤ę,ŗ9HŌØ¨û'žĻ A֓!ƒœVé"mŪH°¨)dŽ‚.¸•L¸ ‚Ø‹˙9ssŒ[ú+Q Ĩ"įå8~áÔ3‡Í'’ŅāzÕ˙(Wœ6Pâ×.ôĒyiÍé ņ%Ļ*NόöMuT’ŲĻõˆ,ÂōWo-[ø<)ŸīD­SĸHP%š‰šŠČ‹ŸNåDiąß ĶÕ˙ Ŗŋū•é´Ļ­šh¯ü:§áz1ˇå€Û 6%-̀ĶK–Õv€‡|īpæĐ\J sՐ”zyõN Áëû.âœĻ+ƒp9Ĩ?É,K‚m; ĸ‡ÉđėW†ŧÛ~I<ܡ6­žÕ/=pÚ]RNLË$uaÅô͟ŒiƒĖ“ē°Î—†˛^…VŖ]đg„úÆĩ—Ŋ? b â  ßg‹g‚:ií¯y™odžÃDąWœPį˙(ē–ĸyÄ˓Ŗ¯kâĘaĻ1YŅЀe× dHR3yą†ŋ_}n­6†ĸ¸’ú`PÂÛYĸj-zCāHhlM¨-›Ī`•}š{Co|šKļC 4#qKkË=lPšDŽÜá´\ĸbß/&¤Ā YJļ^î÷Nũ ÚßjąņŽüĒΚ|[ô‹í'¤ēgž—uj Ņėœ¯UjË#šY] ûųpŌ„5Ķ-ø‘/Ülj ŨO-wžĨxFtŨė‚–™šä؆Ÿræ^HK#Qā‡6Æå´t0H{ŲĢ)DsYŸ­Į‰•Oá›ĸTø‚ãů20-äFÔqž¤$tĄ9`%îYŽLŪã!PSNŸîÚPÍ<}“ԃ XĨAMī{°[ø5â$Ëž‰ā!õ ĩÚ€~rČ4?F}ĪÂyĮ„Žëū–×#ōwמÕüû4ģôšéˇfž¸/vˇ:m' küft[=>hĻĐ:bßoĪ÷w_ -sÁ|ۊ6A˙G‡ą÷(# Ē3ūĻÅ4ĄšĪ¨UæÎme*™đ¨lŗÖˆ[G‡b•}ÎUŊ8RÁ'Mģ}1tũŗŦú-å=P´$€ķBäM(-ŗÁŌB­\lQĩ^Û@€õ–ãÛwī1øyô:ACŌüč • š=Û1Ú^›Zb_JšÕŋZ›?L|¸Á–S LŊŲ+ˆd‘Å/gÎh6¸Î‘íØ4ÍlNAÔFÅōÄÆ“ņŖŨâp`Øˇ¤)`íŪqÜĢ˙$OÍԝ!…ų(ŗŪR>đž}į4ŠæM Yƒėŧ=qÍb8Ida#-ÃņĘŖtŌa8ʨ@g×cõ“YMÜ-„h-™ū[Wē;ˆ…=ũb%˜A_žË5]l§#b=Ë{ z=ũđÃŧ Ö`"¤ZZųiS…˙e–O˙øš˜ 2SĸY“ $%žŊ/íX30‡2øj\0Žú܂âÎy*|˛¸ërsĸā,ŋjaģ9ÍĸüĸáhæįŌC°vļÂŋiOĢū}\“’1žŌœA6ÅējΈ7ę ™{÷!2¸YÍ´l”Ôz°FÅļj¤Ŋgv:Ę' J^ßŖÆß”9|ø(5RĀÎ*˛°ŸųLmáˆLÕ/õ|HĮŨ‚üą“!žļkŠG Û„-¯zž6It7õÂßVR#ęđp4Ô-gžCŋF@pO˙2øāˆ0ׁÃY™Ē%<Ļ*ĪuôžygĒāėŠØočuZîgYĨX'(M(Xrß9ÄN­XTŽ dnx¨žcŲõցbu$Ž1-hS9č2MÜ=ž˙>[øÖfMs51AÕōsë9ŖŽrV&šiĄx¨ĸBoŋŋ:u`Ĩrŋņ$ž5Î×V~áHøŒã-KôÖzîA˜Ë?íÖ/X'ø„ô9_‚îˆöF:PDsM Il}ĀŖNuîĸnpßc9HH\ŒđŦ“ķØÕIâ%¨&Ų‡t"׈YžŌ>ŗĘ5ûú?Æģô×T˛^‰s11p䥹Ķ Z~ âņ}ã˛˜ŦomĀšdōėė˙- Ė`¸M‚,ˇ7Yŧ¨—2ē¨!raQ=EšīŖŖī&gļoÉÖTyúōW“ZXüŒ3ņ÷´k5ŗT9_Cž†€pēØōAô)›|:˛ŲeHĖ”ĩã ģ§ö1ßįÁ(ŋęŽŨ9=Ÿų@f@ږmT8ėˆōš4‹=*?&ˆŗéWLéPŨ›ÂjgvdƒŸ §öE!dãŽaĄwŝ •ĻkŅüwxc˙L cs. §Īč-V/j‘úĮęmˇ•9Ÿ!čæžÔOāefyä6;÷ū߃yáĪ}Äh4čīœabÉdXFVŪËZHTVhāļ+Ļ­ĸ NšŌØ!īžŸ&ˇGŌ’kŽķBŅThF!ë9öSĸGvĘyĪr9s ƒ:áxo|ČmĶOIt'”üKĪLánB䏐žLp^)Ņ^úz´âüŌgúΐáˆËFäꅙÆÁŌ4<0ŸL^9ĖŨŠlŠĨĒUEj5YŅ9h ÷d˙uFdȝ+›Ū”ŖšcžálĻĶa)F:‚ QâÃōn^¤¸‘.Čũ š9É\`§$ÔoS²¨Í`M8P§ģģ'ĨßõhCUŒm€yāI0M6¯™Ņ˜k‚ųūđÚô!Āûp¸ÛzÁYF8œ%ųÚW<õ)@#Æ~‚yķ(āQ÷Düa|ĘšŠDĖĄa+1ēeúŧ<…Û¯]¨Å­%Ÿádëņh˜nŽqõҘožŅ[Ą ĸxĄŠh#Õō5œk&gHŌŖ â ŗ­^ę7?ĸĶÆ]į@Ūw`G2…ÆÄ!Ŋš0`¤}o…HīO§˙…`˙0ĶÂžÛøņ€wŅiųäŅ2=o)ĒCūy<åڂąŊĻjāĶ>@šOoW4Âø0ãĶVĸį“)'RwdF’…]TQæˇŦ*ũ*L ŗ8œ÷1Ūŧ‰Éāƒ|žÚ‘¯ 4Ūä/},’ËjŽ“o˜ŠØlķšĻ…ūJģŋ׀ƒŗ?€ļTf€7‡$Ŗ´LãæS{ˆySŦPōõąĀĶž\Sኊ+Ų}[ j‡<Ô÷‘Xųâj´+;ģŋĻąãë[wžÎúe䠈eĐ%E7Eļs×Â9ƒ†—īs˙ōwöĶYB‹^įĻÖ_đķU\bĸŗlI Š@ˆ?ØÔ°/‹GF[ÉKî#ú ¯9f5…#Ōˆ÷Ĩēu†pĖGĨÅ"ŗÜŽ,~,ü~¯Ō×,éL37šö4ŪČĶ^  1ëIå)Ēí3o&uūXĸa5ˇ}EĨā’ÔžũÄāÂÚĸ‘¯ƒˇ´áiö‹ß‰Ļ!ydP|Á˜Žŗ`Ķ1ß§ š{Mr {D*‰Ë7úōo^*}vÔÛD…v…Č#Ŗ _’åŲĩĻ:ŖāCAÂT5@Õ ;méˇ/ ŽŖ×Q uôGÅī‰Å$ęl„īëuc9ãÁŨ-ÂfzJŽŧøûœīS‡ŦŲ‘T .Øö7ųryzōCĐßkļ÷ˇāø˙âÕ@oęא°[QĮŽ,ŲšģĨƒŖ€š›Ĩ2{ÖwyŪjZ˙eVõED‰æŗL‡ā ĢI}' "DxjĒ1ĸvՌ­b‹:>Á›Ėđ3f 7#P”2¯9}mŧīģŗÅޭߟĒäôf<aƆģ—cúÔüęÃ>ũđš/YĻg.˜|įsÎ>بZæjŌĀôA€˛WXÕ˃œ FčĸꔤņøĨ¨;5LŪrˆ.ųĀĘ'Ä_ķGbÕĄ×Ė%zŒ“1@Hg˜MŒTĪ4eZ5éPÖĄ÷ŋˆ]MŖĨkÄ0¯æęB °Âp=­ÆîbÕMÅõĻÕŦNm¸mKĶO=+Ũvĸ )žĀ—§#tė|æ˜Ŧ Öļári8ŽVĪâ…{cU,ÅxáZŅ\qđ’T×:ÅcøP!/M7ûjŋ °•~Iähļ°õC!ÁžąE—É÷™”R;Jœ8É^Qs‰D[Ņ–Žl 6l)§Á†œU(–b@†‰Y$vŅÍä×Ā(\'w*ÚÍ0š-‹/lRīā7ßčÎ_T ĩŦļC6ĨįßËD\Ūöęŋi‹*%)´ ÛÎMŌPŋ—ˆŗ|‚đk û“ŲžWuĶB7r’Ŋ9īŋö´+@ūC°üõA÷_d ¯­A<;Ģ:°…įŗî\`ŌūWīƒ\•æÕ}žk0˛ĸR¨§Ųu¯´įÚõ‡ļ›ĪŪNĄ{ØËŽį×ĐÆŸXļQ‚bxÛ-Å!IŦ‡ķ_ĪŨ’~1cw}GēUL<-Âq‡aq•uRÖķÃuš5Îe/¸g×ąå°—ĩĖôā#IŗâÕãVGoã'B&ÄLM•‰uģĀf›OtĘģw Rf-´Kâ‚BŲ+a! ß§”§õōŖ˛™ēĩüŌ€~ ņ(¯!]CÆÉ )‘kĶL PYL 4æG^ÛĨ$ąîņ= p–퓆ķ9ävú­{†ÎŒH$å—įgˇšc’ļįü‰"5䛋AHÉc g HL+Œ¯ļQ*y^OAĒŠHŊ rCDåã*|šØ˛ÄđbŦž[ ^ŌãĪ—jŌu-K[‚€Iä›Įt ˙Lč>Đ+¨l<ōîÔ­å?rÕGy.ŊRđw[6UOeÃMž<ŠĩpŊ ’YížDxąÚší ŊIŋᤐ`ģȂģN9ļ—öŸãĐ~ūčÜ7čl׍ ˇÎ°|š$Ūá,ņ*˙JēüČՔV?–C|œ{ÂÂzԐ7Ė‚Aņ‡&;ųäÆÂڙ¸`'(†~/Û@V¸öŽb#ž2 ˜F­2ÜH/›WĮ‰Ũ{b˙>õ˜ęÔÄVé´ĄĶ´~3Ĩ`gNí&D•§‰‹õtJ÷…oZ{fûhųa  BģÕ`Ĩw -ŋķâÜԛڄœ}^ĢĘĹŗ~¤™ûĢ1E(ß ÆŒŧ˛ĸPÖÜôb–<ÕÕ/á_! Ëb?FIAį•Ūĸ÷ŽŌĘų¤ĸ9ΝVãFÛ§qĶŠš,n!˙jų~mrN’WëLcĢåˇ#Ę;æUšhė‚ĸîlHČÄ#zéĖûëYAČoúĪ`ßūĸ‡ũjpEĪũæqp9r=×Ä`Âk>‚įgĢ ģ­’ļũĸ]ČhöM_D’ÁâŽt~ÖĀė8BN}§ĘZ×ۈîB’4˙b>ģ­gČ|. 9ũPīa-ŦëˆØB?q@Ķ}šĘ<×ĖÚ8 Lø23īøĨĮĀøËŽ°€¯( ‚öŅīshÆK™ŋFžŧ;žjÁŸv†›OÂ6Ēę)DîėV G^hķuųRŨŪū/áŽv0”§XÜÖô%ĘŌų+iãcŋ pVĐ@ę Bhh"úÆ!îLtvĘİhe;ŪíĻÆ7GĩJ¸wį¸C\ äШavĪÃPHĸhÜU8OL‹ŽĒI÷Vŗ,ÂÎą/ē”íjvŋmE;ŗŊ>qJ˙]ąŪ/n3ĄOÕÔ~š]r{ĀhšŲˆŠ|AĐęĸt™ŗáÎÖCáe0_÷ ÖÅĒN!;Î~˜=ŊH[nÎnņëlČ'3hŋZ¸ŧqĶlSÔ˜w…ķA. 8ŒüŠ— ô ĩ愞#d íĩ” oO¤gs[÷ÎiØČâvûŧj1‰Vš˙ŲD“ōߝoEõđĒzäā¤(SÔ\sæÁ=aĢ€$ĩIæŠ#’Iœ[g&Öũ1Ų{ņVķbST”Vƒ^ķ>ętĸ]Ÿ"1 gą<ŗ5¤ŋŠ }Ņĩô9Ÿ='=°˛ŠcŌåÉ× Tŋ=Ą¤Š1P,ũۚļ`˙¯žAŪ 7ø|`‘@Z ŧæqD x¸ ŨŽõ ą3~fŽÍ&Nô t>ô7|ŋY(‡ Å1ĘŪĶ.Iĩ G-Ÿ^˛ģõę\…hY´ŌŦgužĀ ÂŽnLj‘…0e†°,‡†C{=˘‹_/mj'ß=G"0õ˜a›IeūŦš`ë˙D°¤đD¸3"-š×^[ãˏŠ6jB~7*ņč˛ÍŌ8Ô¸ëŗžnŒˇûē­z›ZÆÆ‘0p|<ŸÎP­…ĩZ1JĀũBŦ@>{F­}Ā\ ŲA×,¨{HŨSū‡ü˙âņũŽËû_ī¨HžŖŽú„–úŽŋmyzĘõwíŦøūÚ ú{ę5ũ´(I~Ģâų÷ũ|û÷õ^üÔĻŋŠT$ūĒÖ ęĖÖĒ­ō“‘"BŊEė‡:ߙČų ?\K|^˙r+ãv?o†_¨ätŌõæįÕI@žÜˆp”w™:}Į8fOŲŊQxŊĐ1Eˇ‚æ/ѐ‡ˇ7ãAâmŧža&ā,ę띞ˆtĘ ÛŖęXwčV­ōbĸ‚„zņŖÄ ķÚøUŅî+j‰8Ϛ*8õ{āšs D¨$NūĒÍk^õÉôįâ†løŦYÍKézž^­TĨĩŊõ‚DÍŠķ›~áâÃŖ}aÚNšqĶuķ}y—Eâæ6eaë~?3ĻXæ¤Õûōō‚ō~o›ė”žqO—ŗˇ˙.ė†8„„žd˙yąĪüt´ uō}Đ |̍ĐAbļëæųžo›æųžo›æųŋQTķ|ß7Íķ|ß7Íķ|ß7Íķ|ßO—ŗˇ˙.ė†8„„žd˙yąĪüt´ uō}Đ |̍ĐAbļëæųžo›æųžo›æųŋQTķ|ß7Íķ|ß7Íķ|ß7Íķ|ßģC2}ۇgĀ}­Î î€ķF0ķ“ öHi= šŪâ6”īö$÷›O øŦįŠxėõæAå†1*ÜíŨé)BčYAREÄæ_×/˙Čuy–Ə´M Ļ{ë­/žQ& ļÅÖq ×,ƒŅ#–HCĩ4ĸÚŲõPŒ(Sō1ŅO„jė—Đ<#Ôä`)rW“[ŗ&Ą@Ģ"5Gæu–ŧßŦjĸÅ_îŒkôRpÖÜDĨ nXŽöŒ~ŠÛi4Hm51ë“ty=ĩNÆ:3įŧüäQV˙_&øÄy§æøjø˙įŧüäQV˙_&øÄy§æøjø˙élOX8ųā¸īøŽúAëp/ä°OĒÖ:CH]†ûÜDfƒķo@UúÕŒQ?û[ˆxY‰€ĩíjp¸Ō<†1{Ŗ{J€ōęFFƒĖBŽ!ŽĀÆE”Ss|ĸiIŅ›KárIāãá q€Pî¸*,ŪO_\GÄĩŅ4wäÎCČ0`3ʎąßÆLS錋SŽŒ7Ŧƒ‰O_nĸĐFÂÜÖ]ū*8>„ôÖstĩā>ã„HŒÖĻÁH›É6Šũ´hЉ1MîØŠ>8ø>öëøĐLõËA9íû’O—‚H’ŦlĀPŗ˛×ŽÜƒ¤2ŨąiĐÄ/ËM[Iu@ÛĨ90›Ęē‰rĘ wÃP.†5Å'ZéŧâŪŌÄQy†´rGü:!Wp^Dî´ī2H6Š< :)Z&ah 3‰t\ĶĪ}ÉN&GˆÄUH)›:ĻŪye5WÉÁŽ"ųžúĸõŠ@ō3â~uÕķ7Â9—tįˇ_ ÕëķēÜŦg–ÉuLĨ,ŧK$0Œ¤™ŗ?؎–aQ,iŋTZĸțd†H `J ÚĻ5™M뀀D˜†ūE”˛ļ€ø¯w•Į×ûP,$iîŠøŸ˜ī3á'ˆĶu~ĸ­CIdVcMOY HŲĻFļ„#0˙W=;ܒ›2)]q¯â&?ĶëFš2˛Ų˙&Œœ),6Ą˛]=ȌãQufūGoȨûūDįč^ūŋÚxŨ[SÔŧ §œú[Ō,~s2Š…ģŌĸ4IF>žcfØåõSĒBdaÆžz}/ömȚū˙_b’!¨\Į›ÄS|$™eĪ!åąˇ˙î:#Ã|K?>zƒœ˜G÷<ŧ‚L@ĸ3H¯U°mŲ–ŽĀiĘĻš˙¸†ßåāaĪŨ‚eŽ=UKÔw{A‡yrÉļpŗLyw74¨Gę6ŨÉÉb| °›q—*ūCo!ėõĶôîëŒÅg)Ũ%ēųRĄbrƒ„‰lğÂ+÷¯`Č:4)m¨–ˆ`Ŋ†‘>˜ÄY‚¤ ]ŪĨĸ!ruŸ‘¸ŧs’’Ķ-0¤@~gĖ]RnČz=‡Ā”ģŌ˛ų}ęė+ĮU”¯?‹ Ì´_ķZZ„ÜÅúāÄjr%0Ņ*ģJ÷ˆîÁiũ$ }5ĸJ°ĮĒĢņŖ¯ō¯ĪæKŋ4qxmRņqß1Ä䞚ōčgßøĮtŊ¸TZ[ŧr CųƄŦā˜Âä/áÅ@`KĘŲA¸'Xw-L$Čŗb͆ī_üō¨Ũρ`ĩN—…Ī–??(Nˇ'^Į‰"Йu ?LC4ų,_\ãK4"F[}{T:¤ßđ{ÎČXąČŊÎ šN_,š’#üŦü;ŧ_-ę÷Ū+z1´Ĩ—Ö sn<ؐKORŖ%˜0ģ.ԉųëCāSŽŅŊĐŽ8žĄY´đõĢŪãą1NgĄÉHÁ‰‡ĪŽŊ{ŒaŒ8/=;ÚĪuoísNzÂnˇ:MĻLĢ#đ7S™ZOŸ*&JiāÕ-ԜÂŒō@lČГōoĪķÆō§ä>šžåûE'°bâjZ'´ô˛Ž€#@hb5rƒĶVˆ{ČđÅätAHąųYąŦšÕ8BŒy`lē%(â$ã›ŌEg039Ø7˙ÆÁ3DhmS\ūü^>&hŽģĄšÅ›čœ§FČiâéme:¯FP|āi'3ˇHƒsĶËć•qĩ)ūũ}u'`l|Ŗ„×c-”ĮGd´÷ž?zŪ1F<šŧėAø79ZĨŽ†~MZ“‰@­¨Ũ!w­Ōt“¯mûōo|€ĪŽzĸPŠ2&lņj’s3ĸûY¸€¯@J ŋHå3z)pjėÜlV˛%tpYū pğ‘čŒ&î¨äL]ÜŨ‰Y@Ų˙5ŊšĀņÉgĪēĪģŸ#ī3Ō|ŠÚ< ė]÷÷ŒŖ´JHë0ŒčjšŦ§%¯CĻwØ :Đ=õŲõ.ąT˜ŗ [“WáÂ@/fĀ[§„?‰ˆ%âüLŒ™Ņ•hC!ú3'žziĩ§4$`Ąj(sŽ KÚx|}ē(aúm ÕĻŦ'PšÅÖfMāå ö1 xHT´¸ˆę^ŋĩ.vâmķ5åÄÚg0ãY[Ėī(@mâjëĐ>b\TnÚ_ėZ•'Κ”ôK‹9Š @é,æaĶ?Ú;I’ žFĪĖ5b4ĩ˛Ę¸›Îw¯ .âYĶŸQ^˙DĪ`öׯ›ØĪĶą>ÃWÜā˙Sĩ‰ô%+ˆ`šĮë¨ŅI;ĩ9-ļbĪ@sp˛nD†đ<ĮÃÌtjÛ6ž•ū Ī‘˙âT˙Gâā†Ę j L’ŸŌBgtŖ6Ļī7ŋsÚ A3”ôī‰dû%ÖH¨(šДČKĄĘŅWËg4wxÕL*.žÉjö´˙33Ä"ġ$×2üBUî-ëļ3ÖSnigŧ@6áËĖē…ŽÖÁ”šĸŠ@9tö›lv‚÷ĩyœ¸[ĩj['qhAšōĩU†īĖŖV]x;äĢ‘-Ęā&)ÁZˆŒ@¤āŗÉs€Tu¨%0§˙ŲNagstamon-master/Nagstamon/resources/nagstamon.ico000066400000000000000000000226761505160700500227360ustar00rootroot0000000000000000 ¨%(0`               "*18?DJNR V!!X!! Z!! Z!!!Z!! Y W UQMIC=70(     &0;EO Y##"b'&%k**(t-,+|//-„220‹442‘664—875›986996Ÿ997Ÿ886775š653•43111/Š//-‚,,*z))'r&&$i#"!`VMC8. #   #:"""K###U%%$`((&k+*)u.-,€00/Š432•775Ÿ::8Š>=;ŗA@>ŊDDBÆGGEĪJJH×MLJŪONKãNNKâLLIŨJIGÖGFDÍCCAÄ@@>ģ=<;ą:97§664331’00.ˆ--+}**(s''&h%$$^###S"!!H4   ;;;lBBBÛCCCũCCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCC˙CCCûAAAÉ777JCCC…CCC˙kkk˙ĩĩĩ˙ĶĶĶ˙ÖÖÖ˙ÕÕÕ˙ÕÕÕ˙ÔÔÔ˙ĶĶĶ˙ŌŌŌ˙ŌŌŌ˙ŅŅŅ˙ĐĐĐ˙ĪĪĪ˙ÎÎÎ˙ÎÎÎ˙ÍÍÍ˙ĖĖĖ˙ËËË˙ĘĘĘ˙ĘĘĘ˙ÉÉÉ˙ČČČ˙ĮĮĮ˙ĮĮĮ˙ÆÆÆ˙ÅÅÅ˙ÄÄÄ˙ÄÄÄ˙ÃÃÃ˙ÂÂÂ˙ÁÁÁ˙ĀĀĀ˙ĀĀĀ˙ŋŋŋ˙žžž˙ˇˇˇ˙•••˙UUU˙CCCúCCCNCCC4CCCũ˙ããã˙ÛÛÛ˙ÁÁÁ˙ĩĩĩ˙ŗŗŗ˙ąąą˙°°°˙¯¯¯˙­­­˙ŦŦŦ˙ĢĢĢ˙ĒĒĒ˙¨¨¨˙§§§˙ĻĻĻ˙ĨĨĨ˙ŖŖŖ˙ĸĸĸ˙ĄĄĄ˙ŸŸŸ˙žžž˙˙œœœ˙ššš˙™™™˙˜˜˜˙–––˙•••˙”””˙“““˙‘‘‘˙˙˙˙˙   ˙ÁÁÁ˙žžž˙]]]˙CCCęCCC CCCŒTTT˙āāā˙ÖÖÖ˙­­­˙ŠŠŠ˙¨¨¨˙ĻĻĻ˙ĨĨĨ˙¤¤¤˙ĸĸĸ˙ĄĄĄ˙ŸŸŸ˙žžž˙˙›››˙ššš˙˜˜˜˙———˙•••˙”””˙“““˙‘‘‘˙˙ŽŽŽ˙˙ŒŒŒ˙ŠŠŠ˙‰‰‰˙‡‡‡˙†††˙„„„˙ƒƒƒ˙‚‚‚˙€€€˙˙}}}˙|||˙{{{˙ƒƒƒ˙ŊŊŊ˙ŽŽŽ˙DDD˙CCCPCCCІ††˙ååå˙ŗŗŗ˙ŦŦŦ˙ĒĒĒ˙ŠŠŠ˙ĨĨĨ˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙¤¤¤˙ĸĸĸ˙’’’˙}}}˙|||˙zzz˙‘‘‘˙ÆÆÆ˙YYY˙CCCnCCCĢ–––˙ŲŲŲ˙ŽŽŽ˙­­­˙ĒĒĒ˙   ˙žŸŸ˙¯¯˙ožž˙oĀĀ˙pÁÁ˙qÂÂ˙rÃÃ˙sÄÄ˙tÅÅ˙vÆÆ˙wĮĮ˙xČČ˙yÉÉ˙zĘĘ˙{ËË˙|ÍÍ˙}ÎÎ˙~ĪĪ˙ĐĐ˙€ŅŅ˙ŌŌ˙ƒĶĶ˙„ÔÔ˙…ÕÕ˙†ÖÖ˙‡××˙ˆØØ˙‹ØØ˙¯ĪĪ˙ÍÍÍ˙´´´˙~~~˙{{{˙{{{˙ÄÄÄ˙fff˙CCCoCCCĢ———˙ÖÖÖ˙°°°˙ŽŽŽ˙ĄĄĄ˙”ŸŸ˙*āā˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ūū˙[ââ˙ÍÍÍ˙   ˙}}}˙{{{˙ŋŋŋ˙ggg˙CCCoCCCĢ———˙ÖÖÖ˙ąąą˙¯¯¯˙ššš˙FĖĖ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũũ˙ũũ˙ũũ˙ũũ˙ūū˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ũũ˙ũũ˙ũũ˙ũũ˙ũũ˙ũũ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Œ××˙¸¸¸˙~~~˙|||˙ĀĀĀ˙ggg˙CCCoCCC̘˜˜˙×××˙˛˛˛˙ąąą˙˜˜˜˙ęę˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ÉÉ˙˙˙˙˙EE˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ĪĪ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Sææ˙ģģģ˙˙~~~˙ÁÁÁ˙ggg˙CCCoCCC̘˜˜˙ØØØ˙ŗŗŗ˙˛˛˛˙–––˙īī˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ÉÉ˙˙˙˙˙EE˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙NN˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Nęę˙ēēē˙€€€˙˙ÂÂÂ˙hhh˙CCCoCCC̘˜˜˙ŲŲŲ˙ĩĩĩ˙ŗŗŗ˙•••˙īī˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ÉÉ˙˙˙˙˙EE˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ČČ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Nęę˙ššš˙‚‚‚˙€€€˙ÂÂÂ˙hhh˙CCCoCCCĢ™™™˙ÚÚÚ˙ļļļ˙´´´˙“““˙îî˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ÉÉ˙˙˙˙˙EE˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙FF˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Méé˙ššš˙ƒƒƒ˙˙ÃÃÃ˙hhh˙CCCoCCCĢ™™™˙ÛÛÛ˙ˇˇˇ˙ĩĩĩ˙’’’˙îî˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ÉÉ˙˙˙˙˙EE˙˙˙˙˙˙˙˙˙˙˙˙˙ĀĀ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Méé˙¸¸¸˙„„„˙ƒƒƒ˙ÄÄÄ˙hhh˙CCCoCCCĢ™™™˙ÜÜÜ˙¸¸¸˙ļļļ˙˙&Ûė˙ æũ˙áũ˙āũ˙āũ˙āũ˙ąČ˙˙˙˙˙˙‚ú˙…˙˙€õ˙˙˙˙˙ ˙…˙˙…˙˙…˙˙…˙˙…˙˙…˙˙Hšä˙ąąą˙ŽŽŽ˙ŒŒŒ˙ĘĘĘ˙jjj˙CCCoCCC̜œœ˙ããã˙ÂÂÂ˙ĀĀĀ˙………˙e ë˙_Ĩū˙_Ĩū˙_Ĩū˙_Ĩū˙_Ĩū˙_–Ü˙___˙___˙___˙___˙_q†˙_ju˙___˙___˙___˙^‰Ä˙6‘ū˙sũ˙kķ˙˙˙˙˙ ˙oũ˙oũ˙oũ˙oũ˙oũ˙oũ˙HŒã˙°°°˙˙˙ËËË˙jjj˙CCCoCCCĢ˙äää˙ÃÃÃ˙ÁÁÁ˙„„„˙jjė˙ff˙˙ff˙˙ff˙˙ff˙˙ff˙˙ffß˙fff˙fff˙fff˙fff˙ffh˙fff˙fff˙fff˙ff˙ffũ˙ff˙˙WW˙˙õ˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙GGã˙°°°˙˙˙ĖĖĖ˙jjj˙CCCoCCCĢ˙ååå˙ÄÄÄ˙ÂÂÂ˙‚‚‚˙ppė˙ll˙˙ll˙˙ll˙˙ll˙˙ll˙˙llā˙lll˙lll˙lll˙lll˙lll˙lll˙lll˙lll˙llĪ˙ll˙˙ll˙˙ll˙˙llų˙RRR˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙GGã˙¯¯¯˙‘‘‘˙˙ÍÍÍ˙jjj˙CCCoCCCĢžžž˙æææ˙ÅÅÅ˙ÃÃÃ˙˙uuė˙ss˙˙ss˙˙ss˙˙ss˙˙ss˙˙ssâ˙sss˙sss˙sss˙sss˙sss˙sss˙sss˙ssŽ˙ssū˙ss˙˙ss˙˙ss˙˙ssú˙sss˙sss˙ccc˙<<<˙1˙˙˙˙˙˙˙˙˙˙˙˙˙FFâ˙ŽŽŽ˙“““˙‘‘‘˙ÎÎÎ˙jjj˙CCCoCCCĢžžž˙įįį˙ĮĮĮ˙ÅÅÅ˙€€€˙{{ë˙zz˙˙zz˙˙zz˙˙zz˙˙zz˙˙zzã˙zzz˙zzz˙zzz˙zzz˙zzz˙zzz˙zzz˙zz×˙zz˙˙zz˙˙zz˙˙zz˙˙zzú˙zzz˙zzz˙zzz˙zzz˙zz‡˙tt˙˙aa˙˙QQ˙˙DD˙˙::˙˙--˙˙OOâ˙­­­˙”””˙’’’˙ÎÎÎ˙kkk˙CCCoCCCĢžžž˙įįį˙ČČČ˙ÆÆÆ˙~~~˙€€ë˙˙˙˙˙˙˙˙˙˙˙ä˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙ú˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙‘‘á˙ŦŦŦ˙•••˙”””˙ĪĪĪ˙kkk˙CCCoCCC̟ŸŸ˙ččč˙ÉÉÉ˙ĮĮĮ˙}}}˙††ë˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡æ˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡ß˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡ú˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡‡˙‡‡“˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙‡‡˙˙——á˙ŦŦŦ˙–––˙•••˙ĐĐĐ˙kkk˙CCCoCCC̟ŸŸ˙ééé˙ĘĘĘ˙ČČČ˙{{{˙‡‡â˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽí˙ŽŽ¨˙ŽŽ¨˙ŽŽ¨˙ŽŽ¨˙ŽŽ¨˙ŽŽš˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽü˙ŽŽ¨˙ŽŽ¨˙ŽŽ¨˙ŽŽ¨˙ŽŽą˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙ŽŽ˙˙™™Ų˙ĢĢĢ˙———˙–––˙ŅŅŅ˙kkk˙CCCoCCC̟ŸŸ˙ęęę˙ËËË˙ĘĘĘ˙˙‚‚°˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙••˙˙””ũ˙ĨĨģ˙¨¨¨˙˜˜˜˙———˙ŅŅŅ˙lll˙CCCoCCCĢ   ˙ëëë˙ÍÍÍ˙ËËË˙ŸŸŸ˙zz{˙……ē˙””ö˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙˜˜˙˙——ū˙——ķ˙  Ã˙ŦŦŦ˙ĄĄĄ˙ššš˙˜˜˜˙ŌŌŌ˙lll˙CCCoCCCĢžžž˙ōōō˙ÎÎÎ˙ÍÍÍ˙ÉÉÉ˙™™™˙{{{˙{{~˙‡˙€€ˆ˙‚‚Š˙ƒƒ‹˙……˙††Ž˙ˆˆ˙‰‰‘˙‹‹“˙ŒŒ”˙–˙—˙‘‘™˙’’š˙““œ˙••˙––Ÿ˙˜˜ ˙™™ĸ˙››Ŗ˙œœĨ˙žžĻ˙ŸŸ§˙ĄĄŠ˙ĸĸĒ˙ŖŖĢ˙Ļϧ˙¨¨¨˙ŖŖŖ˙œœœ˙›››˙˙ŲŲŲ˙jjj˙CCCoCCC§ƒƒƒ˙ûûû˙ØØØ˙ÎÎÎ˙ĖĖĖ˙ËËË˙ÂÂÂ˙ˇˇˇ˙ļļļ˙ļļļ˙ĩĩĩ˙´´´˙ŗŗŗ˙ŗŗŗ˙˛˛˛˙ąąą˙°°°˙°°°˙¯¯¯˙ŽŽŽ˙­­­˙­­­˙ŦŦŦ˙ĢĢĢ˙ĒĒĒ˙ĒĒĒ˙ŠŠŠ˙¨¨¨˙§§§˙§§§˙ĻĻĻ˙ĨĨĨ˙¤¤¤˙¤¤¤˙ŖŖŖ˙ĄĄĄ˙ŸŸŸ˙žžž˙œœœ˙ˇˇˇ˙ÚÚÚ˙UUU˙CCCkCCC|KKK˙ččč˙õõõ˙ÖÖÖ˙ÍÍÍ˙ĖĖĖ˙ËËË˙ÉÉÉ˙ČČČ˙ÆÆÆ˙ÅÅÅ˙ÄÄÄ˙ÂÂÂ˙ÁÁÁ˙ŋŋŋ˙žžž˙ŧŧŧ˙ģģģ˙ēēē˙¸¸¸˙ˇˇˇ˙ĩĩĩ˙´´´˙˛˛˛˙ąąą˙°°°˙ŽŽŽ˙­­­˙ĢĢĢ˙ĒĒĒ˙ŠŠŠ˙§§§˙ĻĻĻ˙¤¤¤˙ŖŖŖ˙ĸĸĸ˙   ˙ŸŸŸ˙ąąą˙ÚÚÚ˙ŽŽŽ˙CCC˙CCCACCCCCCôkkk˙ííí˙úúú˙ėėė˙ããã˙ááá˙āāā˙ŪŪŪ˙ŨŨŨ˙ÜÜÜ˙ÛÛÛ˙ÚÚÚ˙ŲŲŲ˙ØØØ˙ÖÖÖ˙ÕÕÕ˙ÔÔÔ˙ĶĶĶ˙ŌŌŌ˙ŅŅŅ˙ĐĐĐ˙ĪĪĪ˙ÍÍÍ˙ĖĖĖ˙ËËË˙ĘĘĘ˙ÉÉÉ˙ČČČ˙ÆÆÆ˙ÅÅÅ˙ÄÄÄ˙ÃÃÃ˙ÂÂÂ˙ÁÁÁ˙ĀĀĀ˙ÁÁÁ˙ÎÎÎ˙ŨŨŨ˙ĀĀĀ˙QQQ˙CCCŌCCCCCCRCCC÷RRR˙ššš˙ŊŊŊ˙ĀĀĀ˙ĀĀĀ˙ŋŋŋ˙ŋŋŋ˙žžž˙žžž˙ŊŊŊ˙ŧŧŧ˙ŧŧŧ˙ģģģ˙ģģģ˙ēēē˙ššš˙ššš˙¸¸¸˙¸¸¸˙ˇˇˇ˙ˇˇˇ˙ļļļ˙ĩĩĩ˙ĩĩĩ˙´´´˙´´´˙ŗŗŗ˙ŗŗŗ˙˛˛˛˙ąąą˙ąąą˙°°°˙°°°˙¯¯¯˙¯¯¯˙§§§˙˙GGG˙CCCäCCC)CCC)CCC™CCCÎCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCÖCCCĮCCC„CCC˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙€˙˙˙đ˙øāāĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀĀāāđø˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙˙Nagstamon-master/Nagstamon/resources/nagstamon.rst000066400000000000000000000020461505160700500227610ustar00rootroot00000000000000========= Nagstamon ========= ---------------------------------------------------------------- Nagios status monitor which takes place in systray or on desktop ---------------------------------------------------------------- :Author: This manual page has been written by Carl Chenet and updated by Henri Wahl :Date: 2021-04-04 :Version: 2.0 :Manual section: 1 :Copyright: This manual page is licensed under the GPL-2 license. SYNOPSIS ======== nagstamon [alternate-config] DESCRIPTION =========== Nagstamon is a Nagios status monitor which takes place in systray or on desktop as floating statusbar to inform you in realtime about the status of your Nagios monitored network\&. Nagstamon connects to multiple Nagios, Opsview, Icinga, Centreon, Op5Monitor, Checkmk Multisite and Thruk monitoring servers. Experimental support for Zabbix, Zenoss and Livestatus is included. The command can optionally take one argument giving the path to an alternate configuration file. RESOURCES ========== https://nagstamon.de Nagstamon-master/Nagstamon/resources/nagstamon.sfd000066400000000000000000000344361505160700500227350ustar00rootroot00000000000000SplineFontDB: 3.0 FontName: Nagstamon FullName: Nagstamon FamilyName: Nagstamon Weight: Regular Copyright: Copyright (c) 2016, Henri Wahl UComments: "2016-4-6: Created with FontForge (http://fontforge.org)" Version: 001.000 ItalicAngle: 0 UnderlinePosition: -102.4 UnderlineWidth: 51.2 Ascent: 819 Descent: 205 InvalidEm: 0 LayerCount: 2 Layer: 0 0 "Back" 1 Layer: 1 0 "Zeichen" 0 XUID: [1021 5 1214093225 10093350] StyleMap: 0x0000 FSType: 0 OS2Version: 0 OS2_WeightWidthSlopeOnly: 0 OS2_UseTypoMetrics: 1 CreationTime: 1459941430 ModificationTime: 1462171755 PfmFamily: 17 TTFWeight: 400 TTFWidth: 5 LineGap: 92 VLineGap: 92 OS2TypoAscent: 0 OS2TypoAOffset: 1 OS2TypoDescent: 0 OS2TypoDOffset: 1 OS2TypoLinegap: 92 OS2WinAscent: 0 OS2WinAOffset: 1 OS2WinDescent: 0 OS2WinDOffset: 1 HheadAscent: 0 HheadAOffset: 1 HheadDescent: 0 HheadDOffset: 1 OS2Vendor: 'PfEd' MarkAttachClasses: 1 DEI: 91125 LangName: 1033 "" "" "" "" "" "" "" "" "" "" "" "" "" "Copyright (c) 2016, Henri Wahl (),+AAoA-with Reserved Font Name Untitled1.+AAoACgAA-This Font Software is licensed under the SIL Open Font License, Version 1.1.+AAoA-This license is copied below, and is also available with a FAQ at:+AAoA-http://scripts.sil.org/OFL+AAoACgAK------------------------------------------------------------+AAoA-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007+AAoA------------------------------------------------------------+AAoACgAA-PREAMBLE+AAoA-The goals of the Open Font License (OFL) are to stimulate worldwide+AAoA-development of collaborative font projects, to support the font creation+AAoA-efforts of academic and linguistic communities, and to provide a free and+AAoA-open framework in which fonts may be shared and improved in partnership+AAoA-with others.+AAoACgAA-The OFL allows the licensed fonts to be used, studied, modified and+AAoA-redistributed freely as long as they are not sold by themselves. The+AAoA-fonts, including any derivative works, can be bundled, embedded, +AAoA-redistributed and/or sold with any software provided that any reserved+AAoA-names are not used by derivative works. The fonts and derivatives,+AAoA-however, cannot be released under any other type of license. The+AAoA-requirement for fonts to remain under this license does not apply+AAoA-to any document created using the fonts or their derivatives.+AAoACgAA-DEFINITIONS+AAoAIgAA-Font Software+ACIA refers to the set of files released by the Copyright+AAoA-Holder(s) under this license and clearly marked as such. This may+AAoA-include source files, build scripts and documentation.+AAoACgAi-Reserved Font Name+ACIA refers to any names specified as such after the+AAoA-copyright statement(s).+AAoACgAi-Original Version+ACIA refers to the collection of Font Software components as+AAoA-distributed by the Copyright Holder(s).+AAoACgAi-Modified Version+ACIA refers to any derivative made by adding to, deleting,+AAoA-or substituting -- in part or in whole -- any of the components of the+AAoA-Original Version, by changing formats or by porting the Font Software to a+AAoA-new environment.+AAoACgAi-Author+ACIA refers to any designer, engineer, programmer, technical+AAoA-writer or other person who contributed to the Font Software.+AAoACgAA-PERMISSION & CONDITIONS+AAoA-Permission is hereby granted, free of charge, to any person obtaining+AAoA-a copy of the Font Software, to use, study, copy, merge, embed, modify,+AAoA-redistribute, and sell modified and unmodified copies of the Font+AAoA-Software, subject to the following conditions:+AAoACgAA-1) Neither the Font Software nor any of its individual components,+AAoA-in Original or Modified Versions, may be sold by itself.+AAoACgAA-2) Original or Modified Versions of the Font Software may be bundled,+AAoA-redistributed and/or sold with any software, provided that each copy+AAoA-contains the above copyright notice and this license. These can be+AAoA-included either as stand-alone text files, human-readable headers or+AAoA-in the appropriate machine-readable metadata fields within text or+AAoA-binary files as long as those fields can be easily viewed by the user.+AAoACgAA-3) No Modified Version of the Font Software may use the Reserved Font+AAoA-Name(s) unless explicit written permission is granted by the corresponding+AAoA-Copyright Holder. This restriction only applies to the primary font name as+AAoA-presented to the users.+AAoACgAA-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font+AAoA-Software shall not be used to promote, endorse or advertise any+AAoA-Modified Version, except to acknowledge the contribution(s) of the+AAoA-Copyright Holder(s) and the Author(s) or with their explicit written+AAoA-permission.+AAoACgAA-5) The Font Software, modified or unmodified, in part or in whole,+AAoA-must be distributed entirely under this license, and must not be+AAoA-distributed under any other license. The requirement for fonts to+AAoA-remain under this license does not apply to any document created+AAoA-using the Font Software.+AAoACgAA-TERMINATION+AAoA-This license becomes null and void if any of the above conditions are+AAoA-not met.+AAoACgAA-DISCLAIMER+AAoA-THE FONT SOFTWARE IS PROVIDED +ACIA-AS IS+ACIA, WITHOUT WARRANTY OF ANY KIND,+AAoA-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF+AAoA-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT+AAoA-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE+AAoA-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+AAoA-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL+AAoA-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+AAoA-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM+AAoA-OTHER DEALINGS IN THE FONT SOFTWARE." "http://scripts.sil.org/OFL" Encoding: ISO8859-1 UnicodeInterp: none NameList: AGL For New Fonts DisplaySize: -48 AntiAlias: 1 FitToEm: 0 WinInfo: 63 21 9 BeginPrivate: 0 EndPrivate TeXData: 1 0 0 346030 173015 115343 0 1048576 115343 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 BeginChars: 256 6 StartChar: A Encoding: 65 65 0 Width: 1024 VWidth: 0 Flags: HW LayerCount: 2 Fore SplineSet 828 788 m 0 854 788 878 776 891 754 c 0 904 732 903 706 890 685 c 2 518 55 l 1 518 55 l 2 518 55 l 0 506 36 485 23 462 21 c 0 439 19 415 29 401 46 c 2 173 320 l 1 172 320 l 1 172 320 l 2 170 322 169 325 169 327 c 0 155 345 150 367 157 388 c 0 166 413 189 431 216 434 c 0 239 437 261 426 276 409 c 0 278 408 280 407 282 405 c 2 282 405 l 1 446 209 l 1 766 752 l 1 766 752 l 2 767 753 l 2 767 753 l 0 769 756 l 1 767 753 l 1 779 774 802 788 828 788 c 0 828 788 m 1024 EndSplineSet Validated: 37 EndChar StartChar: D Encoding: 68 68 1 Width: 1024 VWidth: 0 Flags: HW LayerCount: 2 Fore SplineSet 510.130859375 820.473632812 m 0 736.26171875 820.306640625 918.166015625 638.131835938 918 412 c 0 917.844726562 185.879882812 735.66015625 3.9619140625 509.538085938 4.12890625 c 0 283.404296875 4.294921875 101.491210938 186.47265625 101.657226562 412.6015625 c 0 101.822265625 638.733398438 284.005859375 820.639648438 510.130859375 820.473632812 c 0 540.21875 427.099609375 m 1 548.076171875 828.592773438 453.51953125 856.967773438 443.743164062 410.92578125 c 0 443.724609375 391.669921875 452.58203125 374.499023438 466.563476562 363.0078125 c 0 469.541992188 359.637695312 472.920898438 356.520507812 476.698242188 353.740234375 c 0 700.552734375 190.201171875 831.282226562 225.140625 540.21875 427.099609375 c 1 540.21875 427.099609375 m 1024 EndSplineSet EndChar StartChar: P Encoding: 80 80 2 Width: 1024 VWidth: 0 Flags: H LayerCount: 2 Fore SplineSet 284.974609375 810 m 2 746 810 l 2 798.637695312 810 841.798828125 762.37890625 841.798828125 704.31640625 c 2 841.798828125 112.484375 l 2 841.826171875 53.98046875 799.076171875 6.48046875 746 6.7998046875 c 2 284.974609375 6.7998046875 l 2 231.92578125 6.48046875 189.169921875 54.4091796875 189.169921875 112.484375 c 2 189.169921875 704.31640625 l 2 189.169921875 762.37890625 232.328125 810 284.974609375 810 c 2 318.0078125 677.8359375 m 1 318.0078125 131.208007812 l 1 445.793945312 131.208007812 l 1 445.793945312 320.858398438 l 1 530.096679688 320.858398438 l 2 593.155273438 320.858398438 641.50390625 336.236328125 675.141601562 366.993164062 c 0 708.989257812 397.990234375 725.92578125 442.173828125 725.92578125 499.530273438 c 0 725.92578125 556.643554688 708.989257812 600.57421875 675.141601562 631.329101562 c 0 641.50390625 662.327148438 593.155273438 677.8359375 530.096679688 677.8359375 c 2 318.0078125 677.8359375 l 1 445.793945312 575.681640625 m 1 516.487304688 575.681640625 l 2 541.268554688 575.681640625 560.40625 569.08984375 573.907226562 555.915039062 c 0 587.409179688 542.731445312 594.155273438 523.935546875 594.155273438 499.530273438 c 0 594.155273438 475.123046875 587.409179688 456.205078125 573.907226562 442.780273438 c 0 560.40625 429.595703125 541.268554688 423.013671875 516.487304688 423.013671875 c 2 445.793945312 423.013671875 l 1 445.793945312 575.681640625 l 1 445.793945312 575.681640625 m 1024 EndSplineSet Validated: 33 EndChar StartChar: N Encoding: 78 78 3 Width: 1024 VWidth: 0 Flags: H LayerCount: 2 Fore SplineSet 753.772460938 11.3427734375 m 5 495.978515625 168.923828125 l 5 237.655273438 12.21484375 l 5 307.854492188 306.088867188 l 5 78.9951171875 503.356445312 l 5 380.182617188 527.395507812 l 5 497.068359375 806.008789062 l 5 613 527 l 5 914.10546875 501.93359375 l 5 684.5703125 305.446289062 l 5 753.772460938 11.3427734375 l 5 753.772460938 11.3427734375 m 1028 EndSplineSet Validated: 1 EndChar StartChar: F Encoding: 70 70 4 Width: 1024 VWidth: 0 Flags: HWO LayerCount: 2 Fore SplineSet 62.1015625 758.361328125 m 0 72.5205078125 758.37109375 86.8603515625 758.259765625 103.15234375 758.259765625 c 2 304.185546875 758.259765625 l 2 309.717773438 758.259765625 313.795898438 755.147460938 315.573242188 753.116210938 c 0 317.3515625 751.087890625 317.9140625 749.629882812 318.338867188 748.46484375 c 0 319.202148438 746.140625 319.306640625 744.7109375 319.306640625 743.069335938 c 2 319.306640625 144.499023438 l 1 433.311523438 144.31640625 l 1 433.311523438 742.670898438 l 2 433.311523438 745.080078125 433.662109375 747.731445312 435.69140625 750.935546875 c 0 437.719726562 754.13671875 442.969726562 758.079101562 448.72265625 758.077148438 c 2 816.37109375 759.907226562 l 2 823.99609375 759.9453125 828.444335938 754.53125 829.899414062 751.705078125 c 0 831.354492188 748.879882812 831.58203125 746.868164062 831.58203125 744.716796875 c 2 831.58203125 146.146484375 l 1 899.012695312 146.146484375 l 2 917.5 146.146484375 933.731445312 146.096679688 945.38671875 146.012695312 c 0 951.2109375 145.962890625 955.891601562 145.911132812 959.197265625 145.850585938 c 0 960.83984375 145.819335938 962.127929688 145.788085938 963.189453125 145.749023438 c 0 963.709960938 145.728515625 964.146484375 145.70703125 964.782226562 145.657226562 c 0 965.10546875 145.625976562 965.405273438 145.6171875 966.2890625 145.461914062 c 0 966.725585938 145.392578125 967.21484375 145.360351562 968.6796875 144.87109375 c 0 969.41796875 144.615234375 970.396484375 144.370117188 972.319335938 143.064453125 c 0 973.276367188 142.41015625 975.927734375 139.595703125 975.927734375 139.5859375 c 2 975.927734375 139.5859375 978.756835938 131.026367188 978.756835938 131.026367188 c 1 978.756835938 25.259765625 l 2 978.756835938 19.96484375 975.655273438 15.9736328125 973.733398438 14.302734375 c 0 971.809570312 12.619140625 970.560546875 12.1298828125 969.6484375 11.751953125 c 0 967.817382812 10.974609375 967.079101562 10.904296875 966.434570312 10.78125 c 0 965.143554688 10.5263671875 964.532226562 10.49609375 963.834960938 10.4345703125 c 0 962.431640625 10.3125 961.056640625 10.2626953125 959.333984375 10.220703125 c 0 955.869140625 10.119140625 951.168945312 10.0771484375 945.31640625 10.0595703125 c 0 933.6171875 10.0166015625 917.427734375 10.0693359375 899.012695312 10.0693359375 c 2 702.243164062 10.0693359375 l 2 696.708984375 10.0693359375 691.521484375 13.69921875 689.380859375 16.9140625 c 0 687.23828125 20.126953125 686.80078125 23.173828125 686.80078125 25.47265625 c 2 686.80078125 623.830078125 l 1 572 622 l 1 572 23.6416015625 l 2 572 20.458984375 571.103515625 17.296875 568.857421875 14.337890625 c 0 566.602539062 11.380859375 562.296875 8.236328125 556.517578125 8.2392578125 c 2 189.932617188 8.421875 l 2 181.958007812 8.462890625 177.62109375 14.1435546875 176.283203125 16.91015625 c 0 174.931640625 19.673828125 174.741210938 21.5712890625 174.741210938 23.6123046875 c 2 174.741210938 622.182617188 l 1 103.15234375 622.182617188 l 2 86.744140625 622.182617188 72.3330078125 622.23046875 61.9765625 622.323242188 c 0 56.798828125 622.364257812 52.6494140625 622.4140625 49.6875 622.477539062 c 0 48.2099609375 622.508789062 47.0556640625 622.537109375 46.0693359375 622.588867188 c 0 45.580078125 622.610351562 45.173828125 622.629882812 44.5087890625 622.690429688 c 0 44.1650390625 622.721679688 43.8427734375 622.733398438 42.8740234375 622.915039062 c 0 42.3984375 623.0078125 41.8369140625 623.059570312 40.2548828125 623.661132812 c 0 39.46484375 623.965820312 38.3828125 624.303710938 36.4697265625 625.762695312 c 0 35.5146484375 626.485351562 33.080078125 629.403320312 33.080078125 629.403320312 c 1 23.3173828125 662.143554688 35.2021484375 708.452148438 30.73046875 743.069335938 c 0 30.73046875 750.43359375 35.826171875 754.373046875 37.9150390625 755.625976562 c 0 40.0048828125 756.893554688 41.0576171875 757.096679688 41.88671875 757.321289062 c 0 43.541015625 757.782226562 44.2802734375 757.831054688 45.048828125 757.911132812 c 0 46.5771484375 758.086914062 47.8779296875 758.146484375 49.4794921875 758.198242188 c 0 52.7021484375 758.311523438 56.8916015625 758.350585938 62.1015625 758.361328125 c 0 62.1015625 758.361328125 m 1024 EndSplineSet EndChar StartChar: H Encoding: 72 72 5 Width: 1024 VWidth: 0 Flags: HW LayerCount: 2 Fore SplineSet 9.515625 160.0234375 m 1 1017 160.0234375 l 1 1017 -4.302734375 l 1 9.515625 -4.302734375 l 1 9.515625 160.0234375 l 1 9.515625 488.51171875 m 1 1017 488.51171875 l 1 1017 324.188476562 l 1 9.515625 324.188476562 l 1 9.515625 488.51171875 l 1 9.515625 817 m 1 1017 817 l 1 1017 652.67578125 l 1 9.515625 652.67578125 l 1 9.515625 817 l 1 EndSplineSet Validated: 1 EndChar EndChars EndSplineFont Nagstamon-master/Nagstamon/resources/nagstamon.svg000066400000000000000000000230631505160700500227520ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/nagstamon.ttf000066400000000000000000000363641505160700500227600ustar00rootroot00000000000000€`FFTMxz‚Ô<ØGDEF<ŧOS/2Y8bh`cmap įäjcvt "ˆPgasp˙˙<´glyf¨āE¸hhead ē–%ė6hheaŒ$$hmtx +ČlocađĻTmaxpO™H nameË-€6post ģ<€4cŗ _<õ ĶL¨üĶL¨ü ˙ûų55˙û\ų h@.™Ė™Ėë3  PfEd€AP3˙3\5- "’e NŊdHADFHNP˙˙ADFHNP˙˙˙Â˙Ā˙ŋ˙ž˙š˙¸"ˆ***^”2PŒ"2Ē.ą/<˛í2ąÜ<˛í2ą/<˛í2˛ü<˛í23!'3#"îĖĖĒũV"f’21'#&5&7676?63<+(ūŒ%&ä ,"¤@)D#ũŠ )Ä#e—5 ".466'&'&/ŪŧmmŧßŧlmI yI(…5mŧŪŧmmŧßŧūãč@$4PØY *`Ķøfg:;234763%23232323:230#"#"#*+"'&5#!"'&5#"#"#&#*#"'"'0'&6'476362323>Ér o C   Åsū‘ HöũŠWũŠ jWũĒ V W ˙ûų1 7!!!!!! īüīüīü ¤íĨí¤N “' -'%ōūūūūFå-ut-åE ž&ÅūéÅūÚŊJ*%&!2#!"&54635327654'&#32+5Í'98(ū3(88I€T_2332_TF&(&F*>,ũ°+>>+P,>„ũŨž..WU./f(I™Ū> q‹$Ũ B g #ą5 < ] { H“     S ">q 44ŅCopyright (c) 2016, Henri WahlCopyright (c) 2016, Henri WahlNagstamonNagstamonRegularRegularFontForge 2.0 : Nagstamon : 2-5-2016FontForge 2.0 : Nagstamon : 2-5-2016NagstamonNagstamonVersion 001.000 Version 001.000 NagstamonNagstamonCopyright (c) 2016, Henri Wahl (<URL|email>), with Reserved Font Name Untitled1. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.Copyright (c) 2016, Henri Wahl (), with Reserved Font Name Untitled1. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.http://scripts.sil.org/OFLhttp://scripts.sil.org/OFL˙€3 $')+13˙˙ Ō92Ķ* ļĶL¨ëNagstamon-master/Nagstamon/resources/nagstamon_logo_bar.svg000066400000000000000000000221541505160700500246160ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/nagstamon_logo_toparea_template.svg000066400000000000000000000434201505160700500273770ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/nagstamon_systrayicon_empty.svg000066400000000000000000000107461505160700500266430ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/nagstamon_systrayicon_template.svg000066400000000000000000000227331505160700500273170ustar00rootroot00000000000000 image/svg+xml Nagstamon-master/Nagstamon/resources/qt.conf000066400000000000000000000000261505160700500215270ustar00rootroot00000000000000[Paths] Plugins = '.' Nagstamon-master/Nagstamon/resources/qui/000077500000000000000000000000001505160700500210345ustar00rootroot00000000000000Nagstamon-master/Nagstamon/resources/qui/dialog_about.ui000066400000000000000000000140021505160700500240210ustar00rootroot00000000000000 dialog_about 0 0 500 347 0 0 About Nagstamon true true QTabWidget::Rounded 0 About Qt::Vertical 20 40 Nagstamon x Qt::AlignCenter Nagios status monitor Qt::AlignCenter Š2008-2025 Henri Wahl et al. Qt::AlignCenter https://nagstamon.de Qt::AlignCenter true Python Qt versions Qt::AlignCenter Qt::Vertical 20 40 Contribution | Donation Qt::AlignCenter true Qt::Vertical 20 40 Footnote Qt::AlignCenter Credits License Qt::Horizontal QDialogButtonBox::Ok button_box accepted() dialog_about accept() 248 254 157 274 button_box rejected() dialog_about reject() 316 260 286 274 Nagstamon-master/Nagstamon/resources/qui/dialog_acknowledge.ui000066400000000000000000000146501505160700500252030ustar00rootroot00000000000000 dialog_acknowledge 0 0 475 366 0 0 Acknowledge true true Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Change acknowledgement defaults... Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok true 0 0 0 91 Options Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Sticky acknowledgement Send notification Persistent comment Acknowledge all services on host Qt::DefaultContextMenu Expire acknowledgement true Expiry time QAbstractSpinBox::UpDownArrows QDateTimeEdit::HourSection true 0 0 0 5 description - set by QUI.py Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Qt::Vertical QSizePolicy::Expanding 20 0 input_lineedit_comment input_checkbox_sticky_acknowledgement input_checkbox_send_notification input_checkbox_persistent_comment input_checkbox_acknowledge_all_services input_checkbox_use_expire_time input_datetime_expire_time button_change_defaults_acknowledge button_box accepted() dialog_acknowledge accept() 464 355 157 274 button_box rejected() dialog_acknowledge reject() 464 355 286 274 Nagstamon-master/Nagstamon/resources/qui/dialog_authentication.ui000066400000000000000000000056751505160700500257460ustar00rootroot00000000000000 dialog_authentication 0 0 350 226 0 0 350 0 Authentication true Username: Autologin key: Password: true 200 0 0 0 QLineEdit::Password QDialogButtonBox::Cancel|QDialogButtonBox::Ok false Use autologin Save password Nagstamon-master/Nagstamon/resources/qui/dialog_downtime.ui000066400000000000000000000115171505160700500245450ustar00rootroot00000000000000 dialog_downtime 0 0 409 294 0 0 Downtime true true Start time: Qt::Vertical 20 40 description - set by QUI.py Change downtime defaults... Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok hours minutes Duration: End time: n/a n/a Flexible Fixed input_lineedit_comment input_spinbox_duration_hours input_spinbox_duration_minutes button_change_defaults_downtime button_box accepted() dialog_downtime accept() 248 254 157 274 button_box rejected() dialog_downtime reject() 316 260 286 274 Nagstamon-master/Nagstamon/resources/qui/dialog_server_missing.ui000066400000000000000000000045251505160700500257570ustar00rootroot00000000000000 dialog_server_missing 0 0 813 263 0 0 350 0 Nagstamon true <html><head/><body><p>There are no configured servers yet.<br/></p></body></html> true <html><head/><body><p>There are no servers enabled.<br/></p></body></html> true Enable server Create new server Ignore Exit Nagstamon-master/Nagstamon/resources/qui/dialog_submit.ui000066400000000000000000000167311505160700500242250ustar00rootroot00000000000000 dialog_submit 0 0 473 449 0 0 16777215 16777215 Submit check result true true 0 0 Check output: 0 0 Performance data: Check result OK true UP INFORMATION WARNING AVERAGE HIGH CRITICAL DISASTER UNREACHABLE UNKNOWN DOWN Comment: description - set by QUI.py Change submit check result defaults... Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 0 0 Qt::Vertical 20 40 input_radiobutton_result_ok input_radiobutton_result_up input_radiobutton_result_warning input_radiobutton_result_critical input_radiobutton_result_unreachable input_radiobutton_result_unknown input_radiobutton_result_down input_lineedit_check_output input_lineedit_performance_data input_lineedit_comment button_change_defaults_submit_check_result button_box accepted() dialog_submit accept() 248 254 157 274 button_box rejected() dialog_submit reject() 316 260 286 274 Nagstamon-master/Nagstamon/resources/qui/settings_action.ui000066400000000000000000000306221505160700500245730ustar00rootroot00000000000000 settings_action 0 0 752 1017 0 0 Dialog true reverse Close status popup window after action Regular expressions for status informations Leave status popup window open after action 0 0 Available variables for action strings: $HOST$ - host as in monitor $SERVICE$ - service as in monitor $MONITOR$ - monitor address $MONITOR-CGI$ - monitor CGI address $ADDRESS$ - address of host, delivered from connection method $USERNAME$ - username on monitor $STATUS-INFO$ - status information for host or service $PASSWORD$ - username's password on monitor $COMMENT-ACK$ - default acknowledge comment $COMMENT-DOWN$ - default downtime comment $COMMENT-SUBMIT$ - default submit check result comment $TRANSID$ - only useful for Checkmk as _transid=$TRANSID$ Regular expressions for duration 0 0 String: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 0 0 Action type: 0 0 Monitor type: Host Regular expressions for hosts Status popup window: Available action types: Browser: Use given string as URL, evaluate variables and open it in your default browser, for example a graph page in monitor. Command: Execute command as given in string and evaluate variables, for example to open SSH connection. URL: Request given URL string in the background, for example to acknowledge a service with one click. reverse Service reverse Regular expressions for services reverse 0 0 Target: Recheck after action to force result Regular expressions for groups 0 0 Description: 0 0 Name: Regular expressions for attempt reverse reverse Enabled <a href=http://docs.python.org/howto/regex.html>See Python Regular Expressions HOWTO for filtering details.</a> true Qt::TextBrowserInteraction input_checkbox_enabled input_combobox_type input_combobox_monitor_type input_lineedit_name input_lineedit_description input_checkbox_filter_target_host input_checkbox_filter_target_service input_checkbox_re_host_enabled input_lineedit_re_host_pattern input_checkbox_re_host_reverse input_checkbox_re_service_enabled input_lineedit_re_service_pattern input_checkbox_re_service_reverse input_checkbox_re_status_information_enabled input_lineedit_re_status_information_pattern input_checkbox_re_status_information_reverse input_checkbox_re_duration_enabled input_lineedit_re_duration_pattern input_checkbox_re_duration_reverse input_checkbox_re_groups_enabled input_radiobutton_close_popwin input_radiobutton_leave_popwin_open button_box accepted() settings_action accept() 248 254 157 274 button_box rejected() settings_action reject() 316 260 286 274 Nagstamon-master/Nagstamon/resources/qui/settings_main.ui000066400000000000000000003533361505160700500242540ustar00rootroot00000000000000 Henri Wahl settings_main 0 0 665 1193 0 0 16777215 16777215 Nagstamon 2.0 settings true 0 0 1 0 0 Servers 10 10 10 10 5 5 5 5 5 0 Debug mode true Debug to file: 5 0 5 0 5 New server... Edit server... Copy server... Delete server Qt::Vertical 20 40 5 5 5 5 5 Use system keyring for server credentials 5 5 5 5 5 Update interval: 999 seconds Qt::Horizontal 40 20 5 5 5 5 0 Check for new version at startup Qt::Horizontal 40 20 Check for new version now 0 200 16777215 16777215 QListView::Adjust Qt::Vertical 0 0 0 0 Display 10 10 10 10 5 0 0 Statusbar display size: 5 10 10 10 10 5 5 5 5 5 Short [ 1 | 4 ] Long [ 1 WARNING | 4 CRITICAL ] Qt::Vertical 0 0 0 0 Statusbar details popup: 10 10 10 10 5 5 5 5 5 5 Close when clicking statusbar buttongroup_close_statusbar Popup when clicking statusbar buttongroup_popup_statuswindow Popup when hovering over statusbar buttongroup_popup_statuswindow Close when hovering out of popup buttongroup_close_statusbar Close when clicking somewhere buttongroup_close_statusbar Highlight new events in status details overview Show tooltips in status details overview 5 5 5 5 5 Default sort field: Default sort order: 0 0 Font: 5 10 10 10 10 5 5 5 5 5 QFrame::Box QFrame::Sunken The CRITICAL Fox jumped over the WARNING dog. true 5 5 5 5 Change Font... Revert to default font 0 0 Appearance: 10 10 10 10 5 Enable position fix Fullscreen Floating statusbar Icon in systray Window Display to use: Offset for statuswindow: Hide macOS Dock icon Use offset to correct position problems 0 0 Filters 10 10 10 10 5 Regular expression for attempt Regular expression for services 5 5 5 5 5 reverse Qt::Vertical 0 0 5 5 5 5 5 reverse Regular expression for status informations 5 5 5 5 5 reverse Regular expression for groups 5 5 5 5 5 reverse Regular expression for duration 5 5 5 5 5 reverse <a href=http://docs.python.org/howto/regex.html>See Python Regular Expressions HOWTO for filtering details.</a> true Qt::TextBrowserInteraction Regular expression for hosts 0 0 Filter out the following: 10 10 10 10 10 5 All down hosts All unreachable hosts All unreachable services All flapping hosts All critical services Acknowledged hosts and services Hosts and services with disabled notifications Hosts and services with disabled checks Hosts and services down for maintenance Services on acknowledged hosts Services on down hosts All high services All disaster services All flapping services All unknown services All warning services All information services All average services Services on hosts in maintenance Services on unreachable hosts Hosts in soft state Services in soft state 5 5 5 5 5 0 0 reverse 0 0 Actions 10 10 10 10 5 0 0 0 200 QListView::Adjust 5 0 5 0 5 New action... Edit action... Copy action... Delete action Qt::Vertical 20 40 0 0 Connection method: 5 10 10 10 10 Hostname provided by monitor DNS name by looking up IP address IP resolved by hostname Qt::Vertical 0 0 0 0 Browser: 5 10 10 10 10 Use default browser Use alternative browser 0 0 Custom browser: 10 10 10 10 5 Choose browser... 0 0 Notifications 5 10 10 10 10 Enable notification 0 0 Notifications: 5 10 10 10 10 5 0 0 0 0 WARNING CRITICAL UNKNOWN UNREACHABLE DOWN Qt::Horizontal 40 20 INFORMATION DISASTER AVERAGE HIGH Qt::Horizontal 40 20 Flashing statusbar/appindicator/systray icon Desktop notification Enable sound 0 0 Sounds: 10 10 10 10 5 0 0 Custom sounds: 10 10 10 10 5 Choose file... Play WARNING: CRITICAL: Choose file... Choose file... Play DOWN: Play Repeat sound until notification has been noticed Use default Nagios sounds Use custom sounds Enable notification actions 0 0 Notification actions: 10 10 10 10 5 Just one single command to be executed like starting some blinking light etc. CRITICAL DOWN OK WARNING Just one single command to be executed like starting some blinking light etc. Use custom notification action 0 0 Custom notification action: 10 10 10 10 5 Action string: Event separator: Arbitrarily choosen string or character to separate events in $EVENTS$. Run one single action for every event <html><head/><body><p>Available variables for action strings are $EVENTS$ for all events concatenated by the event separator or $EVENT$ if single actions are choosen.</p><p>To get all information into executable the variable should be quoted like '$EVENT$'.</p></body></html> Just one single command to be executed like starting some blinking light etc. Just one single command to be executed like starting some blinking light etc. Qt::Vertical 0 0 0 0 Colors 10 10 10 10 5 Qt::Vertical 20 40 Set colors for text and background of status information: 0 0 Status: 5 10 10 10 5 OK INFORMATION WARNING AVERAGE HIGH CRITICAL DISASTER UNKNOWN UNREACHABLE DOWN ERROR 0 0 Text: 5 10 10 10 5 0 0 Background: 5 10 5 10 10 5 5 5 5 5 Reset to previous colors Revert to default colors 0 0 Colored grid: 10 10 10 10 5 Use alternating colors for better readability true Customize intensity of alternation 0 0 50 Qt::Horizontal 0 5 5 5 5 true INFORMATION Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 true INFORMATION Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true WARNING Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 true WARNING Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true AVERAGE Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 true AVERAGE Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true HIGH Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 true HIGH Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true CRITICAL Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true CRITICAL Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true DISASTER Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true DISASTER Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true UNKNOWN Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true UNKNOWN Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true UNREACHABLE Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true UNREACHABLE Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true DOWN Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true DOWN Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 0 Defaults 5 10 10 10 10 Set some default settings for default actions: 0 0 Acknowledgements: 5 10 10 10 10 Sticky acknowledgement Send notification Persistent comment Acknowledge all services on host 5 5 5 5 Comment: true Use expire acknowledgement 5 0 5 5 5 0 0 Expire in: hours minutes Qt::Horizontal 40 20 0 0 Downtime: 10 10 10 10 5 Comment: Duration: 5 5 5 5 Flexible Fixed Type: Qt::Horizontal 40 20 5 5 5 5 hours minutes Qt::Horizontal 40 20 0 0 Submit check result: 5 10 10 10 10 Comment: Qt::Vertical 0 0 0 0 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok list_servers button_new_server button_edit_server button_copy_server button_delete_server input_spinbox_update_interval_seconds input_checkbox_use_system_keyring input_checkbox_debug_mode input_checkbox_debug_to_file input_lineedit_debug_file input_checkbox_check_for_new_version button_check_for_new_version_now input_radiobutton_long_display input_radiobutton_short_display input_radiobutton_statusbar_floating input_radiobutton_icon_in_systray input_radiobutton_windowed input_radiobutton_fullscreen input_checkbox_systray_offset_use input_spinbox_systray_offset input_combobox_fullscreen_display input_checkbox_hide_macos_dock_icon input_checkbox_enable_position_fix button_fontchooser button_default_font input_radiobutton_popup_details_hover input_radiobutton_popup_details_clicking input_radiobutton_close_details_hover input_radiobutton_close_details_clicking input_radiobutton_close_details_clicking_somewhere input_checkbox_highlight_new_events input_checkbox_show_tooltips input_combobox_default_sort_field input_combobox_default_sort_order input_checkbox_filter_all_down_hosts input_checkbox_filter_all_unreachable_hosts input_checkbox_filter_all_flapping_hosts input_checkbox_filter_all_critical_services input_checkbox_filter_all_flapping_services input_checkbox_filter_all_unknown_services input_checkbox_filter_all_warning_services input_checkbox_filter_all_information_services input_checkbox_filter_all_average_services input_checkbox_filter_all_disaster_services input_checkbox_filter_all_high_services input_checkbox_filter_acknowledged_hosts_services input_checkbox_filter_hosts_services_disabled_notifications input_checkbox_filter_hosts_services_disabled_checks input_checkbox_filter_hosts_services_maintenance input_checkbox_filter_services_on_acknowledged_hosts input_checkbox_filter_services_on_down_hosts input_checkbox_filter_services_on_hosts_in_maintenance input_checkbox_filter_services_on_unreachable_hosts input_checkbox_filter_hosts_in_soft_state input_checkbox_filter_services_in_soft_state input_checkbox_re_host_enabled input_lineedit_re_host_pattern input_checkbox_re_host_reverse input_checkbox_re_service_enabled input_lineedit_re_service_pattern input_checkbox_re_service_reverse input_checkbox_re_status_information_enabled input_lineedit_re_status_information_pattern input_checkbox_re_status_information_reverse input_checkbox_re_duration_enabled input_lineedit_re_duration_pattern input_checkbox_re_duration_reverse input_checkbox_re_attempt_enabled input_lineedit_re_attempt_pattern input_checkbox_re_attempt_reverse input_checkbox_re_groups_enabled input_lineedit_re_groups_pattern input_checkbox_re_groups_reverse list_actions button_new_action button_edit_action button_copy_action button_delete_action input_radiobutton_connect_by_host input_radiobutton_connect_by_dns input_radiobutton_connect_by_ip input_radiobutton_use_default_browser input_radiobutton_use_custom_browser input_lineedit_custom_browser button_choose_browser input_checkbox_notification input_checkbox_notify_if_warning input_checkbox_notify_if_critical input_checkbox_notify_if_unknown input_checkbox_notify_if_unreachable input_checkbox_notify_if_down input_checkbox_notify_if_information input_checkbox_notify_if_disaster input_checkbox_notify_if_average input_checkbox_notify_if_high input_checkbox_notification_flashing input_checkbox_notification_desktop input_checkbox_notification_sound input_checkbox_notification_sound_repeat input_radiobutton_notification_default_sound input_radiobutton_notification_custom_sound input_lineedit_notification_custom_sound_warning button_choose_warning button_play_warning input_lineedit_notification_custom_sound_critical button_choose_critical button_play_critical input_lineedit_notification_custom_sound_down button_choose_down button_play_down input_checkbox_notification_actions input_checkbox_notification_action_warning input_lineedit_notification_action_warning_string input_checkbox_notification_action_critical input_lineedit_notification_action_critical_string input_checkbox_notification_action_down input_lineedit_notification_action_down_string input_checkbox_notification_action_ok input_lineedit_notification_action_ok_string input_checkbox_notification_custom_action input_lineedit_notification_custom_action_string input_button_color_ok_text input_button_color_ok_background input_button_color_information_text input_button_color_information_background input_button_color_warning_text input_button_color_warning_background input_button_color_average_text input_button_color_average_background input_button_color_high_text input_button_color_high_background input_button_color_critical_text input_button_color_critical_background input_button_color_disaster_text input_button_color_disaster_background input_button_color_unknown_text input_button_color_unknown_background input_button_color_unreachable_text input_button_color_unreachable_background input_button_color_down_text input_button_color_down_background input_button_color_error_text input_button_color_error_background button_colors_reset button_colors_defaults input_checkbox_defaults_acknowledge_sticky input_checkbox_defaults_acknowledge_send_notification input_checkbox_defaults_acknowledge_persistent_comment input_checkbox_defaults_acknowledge_all_services input_lineedit_defaults_acknowledge_comment input_checkbox_defaults_acknowledge_expire input_spinbox_defaults_acknowledge_expire_duration_hours input_spinbox_defaults_acknowledge_expire_duration_minutes input_radiobutton_defaults_downtime_type_fixed input_radiobutton_defaults_downtime_type_flexible input_spinbox_defaults_downtime_duration_hours input_spinbox_defaults_downtime_duration_minutes input_lineedit_defaults_downtime_comment input_lineedit_defaults_submit_check_result_comment tabs input_checkbox_show_grid input_checkbox_grid_use_custom_intensity input_slider_grid_alternation_intensity input_lineedit_notification_custom_action_separator input_checkbox_notification_custom_action_single input_checkbox_filter_all_unreachable_services button_box accepted() settings_main accept() 410 1186 386 376 button_box rejected() settings_main reject() 410 1186 386 376 Nagstamon-master/Nagstamon/resources/qui/settings_server.ui000066400000000000000000000663771505160700500246440ustar00rootroot00000000000000 settings_server 0 0 639 1527 0 0 Dialog true 10 10 10 10 5 2 https://monitor-server/monitor/cgi-bin 0 0 Monitor URL: 0 0 Monitor CGI URL: 1234567890 QLineEdit::Password Save password Monitor server https://monitor-server 0 0 Monitor name: username Use proxy 0 0 Password: Enabled 0 0 Proxy: 10 10 10 10 5 0 0 Proxy password: 0 0 Proxy username: proxyusername http://proxy:port/ 0 0 Proxy address: 1234567890 QLineEdit::Password Use proxy from OS 0 0 Monitor type: 0 0 Options: 10 10 10 10 10 5 0 0 Disabled backends: IdP ECP endpoint URL 0 0 Host filter: 0 0 Hashtag filter: Map to DOWN Map to hostname: Qt::Horizontal 40 20 Only show permitted hosts for "see all" users (1.4.0i1 or newer) 0 0 Views: Hosts: Services: Reset Reset Map to OK 0 0 Monitor Site: Use description as service name Use autologin 0 0 Autologin Key: Ignore SSL/TLS certificate Map to WARNING Use display name as service name Site 1 Map to servicename: Filter: Timeout: 5 seconds 1234567890 Qt::Horizontal 40 20 Do not use cookie authentication Custom CA file: Map to UNKNOWN Choose file... Use custom CA file Map to CRITICAL 0 0 Service filter: Authentication: Use display name as host name 0 0 Notification filter: Only show services that the user can change or set downtimes on Map to status_information: 0 0 Notification lookback horizon: Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 0 0 Username: Show more options input_checkbox_enabled input_combobox_type input_lineedit_name input_lineedit_monitor_url input_lineedit_monitor_cgi_url input_lineedit_username input_lineedit_password input_checkbox_save_password input_checkbox_use_proxy input_lineedit_proxy_address input_lineedit_proxy_username input_lineedit_proxy_password input_checkbox_use_proxy_from_os input_checkbox_show_options input_checkbox_ignore_cert input_checkbox_custom_cert_use input_lineedit_custom_cert_ca_file button_choose_custom_cert_ca_file input_combobox_authentication input_spinbox_timeout input_checkbox_use_autologin input_lineedit_monitor_site input_lineedit_autologin_key input_lineedit_hashtag_filter input_lineedit_host_filter input_lineedit_service_filter input_lineedit_notification_filter input_lineedit_notification_lookback input_lineedit_alertmanager_filter input_lineedit_map_to_hostname input_lineedit_map_to_servicename input_lineedit_map_to_status_information input_lineedit_map_to_ok input_lineedit_map_to_warning input_lineedit_map_to_critical input_lineedit_map_to_down input_lineedit_map_to_unknown input_checkbox_can_change_only input_checkbox_no_cookie_auth input_checkbox_use_display_name_host input_checkbox_use_display_name_service input_checkbox_use_description_name_service input_checkbox_force_authuser input_lineedit_checkmk_view_hosts button_checkmk_view_hosts_reset input_lineedit_checkmk_view_services button_checkmk_view_services_reset input_lineedit_idp_ecp_endpoint input_lineedit_disabled_backends Nagstamon-master/Nagstamon/resources/warning.wav000066400000000000000000000427061505160700500224330ustar00rootroot00000000000000RIFFžEWAVEfmt +"VdatašEø ˙˙ūõ˙úķũ˙ųéđ˙û˙ū ˙˙ūüũåöūū˙ūöö˙ûøūũũ˙ ÷˙ ˙õũéũūüīõũúū ü˙˙ ũ ũõų ˙÷÷ūųū÷čüũû˙üũ˙ø˙˙īîüüüũõ˙ķü˙ũųũ ūü ū˙é˙ķüõ˙öôüū˙ûū üūķũ˙˙úķūũūíņ˙õ˙ ûūũ ũûø˙ų˙đö˙÷˙˙î˙üūũ˙õ˙ũķũéøū÷üūōõū ūūũ úõüūđũöūķīņūû˙˙˙  ˙ķ˙÷˙ôô˙ņ˙öū˙öūūū  ˙˙ û˙čķöũũø˙đ ˙ú˙üūü˙ õđ˙˙öūōīūō ôú˙úûđ˙ū˙÷ķ˙öų˙ų˙ø ˙˙ûū ūīī˙đû˙đũ˙˙õũ˙ ū˙˙ûō˙õ÷õũōéũ ˙öūųũū˙øķ ˙úųî˙ķ˙ôū  ũüüöūōę˙öų˙õü ú÷÷ü˙ū  ˙ņô˙îúô˙ęö ū ũ˙ũũõüũû ũúö˙ũöüķįû˙˙˙ū˙ ˙ūū˙ũũũõë˙î˙˙øûū˙ũ˙ņū˙ ūũ üũōúîđųûëũķ˙ ü˙ũū÷üûūöūüũūķ˙ęíũûü˙ũøũøūũīíü˙ūūûöüųõ÷ü ūü ü÷íũę÷˙õīüũũ˙˙ ū˙˙õ˙ ūúũûøũüę˙éö˙˙ūüøũûú˙ũëú˙ūũõü÷ũøö˙ú ū ūūņįūņøūķöū ūû˙üũû˙˙÷ūųúōäōų˙ņ˙ūũøūōôķūúų˙÷ø ū ūøč˙íø÷˙ųú˙ ūúũũūūû˙˙˙ō˙ûņ˙ęęö˙ ũ ū˙ö÷ũûūûû˙ûîūķö˙øøû ūūūëūëō˙ųũûū ūûöūúüūû ū÷˙ņö˙ęė˙ë˙ ˙˙  ˙÷õū˙ū˙˙˙˙ōí˙đöũųø˙˙ūõëüī÷üūūúũũüõôūūúō˙đđëūëõũ ˙ū˙˙ķ˙ųü˙˙˙ūõėūéđū÷úū˙ū ü˙īīô˙ũũũüũø˙đ˙ö ū˙ôūīīūíîđ˙  ˙ũųū÷˙ ūũûđūæéķ˙úūū˙ūūũūöōô˙ûūũûü˙öņūīũ˙ ˙ ûđ˙ëīūîņøū ˙ũøūū˙ ūũķũčäũėøüūũũūũü˙ųøöūúü˙ûųöũõņ˙íõū ũ˙ķ˙ėėūōōũ÷ ū˙ø˙üüūûũū˙ ũũöūęäæ˙ķü ˙ũ ˙ūúûû˙úû˙ ˙ųõ˙īņ˙ëņ˙ũ ˙ūüúūííđūöö˙ūũû˙ôü˙ųū˙˙˙ö˙îæ˙âė˙ų ū ũ ũųúü˙ū˙ ũ˙ôũėë˙ëë˙÷ū˙ ˙ũõ˙ėđöūúüũ˙øōķųüū ūūûūīë˙âįô˙ūū  ˙ ˙ûũųú˙˙ū ûúėúčéüéņũū ūü üųôüîøüû˙üūūûūîīōø˙ ūũ ūōīįũæđũø˙˙üųų˙ū  ū˙˙˙ōüåįũäíø˙˙ūųōø˙ūũū˙ųīüéėũņ˙ũũūø˙đėį˙íôũũũ˙ų˙ų÷ũû ū ÷įäãčôü ˙øú˙ūü˙÷îūæčę˙øûüū˙˙õō˙ëđ˙ķų˙ũüūõ÷ø˙ú ūũ ˙ũíæ˙áäđūõ˙  ˙ũ˙ü ũüøîæūææūņ˙ ūūû÷˙īķö˙õ˙ūüúũøōūķųú˙ ˙ķũęäüāėūņû˙ ˙ ũũ ū ˙ ųī˙ãįūäëūûūū ˙˙ũõ÷ūøõúũųöūöđđū÷ø˙ üū ūųņūęâūčđūķū ˙ ˙  ūũ˙ōåūåäūåôūû ū˙˙ûųüũøũøúķöđë˙ō˙õü ũũüøūōį˙æíūîúūûũ˙˙ũū ūũøčũååũâîũõ˙ū ˙ üũ˙úúņķ˙īčūîōūú ũũũūųíūįė˙éôøö˙ūũū ũ˙î˙ååáęđ÷ ūũũ ˙˙˙˙ũûüđūđņūį˙ëíūķ˙˙üūķũęëį˙đ˙õōūûũ˙˙ ôūįæūŪį˙ęī ūū˙˙ūũ˙ķ˙īņč˙ęéūėũ˙˙˙úũíîũâėūõđø˙øüūũū˙  ˙üîūįŪ˙âęūčųū ũ˙ ˙˙˙ų˙đņũčę˙č˙ãöũ ūū ˙  üūôđũčæūōīūķøõ˙ ˙ûũõíũŪŪüæįûīūũū ū  ũū÷ūķęūæęūßë˙÷˙ū$ũ ˙ôö˙íæūîđũōöũīø˙ū˙ ˙(ūųũõį˙Ņ×Ų˙å˙üūü û%ü(ũ˙ îŌĐÛÄ˙ŧĖ˙Ų˙õü&ū7M˙I2'ū˙ ėÚūĮŊüĀÆūÕßū;@˙8X˙X@˙ÛĢ’˙§ģũŋÔũķū.G7#˙ ûūėÜ×ũÂÉūė˙)˙:GH˙+˙ŪÔ˙С˙ÂŪīü /û'!ū üūņņũīôũ(0"˙ úüīĐûÃąūÍęėũ÷ū.<ũ6)û!!üūøūÔÁūÉÚÃūĐá ū'üū3 ū ūúņÚãâūŲæņņū3*˙õõūŲë˙˙äü÷˙ũ ˙˙ ˙!ū ûūįįũūíéūíūū ūûū˙æūâ÷øöūûāūđø˙øô˙ū+1˙˙ūßŪūĐŌũķ˙ōķ˙&˙ ųōå˙Ņá˙ŨāÕ˙ü˙˙˙ ˙÷˙î˙ä×ũČäūááö˙6˙:4ü úė˙Ü˙ŨŲ˙ę˙ ˙ūėęū˙ūõ˙ū &2˙ū÷ā˙â˙ÚÕūĪ˙$˙ ũū ˙ūķîũØâëûŪäûũ˙#˙˙ķüū˙â˙î˙åû˙ūūķųūũūūũ !ûôũéîūáØūäë˙å !ū˙˙í˙ûüÜúßĖûØã˙éå 7˙&1ũ&ûúíäüâįūī÷ūōÜÜø˙#˙6˙&üųūųáÜė˙Üņ˙˙öø˙ũ ūú%û!ūūîįæ˙ííč˙ė ū'˙ ūūôũ˙đëëüūü ú˙æū˙ūū˙ũåūæų˙ß˙Öėũķ˙˙˙ūū˙ø˙˙ō×˙ĖÔ˙Ûã˙ęī ˙85˙%˙ū˙īåūëöũæéūčÔūâ˙ū ū#3ū+ũôūūūŨāūęōķöōūéņū ū)˙ũūâįíūæęëūë˙" ūūüũøũųķ˙øųôūđūũũüũķũÜęūũįāũíū  ūū˙ūũ ˙ōÎË˙Øāāõōū4ũ4˙˙ū˙ņéôõÚÜūŪßūåũ ˙/ū."˙øū ˙čÛņųīáčÜø #%&ü˙ãîūîäë˙éōū đû˙úūüûūūö÷˙÷ūū ūø÷ë˙âōūũôëü÷ü˙ ū ˙û÷ūįŅūĶãũŪėüôũ/ū(˙&˙˙üî˙ņō˙æÎūŌßũđé˙˙.!ūūūūéāūö÷ūŪØŲ˙ãūū ˙+3&˙ũõõîūėāũęéũ÷úüöûíüū˙ ū˙úđøū ū˙øū úöæūīë˙øüūđü ũųūûú˙ ˙üüûīīūåß˙Ūãäūīüū$˙"˙ü(üīũķí˙íØŅūČīũôķūú˙$ ˙˙˙˙đ˙æķ˙īÜŌ˙Ôė˙õ˙*˙?"˙  üûķüããūßī˙ëí˙ø÷˙ķū ũū üũđū˙˙öû˙÷˙īį˙įņ˙öķ˙ ūûũũø˙˙˙öūū îéũčđũâäūæō˙˙#˙ûüīđ˙é˙ߨũĖÔūōûû˙ũũ%&ū ˙ūúũ˙öüīéũčÚūŅÜëūķū1˙8'˙ũũíáūÜâë˙Ūäūîüū˙ü ˙˙ũū÷ū ˙ķ÷ūüëūâßūéöūō÷ū˙ūũüúûũõõ ˙˙íûæöüķéūåėü˙˙#ū$˙ūũņėüŪŲūŅĐß˙ō"˙+"˙˙úöū˙ũī˙äãÜūØß˙åō ū/ū7*˙ũüëÛũÛâā˙×Ûëüüũ ū˙˙ūķøū˙öūåÛūÛėņôø $(ū˙ ū÷ũđņđūķ÷ūüũđî÷˙øđūëņū˙ ū#' ˙õäŲ˙ŅŅũĶŪūí ˙%.ū%ū  ÷ō˙ūūņã˙ß˙ŪßūŲā˙í,5˙0$ū˙ė˙ÜÛ˙Ü×˙ĐĶįū˙˙!&˙˙ ˙ũũ˙˙ûų˙øņ˙áÔ˙Úäî˙ō˙0˙/&˙ũ˙˙íéë˙íîíķö˙ôõų˙ü˙÷˙˙ ũ üū ķ˙ŨĐūÎŅūÖßūķ ˙˙$+ü,#ũ˙øņõ˙˙ūōūâãũæå˙Ûåô˙ &/,&˙īßÚūĶÎūĮĶéū˙ ˙/.ū ûũüõúũũũúũīėŨ˙Õ×˙äđüū %˙14&˙ ū÷įũâčūæããūęôūüüū ˙ ū ˙˙ū  ūōūÕËūĖĶū׿ü+ū.+)ū ūųø˙öķđ˙öųūûķūčæūėėūæčūû ūū$ū*$ū˙ ųâĶĖ˙ÂÃÎåũ˙!,˙55˙˙˙ûô˙ōö˙˙ūöėũæâ˙Ö˙Öäūķũ$0˙2+ũū ūūįáūáŪ×ũ×áüđ˙˙ ū˙ ˙ūøöūü ū ˙ōûÖČüÍÕüÜíü0û2*û ˙ ÷ņõķūīņ˙ōķķíęņ˙÷ķķ ˙ ˙"˙$˙˙ã˙ÍÆ˙ĀÁĪ˙åū,0˙43˙û˙øõ˙ņø˙ũ˙˙đãũâãūÛÜūėū ū%+ū+(üū ūęŪũÚÕūĖŅß˙ī˙$˙+ū ü˙ōđūøūųė˙ÖĖĶā˙čû˙!˙.0ūũ˙ķīöķėííîūķôũđûüúúüū ˙$ūūāČÄ˙ÃÆüÖđü"0..*˙˙öûūööûūúųū÷ėáũāįũåæũö ũū"%˙&$ ˙ ˙č×ÔüÎĘûŌâûõũ#ũ&+ũüūøđūîúũ ūøũîėûÛŌüŪî÷˙ū(˙'˙ ô˙íôīčę˙ęđ˙öüūû˙ ˙üūũūūū# ˙ũøßūĮÉ˙ĘŅũâųū!.˙*#ũ!ūūöũũúųū÷õō˙īíūááî˙đîūũ ūū"ū ü üũäŅĐ˙ĖÎūÖéū˙(ū'$ũ&üúũõđņūøū˙˙đęîãŨūëûū ūū"˙˙ūô˙ėîëįūčđũņûū˙ û˙û˙˙ūü˙ ˙üũķāũÎĐÖ˙ßîũû'ü%ũū˙øū˙˙öô˙ņîūėđūâæũôûü÷ū ˙ūūøāĪÍÔŅ˙ßôüũ*)ū$˙õõūķķüøũũöų˙ú˙éëūėîũéú˙ ˙ûøūūũûôįũįë˙įė÷ūõū˙ ūūöũø˙ų˙˙ ũķüöãÚũÔčę˙ų˙#ū˙  ˙û˙ņūķđũíë˙ō˙äđ˙õū÷ü ˙ūūüøīũâĪ˙ŌØØåū˙ ũ%˙& ˙ íūúō˙öķúč˙÷ōūōæūķ÷˙ú˙ũ ûüūüũõũōį˙ãōæ˙õķ˙ūūũūüú˙øųī˙ū ûüųũķķÜūâđũõü˙ūū ˙ ˙ūūņũķôíūėîūōī˙ūūũūũũ˙˙ō˙íčÕ˙ØÜ˙Ũî˙!ū"ûüķú˙÷ôđ˙īë˙īöķ˙æø˙ ūũū ų˙öūņũäéüņíũōøū ū˙ ū˙ö˙öđôūü ˙ üøũúôūãîö˙úųū ûūũ˙ūû˙ōô˙ôîį˙ķú˙õ˙ūū  ūūūūūđōūëÜ˙ŲŨæņ˙ü ˙#ūü˙ūöûũûņũéđ˙ëîöņ˙ė˙ ũ û ū ũū˙ũ˙ųų˙īčüėķüííūũ ū ˙ ū˙ņîņ˙ōũū˙ū üú˙ôđūōüūûķ˙ū ū ūūúũüõ˙đ˙ôëũæøū˙üūū˙ū  ū˙ö˙õđũßÕūāęņū ˙%ū ˙üûũūüëįūđîūëķō˙ķ˙ ˙ ũ úúũū ūúö˙õíę˙đéę˙ ˙ ˙ū  ūëėõ˙ôūūūū˙ ūõûũ÷õüũøüđüüüüüũ˙ųū÷īņę˙íûūúũūūų˙ūūûõņŪūÔåūëķūũ˙ # ˙ųęũįīüīéūėøũ˙ū ˙ ū˙ ūööūöė˙įé˙æ˙ėũ˙ ˙ úęđúūøûũ ˙ø÷ū˙÷ú˙ņķūúüüüūũųú˙úķ˙čęņ˙ô˙ ˙úūũúûûūü ˙  ˙øôéũŲÜüčņņ˙ų˙ū!ũ ˙üķūččüíîüâđ˙ ˙  ˙û˙ ˙˙öö˙õč˙āá˙éķ˙ ˙˙ ūõ˙īõ˙˙øķü˙˙ö˙û˙õķũôôũûūūū˙ūūųö˙öīãëüūûū˙˙û÷õũū ūū  ü üũíä˙Ūā˙ęķė˙ö ūüû˙ķ˙ņëæ˙ęëūäķ˙ũūũ ˙ũũū˙ų˙ōņä˙Øā˙īö˙˙ ˙ ˙ūüøūų˙õîü˙ū˙ūüü˙üđ˙īūø÷ūû˙ūü˙øūīōé˙áņūū˙ūūōõ ˙ũ ü ū÷æååäíī˙éúü ū˙ūūėđ˙ęäįūįî˙ų ūü˙˙ū˙ũũũūöîę˙ÛÖūæôû ˙˙ũũūũ˙ųīīūųũú÷˙˙˙øíõ˙üũūū ˙üūúđ˙é˙čåęúūūüķū˙  ˙  ũūîä˙ééūæę˙ë˙đū˙˙!ũüōūęîę˙áãūīø˙ũøū˙ üũ˙ūķé˙á×Üíųû˙ū ˙ ˙öņīūîø˙ũöõ˙ũķđųūū ˙ū˙öö˙ėäãę˙ņũ ū˙ū üú ūųūëčííūææ˙īų˙˙#˙ūũđęũëëáūãõ˙ũ˙ûü˙˙ ˙ūüūûúōũãÜ˙Ųāíúų˙ûū˙ ũ ˙˙îíđūîķ˙÷ķ˙÷ũũ˙öõ˙÷˙ū ˙˙ø˙ōđũčßũâņ˙÷ũũ ˙˙˙ū ūü˙ūúņūėí˙ī˙îæūįú˙ ˙ūūūöūîéūęčũāęũúūũûü˙ ˙ū ūû˙öė˙ŨÛüßæüđųũø˙ ˙˙˙ ūõéíūđīüîņũôúūũ÷÷˙ü˙  ˙ ˙ũúõ˙îėã˙Ûį÷ũū ü üũ˙ũũųũũũ˙ķũíđūđķī˙įņũ ûüūũ÷ķ˙íč˙ææ˙æđūũũûûū ˙ ũũūūøķūæÜß˙åęđ˙÷˙˙ ˙!üũ đūæė˙đîč˙í÷ũ˙˙ūø˙ü˙  ū˙ ūúöō˙ëįûŪŪúęûũ˙˙ũūüũ˙ûųūøúüũøũîđ˙ķöūôđūđû˙ ˙ ü˙ķōîįãč˙îöū˙ū˙ũ  ūū  ūū÷ũëâüŪæûėîûđûũūũũ÷íé˙ëđé˙äīû˙˙ū˙úũũ ü  ũ ũú ûõō˙ė˙åāũŪæüņū˙ ˙ ˙  ˙÷˙ō÷÷˙õô˙đđõūøų˙öøũ ˙ū ūūôōņė˙âãũí÷ũû˙ ū  ū  ˙˙ûüüũōčüâį˙î˙ōņôũū˙˙ ũūôūíę˙ęę˙åį˙ōū˙ūūũ˙ ū ū˙˙ ūúõđūéâūßæí˙öũūũ ũ  ūüđũíķ˙ķīėūîöüųūûûüûû ü ū˙ū÷ō˙īđūčâũæķūūū˙ ˙ū ˙  ūû÷˙ø÷ūîčūčđūöøūôųūūū˙ũøķūîëį˙åįî˙÷˙˙˙ ˙ ū˙úö˙ķîæ˙ßåî˙ķû˙˙ ū˙ ū  õëëđ˙īéë˙ōũũūũ ū  ˙  ˙ ˙úķũîīüëįūæđũūū ũũ ˙ ũũûúũööũõōũīđũôųũüûũųũ ˙ ˙ üõ˙ņīįäūæîũöũ˙˙˙  ūū ų˙ôõ˙ōëäå˙í÷ūøûū ū˙˙ ˙ųíčūįė˙ééīüūũ ˙ ūũ  ˙˙ûöōüîîüęëîū÷ũũũũ˙ ˙ųūōķ˙õôūōņũõú˙ũ˙˙ū˙ũ ū  ū ūūõķ˙ōīįå˙ėöũū˙ũ   ū  ū ũūũôķ˙ķķ˙ėčūëö˙üú˙ú˙ ˙ũũ ũüõüęįûæčũčí˙õū  ūū ū úũõķūķî˙ęė˙đöūüū ˙˙ū˙ûņí˙íôũķķüķūū˙˙ū  ū ˙õ˙ņô˙îęé˙ņü˙˙ ˙˙ũ˙øô˙ķõķ˙ōīôũúūũøú ˙ũũ˙÷đęūææ˙åėūķūũ ˙ ū ū˙ūūôō˙ķöūîėũđ÷˙ü˙ū ũū ūũũķ˙éęūîô˙ô÷˙ûū ˙˙˙˙˙ ˙˙ûõũķķûīíûīúü˙ū˙  ˙˙ūų÷ö˙ö÷õūõø˙öúü˙ûųûũ ū ûņîėūææūčņūû˙˙ û  û  ū˙ũø˙ōņ˙ö÷˙īđö˙ũ˙˙ūū ū ũ  ũüũöđũęėņ˙ôų˙ü ˙ū˙ũũū˙˙ūüøôôķķöūūũ  ūũ˙úö˙ķ÷ûû÷úūũūūúųūųųũü ū ˙˙ųūîīūíéũįîüõũ˙ ˙ūūūũ÷÷˙ōõ÷˙÷ôōū÷ũũũũ˙˙ ū  ũ˙ôũīë˙įí˙đö˙üū ũū˙˙˙ü˙û˙˙üúüûũûūķõö˙öû˙ūūüû  ũū˙˙üõôūôú˙˙ũüūü˙ø÷˙øúū˙ũ ũ ūūöīņ˙îę˙ė˙ņüũ˙˙˙ūũüųüöøū÷÷÷ų÷õüūũūū ũū  ˙ųđėėė˙īķūøū ũ  ũ˙ũũúûũū˙ūú˙ûü˙ķ÷˙ųü˙ũū˙  ūũ˙ũûü˙÷ôô˙øũū˙ūūüööū÷üūũ˙ ˙ū˙÷ōņūīîüīöüũū˙˙˙˙ũųųūöúūüø÷˙ųúūöúũûūüūū ũ ûûöî˙ë˙îī˙īöü˙  ū ˙ũ˙˙ũüúü˙ũū˙ū˙˙úũ˙˙üö˙öüū˙ü˙ũ˙ ˙˙ūúū÷÷ūõ÷˙üūūū÷˙õųũüūūū ũūųôđō˙đķũųũüũ˙  ˙ûûüûüųųũ˙ũøũöúũûøũ÷üũũū  ˙ ˙˙öîíũđđüņųũü˙ ˙ûū˙üûũũū˙û˙˙üüūũü˙öø˙ūūũ˙˙ ũū˙ũ÷÷ū÷÷˙ú˙ūūūū˙ūūøūõûüũ üūōō˙ķôø˙ûũ˙ũ˙˙˙ũüūúüüüũũų÷˙ųũø÷ūü˙ü˙ ˙  ˙ūũöīđūņņûôûū˙ ˙˙ü˙ũūũüū˙ũũū˙˙ũ˙ųüūų˙ú˙˙ũ˙˙˙˙ū˙ö÷ũøųūûū˙˙ūūũũøũõúūûūūũ ūü˙˙û˙ķķüõ÷ųũúūúūũ˙˙ū˙˙˙˙ũüūüüūúũūú÷ūøüū÷÷ü ūūüøüđđüņôöũûũ ˙ũūũū˙ūūūũũ˙û˙üø˙úū˙úûũ˙˙ũü˙˙˙ūũø÷ø˙ųúūũ˙ũūüüū˙ųøú˙ũ˙ūûûũûú÷ôūöųų˙ûū˙ũ˙ũū˙˙˙ūüũ˙úú˙ũüøūųûûøøūûū  ü  ˙ũûø˙ņđķ˙õø˙ũ˙˙˙ū˙ũ˙ũũūūúúũüûũ÷û˙ũüúūũüūũ˙˙ū˙ööųūøûũ˙ũũūüūüųū˙ũú÷øú÷ û û üøöúúųũūūöėîņįãû˙ üüüúû ũü'ø)øûû*øũņėĘž Ä Á Îíü1ų3ø[ķ`ōFöC÷8øũū˙˙âŌŪęĮÆįđí ˙ũ&ü=ųAų-û%üBų:úū*ü-û˙ũ˙âËÂļŠĖ ˙ūūYøsöYøTų7û˙ū˙˙˙Ü ˙1ü ˙ ˙2üŲæØËæōķ ˙FûRúūSú‚÷cųfųVú;üūūô”—´“”ņ˙˙>übú^úTûPûBü2ũuų…øEüBüiú!ūŨ™rA ^„‚¸[û“ø‘øÎõŨôģöĒ÷—ørú'ū˙īŠ‹ąNN°×ė(ūgû{úųē÷}ú?ũú~úAũTüVü-ūũ˙îģhNeM§,ūEũ[üÍ÷ūõÎ÷ĨųqûIũ˙0ū˙¸˙˙2ūæŌåСÍÍæNũ/ū6ū–úhü3ūûŅøÎø„û‚ûiüæ•ö(z^“lüoü¤úßøŠûpü¨ú‹ûsüÅųĢúVũ7ūZũü˙U4ų2Šmâ˛úđøđøø/÷/÷øø˜û˙ ˙Ā(%ČáĨ˙#˙}üųų#ø"øŖû‚üåųÅúƒüCūbũDūģĀ5ąī8›Eū‡üŅúW÷Ąõųöųôų‹ülũfũánŧÛq)o¸¸$˙Iūlũšûoũmũ—ü’ü—üēûRøáú˜ü–üMūŗhõX Īc‰Oūúęú;ų9ųÆûüúÄûyũ üŸüRū˙˙ˆâã  ]„Ŧ+˙øúMųúúNųĖöĐöŖ÷}øŠü˙˙Tü ) |ĻĢUūYūûŠøŠø_ųaųÜû˛üû:úƒũYū+˙}G›7˜Kv/˙ûCúĖ÷-ö øuųŖøGú‹ũ1˙ @¸PÍ`ūũ”ũŊüWúīû3˙`ūõûõû&û‰ų÷û’ũ—ũū˙;ĸr<ŖhÎÉüÉøĮøĖø–ųüÍü3ûü2˙Î2˙—˜0‘0bca˜ũ˙9˙;ûwú?ûvúƒöƒöø¯ųŲüū˙`īĒ Ļƒļ¤ũŨüēųb÷(ø‡úLûQûŪüüOû:˙ū˙:˙˙˙q:˙qUR¯ũËųų~÷z÷Gø ųHø˜útūPS`/]đ%Ovūwūīü)üjûdû¨ú*ü>˙´ũkûmû.ü2ü´ũ@˙ü˙‹MŸTɆ<˙~ūđų¯÷­÷ôųöųļúüü:üūBCū˙„?@?˙~ŋũÄũBüˆûƒûLų ÷Ī÷ËúũE˙9u5Ą Š ëø{B˙MüÕú›øâ÷[ųQü“û•ûQüũˆū˙˙ŧE˙ģŦÜ!Š21ŧũĩøĩøü÷˙÷A÷Ŋørų_üuŸ\Ëú<?Tŧũ!ũũ÷ú8úAúōúküŒū#ũ¯û˛ûŲũ‘ūH˙¸ļŪnŨ˙’#¸ÛũrüâøÁö0øNúVúû/ũ‘ūn‡A‹ƒ!˙˙˙˙™ūū˙›ū/ũ0ũ5ũ{üû˛ų˙øļų2ũūū˙áų[Š,d˜ū*ûŧųeø^øwú:ũŽüˆüđũ˙˙°˛°˙˙a!Îskp]T˙áû}ø'ų-ųÍ÷"÷-ųúŖūšÄĀ $ylļS˙œüĄüîûîų>ų™úŖüKũũũMũ¤üOũ˙˙Y­˛¯ TRX˙ūûVųY÷Ģö[ų­úZûüĢūūų¤øĻĨMûW˙ūūūU˙˛ū\ũü]ũ ūģüeûČúŋúŋüV˙ĨEÜ,‚K]˙¯ūÅüų:ø†ųŌúüjũkũūíK˙˙§Ü3đé“ę˙˙tũāú ų@úŸų]øXøëúūFĐqøœ°‹ū˙zũ5ü6ü÷úų´ų8ü}ũ}ũ|ũ}ũū ũ˙ã^ũ^Ū ‚ũËųë÷ņ÷ømúĒûHüˆũÛRōO´˛˛vŠũīü‹ũũŒũđüUüņüĮūĮūôüšû\üŒũĖū™7>Ž ?Đ˙˙Ėū•ũ,ûÄøÃø0ûüüšũËūÍūĖg˜™1ebË™˜Č™7ūŲû@ûŠúĢúúéøúũū˙Æ\ Mx-Ôū§ũæûPû'úûøŋúũÔū­ũĒũAū“,ū˙č ŖxP(”Øūøûųō÷„øąųŅú˙û˛ũp˙Ų˜lūlļÛū™ü•üģũ(ũ üü.ũKūo˙āūŊũ.ũOūo˙‘@íœîĢÜūČũümúŋøoú§üVūTūåū˙˙Ģ6ĒŽĻŠ˙˙r˙Dũ&ü-ü(ü/üûú+üëūĸēÍü÷YŸw˙^ūNũĒûú úžú:üaūīūaūeū˙˙‹Œ†œžĖŊ™ˆđūVũ,úųĻų:û=ûMü\ũw˙—¯žąĢąĒ•x˙ŪüÍûāüÛüÔûMû[üoū|˙y˙rūíũõūũ˙‚œ‰€˙rūôũfüāúŲųâúíüûū|˙üū˙˙ˆ‹ˆ„ ú˙˙|˙˙ūûũûü{ũ€ũyũ‚üxû‚üū‚||x÷†˙€ūūũ ûúŽû ũū ˙ū ˙|˙‚ų}úskkxz˙ū û+ú1úĄû#ü üüūōëYb]\]eöū§ü-ü¨üĒüĩû2üžũ†˙†˙”ū˙xķy˙˙ķZLŌj˙˙˙ū3ũ@üUûTûCü%ū†˙†˙ņOËØyũ˙đ_šū,ū˙‹˙˙žūˇũ+ūŸūŖū*ūFũÎüēũ˙uįė@Ŗšr˙Ŗū6ū×ü„û ûŪü3ū˙ŠūĻū˙ostnvQ;ĻÉmŒ˙˙Íũü*û üéücũėü`ũ"˙Lĸ›  ė Ŋ‘˙ÖũøüŠüüü"üøüšūū˙’˙#˙GImE‘ŠI’˙ģūģūáũŸü3ü3üũOū)˙–˙’˙n?BjkÕkĀū„ũęũ/˙)˙Æūëũ]ū”˙.˙đũķũ]ū,˙ū˙h;pŪmÔ,˙ĖūĮūbūÄüõû-ũcū3˙1˙eūËūÎÍ˙˙g2û3eœ˙3˙lū<ũ×üØüŖũmūĨũ>ũpūĮøU÷UīpūūHũéü…ü†üNũuū;˙ž˙˙Ā*!Á†į#>˙}ū{ū~ūģũ›üœüžũ~ū@˙@˙ ˙Ūžŋ__C˙ÅũËũ%ūæūãūŠū…ū¤˙\^čūŠūæū¤˙Ą˙Ĩ˙Ą˙_rĐrģũ˙K˙čū‘ū4ū|ũ~ũ’ūG˙§˙ęū—ūG˙ˇYĻ˙ĩjjĩū˙O˙Ŗ˙¨˙ōū@ūįũįũBū@ūŸūæũîũM˙ąc`dle ÷ūJūJūķũDũđüíüOūQ˙¨˙U˙Ļ˙ZĢZ­W§ūYū¨ū­ūūũXũ¯ūU˙Y˙V˙X˙SROũĻüųVĒ˙ļū ūūcū˛ūšū˛ū ˙ü˙Пa˙ĩū°˙Q˙˙_˙­˙RôōR^˙^˙°˙˙Ŋūqūūsū ˙e˙ ˙Åū˙ŗ˙ī˙˙œ<ėą˙˙a˙ŗ˙˛˙˙-ūČū˙ĘūĘū|ū~ūËū˛˙Nįä7ČÍ~JĶūÍūŅū„ūĸũXũīũŌūi˙j˙˙´˙â*áā”ßߕ˙˙$˙ÕūŨūÖūŨūFūūūp˙ĩ˙q˙#˙×m ‘ ×āū›ūSūTūšūŸūšū.˙ũ˙ŒGs˙ģ˙EFē˙.˙ģ˙FŠFu˙1˙ū˙s˙đūęū4˙w˙w˙w˙đūw˙‡Cũ˙‰Čōū:˙ē˙Ā˙x˙úūąū}˙ž˙8˙šūĩūúū|˙@BÄE†ÂĀ˙?˙>˙?˙ŋūŋũūÂū>˙ƒ˙?˙D˙>ũũ}@ŧŧ?˙˙F˙˙H˙˙ ˙ĘūËūÍū˙‰˙˙˙F˙ˇķĩ?˛õ˛Š˙ŅūØūĐūÚūĶūœū˙Â˙?8˙˙˙˙<9Œ˙R˙Å˙8É˙Q˙Ž˙˙˙Ž˙ãū˙’˙Š˙Z˙˙˙Į˙8988787“˙ ˙^˙Ä˙Í˙˙(˙Z˙Ę˙‘˙ŋūíū—˙Į˙Ė˙Ę˙  ՟6˙˙Ė˙b˙,˙Æū[ūĮū.˙d˙e˙b˙›˙eĪĐū˙4š—™˙4˙j˙™˙œ˙6˙˙k˙h˙l˙š˙n˙7˙Ÿ˙0c•_eÅp˙ ˙?˙@˙˙âūāū@˙/1ū˙1\1˙˙Ą˙Ĩ˙ū˙Ō˙Ŗ˙¤˙Ō˙/u˙L˙Ŗ˙Ô˙Ļ˙K˙M˙x˙*Ņ˙.X+Ļ˙~˙P˙Ē˙Š˙Š˙~˙Ē˙Õ˙˙˙Q˙ą˙Ļ˙…˙Ē˙§|˙˙TÖ˙…˙2˙ ˙ ˙5˙]˙^˙_˙°˙ū˙Szū˙)vOŲ˙b˙˙°˙Ú˙˛˙>˙h˙×˙ĩ˙Œ˙h˙h˙˙×˙)#(ooqn&l˙H˙n˙n˙%˙˙K˙¸˙˙˙&ũ˙ik˙˙Ū˙š˙ē˙Ū˙Ú˙â˙ˇ˙ž˙ũ˙ā˙ū˙™˙x˙ž˙Ũ˙œ˙X˙{˙ž˙˙˙C˙˙Aa"Ŋ˙Ŗ˙ģ˙Ä˙ž˙Á˙Ą˙Á˙ū˙ũ˙ ˙†˙ƒ˙Ä˙Ŗ˙†˙Â˙!Xz[ü˙$5‡˙Q˙m˙o˙m˙˙‰˙­˙ū˙;S:6Į˙Ž˙Ę˙Č˙æ˙Č˙–˙­˙č˙â˙™˙x˙š˙Č˙Î˙˙˙eiJ6ũ˙Đ˙™˙Ÿ˙š˙U˙Q˙Ÿ˙Î˙į˙EJ˙˙Đ˙Ō˙˙˙ę˙č˙Ō˙Ō˙é˙é˙ę˙Ô˙ģ˙Ö˙Ķ˙¨˙•˙ŧ˙í˙ū˙ũ˙(UÃ˙Ŧ˙Ų˙Ô˙Ú˙Á˙˛˙ë˙ę˙Ú˙Ų˙Å˙ĩ˙Ų˙´˙´˙í˙88#%§˙“˙Ŋ˙¸˙ģ˙Ē˙¨˙Ī˙˙˙ !Đ˙ā˙ā˙â˙ß˙ã˙Đ˙ã˙đ˙ņ˙ļ˙Ĩ˙Č˙ā˙ô˙ī˙HE* á˙Ī˙¸˙ĩ˙ĸ˙ŋ˙Ų˙į˙˙˙ũ˙ô˙æ˙ü˙đ˙í˙į˙ õ˙ß˙÷˙ķ˙í˙ß˙á˙Ų˙Ū˙đ˙ü˙ô˙Ū˙å˙å˙æ˙Ũ˙Ū˙đ˙ū˙ ũ˙ë˙î˙ņ˙â˙ß˙î˙Ũ˙÷˙ü˙  ü˙ î˙ā˙Ũ˙ë˙į˙ä˙ã˙ę˙û˙ū˙ ú˙ō˙ú˙ö˙ũ˙ö˙û˙ö˙ų˙ú˙ú˙ø˙ę˙ī˙õ˙û˙ų˙ū˙ ū˙ų˙ų˙ô˙÷˙ø˙ų˙ū˙û˙ũ˙ū˙˙˙˙˙Nagstamon-master/Nagstamon/thirdparty/000077500000000000000000000000001505160700500204165ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/000077500000000000000000000000001505160700500213145ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/ChangeLog000066400000000000000000000101221505160700500230620ustar00rootroot000000000000002007-06-10 Mike Grant * (many files): (mgg) Converted tabs to spaces throughout the codebase, using reindent.py (SF id: 1559082) 2007-03-18 Mike Grant * Xlib/display.py: (mgg) Added a get_atom alias that uses the internal cache * Xlib/xobject/drawable.py: (mgg) Added a raise_window() alias to the Window class 2007-02-15 Mike Grant * Xlib/xauth.py: (mgg) Python 2.5 didn't like the way the buffer type was used, resulting in X authorisation failure, so reverted to using slices 2006-11-22 Mike Grant * Xlib/ext/record.py: Addition of RECORD extension by Alex Badea , SF patch id #1538663 (demo program in python-xlib/examples/record_demo.py) 2006-09-20 Mike Meyer * Xlib/ext/xinerama.py: (mwm) Addition of Xinerama extension 2006-07-22 Mike Grant Various typo fixes, general updates. Changelog hasn't been maintained since 2002, but some of the more significant comments from cvs logs follow: * Xlib/display.py: (petli) Fix bug in refresh_keyboard_mapping: ignore modifier and pointer remappings. Plays nice with pydoc. Copied some text from the docs to __doc__ strings in Xlib/display.py so that they appear when you use pydoc. Completed documentation for Display objects. * Xlib/XK.py: (calroc99) Minor doc string changes. Called load_keysym_group() for miscellany and latin1 keysyms, rather than importing the modules. * Xlib/keysymdef/*: (calroc99) Small change to keysym loading. Works the same way. * Xlib/support/*, Xlib/xauth.py, Xlib/error.py: (petli) Added ~/.Xauthority parsing by Python code instead of relying on /usr/X11R6/bin/xauth. Not activated yet in all cases yet? Activated in unix_support.py. * Xlib/xobject/drawable.py: (petli) Fix bugs in definition and method of GrabButton/Pointer * Xlib/xobject/icccm.py: (petli) Add WithdrawnState to WMHints * doc/*: (petli) documentation updates, typos and completing documentation for Display objects 2002-03-30 Peter Liljenberg * support/unix_connect.py: Handle fcntl/FCNTL changes in Python 2.2. 2002-03-11 Peter Liljenberg * xobject/drawable.py (Drawable.fill_arc): This should be a PolyFillArc. Fri Jan 19 17:49:45 2001 Peter Liljenberg * XK.py: Moved all keysyms into separate modules, based on their category. By default only the miscellany and latin1 keysyms are loaded, and other have to be loaded by importing the Xlib.keysymdef. module, or calling load_keysym_group('category'). * display.py (Display.lookup_string): (Display.rebind_string): Functions to translate keysyms to strings, and binding keysyms to new strings. 2001-01-16 * xobject/drawable.py (Window.send_event): * display.py (Display.send_event): Changed the order of the event_mask and propagate arguments. 2001-01-10 * display.py (Display._update_keymap): The first half of the update algorithm operated on an earlier type of code->sym map than the second half. Stupid, stupid. It would have been nice with a type-checker now. Tue Jan 9 13:03:19 2001 Peter Liljenberg * display.py (Display._update_keymap): Fixed call to append with 1.5.2 semantics, broke in newer Pythons. 2000-12-22 * display.py (Display.keycode_to_keysym): (Display.keysym_to_keycode): (Display.keysym_to_keycodes): (Display.refresh_keyboard_mapping): (Display._update_keymap): Added keymap cache implementation. 2000-12-21 * xobject/colormap.py (Colormap.alloc_named_color): Extended to handle #000000 style color specifications. * xobject/drawable.py (Window.reparent): Renamed from reparent_window. * display.py (Display.set_error_handler): Added. 2000-12-20 * display.py (_BaseDisplay): Implement a cache of atom names. Nagstamon-master/Nagstamon/thirdparty/Xlib/X.py000066400000000000000000000237471505160700500221120ustar00rootroot00000000000000# Xlib.X -- basic X constants # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Avoid overwriting None if doing "from Xlib.X import *" NONE = 0 ParentRelative = 1 # background pixmap in CreateWindow # and ChangeWindowAttributes CopyFromParent = 0 # border pixmap in CreateWindow # and ChangeWindowAttributes # special VisualID and special window # class passed to CreateWindow PointerWindow = 0 # destination window in SendEvent InputFocus = 1 # destination window in SendEvent PointerRoot = 1 # focus window in SetInputFocus AnyPropertyType = 0 # special Atom, passed to GetProperty AnyKey = 0 # special Key Code, passed to GrabKey AnyButton = 0 # special Button Code, passed to GrabButton AllTemporary = 0 # special Resource ID passed to KillClient CurrentTime = 0 # special Time NoSymbol = 0 # special KeySym #----------------------------------------------------------------------- # Event masks: # NoEventMask = 0 KeyPressMask = (1<<0) KeyReleaseMask = (1<<1) ButtonPressMask = (1<<2) ButtonReleaseMask = (1<<3) EnterWindowMask = (1<<4) LeaveWindowMask = (1<<5) PointerMotionMask = (1<<6) PointerMotionHintMask = (1<<7) Button1MotionMask = (1<<8) Button2MotionMask = (1<<9) Button3MotionMask = (1<<10) Button4MotionMask = (1<<11) Button5MotionMask = (1<<12) ButtonMotionMask = (1<<13) KeymapStateMask = (1<<14) ExposureMask = (1<<15) VisibilityChangeMask = (1<<16) StructureNotifyMask = (1<<17) ResizeRedirectMask = (1<<18) SubstructureNotifyMask = (1<<19) SubstructureRedirectMask = (1<<20) FocusChangeMask = (1<<21) PropertyChangeMask = (1<<22) ColormapChangeMask = (1<<23) OwnerGrabButtonMask = (1<<24) #----------------------------------------------------------------------- # Event names: # # Used in "type" field in XEvent structures. Not to be confused with event # masks above. They start from 2 because 0 and 1 are reserved in the # protocol for errors and replies. # KeyPress = 2 KeyRelease = 3 ButtonPress = 4 ButtonRelease = 5 MotionNotify = 6 EnterNotify = 7 LeaveNotify = 8 FocusIn = 9 FocusOut = 10 KeymapNotify = 11 Expose = 12 GraphicsExpose = 13 NoExpose = 14 VisibilityNotify = 15 CreateNotify = 16 DestroyNotify = 17 UnmapNotify = 18 MapNotify = 19 MapRequest = 20 ReparentNotify = 21 ConfigureNotify = 22 ConfigureRequest = 23 GravityNotify = 24 ResizeRequest = 25 CirculateNotify = 26 CirculateRequest = 27 PropertyNotify = 28 SelectionClear = 29 SelectionRequest = 30 SelectionNotify = 31 ColormapNotify = 32 ClientMessage = 33 MappingNotify = 34 LASTEvent = 35 # must be bigger than any event #----------------------------------------------------------------------- # Key masks: # # Used as modifiers to GrabButton and GrabKey, results of QueryPointer, # state in various key-, mouse-, and button-related events. # ShiftMask = (1<<0) LockMask = (1<<1) ControlMask = (1<<2) Mod1Mask = (1<<3) Mod2Mask = (1<<4) Mod3Mask = (1<<5) Mod4Mask = (1<<6) Mod5Mask = (1<<7) #----------------------------------------------------------------------- # Modifier names: # # Used to build a SetModifierMapping request or to read a # GetModifierMapping request. These correspond to the masks defined above. # ShiftMapIndex = 0 LockMapIndex = 1 ControlMapIndex = 2 Mod1MapIndex = 3 Mod2MapIndex = 4 Mod3MapIndex = 5 Mod4MapIndex = 6 Mod5MapIndex = 7 #----------------------------------------------------------------------- # Button masks: # # Used in same manner as Key masks above. Not to be confused with button # names below. Note that 0 is already defined above as "AnyButton". # Button1Mask = (1<<8) Button2Mask = (1<<9) Button3Mask = (1<<10) Button4Mask = (1<<11) Button5Mask = (1<<12) AnyModifier = (1<<15) # used in GrabButton, GrabKey #----------------------------------------------------------------------- # Button names: # # Used as arguments to GrabButton and as detail in ButtonPress and # ButtonRelease events. Not to be confused with button masks above. # Note that 0 is already defined above as "AnyButton". # Button1 = 1 Button2 = 2 Button3 = 3 Button4 = 4 Button5 = 5 #----------------------------------------------------------------------- # XXX These still need documentation -- for now, read # NotifyNormal = 0 NotifyGrab = 1 NotifyUngrab = 2 NotifyWhileGrabbed = 3 NotifyHint = 1 NotifyAncestor = 0 NotifyVirtual = 1 NotifyInferior = 2 NotifyNonlinear = 3 NotifyNonlinearVirtual = 4 NotifyPointer = 5 NotifyPointerRoot = 6 NotifyDetailNone = 7 VisibilityUnobscured = 0 VisibilityPartiallyObscured = 1 VisibilityFullyObscured = 2 PlaceOnTop = 0 PlaceOnBottom = 1 FamilyInternet = 0 FamilyDECnet = 1 FamilyChaos = 2 PropertyNewValue = 0 PropertyDelete = 1 ColormapUninstalled = 0 ColormapInstalled = 1 GrabModeSync = 0 GrabModeAsync = 1 GrabSuccess = 0 AlreadyGrabbed = 1 GrabInvalidTime = 2 GrabNotViewable = 3 GrabFrozen = 4 AsyncPointer = 0 SyncPointer = 1 ReplayPointer = 2 AsyncKeyboard = 3 SyncKeyboard = 4 ReplayKeyboard = 5 AsyncBoth = 6 SyncBoth = 7 RevertToNone = 0 RevertToPointerRoot = PointerRoot RevertToParent = 2 Success = 0 BadRequest = 1 BadValue = 2 BadWindow = 3 BadPixmap = 4 BadAtom = 5 BadCursor = 6 BadFont = 7 BadMatch = 8 BadDrawable = 9 BadAccess = 10 BadAlloc = 11 BadColor = 12 BadGC = 13 BadIDChoice = 14 BadName = 15 BadLength = 16 BadImplementation = 17 FirstExtensionError = 128 LastExtensionError = 255 InputOutput = 1 InputOnly = 2 CWBackPixmap = (1<<0) CWBackPixel = (1<<1) CWBorderPixmap = (1<<2) CWBorderPixel = (1<<3) CWBitGravity = (1<<4) CWWinGravity = (1<<5) CWBackingStore = (1<<6) CWBackingPlanes = (1<<7) CWBackingPixel = (1<<8) CWOverrideRedirect = (1<<9) CWSaveUnder = (1<<10) CWEventMask = (1<<11) CWDontPropagate = (1<<12) CWColormap = (1<<13) CWCursor = (1<<14) CWX = (1<<0) CWY = (1<<1) CWWidth = (1<<2) CWHeight = (1<<3) CWBorderWidth = (1<<4) CWSibling = (1<<5) CWStackMode = (1<<6) ForgetGravity = 0 NorthWestGravity = 1 NorthGravity = 2 NorthEastGravity = 3 WestGravity = 4 CenterGravity = 5 EastGravity = 6 SouthWestGravity = 7 SouthGravity = 8 SouthEastGravity = 9 StaticGravity = 10 UnmapGravity = 0 NotUseful = 0 WhenMapped = 1 Always = 2 IsUnmapped = 0 IsUnviewable = 1 IsViewable = 2 SetModeInsert = 0 SetModeDelete = 1 DestroyAll = 0 RetainPermanent = 1 RetainTemporary = 2 Above = 0 Below = 1 TopIf = 2 BottomIf = 3 Opposite = 4 RaiseLowest = 0 LowerHighest = 1 PropModeReplace = 0 PropModePrepend = 1 PropModeAppend = 2 GXclear = 0x0 GXand = 0x1 GXandReverse = 0x2 GXcopy = 0x3 GXandInverted = 0x4 GXnoop = 0x5 GXxor = 0x6 GXor = 0x7 GXnor = 0x8 GXequiv = 0x9 GXinvert = 0xa GXorReverse = 0xb GXcopyInverted = 0xc GXorInverted = 0xd GXnand = 0xe GXset = 0xf LineSolid = 0 LineOnOffDash = 1 LineDoubleDash = 2 CapNotLast = 0 CapButt = 1 CapRound = 2 CapProjecting = 3 JoinMiter = 0 JoinRound = 1 JoinBevel = 2 FillSolid = 0 FillTiled = 1 FillStippled = 2 FillOpaqueStippled = 3 EvenOddRule = 0 WindingRule = 1 ClipByChildren = 0 IncludeInferiors = 1 Unsorted = 0 YSorted = 1 YXSorted = 2 YXBanded = 3 CoordModeOrigin = 0 CoordModePrevious = 1 Complex = 0 Nonconvex = 1 Convex = 2 ArcChord = 0 ArcPieSlice = 1 GCFunction = (1<<0) GCPlaneMask = (1<<1) GCForeground = (1<<2) GCBackground = (1<<3) GCLineWidth = (1<<4) GCLineStyle = (1<<5) GCCapStyle = (1<<6) GCJoinStyle = (1<<7) GCFillStyle = (1<<8) GCFillRule = (1<<9) GCTile = (1<<10) GCStipple = (1<<11) GCTileStipXOrigin = (1<<12) GCTileStipYOrigin = (1<<13) GCFont = (1<<14) GCSubwindowMode = (1<<15) GCGraphicsExposures = (1<<16) GCClipXOrigin = (1<<17) GCClipYOrigin = (1<<18) GCClipMask = (1<<19) GCDashOffset = (1<<20) GCDashList = (1<<21) GCArcMode = (1<<22) GCLastBit = 22 FontLeftToRight = 0 FontRightToLeft = 1 FontChange = 255 XYBitmap = 0 XYPixmap = 1 ZPixmap = 2 AllocNone = 0 AllocAll = 1 DoRed = (1<<0) DoGreen = (1<<1) DoBlue = (1<<2) CursorShape = 0 TileShape = 1 StippleShape = 2 AutoRepeatModeOff = 0 AutoRepeatModeOn = 1 AutoRepeatModeDefault = 2 LedModeOff = 0 LedModeOn = 1 KBKeyClickPercent = (1<<0) KBBellPercent = (1<<1) KBBellPitch = (1<<2) KBBellDuration = (1<<3) KBLed = (1<<4) KBLedMode = (1<<5) KBKey = (1<<6) KBAutoRepeatMode = (1<<7) MappingSuccess = 0 MappingBusy = 1 MappingFailed = 2 MappingModifier = 0 MappingKeyboard = 1 MappingPointer = 2 DontPreferBlanking = 0 PreferBlanking = 1 DefaultBlanking = 2 DisableScreenSaver = 0 DisableScreenInterval = 0 DontAllowExposures = 0 AllowExposures = 1 DefaultExposures = 2 ScreenSaverReset = 0 ScreenSaverActive = 1 HostInsert = 0 HostDelete = 1 EnableAccess = 1 DisableAccess = 0 StaticGray = 0 GrayScale = 1 StaticColor = 2 PseudoColor = 3 TrueColor = 4 DirectColor = 5 LSBFirst = 0 MSBFirst = 1 Nagstamon-master/Nagstamon/thirdparty/Xlib/XK.py000066400000000000000000000062131505160700500222120ustar00rootroot00000000000000# Xlib.XK -- X keysym defs # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # # This module defines some functions for working with X keysyms as well # as a modular keysym definition and loading mechanism. See the keysym # definition modules in the Xlib/keysymdef directory. from Xlib.X import NoSymbol def string_to_keysym(keysym): '''Return the (16 bit) numeric code of keysym. Given the name of a keysym as a string, return its numeric code. Don't include the 'XK_' prefix, just use the base, i.e. 'Delete' instead of 'XK_Delete'.''' return globals().get('XK_' + keysym, NoSymbol) def load_keysym_group(group): '''Load all the keysyms in group. Given a group name such as 'latin1' or 'katakana' load the keysyms defined in module 'Xlib.keysymdef.group-name' into this XK module.''' if '.' in group: raise ValueError('invalid keysym group name: %s' % group) G = globals() #Get a reference to XK.__dict__ a.k.a. globals #Import just the keysyms module. mod = __import__('Xlib.keysymdef.%s' % group, G, locals(), [group]) #Extract names of just the keysyms. keysyms = [n for n in dir(mod) if n.startswith('XK_')] #Copy the named keysyms into XK.__dict__ for keysym in keysyms: ## k = mod.__dict__[keysym]; assert k == int(k) #probably too much. G[keysym] = mod.__dict__[keysym] #And get rid of the keysym module. del mod def _load_keysyms_into_XK(mod): '''keysym definition modules need no longer call Xlib.XK._load_keysyms_into_XK(). You should remove any calls to that function from your keysym modules.''' pass # Always import miscellany and latin1 keysyms load_keysym_group('miscellany') load_keysym_group('latin1') def keysym_to_string(keysym): '''Translate a keysym (16 bit number) into a python string. This will pass 0 to 0xff as well as XK_BackSpace, XK_Tab, XK_Clear, XK_Return, XK_Pause, XK_Scroll_Lock, XK_Escape, XK_Delete. For other values it returns None.''' # ISO latin 1, LSB is the code if keysym & 0xff00 == 0: return chr(keysym & 0xff) if keysym in [XK_BackSpace, XK_Tab, XK_Clear, XK_Return, XK_Pause, XK_Scroll_Lock, XK_Escape, XK_Delete]: return chr(keysym & 0xff) # We should be able to do these things quite automatically # for latin2, latin3, etc, in Python 2.0 using the Unicode, # but that will have to wait. return None Nagstamon-master/Nagstamon/thirdparty/Xlib/Xatom.py000066400000000000000000000036461505160700500227670ustar00rootroot00000000000000# Xlib.Xatom -- Standard X atoms # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA PRIMARY = 1 SECONDARY = 2 ARC = 3 ATOM = 4 BITMAP = 5 CARDINAL = 6 COLORMAP = 7 CURSOR = 8 CUT_BUFFER0 = 9 CUT_BUFFER1 = 10 CUT_BUFFER2 = 11 CUT_BUFFER3 = 12 CUT_BUFFER4 = 13 CUT_BUFFER5 = 14 CUT_BUFFER6 = 15 CUT_BUFFER7 = 16 DRAWABLE = 17 FONT = 18 INTEGER = 19 PIXMAP = 20 POINT = 21 RECTANGLE = 22 RESOURCE_MANAGER = 23 RGB_COLOR_MAP = 24 RGB_BEST_MAP = 25 RGB_BLUE_MAP = 26 RGB_DEFAULT_MAP = 27 RGB_GRAY_MAP = 28 RGB_GREEN_MAP = 29 RGB_RED_MAP = 30 STRING = 31 VISUALID = 32 WINDOW = 33 WM_COMMAND = 34 WM_HINTS = 35 WM_CLIENT_MACHINE = 36 WM_ICON_NAME = 37 WM_ICON_SIZE = 38 WM_NAME = 39 WM_NORMAL_HINTS = 40 WM_SIZE_HINTS = 41 WM_ZOOM_HINTS = 42 MIN_SPACE = 43 NORM_SPACE = 44 MAX_SPACE = 45 END_SPACE = 46 SUPERSCRIPT_X = 47 SUPERSCRIPT_Y = 48 SUBSCRIPT_X = 49 SUBSCRIPT_Y = 50 UNDERLINE_POSITION = 51 UNDERLINE_THICKNESS = 52 STRIKEOUT_ASCENT = 53 STRIKEOUT_DESCENT = 54 ITALIC_ANGLE = 55 X_HEIGHT = 56 QUAD_WIDTH = 57 WEIGHT = 58 POINT_SIZE = 59 RESOLUTION = 60 COPYRIGHT = 61 NOTICE = 62 FONT_NAME = 63 FAMILY_NAME = 64 FULL_NAME = 65 CAP_HEIGHT = 66 WM_CLASS = 67 WM_TRANSIENT_FOR = 68 LAST_PREDEFINED = 68 Nagstamon-master/Nagstamon/thirdparty/Xlib/Xcursorfont.py000066400000000000000000000037561505160700500242350ustar00rootroot00000000000000# Xlib.Xcursorfont -- standard cursors # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA num_glyphs = 154 X_cursor = 0 arrow = 2 based_arrow_down = 4 based_arrow_up = 6 boat = 8 bogosity = 10 bottom_left_corner = 12 bottom_right_corner = 14 bottom_side = 16 bottom_tee = 18 box_spiral = 20 center_ptr = 22 circle = 24 clock = 26 coffee_mug = 28 cross = 30 cross_reverse = 32 crosshair = 34 diamond_cross = 36 dot = 38 dotbox = 40 double_arrow = 42 draft_large = 44 draft_small = 46 draped_box = 48 exchange = 50 fleur = 52 gobbler = 54 gumby = 56 hand1 = 58 hand2 = 60 heart = 62 icon = 64 iron_cross = 66 left_ptr = 68 left_side = 70 left_tee = 72 leftbutton = 74 ll_angle = 76 lr_angle = 78 man = 80 middlebutton = 82 mouse = 84 pencil = 86 pirate = 88 plus = 90 question_arrow = 92 right_ptr = 94 right_side = 96 right_tee = 98 rightbutton = 100 rtl_logo = 102 sailboat = 104 sb_down_arrow = 106 sb_h_double_arrow = 108 sb_left_arrow = 110 sb_right_arrow = 112 sb_up_arrow = 114 sb_v_double_arrow = 116 shuttle = 118 sizing = 120 spider = 122 spraycan = 124 star = 126 target = 128 tcross = 130 top_left_arrow = 132 top_left_corner = 134 top_right_corner = 136 top_side = 138 top_tee = 140 trek = 142 ul_angle = 144 umbrella = 146 ur_angle = 148 watch = 150 xterm = 152 Nagstamon-master/Nagstamon/thirdparty/Xlib/Xutil.py000066400000000000000000000042521505160700500227760ustar00rootroot00000000000000# Xlib.Xutil -- ICCCM definitions and similar stuff # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA NoValue = 0x0000 XValue = 0x0001 YValue = 0x0002 WidthValue = 0x0004 HeightValue = 0x0008 AllValues = 0x000F XNegative = 0x0010 YNegative = 0x0020 USPosition = (1 << 0) USSize = (1 << 1) PPosition = (1 << 2) PSize = (1 << 3) PMinSize = (1 << 4) PMaxSize = (1 << 5) PResizeInc = (1 << 6) PAspect = (1 << 7) PBaseSize = (1 << 8) PWinGravity = (1 << 9) PAllHints = (PPosition|PSize|PMinSize|PMaxSize|PResizeInc|PAspect) InputHint = (1 << 0) StateHint = (1 << 1) IconPixmapHint = (1 << 2) IconWindowHint = (1 << 3) IconPositionHint = (1 << 4) IconMaskHint = (1 << 5) WindowGroupHint = (1 << 6) MessageHint = (1 << 7) UrgencyHint = (1 << 8) AllHints = (InputHint|StateHint|IconPixmapHint|IconWindowHint| IconPositionHint|IconMaskHint|WindowGroupHint|MessageHint| UrgencyHint) WithdrawnState = 0 NormalState = 1 IconicState = 3 DontCareState = 0 ZoomState = 2 InactiveState = 4 RectangleOut = 0 RectangleIn = 1 RectanglePart = 2 VisualNoMask = 0x0 VisualIDMask = 0x1 VisualScreenMask = 0x2 VisualDepthMask = 0x4 VisualClassMask = 0x8 VisualRedMaskMask = 0x10 VisualGreenMaskMask = 0x20 VisualBlueMaskMask = 0x40 VisualColormapSizeMask = 0x80 VisualBitsPerRGBMask = 0x100 VisualAllMask = 0x1FF ReleaseByFreeingColormap = 1 BitmapSuccess = 0 BitmapOpenFailed = 1 BitmapFileInvalid = 2 BitmapNoMemory = 3 XCSUCCESS = 0 XCNOMEM = 1 XCNOENT = 2 Nagstamon-master/Nagstamon/thirdparty/Xlib/__init__.py000066400000000000000000000022411505160700500234240ustar00rootroot00000000000000# Xlib.__init__ -- glue for Xlib package # # Copyright (C) 2000-2002 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA __version__ = (0, 15) __version_extra__ = '' __version_string__ = '.'.join(map(str, __version__)) + __version_extra__ __all__ = [ 'X', 'XK', 'Xatom', 'Xcursorfont', 'Xutil', 'display', 'error', 'rdb', # Explicitly exclude threaded, so that it isn't imported by # from Xlib import * ] Nagstamon-master/Nagstamon/thirdparty/Xlib/display.py000066400000000000000000001053261505160700500233420ustar00rootroot00000000000000# Xlib.display -- high level display object # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Python modules import types # Xlib modules from . import error, ext, X # Xlib.protocol modules from Xlib.protocol import display, request, event, rq # Xlib.xobjects modules import Xlib.xobject.resource import Xlib.xobject.drawable import Xlib.xobject.fontable import Xlib.xobject.colormap import Xlib.xobject.cursor _resource_baseclasses = { 'resource': Xlib.xobject.resource.Resource, 'drawable': Xlib.xobject.drawable.Drawable, 'window': Xlib.xobject.drawable.Window, 'pixmap': Xlib.xobject.drawable.Pixmap, 'fontable': Xlib.xobject.fontable.Fontable, 'font': Xlib.xobject.fontable.Font, 'gc': Xlib.xobject.fontable.GC, 'colormap': Xlib.xobject.colormap.Colormap, 'cursor': Xlib.xobject.cursor.Cursor, } _resource_hierarchy = { 'resource': ('drawable', 'window', 'pixmap', 'fontable', 'font', 'gc', 'colormap', 'cursor'), 'drawable': ('window', 'pixmap'), 'fontable': ('font', 'gc') } class _BaseDisplay(display.Display): resource_classes = _resource_baseclasses.copy() # Implement a cache of atom names, used by Window objects when # dealing with some ICCCM properties not defined in Xlib.Xatom def __init__(self, *args, **keys): display.Display.__init__(*(self, ) + args, **keys) self._atom_cache = {} def get_atom(self, atomname, only_if_exists=0): if atomname in self._atom_cache: return self._atom_cache[atomname] r = request.InternAtom(display = self, name = atomname, only_if_exists = only_if_exists) # don't cache NONE responses in case someone creates this later if r.atom != X.NONE: self._atom_cache[atomname] = r.atom return r.atom class Display: def __init__(self, display = None): self.display = _BaseDisplay(display) # Create the keymap cache self._keymap_codes = [()] * 256 self._keymap_syms = {} self._update_keymap(self.display.info.min_keycode, (self.display.info.max_keycode - self.display.info.min_keycode + 1)) # Translations for keysyms to strings. self.keysym_translations = {} # Find all supported extensions self.extensions = [] self.class_extension_dicts = {} self.display_extension_methods = {} self.extension_event = rq.DictWrapper({}) exts = self.list_extensions() # Go through all extension modules for extname, modname in ext.__extensions__: if extname in exts: # Import the module and fetch it __import__('ext.' + modname,globals(),level=1) mod = getattr(ext, modname) info = self.query_extension(extname) self.display.set_extension_major(extname, info.major_opcode) # Call initialiasation function mod.init(self, info) self.extensions.append(extname) # Finalize extensions by creating new classes for type_, dict in self.class_extension_dicts.items(): origcls = self.display.resource_classes[type_] self.display.resource_classes[type_] = type(origcls.__name__, (origcls, object), dict) # Problem: we have already created some objects without the # extensions: the screen roots and default colormaps. # Fix that by reinstantiating them. for screen in self.display.info.roots: screen.root = self.display.resource_classes['window'](self.display, screen.root.id) screen.default_colormap = self.display.resource_classes['colormap'](self.display, screen.default_colormap.id) def get_display_name(self): """Returns the name used to connect to the server, either provided when creating the Display object, or fetched from the environmental variable $DISPLAY.""" return self.display.get_display_name() def fileno(self): """Returns the file descriptor number of the underlying socket. This method is provided to allow Display objects to be passed select.select().""" return self.display.fileno() def close(self): """Close the display, freeing the resources that it holds.""" self.display.close() def set_error_handler(self, handler): """Set the default error handler which will be called for all unhandled errors. handler should take two arguments as a normal request error handler, but the second argument (the request) will be None. See section Error Handling.""" self.display.set_error_handler(handler) def flush(self): """Flush the request queue, building and sending the queued requests. This can be necessary in applications that never wait for events, and in threaded applications.""" self.display.flush() def sync(self): """Flush the queue and wait until the server has processed all the queued requests. Use this e.g. when it is important that errors caused by a certain request is trapped.""" # Do a light-weight replyrequest to sync. There must # be a better way to do it... self.get_pointer_control() def next_event(self): """Return the next event. If there are no events queued, it will block until the next event is fetched from the server.""" return self.display.next_event() def pending_events(self): """Return the number of events queued, i.e. the number of times that Display.next_event() can be called without blocking.""" return self.display.pending_events() def has_extension(self, extension): """Check if both the server and the client library support the X extension named extension.""" return extension in self.extensions def create_resource_object(self, type, id): """Create a resource object of type for the integer id. type should be one of the following strings: resource drawable window pixmap fontable font gc colormap cursor This function can be used when a resource ID has been fetched e.g. from an resource or a command line argument. Resource objects should never be created by instantiating the appropriate class directly, since any X extensions dynamically added by the library will not be available. """ return self.display.resource_classes[type](self.display, id) # We need this to handle display extension methods def __getattr__(self, attr): try: function = self.display_extension_methods[attr] return types.MethodType(function, self) except KeyError: raise AttributeError(attr) ### ### display information retrieval ### def screen(self, sno = None): if sno is None: return self.display.info.roots[self.display.default_screen] else: return self.display.info.roots[sno] def screen_count(self): """Return the total number of screens on the display.""" return len(self.display.info.roots) def get_default_screen(self): """Return the number of the default screen, extracted from the display name.""" return self.display.get_default_screen() ### ### Extension module interface ### def extension_add_method(self, object, name, function): """extension_add_method(object, name, function) Add an X extension module method. OBJECT is the type of object to add the function to, a string from this list: display resource drawable window pixmap fontable font gc colormap cursor NAME is the name of the method, a string. FUNCTION is a normal function whose first argument is a 'self'. """ if object == 'display': if hasattr(self, name): raise AssertionError('attempting to replace display method: %s' % name) self.display_extension_methods[name] = function else: types = (object, ) + _resource_hierarchy.get(object, ()) for type in types: cls = _resource_baseclasses[type] if hasattr(cls, name): raise AssertionError('attempting to replace %s method: %s' % (type, name)) # Maybe should check extension overrides too try: self.class_extension_dicts[type][name] = function except KeyError: self.class_extension_dicts[type] = { name: function } def extension_add_event(self, code, evt, name = None): """extension_add_event(code, evt, [name]) Add an extension event. CODE is the numeric code, and EVT is the event class. EVT will be cloned, and the attribute _code of the new event class will be set to CODE. If NAME is omitted, it will be set to the name of EVT. This name is used to insert an entry in the DictWrapper extension_event. """ newevt = type('{0}.SUB{1}'.format(evt.__name__, code), evt.__bases__, evt.__dict__.copy()) newevt._code = code self.display.add_extension_event(code, newevt) if name is None: name = evt.__name__ setattr(self.extension_event, name, code) def add_extension_error(self, code, err): """add_extension_error(code, err) Add an extension error. CODE is the numeric code, and ERR is the error class. """ self.display.add_extension_error(code, err) ### ### keymap cache implementation ### # The keycode->keysym map is stored in a list with 256 elements. # Each element represents a keycode, and the tuple elements are # the keysyms bound to the key. # The keysym->keycode map is stored in a mapping, where the keys # are keysyms. The values are a sorted list of tuples with two # elements each: (index, keycode) # keycode is the code for a key to which this keysym is bound, and # index is the keysyms index in the map for that keycode. def keycode_to_keysym(self, keycode, index): """Convert a keycode to a keysym, looking in entry index. Normally index 0 is unshifted, 1 is shifted, 2 is alt grid, and 3 is shift+alt grid. If that key entry is not bound, X.NoSymbol is returned.""" try: return self._keymap_codes[keycode][index] except IndexError: return X.NoSymbol def keysym_to_keycode(self, keysym): """Look up the primary keycode that is bound to keysym. If several keycodes are found, the one with the lowest index and lowest code is returned. If keysym is not bound to any key, 0 is returned.""" try: return self._keymap_syms[keysym][0][1] except (KeyError, IndexError): return 0 def keysym_to_keycodes(self, keysym): """Look up all the keycodes that is bound to keysym. A list of tuples (keycode, index) is returned, sorted primarily on the lowest index and secondarily on the lowest keycode.""" try: # Copy the map list, reversing the arguments return [(x[1], x[0]) for x in self._keymap_syms[keysym]] except KeyError: return [] def refresh_keyboard_mapping(self, evt): """This method should be called once when a MappingNotify event is received, to update the keymap cache. evt should be the event object.""" if isinstance(evt, event.MappingNotify): if evt.request == X.MappingKeyboard: self._update_keymap(evt.first_keycode, evt.count) else: raise TypeError('expected a MappingNotify event') def _update_keymap(self, first_keycode, count): """Internal function, called to refresh the keymap cache. """ # Delete all sym->code maps for the changed codes lastcode = first_keycode + count for keysym, codes in self._keymap_syms.items(): i = 0 while i < len(codes): code = codes[i][1] if code >= first_keycode and code < lastcode: del codes[i] else: i = i + 1 # Get the new keyboard mapping keysyms = self.get_keyboard_mapping(first_keycode, count) # Replace code->sym map with the new map self._keymap_codes[first_keycode:lastcode] = keysyms # Update sym->code map code = first_keycode for syms in keysyms: index = 0 for sym in syms: if sym != X.NoSymbol: if sym in self._keymap_syms: symcodes = self._keymap_syms[sym] symcodes.append((index, code)) symcodes.sort() else: self._keymap_syms[sym] = [(index, code)] index = index + 1 code = code + 1 ### ### client-internal keysym to string translations ### def lookup_string(self, keysym): """Return a string corresponding to KEYSYM, or None if no reasonable translation is found. """ s = self.keysym_translations.get(keysym) if s is not None: return s import Xlib.XK return Xlib.XK.keysym_to_string(keysym) def rebind_string(self, keysym, newstring): """Change the translation of KEYSYM to NEWSTRING. If NEWSTRING is None, remove old translation if any. """ if newstring is None: try: del self.keysym_translations[keysym] except KeyError: pass else: self.keysym_translations[keysym] = newstring ### ### X requests ### def intern_atom(self, name, only_if_exists = 0): """Intern the string name, returning its atom number. If only_if_exists is true and the atom does not already exist, it will not be created and X.NONE is returned.""" r = request.InternAtom(display = self.display, name = name, only_if_exists = only_if_exists) return r.atom def get_atom(self, atom, only_if_exists = 0): """Alias for intern_atom, using internal cache""" return self.display.get_atom(atom, only_if_exists) def get_atom_name(self, atom): """Look up the name of atom, returning it as a string. Will raise BadAtom if atom does not exist.""" r = request.GetAtomName(display = self.display, atom = atom) return r.name def get_selection_owner(self, selection): """Return the window that owns selection (an atom), or X.NONE if there is no owner for the selection. Can raise BadAtom.""" r = request.GetSelectionOwner(display = self.display, selection = selection) return r.owner def send_event(self, destination, event, event_mask = 0, propagate = 0, onerror = None): """Send a synthetic event to the window destination which can be a window object, or X.PointerWindow or X.InputFocus. event is the event object to send, instantiated from one of the classes in protocol.events. See XSendEvent(3X11) for details. There is also a Window.send_event() method.""" request.SendEvent(display = self.display, onerror = onerror, propagate = propagate, destination = destination, event_mask = event_mask, event = event) def ungrab_pointer(self, time, onerror = None): """elease a grabbed pointer and any queued events. See XUngrabPointer(3X11).""" request.UngrabPointer(display = self.display, onerror = onerror, time = time) def change_active_pointer_grab(self, event_mask, cursor, time, onerror = None): """Change the dynamic parameters of a pointer grab. See XChangeActivePointerGrab(3X11).""" request.ChangeActivePointerGrab(display = self.display, onerror = onerror, cursor = cursor, time = time, event_mask = event_mask) def ungrab_keyboard(self, time, onerror = None): """Ungrab a grabbed keyboard and any queued events. See XUngrabKeyboard(3X11).""" request.UngrabKeyboard(display = self.display, onerror = onerror, time = time) def allow_events(self, mode, time, onerror = None): """Release some queued events. mode should be one of X.AsyncPointer, X.SyncPointer, X.AsyncKeyboard, X.SyncKeyboard, X.ReplayPointer, X.ReplayKeyboard, X.AsyncBoth, or X.SyncBoth. time should be a timestamp or X.CurrentTime.""" request.AllowEvents(display = self.display, onerror = onerror, mode = mode, time = time) def grab_server(self, onerror = None): """Disable processing of requests on all other client connections until the server is ungrabbed. Server grabbing should be avoided as much as possible.""" request.GrabServer(display = self.display, onerror = onerror) def ungrab_server(self, onerror = None): """Release the server if it was previously grabbed by this client.""" request.UngrabServer(display = self.display, onerror = onerror) def warp_pointer(self, x, y, src_window = X.NONE, src_x = 0, src_y = 0, src_width = 0, src_height = 0, onerror = None): """Move the pointer relative its current position by the offsets (x, y). However, if src_window is a window the pointer is only moved if the specified rectangle in src_window contains it. If src_width is 0 it will be replaced with the width of src_window - src_x. src_height is treated in a similar way. To move the pointer to absolute coordinates, use Window.warp_pointer().""" request.WarpPointer(display = self.display, onerror = onerror, src_window = src_window, dst_window = X.NONE, src_x = src_x, src_y = src_y, src_width = src_width, src_height = src_height, dst_x = x, dst_y = y) def set_input_focus(self, focus, revert_to, time, onerror = None): """Set input focus to focus, which should be a window, X.PointerRoot or X.NONE. revert_to specifies where the focus reverts to if the focused window becomes not visible, and should be X.RevertToParent, RevertToPointerRoot, or RevertToNone. See XSetInputFocus(3X11) for details. There is also a Window.set_input_focus().""" request.SetInputFocus(display = self.display, onerror = onerror, revert_to = revert_to, focus = focus, time = time) def get_input_focus(self): """Return an object with the following attributes: focus The window which currently holds the input focus, X.NONE or X.PointerRoot. revert_to Where the focus will revert, one of X.RevertToParent, RevertToPointerRoot, or RevertToNone. """ return request.GetInputFocus(display = self.display) def query_keymap(self): """Return a bit vector for the logical state of the keyboard, where each bit set to 1 indicates that the corresponding key is currently pressed down. The vector is represented as a list of 32 integers. List item N contains the bits for keys 8N to 8N + 7 with the least significant bit in the byte representing key 8N.""" r = request.QueryKeymap(display = self.display) return r.map def open_font(self, name): """Open the font identifed by the pattern name and return its font object. If name does not match any font, None is returned.""" fid = self.display.allocate_resource_id() ec = error.CatchError(error.BadName) request.OpenFont(display = self.display, onerror = ec, fid = fid, name = name) self.sync() if ec.get_error(): self.display.free_resource_id(fid) return None else: cls = self.display.get_resource_class('font', Xlib.xobject.fontable.Font) return cls(self.display, fid, owner = 1) def list_fonts(self, pattern, max_names): """Return a list of font names matching pattern. No more than max_names will be returned.""" r = request.ListFonts(display = self.display, max_names = max_names, pattern = pattern) return r.fonts def list_fonts_with_info(self, pattern, max_names): """Return a list of fonts matching pattern. No more than max_names will be returned. Each list item represents one font and has the following properties: name The name of the font. min_bounds max_bounds min_char_or_byte2 max_char_or_byte2 default_char draw_direction min_byte1 max_byte1 all_chars_exist font_ascent font_descent replies_hint See the descripton of XFontStruct in XGetFontProperty(3X11) for details on these values. properties A list of properties. Each entry has two attributes: name The atom identifying this property. value A 32-bit unsigned value. """ return request.ListFontsWithInfo(display = self.display, max_names = max_names, pattern = pattern) def set_font_path(self, path, onerror = None): """Set the font path to path, which should be a list of strings. If path is empty, the default font path of the server will be restored.""" request.SetFontPath(display = self.display, onerror = onerror, path = path) def get_font_path(self): """Return the current font path as a list of strings.""" r = request.GetFontPath(display = self.display) return r.paths def query_extension(self, name): """Ask the server if it supports the extension name. If it is supported an object with the following attributes is returned: major_opcode The major opcode that the requests of this extension uses. first_event The base event code if the extension have additional events, or 0. first_error The base error code if the extension have additional errors, or 0. If the extension is not supported, None is returned.""" r = request.QueryExtension(display = self.display, name = name) if r.present: return r else: return None def list_extensions(self): """Return a list of all the extensions provided by the server.""" r = request.ListExtensions(display = self.display) return r.names def change_keyboard_mapping(self, first_keycode, keysyms, onerror = None): """Modify the keyboard mapping, starting with first_keycode. keysyms is a list of tuples of keysyms. keysyms[n][i] will be assigned to keycode first_keycode+n at index i.""" request.ChangeKeyboardMapping(display = self.display, onerror = onerror, first_keycode = first_keycode, keysyms = keysyms) def get_keyboard_mapping(self, first_keycode, count): """Return the current keyboard mapping as a list of tuples, starting at first_keycount and no more than count.""" r = request.GetKeyboardMapping(display = self.display, first_keycode = first_keycode, count = count) return r.keysyms def change_keyboard_control(self, onerror = None, **keys): """Change the parameters provided as keyword arguments: key_click_percent The volume of key clicks between 0 (off) and 100 (load). -1 will restore default setting. bell_percent The base volume of the bell, coded as above. bell_pitch The pitch of the bell in Hz, -1 restores the default. bell_duration The duration of the bell in milliseconds, -1 restores the default. led led_mode led_mode should be X.LedModeOff or X.LedModeOn. If led is provided, it should be a 32-bit mask listing the LEDs that should change. If led is not provided, all LEDs are changed. key auto_repeat_mode auto_repeat_mode should be one of X.AutoRepeatModeOff, X.AutoRepeatModeOn, or X.AutoRepeatModeDefault. If key is provided, that key will be modified, otherwise the global state for the entire keyboard will be modified.""" request.ChangeKeyboardControl(display = self.display, onerror = onerror, attrs = keys) def get_keyboard_control(self): """Return an object with the following attributes: global_auto_repeat X.AutoRepeatModeOn or X.AutoRepeatModeOff. auto_repeats A list of 32 integers. List item N contains the bits for keys 8N to 8N + 7 with the least significant bit in the byte representing key 8N. If a bit is on, autorepeat is enabled for the corresponding key. led_mask A 32-bit mask indicating which LEDs are on. key_click_percent The volume of key click, from 0 to 100. bell_percent bell_pitch bell_duration The volume, pitch and duration of the bell. """ return request.GetKeyboardControl(display = self.display) def bell(self, percent = 0, onerror = None): """Ring the bell at the volume percent which is relative the base volume. See XBell(3X11).""" request.Bell(display = self.display, onerror = onerror, percent = percent) def change_pointer_control(self, accel = None, threshold = None, onerror = None): """To change the pointer acceleration, set accel to a tuple (num, denum). The pointer will then move num/denum times the normal speed if it moves beyond the threshold number of pixels at once. To change the threshold, set it to the number of pixels. -1 restores the default.""" if accel is None: do_accel = 0 accel_num = 0 accel_denum = 0 else: do_accel = 1 accel_num, accel_denum = accel if threshold is None: do_threshold = 0 else: do_threshold = 1 request.ChangePointerControl(display = self.display, onerror = onerror, do_accel = do_accel, do_thres = do_threshold, accel_num = accel_num, accel_denum = accel_denum, threshold = threshold) def get_pointer_control(self): """Return an object with the following attributes: accel_num accel_denom The acceleration as numerator/denumerator. threshold The number of pixels the pointer must move before the acceleration kicks in.""" return request.GetPointerControl(display = self.display) def set_screen_saver(self, timeout, interval, prefer_blank, allow_exposures, onerror = None): """See XSetScreenSaver(3X11).""" request.SetScreenSaver(display = self.display, onerror = onerror, timeout = timeout, interval = interval, prefer_blank = prefer_blank, allow_exposures = allow_exposures) def get_screen_saver(self): """Return an object with the attributes timeout, interval, prefer_blanking, allow_exposures. See XGetScreenSaver(3X11) for details.""" return request.GetScreenSaver(display = self.display) def change_hosts(self, mode, host_family, host, onerror = None): """mode is either X.HostInsert or X.HostDelete. host_family is one of X.FamilyInternet, X.FamilyDECnet or X.FamilyChaos. host is a list of bytes. For the Internet family, it should be the four bytes of an IPv4 address.""" request.ChangeHosts(display = self.display, onerror = onerror, mode = mode, host_family = host_family, host = host) def list_hosts(self): """Return an object with the following attributes: mode X.EnableAccess if the access control list is used, X.DisableAccess otherwise. hosts The hosts on the access list. Each entry has the following attributes: family X.FamilyInternet, X.FamilyDECnet, or X.FamilyChaos. name A list of byte values, the coding depends on family. For the Internet family, it is the 4 bytes of an IPv4 address. """ return request.ListHosts(display = self.display) def set_access_control(self, mode, onerror = None): """Enable use of access control lists at connection setup if mode is X.EnableAccess, disable if it is X.DisableAccess.""" request.SetAccessControl(display = self.display, onerror = onerror, mode = mode) def set_close_down_mode(self, mode, onerror = None): """Control what will happen with the client's resources at connection close. The default is X.DestroyAll, the other values are X.RetainPermanent and X.RetainTemporary.""" request.SetCloseDownMode(display = self.display, onerror = onerror, mode = mode) def force_screen_saver(self, mode, onerror = None): """If mode is X.ScreenSaverActive the screen saver is activated. If it is X.ScreenSaverReset, the screen saver is deactivated as if device input had been received.""" request.ForceScreenSaver(display = self.display, onerror = onerror, mode = mode) def set_pointer_mapping(self, map): """Set the mapping of the pointer buttons. map is a list of logical button numbers. map must be of the same length as the list returned by Display.get_pointer_mapping(). map[n] sets the logical number for the physical button n+1. Logical number 0 disables the button. Two physical buttons cannot be mapped to the same logical number. If one of the buttons to be altered are logically in the down state, X.MappingBusy is returned and the mapping is not changed. Otherwise the mapping is changed and X.MappingSuccess is returned.""" r = request.SetPointerMapping(display = self.display, map = map) return r.status def get_pointer_mapping(self): """Return a list of the pointer button mappings. Entry N in the list sets the logical button number for the physical button N+1.""" r = request.GetPointerMapping(display = self.display) return r.map def set_modifier_mapping(self, keycodes): """Set the keycodes for the eight modifiers X.Shift, X.Lock, X.Control, X.Mod1, X.Mod2, X.Mod3, X.Mod4 and X.Mod5. keycodes should be a eight-element list where each entry is a list of the keycodes that should be bound to that modifier. If any changed key is logically in the down state, X.MappingBusy is returned and the mapping is not changed. If the mapping violates some server restriction, X.MappingFailed is returned. Otherwise the mapping is changed and X.MappingSuccess is returned.""" r = request.SetModifierMapping(display = self.display, keycodes = keycodes) return r.status def get_modifier_mapping(self): """Return a list of eight lists, one for each modifier. The list can be indexed using X.ShiftMapIndex, X.Mod1MapIndex, and so on. The sublists list the keycodes bound to that modifier.""" r = request.GetModifierMapping(display = self.display) return r.keycodes def no_operation(self, onerror = None): """Do nothing but send a request to the server.""" request.NoOperation(display = self.display, onerror = onerror) Nagstamon-master/Nagstamon/thirdparty/Xlib/error.py000066400000000000000000000112031505160700500230140ustar00rootroot00000000000000# Xlib.error -- basic error classes # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Xlib modules from Xlib import X # Xlib.protocol modules from Xlib.protocol import rq class DisplayError(Exception): def __init__(self, display): self.display = display def __str__(self): return 'Display error "%s"' % self.display class DisplayNameError(DisplayError): def __str__(self): return 'Bad display name "%s"' % self.display class DisplayConnectionError(DisplayError): def __init__(self, display, msg): self.display = display self.msg = msg def __str__(self): return 'Can\'t connect to display "%s": %s' % (self.display, self.msg) class ConnectionClosedError(Exception): def __init__(self, whom): self.whom = whom def __str__(self): return 'Display connection closed by %s' % self.whom class XauthError(Exception): pass class XNoAuthError(Exception): pass class ResourceIDError(Exception): pass class XError(rq.GetAttrData, Exception): _fields = rq.Struct( rq.Card8('type'), # Always 0 rq.Card8('code'), rq.Card16('sequence_number'), rq.Card32('resource_id'), rq.Card16('minor_opcode'), rq.Card8('major_opcode'), rq.Pad(21) ) def __init__(self, display, data): self._data, data = self._fields.parse_binary(data, display, rawdict = 1) def __str__(self): s = [] for f in ('code', 'resource_id', 'sequence_number', 'major_opcode', 'minor_opcode'): s.append('%s = %s' % (f, self._data[f])) return '%s: %s' % (self.__class__, ', '.join(s)) class XResourceError(XError): _fields = rq.Struct( rq.Card8('type'), # Always 0 rq.Card8('code'), rq.Card16('sequence_number'), rq.Resource('resource_id'), rq.Card16('minor_opcode'), rq.Card8('major_opcode'), rq.Pad(21) ) class BadRequest(XError): pass class BadValue(XError): pass class BadWindow(XResourceError): pass class BadPixmap(XResourceError): pass class BadAtom(XError): pass class BadCursor(XResourceError): pass class BadFont(XResourceError): pass class BadMatch(XError): pass class BadDrawable(XResourceError): pass class BadAccess(XError): pass class BadAlloc(XError): pass class BadColor(XResourceError): pass class BadGC(XResourceError): pass class BadIDChoice(XResourceError): pass class BadName(XError): pass class BadLength(XError): pass class BadImplementation(XError): pass xerror_class = { X.BadRequest: BadRequest, X.BadValue: BadValue, X.BadWindow: BadWindow, X.BadPixmap: BadPixmap, X.BadAtom: BadAtom, X.BadCursor: BadCursor, X.BadFont: BadFont, X.BadMatch: BadMatch, X.BadDrawable: BadDrawable, X.BadAccess: BadAccess, X.BadAlloc: BadAlloc, X.BadColor: BadColor, X.BadGC: BadGC, X.BadIDChoice: BadIDChoice, X.BadName: BadName, X.BadLength: BadLength, X.BadImplementation: BadImplementation, } class CatchError: def __init__(self, *errors): self.error_types = errors self.error = None self.request = None def __call__(self, error, request): if self.error_types: for etype in self.error_types: if isinstance(error, etype): self.error = error self.request = request return 1 return 0 else: self.error = error self.request = request return 1 def get_error(self): return self.error def get_request(self): return self.request def reset(self): self.error = None self.request = None Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/000077500000000000000000000000001505160700500221145ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/__init__.py000066400000000000000000000023471505160700500242330ustar00rootroot00000000000000# Xlib.ext.__init__ -- X extension modules # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # __extensions__ is a list of tuples: (extname, extmod) # extname is the name of the extension according to the X # protocol. extmod is the name of the module in this package. __extensions__ = [ ('XTEST', 'xtest'), ('SHAPE', 'shape'), ('XINERAMA', 'xinerama'), ('RECORD', 'record'), ('Composite', 'composite'), ('RANDR', 'randr'), ] __all__ = [x[1] for x in __extensions__] Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/composite.py000066400000000000000000000164321505160700500244760ustar00rootroot00000000000000# $Id: xtest.py,v 1.1 2000/08/21 10:03:45 petli Exp $ # # Xlib.ext.composite -- Composite extension module # # Copyright (C) 2007 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Composite extension, allowing windows to be rendered to off-screen storage. For detailed description, see the protocol specification at http://freedesktop.org/wiki/Software/CompositeExt By itself this extension is not very useful, it is intended to be used together with the DAMAGE and XFIXES extensions. Typically you would also need RENDER or glX or some similar method of creating fancy graphics. """ from Xlib import X from Xlib.protocol import rq from Xlib.xobject import drawable extname = 'Composite' RedirectAutomatic = 0 RedirectManual = 1 class QueryVersion(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), rq.Card32('major_version'), rq.Card32('minor_version') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('major_version'), rq.Card32('minor_version'), rq.Pad(16), ) def query_version(self): return QueryVersion( display = self.display, opcode = self.display.get_extension_major(extname), ) class RedirectWindow(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(1), rq.RequestLength(), rq.Window('window'), rq.Set('update', 1, (RedirectAutomatic, RedirectManual)), rq.Pad(3), ) def redirect_window(self, update): """Redirect the hierarchy starting at this window to off-screen storage. """ RedirectWindow(display = self.display, opcode = self.display.get_extension_major(extname), window = self, update = update, ) class RedirectSubwindows(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Window('window'), rq.Set('update', 1, (RedirectAutomatic, RedirectManual)), rq.Pad(3), ) def redirect_subwindows(self, update): """Redirect the hierarchies starting at all current and future children to this window to off-screen storage. """ RedirectSubwindows(display = self.display, opcode = self.display.get_extension_major(extname), window = self, update = update, ) class UnredirectWindow(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(3), rq.RequestLength(), rq.Window('window'), rq.Set('update', 1, (RedirectAutomatic, RedirectManual)), rq.Pad(3), ) def unredirect_window(self, update): """Stop redirecting this window hierarchy. """ UnredirectWindow(display = self.display, opcode = self.display.get_extension_major(extname), window = self, update = update, ) class UnredirectSubindows(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.Window('window'), rq.Set('update', 1, (RedirectAutomatic, RedirectManual)), rq.Pad(3), ) def unredirect_subwindows(self, update): """Stop redirecting the hierarchies of children to this window. """ RedirectWindow(display = self.display, opcode = self.display.get_extension_major(extname), window = self, update = update, ) class CreateRegionFromBorderClip(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(5), rq.RequestLength(), rq.Card32('region'), # FIXME: this should be a Region from XFIXES extension rq.Window('window'), ) def create_region_from_border_clip(self): """Create a region of the border clip of the window, i.e. the area that is not clipped by the parent and any sibling windows. """ rid = self.display.allocate_resource_id() CreateRegionFromBorderClip( display = self.display, opcode = self.display.get_extension_major(extname), region = rid, window = self, ) # FIXME: create Region object and return it return rid class NameWindowPixmap(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(6), rq.RequestLength(), rq.Window('window'), rq.Pixmap('pixmap'), ) def name_window_pixmap(self): """Create a new pixmap that refers to the off-screen storage of the window, including its border. This pixmap will remain allocated until freed whatever happens with the window. However, the window will get a new off-screen pixmap every time it is mapped or resized, so to keep track of the contents you must listen for these events and get a new pixmap after them. """ pid = self.display.allocate_resource_id() NameWindowPixmap(display = self.display, opcode = self.display.get_extension_major(extname), window = self, pixmap = pid, ) cls = self.display.get_resource_class('pixmap', drawable.Pixmap) return cls(self.display, pid, owner = 1) def init(disp, info): disp.extension_add_method('display', 'composite_query_version', query_version) disp.extension_add_method('window', 'composite_redirect_window', redirect_window) disp.extension_add_method('window', 'composite_redirect_subwindows', redirect_subwindows) disp.extension_add_method('window', 'composite_unredirect_window', unredirect_window) disp.extension_add_method('window', 'composite_unredirect_subwindows', unredirect_subwindows) disp.extension_add_method('window', 'composite_create_region_from_border_clip', create_region_from_border_clip) disp.extension_add_method('window', 'composite_name_window_pixmap', name_window_pixmap) Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/randr.py000066400000000000000000001026011505160700500235740ustar00rootroot00000000000000# Xlib.ext.randr -- RandR extension module # # Copyright (C) 2006 Mike Meyer # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """RandR - provide access to the RandR extension information. This implementation is based off version 1.3 of the XRandR protocol, and may not be compatible with other versions. Version 1.2 of the protocol is documented at: http://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt """ from Xlib import X from Xlib.protocol import rq, structs extname = 'RANDR' # Event codes # RRScreenChangeNotify = 0 # V1.2 additions RRNotify = 1 # RRNotify Subcodes RRNotify_CrtcChange = 0 RRNotify_OutputChange = 1 RRNotify_OutputProperty = 2 # Event selection bits # RRScreenChangeNotifyMask = (1 << 0) # V1.2 additions RRCrtcChangeNotifyMask = (1 << 1) RROutputChangeNotifyMask = (1 << 2) RROutputPropertyNotifyMask = (1 << 3) # Constants # SetConfigSuccess = 0 SetConfigInvalidConfigTime = 1 SetConfigInvalidTime = 2 SetConfigFailed = 3 # used in the rotation field; rotation and reflection in 0.1 proto. Rotate_0 = 1 Rotate_90 = 2 Rotate_180 = 4 Rotate_270 = 8 # new in 1.0 protocol, to allow reflection of screen Reflect_X = 16 Reflect_Y = 32 # new in 1.2 protocol HSyncPositive = 0x00000001 HSyncNegative = 0x00000002 VSyncPositive = 0x00000004 VSyncNegative = 0x00000008 Interlace = 0x00000010 DoubleScan = 0x00000020 CSync = 0x00000040 CSyncPositive = 0x00000080 CSyncNegative = 0x00000100 HSkewPresent = 0x00000200 BCast = 0x00000400 PixelMultiplex = 0x00000800 DoubleClock = 0x00001000 ClockDivideBy2 = 0x00002000 # event types? Connected = 0 Disconnected = 1 UnknownConnection = 2 # Conventional RandR output properties PROPERTY_RANDR_EDID = "EDID" PROPERTY_SIGNAL_FORMAT = "SignalFormat" PROPERTY_SIGNAL_PROPERTIES = "SignalProperties" PROPERTY_CONNECTOR_TYPE = "ConnectorType" PROPERTY_CONNECTOR_NUMBER = "ConnectorNumber" PROPERTY_COMPATIBILITY_LIST = "CompatibilityList" PROPERTY_CLONE_LIST = "CloneList" # subpixel order - TODO: These constants are part of the RENDER extension and # should be moved there if/when that extension is added to python-xlib. SubPixelUnknown = 0 SubPixelHorizontalRGB = 1 SubPixelHorizontalBGR = 2 SubPixelVerticalRGB = 3 SubPixelVerticalBGR = 4 SubPixelNone = 5 # Error Codes # BadRROutput = 0 BadRRCrtc = 1 BadRRMode = 2 # Data Structures # RandR_ScreenSizes = rq.Struct( rq.Card16('width_in_pixels'), rq.Card16('height_in_pixels'), rq.Card16('width_in_millimeters'), rq.Card16('height_in_millimeters'), ) RandR_ModeInfo = rq.Struct( rq.Card32('id'), rq.Card16('width'), rq.Card16('height'), rq.Card32('dot_clock'), rq.Card16('h_sync_start'), rq.Card16('h_sync_end'), rq.Card16('h_total'), rq.Card16('h_skew'), rq.Card16('v_sync_start'), rq.Card16('v_sync_end'), rq.Card16('v_total'), rq.Card16('name_length'), rq.Card32('flags'), ) RandR_Rates = rq.Struct( rq.LengthOf('rates', 2), rq.List('rates', rq.Card16Obj) ) # TODO: This struct is part of the RENDER extension and should be moved there # if/when that extension is added to python-xlib. Render_Transform = rq.Struct( rq.Card32('matrix11'), #FIXME: All of these are listed as FIXED in the protocol header. rq.Card32('matrix12'), rq.Card32('matrix13'), rq.Card32('matrix21'), rq.Card32('matrix22'), rq.Card32('matrix23'), rq.Card32('matrix31'), rq.Card32('matrix32'), rq.Card32('matrix33'), ) # Requests # class QueryVersion(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), rq.Card32('major_version'), rq.Card32('minor_version'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('major_version'), rq.Card32('minor_version'), rq.Pad(16), ) def query_version(self): """Get the current version of the RandR extension. """ return QueryVersion( display=self.display, opcode=self.display.get_extension_major(extname), major_version=1, minor_version=3, ) class _1_0SetScreenConfig(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Drawable('drawable'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.Card16('size_id'), rq.Card16('rotation'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('new_timestamp'), rq.Card32('new_config_timestamp'), rq.Window('root'), rq.Card16('subpixel_order'), rq.Pad(10), ) def _1_0set_screen_config(self, size_id, rotation, config_timestamp, timestamp=X.CurrentTime): """Sets the screen to the specified size and rotation. """ return _1_0SetScreenConfig( display=self.display, opcode=self.display.get_extension_major(extname), drawable=self, timestamp=timestamp, config_timestamp=config_timestamp, size_id=size_id, rotation=rotation, ) class SetScreenConfig(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Drawable('drawable'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.Card16('size_id'), rq.Card16('rotation'), rq.Card16('rate'), # added in version 1.1 rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('new_timestamp'), rq.Card32('new_config_timestamp'), rq.Window('root'), rq.Card16('subpixel_order'), rq.Pad(10), ) def set_screen_config(self, size_id, rotation, config_timestamp, rate=0, timestamp=X.CurrentTime): """Sets the screen to the specified size, rate, rotation and reflection. rate can be 0 to have the server select an appropriate rate. """ return SetScreenConfig( display=self.display, opcode=self.display.get_extension_major(extname), drawable=self, timestamp=timestamp, config_timestamp=config_timestamp, size_id=size_id, rotation=rotation, rate=rate, ) class SelectInput(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.Window('window'), rq.Card16('mask'), rq.Pad(2), ) def select_input(self, mask): return SelectInput( display=self.display, opcode=self.display.get_extension_major(extname), window=self, mask=mask, ) class GetScreenInfo(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(5), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('set_of_rotations'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('root'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.LengthOf('sizes', 2), rq.Card16('size_id'), rq.Card16('rotation'), rq.Card16('rate'), # added in version 1.1 rq.Card16('n_rate_ents'), # XCB's protocol description disagrees with the X headers on this; ignoring. rq.Pad(2), rq.List('sizes', RandR_ScreenSizes), #rq.List('rates', RandR_Rates) #FIXME: Why does uncommenting this cause an error? ) def get_screen_info(self): """Retrieve information about the current and available configurations for the screen associated with this window. """ return GetScreenInfo( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) # version 1.2 class GetScreenSizeRange(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(6), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('min_width'), rq.Card16('min_height'), rq.Card16('max_width'), rq.Card16('max_height'), rq.Pad(16), ) def get_screen_size_range(self): """Retrieve the range of possible screen sizes. The screen may be set to any size within this range. """ return GetScreenSizeRange( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) class SetScreenSize(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(7), rq.RequestLength(), rq.Window('window'), rq.Card16('width'), rq.Card16('height'), rq.Card32('width_in_millimeters'), rq.Card32('height_in_millimeters'), ) def set_screen_size(self, width, height, width_in_millimeters=None, height_in_millimeters=None): return SetScreenSize( display=self.display, opcode=self.display.get_extension_major(extname), window=self, width=width, height=height, width_in_millimeters=width_in_millimeters, height_in_millimeters=height_in_millimeters, ) class GetScreenResources(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(8), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.LengthOf('crtcs', 2), rq.LengthOf('outputs', 2), rq.LengthOf('modes', 2), rq.LengthOf('mode_names', 2), rq.Pad(8), rq.List('crtcs', rq.Card32Obj), rq.List('outputs', rq.Card32Obj), rq.List('modes', RandR_ModeInfo), rq.String8('mode_names'), ) def get_screen_resources(self): return GetScreenResources( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) class GetOutputInfo(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(9), rq.RequestLength(), rq.Card32('output'), rq.Card32('config_timestamp'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('timestamp'), rq.Card32('crtc'), rq.Card32('mm_width'), rq.Card32('mm_height'), rq.Card8('connection'), rq.Card8('subpixel_order'), rq.LengthOf('crtcs', 2), rq.LengthOf('modes', 2), rq.LengthOf('preferred', 2), rq.LengthOf('clones', 2), rq.LengthOf('name', 2), rq.List('crtcs', rq.Card32Obj), rq.List('modes', rq.Card32Obj), rq.List('preferred', rq.Card32Obj), rq.List('clones', rq.Card32Obj), rq.String8('name'), ) def get_output_info(self, output, config_timestamp): return GetOutputInfo( display=self.display, opcode=self.display.get_extension_major(extname), output=output, config_timestamp=config_timestamp, ) class ListOutputProperties(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(10), rq.RequestLength(), rq.Card32('output'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('atoms', 2), rq.Pad(22), rq.List('atoms', rq.Card32Obj), ) def list_output_properties(self, output): return ListOutputProperties ( display=self.display, opcode=self.display.get_extension_major(extname), output=output, ) class QueryOutputProperty(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(11), rq.RequestLength(), rq.Card32('output'), rq.Card32('property'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Bool('pending'), rq.Bool('range'), rq.Bool('immutable'), rq.Pad(21), rq.List('valid_values', rq.Card32Obj), ) def query_output_property(self, output, property): return QueryOutputProperty ( display=self.display, opcode=self.display.get_extension_major(extname), output=output, property=property, ) class ConfigureOutputProperty (rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(12), rq.RequestLength(), rq.Card32('output'), rq.Card32('property'), rq.Bool('pending'), rq.Bool('range'), rq.Pad(2), rq.List('valid_values', rq.Card32Obj), ) def configure_output_property (self, output, property): return ConfigureOutputProperty ( display=self.display, opcode=self.display.get_extension_major(extname), output=output, property=property, ) class ChangeOutputProperty(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(13), rq.RequestLength(), rq.Card32('output'), rq.Card32('property'), rq.Card32('type'), rq.Format('value', 1), rq.Card8('mode'), rq.Pad(2), rq.LengthOf('value', 4), rq.List('value', rq.Card8Obj), ) def change_output_property(self, output, property, type, format, mode, nUnits): return ChangeOutputProperty( display=self.display, opcode=self.display.get_extension_major(extname), output=output, property=property, type=type, format=format, mode=mode, nUnits=nUnits, ) class DeleteOutputProperty(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(14), rq.RequestLength(), rq.Card32('output'), rq.Card32('property'), ) def delete_output_property(self, output, property): return DeleteOutputProperty( display=self.display, opcode=self.display.get_extension_major(extname), output=output, property=property, ) class GetOutputProperty(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(15), rq.RequestLength(), rq.Card32('output'), rq.Card32('property'), rq.Card32('type'), rq.Card32('long_offset'), rq.Card32('long_length'), rq.Bool('delete'), rq.Bool('pending'), rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Format('value', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('property_type'), rq.Card32('bytes_after'), rq.LengthOf('value', 4), rq.Pad(12), rq.List('value', rq.Card8Obj), ) def get_output_property(self, output, property, type, longOffset, longLength): return GetOutputProperty( display=self.display, opcode=self.display.get_extension_major(extname), output=output, property=property, type=type, longOffset=longOffset, longLength=longLength, ) class CreateMode(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(16), rq.RequestLength(), rq.Window('window'), rq.Object('mode', RandR_ModeInfo), rq.String8('name'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('mode'), rq.Pad(20), ) def create_mode(self): return CreateMode ( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) class DestroyMode(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(17), rq.RequestLength(), rq.Card32('mode'), ) def destroy_mode(self, mode): return DestroyMode( display=self.display, opcode=self.display.get_extension_major(extname), mode=mode, ) class AddOutputMode(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(18), rq.RequestLength(), rq.Card32('output'), rq.Card32('mode'), ) def add_output_mode(self): return AddOutputMode( display=self.display, opcode=self.display.get_extension_major(extname), output=output, mode=mode, ) class DeleteOutputMode(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(19), rq.RequestLength(), rq.Card32('output'), rq.Card32('mode'), ) def delete_output_mode(self): return DeleteOutputMode( display=self.display, opcode=self.display.get_extension_major(extname), output=output, mode=mode, ) class GetCrtcInfo(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(20), rq.RequestLength(), rq.Card32('crtc'), rq.Card32('config_timestamp'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('timestamp'), rq.Card16('width'), rq.Card16('height'), rq.Card32('mode'), rq.Card16('rotation'), rq.Card16('possible_rotations'), rq.LengthOf('outputs', 2), rq.LengthOf('possible_outputs', 2), rq.List('outputs', rq.Card32Obj), rq.List('possible_outputs', rq.Card32Obj), ) def get_crtc_info(self, crtc, config_timestamp): return GetCrtcInfo ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, config_timestamp=config_timestamp, ) class SetCrtcConfig(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(21), rq.RequestLength(), rq.Card32('crtc'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.Int16('x'), rq.Int16('y'), rq.Card32('mode'), rq.Card16('rotation'), rq.Pad(2), rq.List('outputs', rq.Card32Obj), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('new_timestamp'), rq.Pad(20), ) def set_crtc_config(self, crtc, config_timestamp, mode, rotation, timestamp=X.CurrentTime): return SetCrtcConfig ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, config_timestamp=config_timestamp, mode=mode, rotation=rotation, timestamp=timestamp, ) class GetCrtcGammaSize(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(22), rq.RequestLength(), rq.Card32('crtc'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('size'), rq.Pad(22), ) def get_crtc_gamma_size(self, crtc): return GetCrtcGammaSize ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, ) class GetCrtcGamma(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(23), rq.RequestLength(), rq.Card32('crtc'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('size'), rq.Pad(22), rq.List('red', rq.Card16Obj), rq.List('green', rq.Card16Obj), rq.List('blue', rq.Card16Obj), ) def get_crtc_gamma(self, crtc): return GetCrtcGamma ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, ) class SetCrtcGamma(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(24), rq.RequestLength(), rq.Card32('crtc'), rq.Card16('size'), rq.Pad(2), rq.List('red', rq.Card16Obj), rq.List('green', rq.Card16Obj), rq.List('blue', rq.Card16Obj), ) def set_crtc_gamma(self, crtc, size): return SetCrtcGamma( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, size=size, ) # version 1.3 class GetScreenResourcesCurrent(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(25), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.LengthOf('crtcs', 2), rq.LengthOf('outputs', 2), rq.LengthOf('modes', 2), rq.LengthOf('names', 2), rq.Pad(8), rq.List('crtcs', rq.Card32Obj), rq.List('outputs', rq.Card32Obj), rq.List('modes', RandR_ModeInfo), rq.String8('names'), ) def get_screen_resources_current(self): return GetScreenResourcesCurrent( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) class SetCrtcTransform(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(26), rq.RequestLength(), rq.Card32('crtc'), rq.Object('transform', Render_Transform), rq.LengthOf('filter_name', 2), rq.Pad(2), rq.String8('filter_name'), rq.List('filter_params', rq.Card32Obj), #FIXME: The protocol says FIXED? http://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt#n2161 ) def set_crtc_transform(self, crtc, n_bytes_filter): return SetCrtcTransform( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, n_bytes_filter=n_bytes_filter, ) class GetCrtcTransform(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(27), rq.RequestLength(), rq.Card32('crtc'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Object('pending_transform', Render_Transform), rq.Bool('has_transforms'), rq.Pad(3), rq.Object('current_transform', Render_Transform), rq.Pad(4), rq.LengthOf('pending_filter_name', 2), rq.LengthOf('pending_filter_params', 2), rq.LengthOf('current_filter_name', 2), rq.LengthOf('current_filter_params', 2), rq.String8('pending_filter_name'), rq.List('pending_filter_params', rq.Card32Obj), #FIXME: The protocol says FIXED? http://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt#n2161 rq.String8('current_filter_name'), rq.List('current_filter_params', rq.Card32Obj), #FIXME: The protocol says FIXED? http://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt#n2161 ) def get_crtc_transform(self, crtc): return GetCrtcTransform( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, ) class GetPanning(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(28), rq.RequestLength(), rq.Card32('crtc'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('timestamp'), rq.Card16('left'), rq.Card16('top'), rq.Card16('width'), rq.Card16('height'), rq.Card16('track_left'), rq.Card16('track_top'), rq.Card16('track_width'), rq.Card16('track_height'), rq.Int16('border_left'), rq.Int16('border_top'), rq.Int16('border_right'), rq.Int16('border_bottom'), ) def get_panning(self, crtc): return GetPanning ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, ) class SetPanning(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(29), rq.RequestLength(), rq.Card32('crtc'), rq.Card32('timestamp'), rq.Card16('left'), rq.Card16('top'), rq.Card16('width'), rq.Card16('height'), rq.Card16('track_left'), rq.Card16('track_top'), rq.Card16('track_width'), rq.Card16('track_height'), rq.Int16('border_left'), rq.Int16('border_top'), rq.Int16('border_right'), rq.Int16('border_bottom'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('new_timestamp'), rq.Pad(20), ) def set_panning(self, crtc, left, top, width, height, track_left, track_top, track_width, track_height, border_left, border_top, border_width, border_height, timestamp=X.CurrentTime): return SetPanning ( display=self.display, opcode=self.display.get_extension_major(extname), crtc=crtc, left=left, top=top, width=width, height=height, track_left=track_left, track_top=track_top, track_width=track_width, track_height=track_height, border_left=border_left, border_top=border_top, border_width=border_width, border_height=border_height, timestamp=timestamp, ) class SetOutputPrimary(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(30), rq.RequestLength(), rq.Window('window'), rq.Card32('output'), ) def set_output_primary(self, output): return SetOutputPrimary( display=self.display, opcode=self.display.get_extension_major(extname), window=self, output=output, ) class GetOutputPrimary(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(31), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('output'), rq.Pad(20), ) def get_output_primary(self): return GetOutputPrimary( display=self.display, opcode=self.display.get_extension_major(extname), window=self, ) # Events # class ScreenChangeNotify(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('rotation'), rq.Card16('sequence_number'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.Window('root'), rq.Window('window'), rq.Card16('size_id'), rq.Card16('subpixel_order'), rq.Card16('width_in_pixels'), rq.Card16('height_in_pixels'), rq.Card16('width_in_millimeters'), rq.Card16('height_in_millimeters'), ) class CrtcChangeNotify(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('sub_code'), rq.Card16('sequence_number'), rq.Card32('timestamp'), rq.Window('window'), rq.Card32('crtc'), rq.Card32('mode'), rq.Card16('rotation'), rq.Pad(2), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), ) class OutputChangeNotify(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('sub_code'), rq.Card16('sequence_number'), rq.Card32('timestamp'), rq.Card32('config_timestamp'), rq.Window('window'), rq.Card32('output'), rq.Card32('crtc'), rq.Card32('mode'), rq.Card16('rotation'), rq.Card8('connection'), rq.Card8('subpixel_order'), ) class OutputPropertyNotify(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('sub_code'), rq.Card16('sequence_number'), rq.Window('window'), rq.Card32('output'), rq.Card32('atom'), rq.Card32('timestamp'), rq.Card8('state'), rq.Pad(11), ) # Initialization # def init(disp, info): disp.extension_add_method('display', 'xrandr_query_version', query_version) disp.extension_add_method('window', 'xrandr_select_input', select_input) disp.extension_add_method('window', 'xrandr_get_screen_info', get_screen_info) disp.extension_add_method('drawable', 'xrandr_1_0set_screen_config', _1_0set_screen_config) disp.extension_add_method('drawable', 'xrandr_set_screen_config', set_screen_config) disp.extension_add_method('window', 'xrandr_get_screen_size_range', get_screen_size_range) disp.extension_add_method('window', 'xrandr_set_screen_size', set_screen_size) disp.extension_add_method('window', 'xrandr_get_screen_resources', get_screen_resources) disp.extension_add_method('display', 'xrandr_get_output_info', get_output_info) disp.extension_add_method('display', 'xrandr_list_output_properties', list_output_properties) disp.extension_add_method('display', 'xrandr_query_output_property', query_output_property) disp.extension_add_method('display', 'xrandr_configure_output_property ', configure_output_property ) disp.extension_add_method('display', 'xrandr_change_output_property', change_output_property) disp.extension_add_method('display', 'xrandr_delete_output_property', delete_output_property) disp.extension_add_method('display', 'xrandr_get_output_property', get_output_property) disp.extension_add_method('window', 'xrandr_create_mode', create_mode) disp.extension_add_method('display', 'xrandr_destroy_mode', destroy_mode) disp.extension_add_method('display', 'xrandr_add_output_mode', add_output_mode) disp.extension_add_method('display', 'xrandr_delete_output_mode', delete_output_mode) disp.extension_add_method('display', 'xrandr_get_crtc_info', get_crtc_info) disp.extension_add_method('display', 'xrandr_set_crtc_config', set_crtc_config) disp.extension_add_method('display', 'xrandr_get_crtc_gamma_size', get_crtc_gamma_size) disp.extension_add_method('display', 'xrandr_get_crtc_gamma', get_crtc_gamma) disp.extension_add_method('display', 'xrandr_set_crtc_gamma', set_crtc_gamma) disp.extension_add_method('window', 'xrandr_get_screen_resources_current', get_screen_resources_current) disp.extension_add_method('display', 'xrandr_set_crtc_transform', set_crtc_transform) disp.extension_add_method('display', 'xrandr_get_crtc_transform', get_crtc_transform) disp.extension_add_method('window', 'xrandr_set_output_primary', set_output_primary) disp.extension_add_method('window', 'xrandr_get_output_primary', get_output_primary) disp.extension_add_method('display', 'xrandr_get_panning', get_panning) disp.extension_add_method('display', 'xrandr_set_panning', set_panning) disp.extension_add_event(info.first_event, ScreenChangeNotify) disp.extension_add_event(info.first_event + 1, CrtcChangeNotify) disp.extension_add_event(info.first_event + 2, OutputChangeNotify) disp.extension_add_event(info.first_event + 3, OutputPropertyNotify) #disp.extension_add_error(BadRROutput, BadRROutputError) #disp.extension_add_error(BadRRCrtc, BadRRCrtcError) #disp.extension_add_error(BadRRMode, BadRRModeError) Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/record.py000066400000000000000000000222011505160700500237410ustar00rootroot00000000000000# Xlib.ext.record -- RECORD extension module # # Copyright (C) 2006 Alex Badea # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib import X from Xlib.protocol import rq extname = 'RECORD' FromServerTime = 0x01 FromClientTime = 0x02 FromClientSequence = 0x04 CurrentClients = 1 FutureClients = 2 AllClients = 3 FromServer = 0 FromClient = 1 ClientStarted = 2 ClientDied = 3 StartOfData = 4 EndOfData = 5 Record_Range8 = rq.Struct( rq.Card8('first'), rq.Card8('last')) Record_Range16 = rq.Struct( rq.Card16('first'), rq.Card16('last')) Record_ExtRange = rq.Struct( rq.Object('major_range', Record_Range8), rq.Object('minor_range', Record_Range16)) Record_Range = rq.Struct( rq.Object('core_requests', Record_Range8), rq.Object('core_replies', Record_Range8), rq.Object('ext_requests', Record_ExtRange), rq.Object('ext_replies', Record_ExtRange), rq.Object('delivered_events', Record_Range8), rq.Object('device_events', Record_Range8), rq.Object('errors', Record_Range8), rq.Bool('client_started'), rq.Bool('client_died')) Record_ClientInfo = rq.Struct( rq.Card32('client_resource'), rq.LengthOf('ranges', 4), rq.List('ranges', Record_Range)) class RawField(rq.ValueField): """A field with raw data, stored as a string""" structcode = None def pack_value(self, val): return val, len(val), None def parse_binary_value(self, data, display, length, format): return data, '' class GetVersion(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), rq.Card16('major_version'), rq.Card16('minor_version')) _reply = rq.Struct( rq.Pad(2), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('major_version'), rq.Card16('minor_version'), rq.Pad(20)) def get_version(self, major, minor): return GetVersion( display = self.display, opcode = self.display.get_extension_major(extname), major_version = major, minor_version = minor) class CreateContext(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(1), rq.RequestLength(), rq.Card32('context'), # Record_RC rq.Card8('element_header'), # Record_Element_Header rq.Pad(3), rq.LengthOf('clients', 4), rq.LengthOf('ranges', 4), rq.List('clients', rq.Card32Obj), rq.List('ranges', Record_Range)) def create_context(self, datum_flags, clients, ranges): context = self.display.allocate_resource_id() CreateContext( display = self.display, opcode = self.display.get_extension_major(extname), context = context, element_header = datum_flags, clients = clients, ranges = ranges) return context class RegisterClients(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Card32('context'), # Record_RC rq.Card8('element_header'), # Record_Element_Header rq.Pad(3), rq.LengthOf('clients', 4), rq.LengthOf('ranges', 4), rq.List('clients', rq.Card32Obj), rq.List('ranges', Record_Range)) def register_clients(self, context, element_header, clients, ranges): RegisterClients( display = self.display, opcode = self.display.get_extension_major(extname), context = context, element_header = element_header, clients = clients, ranges = ranges) class UnregisterClients(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(3), rq.RequestLength(), rq.Card32('context'), # Record_RC rq.LengthOf('clients', 4), rq.List('clients', rq.Card32Obj)) def unregister_clients(self, context, clients): UnregisterClients( display = self.display, opcode = self.display.get_extension_major(extname), context = context, clients = clients) class GetContext(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.Card32('context')) # Record_RC _reply = rq.Struct( rq.Pad(2), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card8('element_header'), # Record_Element_Header rq.Pad(3), rq.LengthOf('client_info', 4), rq.Pad(16), rq.List('client_info', Record_ClientInfo)) def get_context(self, context): return GetContext( display = self.display, opcode = self.display.get_extension_major(extname), context = context) class EnableContext(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(5), rq.RequestLength(), rq.Card32('context')) # Record_RC _reply = rq.Struct( rq.Pad(1), rq.Card8('category'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card8('element_header'), # Record_Element_Header rq.Bool('client_swapped'), rq.Pad(2), rq.Card32('id_base'), # Record_XIDBase rq.Card32('server_time'), rq.Card32('recorded_sequence_number'), rq.Pad(8), RawField('data')) # This request receives multiple responses, so we need to keep # ourselves in the 'sent_requests' list in order to receive them all. # See the discussion on ListFonstsWithInfo in request.py def __init__(self, callback, *args, **keys): self._callback = callback rq.ReplyRequest.__init__(self, *args, **keys) def _parse_response(self, data): r, d = self._reply.parse_binary(data, self._display) self._callback(r) if r.category == StartOfData: # Hack ourselves a sequence number, used by the code in # Xlib.protocol.display.Display.parse_request_response() self.sequence_number = r.sequence_number if r.category == EndOfData: self._response_lock.acquire() self._data = r self._response_lock.release() else: self._display.sent_requests.insert(0, self) def enable_context(self, context, callback): EnableContext( callback = callback, display = self.display, opcode = self.display.get_extension_major(extname), context = context) class DisableContext(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(6), rq.RequestLength(), rq.Card32('context')) # Record_RC def disable_context(self, context): DisableContext( display = self.display, opcode = self.display.get_extension_major(extname), context = context) class FreeContext(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(7), rq.RequestLength(), rq.Card32('context')) # Record_RC def free_context(self, context): FreeContext( display = self.display, opcode = self.display.get_extension_major(extname), context = context) self.display.free_resource_id(context) def init(disp, info): disp.extension_add_method('display', 'record_get_version', get_version) disp.extension_add_method('display', 'record_create_context', create_context) disp.extension_add_method('display', 'record_register_clients', register_clients) disp.extension_add_method('display', 'record_unregister_clients', unregister_clients) disp.extension_add_method('display', 'record_get_context', get_context) disp.extension_add_method('display', 'record_enable_context', enable_context) disp.extension_add_method('display', 'record_disable_context', disable_context) disp.extension_add_method('display', 'record_free_context', free_context) Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/shape.py000066400000000000000000000243531505160700500235750ustar00rootroot00000000000000# Xlib.ext.shape -- SHAPE extension module # # Copyright (C) 2002 Jeffrey Boser # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Constants to use # # Regions of a window ShapeBounding = 0 # the 'edge' of a shaped window ShapeClip = 1 # the clipping region # Shape Operations ShapeSet = 0 # Set the region unmodified (dest=src) ShapeUnion = 1 # Add the new region to the old (dest=src|dest) ShapeIntersect = 2 # Use the intersection (dest=src&dest) ShapeSubtract = 3 # remove region (dest = dest - intersect) ShapeInvert = 4 # opposite of subtract (dest = src - intersect) # Events ShapeNotifyMask = (1<<0) #a keypress mask? ShapeNotify = 0 #still unsure of these values # How to Use # The basic functions that change the shapes of things are: # shape_rectangles (uses a set of rectangles as the source) # operation, region, ordering, rects # shape_mask (uses a bitmap as the source) # operation, region, x_offset, y_offset, bitmap # shape_combine (uses a window as the source) # operation, src_region, dest_region, x_offset, y_offset, src_window # shape_offset (moves the region) # region, x_offset, y_offset # The functions to find stuff out (these three return mappings of field/values): # shape_query_version (shape extension version) # major_version, minor_version # shape_query_extents (rectangle boundaries of a window's regions) # clip_shaped, clip_x, clip_y, clip_width, clip_height, # bounding_shaped, bounding_x, bounding_y, bounding_width, bounding_height # shape_input_selected (if the window products shapenotify events) # enabled # shape_get_rectangles (the rectangles set by shape_rectangles) # ordering, rects # And to turn on shape notify events: # shape_select_input # enable from Xlib import X from Xlib.protocol import rq, structs extname = 'SHAPE' class QueryVersion(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('major_version'), rq.Card16('minor_version'), rq.Pad(20), ) def query_version(self): return QueryVersion( display = self.display, opcode = self.display.get_extension_major(extname), ) class Rectangles(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(1), rq.RequestLength(), rq.Card8('operation'), rq.Set('region', 1, (ShapeBounding, ShapeClip)), rq.Card8('ordering'), rq.Pad(1), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.List('rectangles', structs.Rectangle), ) def rectangles(self, region, operation, ordering, x, y, rectangles): Rectangles( display = self.display, opcode = self.display.get_extension_major(extname), operation = operation, region = region, ordering = ordering, window = self.id, x = x, y = y, rectangles = rectangles, ) class Mask(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Card8('operation'), rq.Set('region', 1, (ShapeBounding, ShapeClip)), rq.Pad(2), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.Pixmap('source', (X.NONE, )), ) def mask(self, operation, region, x, y, source): Mask(display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, operation = operation, region = region, x = x, y = y, source = source, ) class Combine(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(3), rq.RequestLength(), rq.Card8('operation'), rq.Set('dest_region', 1, (ShapeBounding, ShapeClip)), rq.Set('source_region', 1, (ShapeBounding, ShapeClip)), rq.Pad(1), rq.Window('dest'), rq.Int16('x'), rq.Int16('y'), rq.Window('source'), ) def combine(self, operation, region, source, source_region, x, y): Combine( display = self.display, opcode = self.display.get_extension_major(extname), operation = operation, dest_region = region, source_region = source_region, dest = self.id, x = x, y = y, source = source, ) class Offset(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.Set('region', 1, (ShapeBounding, ShapeClip)), rq.Pad(3), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), ) def offset(self, region, x, y): Offset( display = self.display, opcode = self.display.get_extension_major(extname), region = region, window = self.id, x = x, y = y, ) class QueryExtents(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(5), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Bool('bounding_shaped'), rq.Bool('clip_shaped'), rq.Pad(2), rq.Int16('bounding_x'), rq.Int16('bounding_y'), rq.Card16('bounding_width'), rq.Card16('bounding_height'), rq.Int16('clip_x'), rq.Int16('clip_y'), rq.Card16('clip_width'), rq.Card16('clip_height'), rq.Pad(4), ) def query_extents(self): return QueryExtents( display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, ) class SelectInput(rq.Request): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(6), rq.RequestLength(), rq.Window('window'), rq.Bool('enable'), rq.Pad(3), ) def select_input(self, enable = 1): SelectInput( display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, enable = enable, ) class InputSelected(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(7), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Bool('enabled'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), ) def input_selected(self): reply = InputSelected( display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, ) return reply.enabled class GetRectangles(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(8), rq.RequestLength(), rq.Window('window'), rq.Set('region', 1, (ShapeBounding, ShapeClip)), rq.Pad(3), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('ordering'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('rectangles', 4), rq.Pad(20), rq.List('rectangles', structs.Rectangle), ) def get_rectangles(self, region): return GetRectangles( display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, region = region, ) class ShapeNotify(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Set('region', 1, (ShapeBounding, ShapeClip)), rq.Card16('sequence_number'), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card32('time'), rq.Bool('shaped'), rq.Pad(11), ) def init(disp, info): disp.extension_add_method('display', 'shape_query_version', query_version ) disp.extension_add_method('window', 'shape_rectangles', rectangles ) disp.extension_add_method('window', 'shape_mask', mask ) disp.extension_add_method('window', 'shape_combine', combine ) disp.extension_add_method('window', 'shape_offset', offset ) disp.extension_add_method('window', 'shape_query_extents', query_extents ) disp.extension_add_method('window', 'shape_select_input', select_input ) disp.extension_add_method('window', 'shape_input_selected', input_selected ) disp.extension_add_method('window', 'shape_get_rectangles', get_rectangles ) disp.extension_add_event(info.first_event, ShapeNotify) Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/xinerama.py000066400000000000000000000154261505160700500243020ustar00rootroot00000000000000# Xlib.ext.xinerama -- Xinerama extension module # # Copyright (C) 2006 Mike Meyer # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Xinerama - provide access to the Xinerama extension information. There are at least there different - and mutually incomparable - Xinerama extensions available. This uses the one bundled with XFree86 4.6 and/or Xorg 6.9 in the ati/radeon driver. It uses the include files from that X distribution, so should work with it as well. I provide code for the lone Sun 1.0 request that isn't part of 1.1, but this is untested because I don't have a server that implements it. The functions loosely follow the libXineram functions. Mostly, they return an rq.Struct in lieue of passing in pointers that get data from the rq.Struct crammed into them. The exception is isActive, which returns the state information - because that's what libXinerama does.""" from Xlib import X from Xlib.protocol import rq, structs extname = 'XINERAMA' class QueryVersion(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), rq.Card8('major_version'), rq.Card8('minor_version'), rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('major_version'), rq.Card16('minor_version'), rq.Pad(20), ) def query_version(self): return QueryVersion(display=self.display, opcode=self.display.get_extension_major(extname), major_version=1, minor_version=1) class GetState(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(1), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Bool('state'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('window'), rq.Pad(20), ) def get_state(self): return GetState(display=self.display, opcode=self.display.get_extension_major(extname), window=self.id, ) class GetScreenCount(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Window('window'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('screen_count'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('window'), rq.Pad(20), ) def get_screen_count(self): return GetScreenCount(display=self.display, opcode=self.display.get_extension_major(extname), window=self.id, ) class GetScreenSize(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(3), rq.RequestLength(), rq.Window('window'), rq.Card32('screen'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.Card32('length'), rq.Card32('width'), rq.Card32('height'), rq.Window('window'), rq.Card32('screen'), rq.Pad(8), ) def get_screen_size(self, screen_no): """Returns the size of the given screen number""" return GetScreenSize(display=self.display, opcode=self.display.get_extension_major(extname), window=self.id, screen=screen_no, ) # IsActive is only available from Xinerama 1.1 and later. # It should be used in preference to GetState. class IsActive(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('state'), rq.Pad(20), ) def is_active(self): r = IsActive(display=self.display, opcode=self.display.get_extension_major(extname), ) return r.state # QueryScreens is only available from Xinerama 1.1 and later class QueryScreens(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(5), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('number'), rq.Pad(20), rq.List('screens', structs.Rectangle), ) def query_screens(self): # Hmm. This one needs to read the screen data from the socket. Ooops... return QueryScreens(display=self.display, opcode=self.display.get_extension_major(extname), ) # GetInfo is only available from some Xinerama 1.0, and *NOT* later! Untested class GetInfo(rq.ReplyRequest): _request = rq.Struct( rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.Card32('visual'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('window'), # An array of subwindow slots goes here. Bah. ) def get_info(self, visual): r = GetInfo(display=self.display, opcode=self.display.get_extension_major(extname), visual=visual) def init(disp, info): disp.extension_add_method('display', 'xinerama_query_version', query_version) disp.extension_add_method('window', 'xinerama_get_state', get_state) disp.extension_add_method('window', 'xinerama_get_screen_count', get_screen_count) disp.extension_add_method('window', 'xinerama_get_screen_size', get_screen_size) disp.extension_add_method('display', 'xinerama_is_active', is_active) disp.extension_add_method('display', 'xinerama_query_screens', query_screens) disp.extension_add_method('display', 'xinerama_get_info', get_info) Nagstamon-master/Nagstamon/thirdparty/Xlib/ext/xtest.py000066400000000000000000000107251505160700500236420ustar00rootroot00000000000000# Xlib.ext.xtest -- XTEST extension module # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib import X from Xlib.protocol import rq extname = 'XTEST' CurrentCursor = 1 class GetVersion(rq.ReplyRequest): _request = rq.Struct(rq.Card8('opcode'), rq.Opcode(0), rq.RequestLength(), rq.Card8('major_version'), rq.Pad(1), rq.Card16('minor_version') ) _reply = rq.Struct(rq.Pad(1), rq.Card8('major_version'), rq.Card16('sequence_number'), rq.Pad(4), rq.Card16('minor_version'), rq.Pad(22) ) def get_version(self, major, minor): return GetVersion(display = self.display, opcode = self.display.get_extension_major(extname), major_version = major, minor_version = minor) class CompareCursor(rq.ReplyRequest): _request = rq.Struct(rq.Card8('opcode'), rq.Opcode(1), rq.RequestLength(), rq.Window('window'), rq.Cursor('cursor', (X.NONE, CurrentCursor)), ) _reply = rq.Struct(rq.Pad(1), rq.Card8('same'), rq.Card16('sequence_number'), rq.Pad(28), ) def compare_cursor(self, cursor): r = CompareCursor(display = self.display, opcode = self.display.get_extension_major(extname), window = self.id, cursor = cursor) return r.same class FakeInput(rq.Request): _request = rq.Struct(rq.Card8('opcode'), rq.Opcode(2), rq.RequestLength(), rq.Set('event_type', 1, (X.KeyPress, X.KeyRelease, X.ButtonPress, X.ButtonRelease, X.MotionNotify)), rq.Card8('detail'), rq.Pad(2), rq.Card32('time'), rq.Window('root', (X.NONE, )), rq.Pad(8), rq.Int16('x'), rq.Int16('y'), rq.Pad(8) ) def fake_input(self, event_type, detail = 0, time = X.CurrentTime, root = X.NONE, x = 0, y = 0): FakeInput(display = self.display, opcode = self.display.get_extension_major(extname), event_type = event_type, detail = detail, time = time, root = root, x = x, y = y) class GrabControl(rq.Request): _request = rq.Struct(rq.Card8('opcode'), rq.Opcode(3), rq.RequestLength(), rq.Bool('impervious'), rq.Pad(3) ) def grab_control(self, impervious): GrabControl(display = self.display, opcode = self.display.get_extension_major(extname), impervious = impervious) def init(disp, info): disp.extension_add_method('display', 'xtest_get_version', get_version) disp.extension_add_method('window', 'xtest_compare_cursor', compare_cursor) disp.extension_add_method('display', 'xtest_fake_input', fake_input) disp.extension_add_method('display', 'xtest_grab_control', grab_control) Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/000077500000000000000000000000001505160700500233145ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/__init__.py000066400000000000000000000021521505160700500254250ustar00rootroot00000000000000# Xlib.keysymdef -- X keysym defs # # Copyright (C) 2001 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA __all__ = [ 'apl', 'arabic', 'cyrillic', 'greek', 'hebrew', 'katakana', 'korean', 'latin1', 'latin2', 'latin3', 'latin4', 'miscellany', 'publishing', 'special', 'technical', 'thai', 'xf86', 'xk3270', 'xkb', ] Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/apl.py000066400000000000000000000005611505160700500244440ustar00rootroot00000000000000XK_leftcaret = 0xba3 XK_rightcaret = 0xba6 XK_downcaret = 0xba8 XK_upcaret = 0xba9 XK_overbar = 0xbc0 XK_downtack = 0xbc2 XK_upshoe = 0xbc3 XK_downstile = 0xbc4 XK_underbar = 0xbc6 XK_jot = 0xbca XK_quad = 0xbcc XK_uptack = 0xbce XK_circle = 0xbcf XK_upstile = 0xbd3 XK_downshoe = 0xbd6 XK_rightshoe = 0xbd8 XK_leftshoe = 0xbda XK_lefttack = 0xbdc XK_righttack = 0xbfc Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/arabic.py000066400000000000000000000023051505160700500251070ustar00rootroot00000000000000XK_Arabic_comma = 0x5ac XK_Arabic_semicolon = 0x5bb XK_Arabic_question_mark = 0x5bf XK_Arabic_hamza = 0x5c1 XK_Arabic_maddaonalef = 0x5c2 XK_Arabic_hamzaonalef = 0x5c3 XK_Arabic_hamzaonwaw = 0x5c4 XK_Arabic_hamzaunderalef = 0x5c5 XK_Arabic_hamzaonyeh = 0x5c6 XK_Arabic_alef = 0x5c7 XK_Arabic_beh = 0x5c8 XK_Arabic_tehmarbuta = 0x5c9 XK_Arabic_teh = 0x5ca XK_Arabic_theh = 0x5cb XK_Arabic_jeem = 0x5cc XK_Arabic_hah = 0x5cd XK_Arabic_khah = 0x5ce XK_Arabic_dal = 0x5cf XK_Arabic_thal = 0x5d0 XK_Arabic_ra = 0x5d1 XK_Arabic_zain = 0x5d2 XK_Arabic_seen = 0x5d3 XK_Arabic_sheen = 0x5d4 XK_Arabic_sad = 0x5d5 XK_Arabic_dad = 0x5d6 XK_Arabic_tah = 0x5d7 XK_Arabic_zah = 0x5d8 XK_Arabic_ain = 0x5d9 XK_Arabic_ghain = 0x5da XK_Arabic_tatweel = 0x5e0 XK_Arabic_feh = 0x5e1 XK_Arabic_qaf = 0x5e2 XK_Arabic_kaf = 0x5e3 XK_Arabic_lam = 0x5e4 XK_Arabic_meem = 0x5e5 XK_Arabic_noon = 0x5e6 XK_Arabic_ha = 0x5e7 XK_Arabic_heh = 0x5e7 XK_Arabic_waw = 0x5e8 XK_Arabic_alefmaksura = 0x5e9 XK_Arabic_yeh = 0x5ea XK_Arabic_fathatan = 0x5eb XK_Arabic_dammatan = 0x5ec XK_Arabic_kasratan = 0x5ed XK_Arabic_fatha = 0x5ee XK_Arabic_damma = 0x5ef XK_Arabic_kasra = 0x5f0 XK_Arabic_shadda = 0x5f1 XK_Arabic_sukun = 0x5f2 XK_Arabic_switch = 0xFF7E Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/cyrillic.py000066400000000000000000000047541505160700500255120ustar00rootroot00000000000000XK_Serbian_dje = 0x6a1 XK_Macedonia_gje = 0x6a2 XK_Cyrillic_io = 0x6a3 XK_Ukrainian_ie = 0x6a4 XK_Ukranian_je = 0x6a4 XK_Macedonia_dse = 0x6a5 XK_Ukrainian_i = 0x6a6 XK_Ukranian_i = 0x6a6 XK_Ukrainian_yi = 0x6a7 XK_Ukranian_yi = 0x6a7 XK_Cyrillic_je = 0x6a8 XK_Serbian_je = 0x6a8 XK_Cyrillic_lje = 0x6a9 XK_Serbian_lje = 0x6a9 XK_Cyrillic_nje = 0x6aa XK_Serbian_nje = 0x6aa XK_Serbian_tshe = 0x6ab XK_Macedonia_kje = 0x6ac XK_Byelorussian_shortu = 0x6ae XK_Cyrillic_dzhe = 0x6af XK_Serbian_dze = 0x6af XK_numerosign = 0x6b0 XK_Serbian_DJE = 0x6b1 XK_Macedonia_GJE = 0x6b2 XK_Cyrillic_IO = 0x6b3 XK_Ukrainian_IE = 0x6b4 XK_Ukranian_JE = 0x6b4 XK_Macedonia_DSE = 0x6b5 XK_Ukrainian_I = 0x6b6 XK_Ukranian_I = 0x6b6 XK_Ukrainian_YI = 0x6b7 XK_Ukranian_YI = 0x6b7 XK_Cyrillic_JE = 0x6b8 XK_Serbian_JE = 0x6b8 XK_Cyrillic_LJE = 0x6b9 XK_Serbian_LJE = 0x6b9 XK_Cyrillic_NJE = 0x6ba XK_Serbian_NJE = 0x6ba XK_Serbian_TSHE = 0x6bb XK_Macedonia_KJE = 0x6bc XK_Byelorussian_SHORTU = 0x6be XK_Cyrillic_DZHE = 0x6bf XK_Serbian_DZE = 0x6bf XK_Cyrillic_yu = 0x6c0 XK_Cyrillic_a = 0x6c1 XK_Cyrillic_be = 0x6c2 XK_Cyrillic_tse = 0x6c3 XK_Cyrillic_de = 0x6c4 XK_Cyrillic_ie = 0x6c5 XK_Cyrillic_ef = 0x6c6 XK_Cyrillic_ghe = 0x6c7 XK_Cyrillic_ha = 0x6c8 XK_Cyrillic_i = 0x6c9 XK_Cyrillic_shorti = 0x6ca XK_Cyrillic_ka = 0x6cb XK_Cyrillic_el = 0x6cc XK_Cyrillic_em = 0x6cd XK_Cyrillic_en = 0x6ce XK_Cyrillic_o = 0x6cf XK_Cyrillic_pe = 0x6d0 XK_Cyrillic_ya = 0x6d1 XK_Cyrillic_er = 0x6d2 XK_Cyrillic_es = 0x6d3 XK_Cyrillic_te = 0x6d4 XK_Cyrillic_u = 0x6d5 XK_Cyrillic_zhe = 0x6d6 XK_Cyrillic_ve = 0x6d7 XK_Cyrillic_softsign = 0x6d8 XK_Cyrillic_yeru = 0x6d9 XK_Cyrillic_ze = 0x6da XK_Cyrillic_sha = 0x6db XK_Cyrillic_e = 0x6dc XK_Cyrillic_shcha = 0x6dd XK_Cyrillic_che = 0x6de XK_Cyrillic_hardsign = 0x6df XK_Cyrillic_YU = 0x6e0 XK_Cyrillic_A = 0x6e1 XK_Cyrillic_BE = 0x6e2 XK_Cyrillic_TSE = 0x6e3 XK_Cyrillic_DE = 0x6e4 XK_Cyrillic_IE = 0x6e5 XK_Cyrillic_EF = 0x6e6 XK_Cyrillic_GHE = 0x6e7 XK_Cyrillic_HA = 0x6e8 XK_Cyrillic_I = 0x6e9 XK_Cyrillic_SHORTI = 0x6ea XK_Cyrillic_KA = 0x6eb XK_Cyrillic_EL = 0x6ec XK_Cyrillic_EM = 0x6ed XK_Cyrillic_EN = 0x6ee XK_Cyrillic_O = 0x6ef XK_Cyrillic_PE = 0x6f0 XK_Cyrillic_YA = 0x6f1 XK_Cyrillic_ER = 0x6f2 XK_Cyrillic_ES = 0x6f3 XK_Cyrillic_TE = 0x6f4 XK_Cyrillic_U = 0x6f5 XK_Cyrillic_ZHE = 0x6f6 XK_Cyrillic_VE = 0x6f7 XK_Cyrillic_SOFTSIGN = 0x6f8 XK_Cyrillic_YERU = 0x6f9 XK_Cyrillic_ZE = 0x6fa XK_Cyrillic_SHA = 0x6fb XK_Cyrillic_E = 0x6fc XK_Cyrillic_SHCHA = 0x6fd XK_Cyrillic_CHE = 0x6fe XK_Cyrillic_HARDSIGN = 0x6ff Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/greek.py000066400000000000000000000034601505160700500247660ustar00rootroot00000000000000XK_Greek_ALPHAaccent = 0x7a1 XK_Greek_EPSILONaccent = 0x7a2 XK_Greek_ETAaccent = 0x7a3 XK_Greek_IOTAaccent = 0x7a4 XK_Greek_IOTAdiaeresis = 0x7a5 XK_Greek_OMICRONaccent = 0x7a7 XK_Greek_UPSILONaccent = 0x7a8 XK_Greek_UPSILONdieresis = 0x7a9 XK_Greek_OMEGAaccent = 0x7ab XK_Greek_accentdieresis = 0x7ae XK_Greek_horizbar = 0x7af XK_Greek_alphaaccent = 0x7b1 XK_Greek_epsilonaccent = 0x7b2 XK_Greek_etaaccent = 0x7b3 XK_Greek_iotaaccent = 0x7b4 XK_Greek_iotadieresis = 0x7b5 XK_Greek_iotaaccentdieresis = 0x7b6 XK_Greek_omicronaccent = 0x7b7 XK_Greek_upsilonaccent = 0x7b8 XK_Greek_upsilondieresis = 0x7b9 XK_Greek_upsilonaccentdieresis = 0x7ba XK_Greek_omegaaccent = 0x7bb XK_Greek_ALPHA = 0x7c1 XK_Greek_BETA = 0x7c2 XK_Greek_GAMMA = 0x7c3 XK_Greek_DELTA = 0x7c4 XK_Greek_EPSILON = 0x7c5 XK_Greek_ZETA = 0x7c6 XK_Greek_ETA = 0x7c7 XK_Greek_THETA = 0x7c8 XK_Greek_IOTA = 0x7c9 XK_Greek_KAPPA = 0x7ca XK_Greek_LAMDA = 0x7cb XK_Greek_LAMBDA = 0x7cb XK_Greek_MU = 0x7cc XK_Greek_NU = 0x7cd XK_Greek_XI = 0x7ce XK_Greek_OMICRON = 0x7cf XK_Greek_PI = 0x7d0 XK_Greek_RHO = 0x7d1 XK_Greek_SIGMA = 0x7d2 XK_Greek_TAU = 0x7d4 XK_Greek_UPSILON = 0x7d5 XK_Greek_PHI = 0x7d6 XK_Greek_CHI = 0x7d7 XK_Greek_PSI = 0x7d8 XK_Greek_OMEGA = 0x7d9 XK_Greek_alpha = 0x7e1 XK_Greek_beta = 0x7e2 XK_Greek_gamma = 0x7e3 XK_Greek_delta = 0x7e4 XK_Greek_epsilon = 0x7e5 XK_Greek_zeta = 0x7e6 XK_Greek_eta = 0x7e7 XK_Greek_theta = 0x7e8 XK_Greek_iota = 0x7e9 XK_Greek_kappa = 0x7ea XK_Greek_lamda = 0x7eb XK_Greek_lambda = 0x7eb XK_Greek_mu = 0x7ec XK_Greek_nu = 0x7ed XK_Greek_xi = 0x7ee XK_Greek_omicron = 0x7ef XK_Greek_pi = 0x7f0 XK_Greek_rho = 0x7f1 XK_Greek_sigma = 0x7f2 XK_Greek_finalsmallsigma = 0x7f3 XK_Greek_tau = 0x7f4 XK_Greek_upsilon = 0x7f5 XK_Greek_phi = 0x7f6 XK_Greek_chi = 0x7f7 XK_Greek_psi = 0x7f8 XK_Greek_omega = 0x7f9 XK_Greek_switch = 0xFF7E Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/hebrew.py000066400000000000000000000016751505160700500251530ustar00rootroot00000000000000XK_hebrew_doublelowline = 0xcdf XK_hebrew_aleph = 0xce0 XK_hebrew_bet = 0xce1 XK_hebrew_beth = 0xce1 XK_hebrew_gimel = 0xce2 XK_hebrew_gimmel = 0xce2 XK_hebrew_dalet = 0xce3 XK_hebrew_daleth = 0xce3 XK_hebrew_he = 0xce4 XK_hebrew_waw = 0xce5 XK_hebrew_zain = 0xce6 XK_hebrew_zayin = 0xce6 XK_hebrew_chet = 0xce7 XK_hebrew_het = 0xce7 XK_hebrew_tet = 0xce8 XK_hebrew_teth = 0xce8 XK_hebrew_yod = 0xce9 XK_hebrew_finalkaph = 0xcea XK_hebrew_kaph = 0xceb XK_hebrew_lamed = 0xcec XK_hebrew_finalmem = 0xced XK_hebrew_mem = 0xcee XK_hebrew_finalnun = 0xcef XK_hebrew_nun = 0xcf0 XK_hebrew_samech = 0xcf1 XK_hebrew_samekh = 0xcf1 XK_hebrew_ayin = 0xcf2 XK_hebrew_finalpe = 0xcf3 XK_hebrew_pe = 0xcf4 XK_hebrew_finalzade = 0xcf5 XK_hebrew_finalzadi = 0xcf5 XK_hebrew_zade = 0xcf6 XK_hebrew_zadi = 0xcf6 XK_hebrew_qoph = 0xcf7 XK_hebrew_kuf = 0xcf7 XK_hebrew_resh = 0xcf8 XK_hebrew_shin = 0xcf9 XK_hebrew_taw = 0xcfa XK_hebrew_taf = 0xcfa XK_Hebrew_switch = 0xFF7E Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/katakana.py000066400000000000000000000025651505160700500254510ustar00rootroot00000000000000XK_overline = 0x47e XK_kana_fullstop = 0x4a1 XK_kana_openingbracket = 0x4a2 XK_kana_closingbracket = 0x4a3 XK_kana_comma = 0x4a4 XK_kana_conjunctive = 0x4a5 XK_kana_middledot = 0x4a5 XK_kana_WO = 0x4a6 XK_kana_a = 0x4a7 XK_kana_i = 0x4a8 XK_kana_u = 0x4a9 XK_kana_e = 0x4aa XK_kana_o = 0x4ab XK_kana_ya = 0x4ac XK_kana_yu = 0x4ad XK_kana_yo = 0x4ae XK_kana_tsu = 0x4af XK_kana_tu = 0x4af XK_prolongedsound = 0x4b0 XK_kana_A = 0x4b1 XK_kana_I = 0x4b2 XK_kana_U = 0x4b3 XK_kana_E = 0x4b4 XK_kana_O = 0x4b5 XK_kana_KA = 0x4b6 XK_kana_KI = 0x4b7 XK_kana_KU = 0x4b8 XK_kana_KE = 0x4b9 XK_kana_KO = 0x4ba XK_kana_SA = 0x4bb XK_kana_SHI = 0x4bc XK_kana_SU = 0x4bd XK_kana_SE = 0x4be XK_kana_SO = 0x4bf XK_kana_TA = 0x4c0 XK_kana_CHI = 0x4c1 XK_kana_TI = 0x4c1 XK_kana_TSU = 0x4c2 XK_kana_TU = 0x4c2 XK_kana_TE = 0x4c3 XK_kana_TO = 0x4c4 XK_kana_NA = 0x4c5 XK_kana_NI = 0x4c6 XK_kana_NU = 0x4c7 XK_kana_NE = 0x4c8 XK_kana_NO = 0x4c9 XK_kana_HA = 0x4ca XK_kana_HI = 0x4cb XK_kana_FU = 0x4cc XK_kana_HU = 0x4cc XK_kana_HE = 0x4cd XK_kana_HO = 0x4ce XK_kana_MA = 0x4cf XK_kana_MI = 0x4d0 XK_kana_MU = 0x4d1 XK_kana_ME = 0x4d2 XK_kana_MO = 0x4d3 XK_kana_YA = 0x4d4 XK_kana_YU = 0x4d5 XK_kana_YO = 0x4d6 XK_kana_RA = 0x4d7 XK_kana_RI = 0x4d8 XK_kana_RU = 0x4d9 XK_kana_RE = 0x4da XK_kana_RO = 0x4db XK_kana_WA = 0x4dc XK_kana_N = 0x4dd XK_voicedsound = 0x4de XK_semivoicedsound = 0x4df XK_kana_switch = 0xFF7E Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/korean.py000066400000000000000000000054541505160700500251550ustar00rootroot00000000000000XK_Hangul = 0xff31 XK_Hangul_Start = 0xff32 XK_Hangul_End = 0xff33 XK_Hangul_Hanja = 0xff34 XK_Hangul_Jamo = 0xff35 XK_Hangul_Romaja = 0xff36 XK_Hangul_Codeinput = 0xff37 XK_Hangul_Jeonja = 0xff38 XK_Hangul_Banja = 0xff39 XK_Hangul_PreHanja = 0xff3a XK_Hangul_PostHanja = 0xff3b XK_Hangul_SingleCandidate = 0xff3c XK_Hangul_MultipleCandidate = 0xff3d XK_Hangul_PreviousCandidate = 0xff3e XK_Hangul_Special = 0xff3f XK_Hangul_switch = 0xFF7E XK_Hangul_Kiyeog = 0xea1 XK_Hangul_SsangKiyeog = 0xea2 XK_Hangul_KiyeogSios = 0xea3 XK_Hangul_Nieun = 0xea4 XK_Hangul_NieunJieuj = 0xea5 XK_Hangul_NieunHieuh = 0xea6 XK_Hangul_Dikeud = 0xea7 XK_Hangul_SsangDikeud = 0xea8 XK_Hangul_Rieul = 0xea9 XK_Hangul_RieulKiyeog = 0xeaa XK_Hangul_RieulMieum = 0xeab XK_Hangul_RieulPieub = 0xeac XK_Hangul_RieulSios = 0xead XK_Hangul_RieulTieut = 0xeae XK_Hangul_RieulPhieuf = 0xeaf XK_Hangul_RieulHieuh = 0xeb0 XK_Hangul_Mieum = 0xeb1 XK_Hangul_Pieub = 0xeb2 XK_Hangul_SsangPieub = 0xeb3 XK_Hangul_PieubSios = 0xeb4 XK_Hangul_Sios = 0xeb5 XK_Hangul_SsangSios = 0xeb6 XK_Hangul_Ieung = 0xeb7 XK_Hangul_Jieuj = 0xeb8 XK_Hangul_SsangJieuj = 0xeb9 XK_Hangul_Cieuc = 0xeba XK_Hangul_Khieuq = 0xebb XK_Hangul_Tieut = 0xebc XK_Hangul_Phieuf = 0xebd XK_Hangul_Hieuh = 0xebe XK_Hangul_A = 0xebf XK_Hangul_AE = 0xec0 XK_Hangul_YA = 0xec1 XK_Hangul_YAE = 0xec2 XK_Hangul_EO = 0xec3 XK_Hangul_E = 0xec4 XK_Hangul_YEO = 0xec5 XK_Hangul_YE = 0xec6 XK_Hangul_O = 0xec7 XK_Hangul_WA = 0xec8 XK_Hangul_WAE = 0xec9 XK_Hangul_OE = 0xeca XK_Hangul_YO = 0xecb XK_Hangul_U = 0xecc XK_Hangul_WEO = 0xecd XK_Hangul_WE = 0xece XK_Hangul_WI = 0xecf XK_Hangul_YU = 0xed0 XK_Hangul_EU = 0xed1 XK_Hangul_YI = 0xed2 XK_Hangul_I = 0xed3 XK_Hangul_J_Kiyeog = 0xed4 XK_Hangul_J_SsangKiyeog = 0xed5 XK_Hangul_J_KiyeogSios = 0xed6 XK_Hangul_J_Nieun = 0xed7 XK_Hangul_J_NieunJieuj = 0xed8 XK_Hangul_J_NieunHieuh = 0xed9 XK_Hangul_J_Dikeud = 0xeda XK_Hangul_J_Rieul = 0xedb XK_Hangul_J_RieulKiyeog = 0xedc XK_Hangul_J_RieulMieum = 0xedd XK_Hangul_J_RieulPieub = 0xede XK_Hangul_J_RieulSios = 0xedf XK_Hangul_J_RieulTieut = 0xee0 XK_Hangul_J_RieulPhieuf = 0xee1 XK_Hangul_J_RieulHieuh = 0xee2 XK_Hangul_J_Mieum = 0xee3 XK_Hangul_J_Pieub = 0xee4 XK_Hangul_J_PieubSios = 0xee5 XK_Hangul_J_Sios = 0xee6 XK_Hangul_J_SsangSios = 0xee7 XK_Hangul_J_Ieung = 0xee8 XK_Hangul_J_Jieuj = 0xee9 XK_Hangul_J_Cieuc = 0xeea XK_Hangul_J_Khieuq = 0xeeb XK_Hangul_J_Tieut = 0xeec XK_Hangul_J_Phieuf = 0xeed XK_Hangul_J_Hieuh = 0xeee XK_Hangul_RieulYeorinHieuh = 0xeef XK_Hangul_SunkyeongeumMieum = 0xef0 XK_Hangul_SunkyeongeumPieub = 0xef1 XK_Hangul_PanSios = 0xef2 XK_Hangul_KkogjiDalrinIeung = 0xef3 XK_Hangul_SunkyeongeumPhieuf = 0xef4 XK_Hangul_YeorinHieuh = 0xef5 XK_Hangul_AraeA = 0xef6 XK_Hangul_AraeAE = 0xef7 XK_Hangul_J_PanSios = 0xef8 XK_Hangul_J_KkogjiDalrinIeung = 0xef9 XK_Hangul_J_YeorinHieuh = 0xefa XK_Korean_Won = 0xeff Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/latin1.py000066400000000000000000000065421505160700500250650ustar00rootroot00000000000000XK_space = 0x020 XK_exclam = 0x021 XK_quotedbl = 0x022 XK_numbersign = 0x023 XK_dollar = 0x024 XK_percent = 0x025 XK_ampersand = 0x026 XK_apostrophe = 0x027 XK_quoteright = 0x027 XK_parenleft = 0x028 XK_parenright = 0x029 XK_asterisk = 0x02a XK_plus = 0x02b XK_comma = 0x02c XK_minus = 0x02d XK_period = 0x02e XK_slash = 0x02f XK_0 = 0x030 XK_1 = 0x031 XK_2 = 0x032 XK_3 = 0x033 XK_4 = 0x034 XK_5 = 0x035 XK_6 = 0x036 XK_7 = 0x037 XK_8 = 0x038 XK_9 = 0x039 XK_colon = 0x03a XK_semicolon = 0x03b XK_less = 0x03c XK_equal = 0x03d XK_greater = 0x03e XK_question = 0x03f XK_at = 0x040 XK_A = 0x041 XK_B = 0x042 XK_C = 0x043 XK_D = 0x044 XK_E = 0x045 XK_F = 0x046 XK_G = 0x047 XK_H = 0x048 XK_I = 0x049 XK_J = 0x04a XK_K = 0x04b XK_L = 0x04c XK_M = 0x04d XK_N = 0x04e XK_O = 0x04f XK_P = 0x050 XK_Q = 0x051 XK_R = 0x052 XK_S = 0x053 XK_T = 0x054 XK_U = 0x055 XK_V = 0x056 XK_W = 0x057 XK_X = 0x058 XK_Y = 0x059 XK_Z = 0x05a XK_bracketleft = 0x05b XK_backslash = 0x05c XK_bracketright = 0x05d XK_asciicircum = 0x05e XK_underscore = 0x05f XK_grave = 0x060 XK_quoteleft = 0x060 XK_a = 0x061 XK_b = 0x062 XK_c = 0x063 XK_d = 0x064 XK_e = 0x065 XK_f = 0x066 XK_g = 0x067 XK_h = 0x068 XK_i = 0x069 XK_j = 0x06a XK_k = 0x06b XK_l = 0x06c XK_m = 0x06d XK_n = 0x06e XK_o = 0x06f XK_p = 0x070 XK_q = 0x071 XK_r = 0x072 XK_s = 0x073 XK_t = 0x074 XK_u = 0x075 XK_v = 0x076 XK_w = 0x077 XK_x = 0x078 XK_y = 0x079 XK_z = 0x07a XK_braceleft = 0x07b XK_bar = 0x07c XK_braceright = 0x07d XK_asciitilde = 0x07e XK_nobreakspace = 0x0a0 XK_exclamdown = 0x0a1 XK_cent = 0x0a2 XK_sterling = 0x0a3 XK_currency = 0x0a4 XK_yen = 0x0a5 XK_brokenbar = 0x0a6 XK_section = 0x0a7 XK_diaeresis = 0x0a8 XK_copyright = 0x0a9 XK_ordfeminine = 0x0aa XK_guillemotleft = 0x0ab XK_notsign = 0x0ac XK_hyphen = 0x0ad XK_registered = 0x0ae XK_macron = 0x0af XK_degree = 0x0b0 XK_plusminus = 0x0b1 XK_twosuperior = 0x0b2 XK_threesuperior = 0x0b3 XK_acute = 0x0b4 XK_mu = 0x0b5 XK_paragraph = 0x0b6 XK_periodcentered = 0x0b7 XK_cedilla = 0x0b8 XK_onesuperior = 0x0b9 XK_masculine = 0x0ba XK_guillemotright = 0x0bb XK_onequarter = 0x0bc XK_onehalf = 0x0bd XK_threequarters = 0x0be XK_questiondown = 0x0bf XK_Agrave = 0x0c0 XK_Aacute = 0x0c1 XK_Acircumflex = 0x0c2 XK_Atilde = 0x0c3 XK_Adiaeresis = 0x0c4 XK_Aring = 0x0c5 XK_AE = 0x0c6 XK_Ccedilla = 0x0c7 XK_Egrave = 0x0c8 XK_Eacute = 0x0c9 XK_Ecircumflex = 0x0ca XK_Ediaeresis = 0x0cb XK_Igrave = 0x0cc XK_Iacute = 0x0cd XK_Icircumflex = 0x0ce XK_Idiaeresis = 0x0cf XK_ETH = 0x0d0 XK_Eth = 0x0d0 XK_Ntilde = 0x0d1 XK_Ograve = 0x0d2 XK_Oacute = 0x0d3 XK_Ocircumflex = 0x0d4 XK_Otilde = 0x0d5 XK_Odiaeresis = 0x0d6 XK_multiply = 0x0d7 XK_Ooblique = 0x0d8 XK_Ugrave = 0x0d9 XK_Uacute = 0x0da XK_Ucircumflex = 0x0db XK_Udiaeresis = 0x0dc XK_Yacute = 0x0dd XK_THORN = 0x0de XK_Thorn = 0x0de XK_ssharp = 0x0df XK_agrave = 0x0e0 XK_aacute = 0x0e1 XK_acircumflex = 0x0e2 XK_atilde = 0x0e3 XK_adiaeresis = 0x0e4 XK_aring = 0x0e5 XK_ae = 0x0e6 XK_ccedilla = 0x0e7 XK_egrave = 0x0e8 XK_eacute = 0x0e9 XK_ecircumflex = 0x0ea XK_ediaeresis = 0x0eb XK_igrave = 0x0ec XK_iacute = 0x0ed XK_icircumflex = 0x0ee XK_idiaeresis = 0x0ef XK_eth = 0x0f0 XK_ntilde = 0x0f1 XK_ograve = 0x0f2 XK_oacute = 0x0f3 XK_ocircumflex = 0x0f4 XK_otilde = 0x0f5 XK_odiaeresis = 0x0f6 XK_division = 0x0f7 XK_oslash = 0x0f8 XK_ugrave = 0x0f9 XK_uacute = 0x0fa XK_ucircumflex = 0x0fb XK_udiaeresis = 0x0fc XK_yacute = 0x0fd XK_thorn = 0x0fe XK_ydiaeresis = 0x0ff Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/latin2.py000066400000000000000000000020631505160700500250600ustar00rootroot00000000000000XK_Aogonek = 0x1a1 XK_breve = 0x1a2 XK_Lstroke = 0x1a3 XK_Lcaron = 0x1a5 XK_Sacute = 0x1a6 XK_Scaron = 0x1a9 XK_Scedilla = 0x1aa XK_Tcaron = 0x1ab XK_Zacute = 0x1ac XK_Zcaron = 0x1ae XK_Zabovedot = 0x1af XK_aogonek = 0x1b1 XK_ogonek = 0x1b2 XK_lstroke = 0x1b3 XK_lcaron = 0x1b5 XK_sacute = 0x1b6 XK_caron = 0x1b7 XK_scaron = 0x1b9 XK_scedilla = 0x1ba XK_tcaron = 0x1bb XK_zacute = 0x1bc XK_doubleacute = 0x1bd XK_zcaron = 0x1be XK_zabovedot = 0x1bf XK_Racute = 0x1c0 XK_Abreve = 0x1c3 XK_Lacute = 0x1c5 XK_Cacute = 0x1c6 XK_Ccaron = 0x1c8 XK_Eogonek = 0x1ca XK_Ecaron = 0x1cc XK_Dcaron = 0x1cf XK_Dstroke = 0x1d0 XK_Nacute = 0x1d1 XK_Ncaron = 0x1d2 XK_Odoubleacute = 0x1d5 XK_Rcaron = 0x1d8 XK_Uring = 0x1d9 XK_Udoubleacute = 0x1db XK_Tcedilla = 0x1de XK_racute = 0x1e0 XK_abreve = 0x1e3 XK_lacute = 0x1e5 XK_cacute = 0x1e6 XK_ccaron = 0x1e8 XK_eogonek = 0x1ea XK_ecaron = 0x1ec XK_dcaron = 0x1ef XK_dstroke = 0x1f0 XK_nacute = 0x1f1 XK_ncaron = 0x1f2 XK_odoubleacute = 0x1f5 XK_udoubleacute = 0x1fb XK_rcaron = 0x1f8 XK_uring = 0x1f9 XK_tcedilla = 0x1fe XK_abovedot = 0x1ff Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/latin3.py000066400000000000000000000007211505160700500250600ustar00rootroot00000000000000XK_Hstroke = 0x2a1 XK_Hcircumflex = 0x2a6 XK_Iabovedot = 0x2a9 XK_Gbreve = 0x2ab XK_Jcircumflex = 0x2ac XK_hstroke = 0x2b1 XK_hcircumflex = 0x2b6 XK_idotless = 0x2b9 XK_gbreve = 0x2bb XK_jcircumflex = 0x2bc XK_Cabovedot = 0x2c5 XK_Ccircumflex = 0x2c6 XK_Gabovedot = 0x2d5 XK_Gcircumflex = 0x2d8 XK_Ubreve = 0x2dd XK_Scircumflex = 0x2de XK_cabovedot = 0x2e5 XK_ccircumflex = 0x2e6 XK_gabovedot = 0x2f5 XK_gcircumflex = 0x2f8 XK_ubreve = 0x2fd XK_scircumflex = 0x2fe Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/latin4.py000066400000000000000000000012461505160700500250640ustar00rootroot00000000000000XK_kra = 0x3a2 XK_kappa = 0x3a2 XK_Rcedilla = 0x3a3 XK_Itilde = 0x3a5 XK_Lcedilla = 0x3a6 XK_Emacron = 0x3aa XK_Gcedilla = 0x3ab XK_Tslash = 0x3ac XK_rcedilla = 0x3b3 XK_itilde = 0x3b5 XK_lcedilla = 0x3b6 XK_emacron = 0x3ba XK_gcedilla = 0x3bb XK_tslash = 0x3bc XK_ENG = 0x3bd XK_eng = 0x3bf XK_Amacron = 0x3c0 XK_Iogonek = 0x3c7 XK_Eabovedot = 0x3cc XK_Imacron = 0x3cf XK_Ncedilla = 0x3d1 XK_Omacron = 0x3d2 XK_Kcedilla = 0x3d3 XK_Uogonek = 0x3d9 XK_Utilde = 0x3dd XK_Umacron = 0x3de XK_amacron = 0x3e0 XK_iogonek = 0x3e7 XK_eabovedot = 0x3ec XK_imacron = 0x3ef XK_ncedilla = 0x3f1 XK_omacron = 0x3f2 XK_kcedilla = 0x3f3 XK_uogonek = 0x3f9 XK_utilde = 0x3fd XK_umacron = 0x3fe Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/miscellany.py000066400000000000000000000060751505160700500260360ustar00rootroot00000000000000XK_BackSpace = 0xFF08 XK_Tab = 0xFF09 XK_Linefeed = 0xFF0A XK_Clear = 0xFF0B XK_Return = 0xFF0D XK_Pause = 0xFF13 XK_Scroll_Lock = 0xFF14 XK_Sys_Req = 0xFF15 XK_Escape = 0xFF1B XK_Delete = 0xFFFF XK_Multi_key = 0xFF20 XK_SingleCandidate = 0xFF3C XK_MultipleCandidate = 0xFF3D XK_PreviousCandidate = 0xFF3E XK_Kanji = 0xFF21 XK_Muhenkan = 0xFF22 XK_Henkan_Mode = 0xFF23 XK_Henkan = 0xFF23 XK_Romaji = 0xFF24 XK_Hiragana = 0xFF25 XK_Katakana = 0xFF26 XK_Hiragana_Katakana = 0xFF27 XK_Zenkaku = 0xFF28 XK_Hankaku = 0xFF29 XK_Zenkaku_Hankaku = 0xFF2A XK_Touroku = 0xFF2B XK_Massyo = 0xFF2C XK_Kana_Lock = 0xFF2D XK_Kana_Shift = 0xFF2E XK_Eisu_Shift = 0xFF2F XK_Eisu_toggle = 0xFF30 XK_Zen_Koho = 0xFF3D XK_Mae_Koho = 0xFF3E XK_Home = 0xFF50 XK_Left = 0xFF51 XK_Up = 0xFF52 XK_Right = 0xFF53 XK_Down = 0xFF54 XK_Prior = 0xFF55 XK_Page_Up = 0xFF55 XK_Next = 0xFF56 XK_Page_Down = 0xFF56 XK_End = 0xFF57 XK_Begin = 0xFF58 XK_Select = 0xFF60 XK_Print = 0xFF61 XK_Execute = 0xFF62 XK_Insert = 0xFF63 XK_Undo = 0xFF65 XK_Redo = 0xFF66 XK_Menu = 0xFF67 XK_Find = 0xFF68 XK_Cancel = 0xFF69 XK_Help = 0xFF6A XK_Break = 0xFF6B XK_Mode_switch = 0xFF7E XK_script_switch = 0xFF7E XK_Num_Lock = 0xFF7F XK_KP_Space = 0xFF80 XK_KP_Tab = 0xFF89 XK_KP_Enter = 0xFF8D XK_KP_F1 = 0xFF91 XK_KP_F2 = 0xFF92 XK_KP_F3 = 0xFF93 XK_KP_F4 = 0xFF94 XK_KP_Home = 0xFF95 XK_KP_Left = 0xFF96 XK_KP_Up = 0xFF97 XK_KP_Right = 0xFF98 XK_KP_Down = 0xFF99 XK_KP_Prior = 0xFF9A XK_KP_Page_Up = 0xFF9A XK_KP_Next = 0xFF9B XK_KP_Page_Down = 0xFF9B XK_KP_End = 0xFF9C XK_KP_Begin = 0xFF9D XK_KP_Insert = 0xFF9E XK_KP_Delete = 0xFF9F XK_KP_Equal = 0xFFBD XK_KP_Multiply = 0xFFAA XK_KP_Add = 0xFFAB XK_KP_Separator = 0xFFAC XK_KP_Subtract = 0xFFAD XK_KP_Decimal = 0xFFAE XK_KP_Divide = 0xFFAF XK_KP_0 = 0xFFB0 XK_KP_1 = 0xFFB1 XK_KP_2 = 0xFFB2 XK_KP_3 = 0xFFB3 XK_KP_4 = 0xFFB4 XK_KP_5 = 0xFFB5 XK_KP_6 = 0xFFB6 XK_KP_7 = 0xFFB7 XK_KP_8 = 0xFFB8 XK_KP_9 = 0xFFB9 XK_F1 = 0xFFBE XK_F2 = 0xFFBF XK_F3 = 0xFFC0 XK_F4 = 0xFFC1 XK_F5 = 0xFFC2 XK_F6 = 0xFFC3 XK_F7 = 0xFFC4 XK_F8 = 0xFFC5 XK_F9 = 0xFFC6 XK_F10 = 0xFFC7 XK_F11 = 0xFFC8 XK_L1 = 0xFFC8 XK_F12 = 0xFFC9 XK_L2 = 0xFFC9 XK_F13 = 0xFFCA XK_L3 = 0xFFCA XK_F14 = 0xFFCB XK_L4 = 0xFFCB XK_F15 = 0xFFCC XK_L5 = 0xFFCC XK_F16 = 0xFFCD XK_L6 = 0xFFCD XK_F17 = 0xFFCE XK_L7 = 0xFFCE XK_F18 = 0xFFCF XK_L8 = 0xFFCF XK_F19 = 0xFFD0 XK_L9 = 0xFFD0 XK_F20 = 0xFFD1 XK_L10 = 0xFFD1 XK_F21 = 0xFFD2 XK_R1 = 0xFFD2 XK_F22 = 0xFFD3 XK_R2 = 0xFFD3 XK_F23 = 0xFFD4 XK_R3 = 0xFFD4 XK_F24 = 0xFFD5 XK_R4 = 0xFFD5 XK_F25 = 0xFFD6 XK_R5 = 0xFFD6 XK_F26 = 0xFFD7 XK_R6 = 0xFFD7 XK_F27 = 0xFFD8 XK_R7 = 0xFFD8 XK_F28 = 0xFFD9 XK_R8 = 0xFFD9 XK_F29 = 0xFFDA XK_R9 = 0xFFDA XK_F30 = 0xFFDB XK_R10 = 0xFFDB XK_F31 = 0xFFDC XK_R11 = 0xFFDC XK_F32 = 0xFFDD XK_R12 = 0xFFDD XK_F33 = 0xFFDE XK_R13 = 0xFFDE XK_F34 = 0xFFDF XK_R14 = 0xFFDF XK_F35 = 0xFFE0 XK_R15 = 0xFFE0 XK_Shift_L = 0xFFE1 XK_Shift_R = 0xFFE2 XK_Control_L = 0xFFE3 XK_Control_R = 0xFFE4 XK_Caps_Lock = 0xFFE5 XK_Shift_Lock = 0xFFE6 XK_Meta_L = 0xFFE7 XK_Meta_R = 0xFFE8 XK_Alt_L = 0xFFE9 XK_Alt_R = 0xFFEA XK_Super_L = 0xFFEB XK_Super_R = 0xFFEC XK_Hyper_L = 0xFFED XK_Hyper_R = 0xFFEE Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/publishing.py000066400000000000000000000036711505160700500260410ustar00rootroot00000000000000XK_emspace = 0xaa1 XK_enspace = 0xaa2 XK_em3space = 0xaa3 XK_em4space = 0xaa4 XK_digitspace = 0xaa5 XK_punctspace = 0xaa6 XK_thinspace = 0xaa7 XK_hairspace = 0xaa8 XK_emdash = 0xaa9 XK_endash = 0xaaa XK_signifblank = 0xaac XK_ellipsis = 0xaae XK_doubbaselinedot = 0xaaf XK_onethird = 0xab0 XK_twothirds = 0xab1 XK_onefifth = 0xab2 XK_twofifths = 0xab3 XK_threefifths = 0xab4 XK_fourfifths = 0xab5 XK_onesixth = 0xab6 XK_fivesixths = 0xab7 XK_careof = 0xab8 XK_figdash = 0xabb XK_leftanglebracket = 0xabc XK_decimalpoint = 0xabd XK_rightanglebracket = 0xabe XK_marker = 0xabf XK_oneeighth = 0xac3 XK_threeeighths = 0xac4 XK_fiveeighths = 0xac5 XK_seveneighths = 0xac6 XK_trademark = 0xac9 XK_signaturemark = 0xaca XK_trademarkincircle = 0xacb XK_leftopentriangle = 0xacc XK_rightopentriangle = 0xacd XK_emopencircle = 0xace XK_emopenrectangle = 0xacf XK_leftsinglequotemark = 0xad0 XK_rightsinglequotemark = 0xad1 XK_leftdoublequotemark = 0xad2 XK_rightdoublequotemark = 0xad3 XK_prescription = 0xad4 XK_minutes = 0xad6 XK_seconds = 0xad7 XK_latincross = 0xad9 XK_hexagram = 0xada XK_filledrectbullet = 0xadb XK_filledlefttribullet = 0xadc XK_filledrighttribullet = 0xadd XK_emfilledcircle = 0xade XK_emfilledrect = 0xadf XK_enopencircbullet = 0xae0 XK_enopensquarebullet = 0xae1 XK_openrectbullet = 0xae2 XK_opentribulletup = 0xae3 XK_opentribulletdown = 0xae4 XK_openstar = 0xae5 XK_enfilledcircbullet = 0xae6 XK_enfilledsqbullet = 0xae7 XK_filledtribulletup = 0xae8 XK_filledtribulletdown = 0xae9 XK_leftpointer = 0xaea XK_rightpointer = 0xaeb XK_club = 0xaec XK_diamond = 0xaed XK_heart = 0xaee XK_maltesecross = 0xaf0 XK_dagger = 0xaf1 XK_doubledagger = 0xaf2 XK_checkmark = 0xaf3 XK_ballotcross = 0xaf4 XK_musicalsharp = 0xaf5 XK_musicalflat = 0xaf6 XK_malesymbol = 0xaf7 XK_femalesymbol = 0xaf8 XK_telephone = 0xaf9 XK_telephonerecorder = 0xafa XK_phonographcopyright = 0xafb XK_caret = 0xafc XK_singlelowquotemark = 0xafd XK_doublelowquotemark = 0xafe XK_cursor = 0xaff Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/special.py000066400000000000000000000007521505160700500253120ustar00rootroot00000000000000XK_blank = 0x9df XK_soliddiamond = 0x9e0 XK_checkerboard = 0x9e1 XK_ht = 0x9e2 XK_ff = 0x9e3 XK_cr = 0x9e4 XK_lf = 0x9e5 XK_nl = 0x9e8 XK_vt = 0x9e9 XK_lowrightcorner = 0x9ea XK_uprightcorner = 0x9eb XK_upleftcorner = 0x9ec XK_lowleftcorner = 0x9ed XK_crossinglines = 0x9ee XK_horizlinescan1 = 0x9ef XK_horizlinescan3 = 0x9f0 XK_horizlinescan5 = 0x9f1 XK_horizlinescan7 = 0x9f2 XK_horizlinescan9 = 0x9f3 XK_leftt = 0x9f4 XK_rightt = 0x9f5 XK_bott = 0x9f6 XK_topt = 0x9f7 XK_vertbar = 0x9f8 Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/technical.py000066400000000000000000000022631505160700500256230ustar00rootroot00000000000000XK_leftradical = 0x8a1 XK_topleftradical = 0x8a2 XK_horizconnector = 0x8a3 XK_topintegral = 0x8a4 XK_botintegral = 0x8a5 XK_vertconnector = 0x8a6 XK_topleftsqbracket = 0x8a7 XK_botleftsqbracket = 0x8a8 XK_toprightsqbracket = 0x8a9 XK_botrightsqbracket = 0x8aa XK_topleftparens = 0x8ab XK_botleftparens = 0x8ac XK_toprightparens = 0x8ad XK_botrightparens = 0x8ae XK_leftmiddlecurlybrace = 0x8af XK_rightmiddlecurlybrace = 0x8b0 XK_topleftsummation = 0x8b1 XK_botleftsummation = 0x8b2 XK_topvertsummationconnector = 0x8b3 XK_botvertsummationconnector = 0x8b4 XK_toprightsummation = 0x8b5 XK_botrightsummation = 0x8b6 XK_rightmiddlesummation = 0x8b7 XK_lessthanequal = 0x8bc XK_notequal = 0x8bd XK_greaterthanequal = 0x8be XK_integral = 0x8bf XK_therefore = 0x8c0 XK_variation = 0x8c1 XK_infinity = 0x8c2 XK_nabla = 0x8c5 XK_approximate = 0x8c8 XK_similarequal = 0x8c9 XK_ifonlyif = 0x8cd XK_implies = 0x8ce XK_identical = 0x8cf XK_radical = 0x8d6 XK_includedin = 0x8da XK_includes = 0x8db XK_intersection = 0x8dc XK_union = 0x8dd XK_logicaland = 0x8de XK_logicalor = 0x8df XK_partialderivative = 0x8ef XK_function = 0x8f6 XK_leftarrow = 0x8fb XK_uparrow = 0x8fc XK_rightarrow = 0x8fd XK_downarrow = 0x8fe Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/thai.py000066400000000000000000000037241505160700500246210ustar00rootroot00000000000000XK_Thai_kokai = 0xda1 XK_Thai_khokhai = 0xda2 XK_Thai_khokhuat = 0xda3 XK_Thai_khokhwai = 0xda4 XK_Thai_khokhon = 0xda5 XK_Thai_khorakhang = 0xda6 XK_Thai_ngongu = 0xda7 XK_Thai_chochan = 0xda8 XK_Thai_choching = 0xda9 XK_Thai_chochang = 0xdaa XK_Thai_soso = 0xdab XK_Thai_chochoe = 0xdac XK_Thai_yoying = 0xdad XK_Thai_dochada = 0xdae XK_Thai_topatak = 0xdaf XK_Thai_thothan = 0xdb0 XK_Thai_thonangmontho = 0xdb1 XK_Thai_thophuthao = 0xdb2 XK_Thai_nonen = 0xdb3 XK_Thai_dodek = 0xdb4 XK_Thai_totao = 0xdb5 XK_Thai_thothung = 0xdb6 XK_Thai_thothahan = 0xdb7 XK_Thai_thothong = 0xdb8 XK_Thai_nonu = 0xdb9 XK_Thai_bobaimai = 0xdba XK_Thai_popla = 0xdbb XK_Thai_phophung = 0xdbc XK_Thai_fofa = 0xdbd XK_Thai_phophan = 0xdbe XK_Thai_fofan = 0xdbf XK_Thai_phosamphao = 0xdc0 XK_Thai_moma = 0xdc1 XK_Thai_yoyak = 0xdc2 XK_Thai_rorua = 0xdc3 XK_Thai_ru = 0xdc4 XK_Thai_loling = 0xdc5 XK_Thai_lu = 0xdc6 XK_Thai_wowaen = 0xdc7 XK_Thai_sosala = 0xdc8 XK_Thai_sorusi = 0xdc9 XK_Thai_sosua = 0xdca XK_Thai_hohip = 0xdcb XK_Thai_lochula = 0xdcc XK_Thai_oang = 0xdcd XK_Thai_honokhuk = 0xdce XK_Thai_paiyannoi = 0xdcf XK_Thai_saraa = 0xdd0 XK_Thai_maihanakat = 0xdd1 XK_Thai_saraaa = 0xdd2 XK_Thai_saraam = 0xdd3 XK_Thai_sarai = 0xdd4 XK_Thai_saraii = 0xdd5 XK_Thai_saraue = 0xdd6 XK_Thai_sarauee = 0xdd7 XK_Thai_sarau = 0xdd8 XK_Thai_sarauu = 0xdd9 XK_Thai_phinthu = 0xdda XK_Thai_maihanakat_maitho = 0xdde XK_Thai_baht = 0xddf XK_Thai_sarae = 0xde0 XK_Thai_saraae = 0xde1 XK_Thai_sarao = 0xde2 XK_Thai_saraaimaimuan = 0xde3 XK_Thai_saraaimaimalai = 0xde4 XK_Thai_lakkhangyao = 0xde5 XK_Thai_maiyamok = 0xde6 XK_Thai_maitaikhu = 0xde7 XK_Thai_maiek = 0xde8 XK_Thai_maitho = 0xde9 XK_Thai_maitri = 0xdea XK_Thai_maichattawa = 0xdeb XK_Thai_thanthakhat = 0xdec XK_Thai_nikhahit = 0xded XK_Thai_leksun = 0xdf0 XK_Thai_leknung = 0xdf1 XK_Thai_leksong = 0xdf2 XK_Thai_leksam = 0xdf3 XK_Thai_leksi = 0xdf4 XK_Thai_lekha = 0xdf5 XK_Thai_lekhok = 0xdf6 XK_Thai_lekchet = 0xdf7 XK_Thai_lekpaet = 0xdf8 XK_Thai_lekkao = 0xdf9 Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/xf86.py000066400000000000000000000135071505160700500244670ustar00rootroot00000000000000XK_XF86_MonBrightnessUp = 0x1008FF02 XK_XF86_MonBrightnessDown = 0x1008FF03 XK_XF86_KbdLightOnOff = 0x1008FF04 XK_XF86_KbdBrightnessUp = 0x1008FF05 XK_XF86_KbdBrightnessDown = 0x1008FF06 XK_XF86_Standby = 0x1008FF10 XK_XF86_AudioLowerVolume = 0x1008FF11 XK_XF86_AudioMute = 0x1008FF12 XK_XF86_AudioRaiseVolume = 0x1008FF13 XK_XF86_AudioPlay = 0x1008FF14 XK_XF86_AudioStop = 0x1008FF15 XK_XF86_AudioPrev = 0x1008FF16 XK_XF86_AudioNext = 0x1008FF17 XK_XF86_HomePage = 0x1008FF18 XK_XF86_Mail = 0x1008FF19 XK_XF86_Start = 0x1008FF1A XK_XF86_Search = 0x1008FF1B XK_XF86_AudioRecord = 0x1008FF1C XK_XF86_Calculator = 0x1008FF1D XK_XF86_Memo = 0x1008FF1E XK_XF86_ToDoList = 0x1008FF1F XK_XF86_Calendar = 0x1008FF20 XK_XF86_PowerDown = 0x1008FF21 XK_XF86_ContrastAdjust = 0x1008FF22 XK_XF86_RockerUp = 0x1008FF23 XK_XF86_RockerDown = 0x1008FF24 XK_XF86_RockerEnter = 0x1008FF25 XK_XF86_Back = 0x1008FF26 XK_XF86_Forward = 0x1008FF27 XK_XF86_Stop = 0x1008FF28 XK_XF86_Refresh = 0x1008FF29 XK_XF86_PowerOff = 0x1008FF2A XK_XF86_WakeUp = 0x1008FF2B XK_XF86_Eject = 0x1008FF2C XK_XF86_ScreenSaver = 0x1008FF2D XK_XF86_WWW = 0x1008FF2E XK_XF86_Sleep = 0x1008FF2F XK_XF86_Favorites = 0x1008FF30 XK_XF86_AudioPause = 0x1008FF31 XK_XF86_AudioMedia = 0x1008FF32 XK_XF86_MyComputer = 0x1008FF33 XK_XF86_VendorHome = 0x1008FF34 XK_XF86_LightBulb = 0x1008FF35 XK_XF86_Shop = 0x1008FF36 XK_XF86_History = 0x1008FF37 XK_XF86_OpenURL = 0x1008FF38 XK_XF86_AddFavorite = 0x1008FF39 XK_XF86_HotLinks = 0x1008FF3A XK_XF86_BrightnessAdjust = 0x1008FF3B XK_XF86_Finance = 0x1008FF3C XK_XF86_Community = 0x1008FF3D XK_XF86_AudioRewind = 0x1008FF3E XK_XF86_XF86BackForward = 0x1008FF3F XK_XF86_Launch0 = 0x1008FF40 XK_XF86_Launch1 = 0x1008FF41 XK_XF86_Launch2 = 0x1008FF42 XK_XF86_Launch3 = 0x1008FF43 XK_XF86_Launch4 = 0x1008FF44 XK_XF86_Launch5 = 0x1008FF45 XK_XF86_Launch6 = 0x1008FF46 XK_XF86_Launch7 = 0x1008FF47 XK_XF86_Launch8 = 0x1008FF48 XK_XF86_Launch9 = 0x1008FF49 XK_XF86_LaunchA = 0x1008FF4A XK_XF86_LaunchB = 0x1008FF4B XK_XF86_LaunchC = 0x1008FF4C XK_XF86_LaunchD = 0x1008FF4D XK_XF86_LaunchE = 0x1008FF4E XK_XF86_LaunchF = 0x1008FF4F XK_XF86_ApplicationLeft = 0x1008FF50 XK_XF86_ApplicationRight = 0x1008FF51 XK_XF86_Book = 0x1008FF52 XK_XF86_CD = 0x1008FF53 XK_XF86_Calculater = 0x1008FF54 XK_XF86_Clear = 0x1008FF55 XK_XF86_Close = 0x1008FF56 XK_XF86_Copy = 0x1008FF57 XK_XF86_Cut = 0x1008FF58 XK_XF86_Display = 0x1008FF59 XK_XF86_DOS = 0x1008FF5A XK_XF86_Documents = 0x1008FF5B XK_XF86_Excel = 0x1008FF5C XK_XF86_Explorer = 0x1008FF5D XK_XF86_Game = 0x1008FF5E XK_XF86_Go = 0x1008FF5F XK_XF86_iTouch = 0x1008FF60 XK_XF86_LogOff = 0x1008FF61 XK_XF86_Market = 0x1008FF62 XK_XF86_Meeting = 0x1008FF63 XK_XF86_MenuKB = 0x1008FF65 XK_XF86_MenuPB = 0x1008FF66 XK_XF86_MySites = 0x1008FF67 XK_XF86_New = 0x1008FF68 XK_XF86_News = 0x1008FF69 XK_XF86_OfficeHome = 0x1008FF6A XK_XF86_Open = 0x1008FF6B XK_XF86_Option = 0x1008FF6C XK_XF86_Paste = 0x1008FF6D XK_XF86_Phone = 0x1008FF6E XK_XF86_Q = 0x1008FF70 XK_XF86_Reply = 0x1008FF72 XK_XF86_Reload = 0x1008FF73 XK_XF86_RotateWindows = 0x1008FF74 XK_XF86_RotationPB = 0x1008FF75 XK_XF86_RotationKB = 0x1008FF76 XK_XF86_Save = 0x1008FF77 XK_XF86_ScrollUp = 0x1008FF78 XK_XF86_ScrollDown = 0x1008FF79 XK_XF86_ScrollClick = 0x1008FF7A XK_XF86_Send = 0x1008FF7B XK_XF86_Spell = 0x1008FF7C XK_XF86_SplitScreen = 0x1008FF7D XK_XF86_Support = 0x1008FF7E XK_XF86_TaskPane = 0x1008FF7F XK_XF86_Terminal = 0x1008FF80 XK_XF86_Tools = 0x1008FF81 XK_XF86_Travel = 0x1008FF82 XK_XF86_UserPB = 0x1008FF84 XK_XF86_User1KB = 0x1008FF85 XK_XF86_User2KB = 0x1008FF86 XK_XF86_Video = 0x1008FF87 XK_XF86_WheelButton = 0x1008FF88 XK_XF86_Word = 0x1008FF89 XK_XF86_Xfer = 0x1008FF8A XK_XF86_ZoomIn = 0x1008FF8B XK_XF86_ZoomOut = 0x1008FF8C XK_XF86_Away = 0x1008FF8D XK_XF86_Messenger = 0x1008FF8E XK_XF86_WebCam = 0x1008FF8F XK_XF86_MailForward = 0x1008FF90 XK_XF86_Pictures = 0x1008FF91 XK_XF86_Music = 0x1008FF92 XK_XF86_Battery = 0x1008FF93 XK_XF86_Bluetooth = 0x1008FF94 XK_XF86_WLAN = 0x1008FF95 XK_XF86_UWB = 0x1008FF96 XK_XF86_AudioForward = 0x1008FF97 XK_XF86_AudioRepeat = 0x1008FF98 XK_XF86_AudioRandomPlay = 0x1008FF99 XK_XF86_Subtitle = 0x1008FF9A XK_XF86_AudioCycleTrack = 0x1008FF9B XK_XF86_CycleAngle = 0x1008FF9C XK_XF86_FrameBack = 0x1008FF9D XK_XF86_FrameForward = 0x1008FF9E XK_XF86_Time = 0x1008FF9F XK_XF86_Select = 0x1008FFA0 XK_XF86_View = 0x1008FFA1 XK_XF86_TopMenu = 0x1008FFA2 XK_XF86_Red = 0x1008FFA3 XK_XF86_Green = 0x1008FFA4 XK_XF86_Yellow = 0x1008FFA5 XK_XF86_Blue = 0x1008FFA6 XK_XF86_Switch_VT_1 = 0x1008FE01 XK_XF86_Switch_VT_2 = 0x1008FE02 XK_XF86_Switch_VT_3 = 0x1008FE03 XK_XF86_Switch_VT_4 = 0x1008FE04 XK_XF86_Switch_VT_5 = 0x1008FE05 XK_XF86_Switch_VT_6 = 0x1008FE06 XK_XF86_Switch_VT_7 = 0x1008FE07 XK_XF86_Switch_VT_8 = 0x1008FE08 XK_XF86_Switch_VT_9 = 0x1008FE09 XK_XF86_Switch_VT_10 = 0x1008FE0A XK_XF86_Switch_VT_11 = 0x1008FE0B XK_XF86_Switch_VT_12 = 0x1008FE0C XK_XF86_Ungrab = 0x1008FE20 XK_XF86_ClearGrab = 0x1008FE21 XK_XF86_Next_VMode = 0x1008FE22 XK_XF86_Prev_VMode = 0x1008FE23 Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/xk3270.py000066400000000000000000000013421505160700500246240ustar00rootroot00000000000000XK_3270_Duplicate = 0xFD01 XK_3270_FieldMark = 0xFD02 XK_3270_Right2 = 0xFD03 XK_3270_Left2 = 0xFD04 XK_3270_BackTab = 0xFD05 XK_3270_EraseEOF = 0xFD06 XK_3270_EraseInput = 0xFD07 XK_3270_Reset = 0xFD08 XK_3270_Quit = 0xFD09 XK_3270_PA1 = 0xFD0A XK_3270_PA2 = 0xFD0B XK_3270_PA3 = 0xFD0C XK_3270_Test = 0xFD0D XK_3270_Attn = 0xFD0E XK_3270_CursorBlink = 0xFD0F XK_3270_AltCursor = 0xFD10 XK_3270_KeyClick = 0xFD11 XK_3270_Jump = 0xFD12 XK_3270_Ident = 0xFD13 XK_3270_Rule = 0xFD14 XK_3270_Copy = 0xFD15 XK_3270_Play = 0xFD16 XK_3270_Setup = 0xFD17 XK_3270_Record = 0xFD18 XK_3270_ChangeScreen = 0xFD19 XK_3270_DeleteWord = 0xFD1A XK_3270_ExSelect = 0xFD1B XK_3270_CursorSelect = 0xFD1C XK_3270_PrintScreen = 0xFD1D XK_3270_Enter = 0xFD1E Nagstamon-master/Nagstamon/thirdparty/Xlib/keysymdef/xkb.py000066400000000000000000000055521505160700500244610ustar00rootroot00000000000000XK_ISO_Lock = 0xFE01 XK_ISO_Level2_Latch = 0xFE02 XK_ISO_Level3_Shift = 0xFE03 XK_ISO_Level3_Latch = 0xFE04 XK_ISO_Level3_Lock = 0xFE05 XK_ISO_Group_Shift = 0xFF7E XK_ISO_Group_Latch = 0xFE06 XK_ISO_Group_Lock = 0xFE07 XK_ISO_Next_Group = 0xFE08 XK_ISO_Next_Group_Lock = 0xFE09 XK_ISO_Prev_Group = 0xFE0A XK_ISO_Prev_Group_Lock = 0xFE0B XK_ISO_First_Group = 0xFE0C XK_ISO_First_Group_Lock = 0xFE0D XK_ISO_Last_Group = 0xFE0E XK_ISO_Last_Group_Lock = 0xFE0F XK_ISO_Left_Tab = 0xFE20 XK_ISO_Move_Line_Up = 0xFE21 XK_ISO_Move_Line_Down = 0xFE22 XK_ISO_Partial_Line_Up = 0xFE23 XK_ISO_Partial_Line_Down = 0xFE24 XK_ISO_Partial_Space_Left = 0xFE25 XK_ISO_Partial_Space_Right = 0xFE26 XK_ISO_Set_Margin_Left = 0xFE27 XK_ISO_Set_Margin_Right = 0xFE28 XK_ISO_Release_Margin_Left = 0xFE29 XK_ISO_Release_Margin_Right = 0xFE2A XK_ISO_Release_Both_Margins = 0xFE2B XK_ISO_Fast_Cursor_Left = 0xFE2C XK_ISO_Fast_Cursor_Right = 0xFE2D XK_ISO_Fast_Cursor_Up = 0xFE2E XK_ISO_Fast_Cursor_Down = 0xFE2F XK_ISO_Continuous_Underline = 0xFE30 XK_ISO_Discontinuous_Underline = 0xFE31 XK_ISO_Emphasize = 0xFE32 XK_ISO_Center_Object = 0xFE33 XK_ISO_Enter = 0xFE34 XK_dead_grave = 0xFE50 XK_dead_acute = 0xFE51 XK_dead_circumflex = 0xFE52 XK_dead_tilde = 0xFE53 XK_dead_macron = 0xFE54 XK_dead_breve = 0xFE55 XK_dead_abovedot = 0xFE56 XK_dead_diaeresis = 0xFE57 XK_dead_abovering = 0xFE58 XK_dead_doubleacute = 0xFE59 XK_dead_caron = 0xFE5A XK_dead_cedilla = 0xFE5B XK_dead_ogonek = 0xFE5C XK_dead_iota = 0xFE5D XK_dead_voiced_sound = 0xFE5E XK_dead_semivoiced_sound = 0xFE5F XK_dead_belowdot = 0xFE60 XK_First_Virtual_Screen = 0xFED0 XK_Prev_Virtual_Screen = 0xFED1 XK_Next_Virtual_Screen = 0xFED2 XK_Last_Virtual_Screen = 0xFED4 XK_Terminate_Server = 0xFED5 XK_AccessX_Enable = 0xFE70 XK_AccessX_Feedback_Enable = 0xFE71 XK_RepeatKeys_Enable = 0xFE72 XK_SlowKeys_Enable = 0xFE73 XK_BounceKeys_Enable = 0xFE74 XK_StickyKeys_Enable = 0xFE75 XK_MouseKeys_Enable = 0xFE76 XK_MouseKeys_Accel_Enable = 0xFE77 XK_Overlay1_Enable = 0xFE78 XK_Overlay2_Enable = 0xFE79 XK_AudibleBell_Enable = 0xFE7A XK_Pointer_Left = 0xFEE0 XK_Pointer_Right = 0xFEE1 XK_Pointer_Up = 0xFEE2 XK_Pointer_Down = 0xFEE3 XK_Pointer_UpLeft = 0xFEE4 XK_Pointer_UpRight = 0xFEE5 XK_Pointer_DownLeft = 0xFEE6 XK_Pointer_DownRight = 0xFEE7 XK_Pointer_Button_Dflt = 0xFEE8 XK_Pointer_Button1 = 0xFEE9 XK_Pointer_Button2 = 0xFEEA XK_Pointer_Button3 = 0xFEEB XK_Pointer_Button4 = 0xFEEC XK_Pointer_Button5 = 0xFEED XK_Pointer_DblClick_Dflt = 0xFEEE XK_Pointer_DblClick1 = 0xFEEF XK_Pointer_DblClick2 = 0xFEF0 XK_Pointer_DblClick3 = 0xFEF1 XK_Pointer_DblClick4 = 0xFEF2 XK_Pointer_DblClick5 = 0xFEF3 XK_Pointer_Drag_Dflt = 0xFEF4 XK_Pointer_Drag1 = 0xFEF5 XK_Pointer_Drag2 = 0xFEF6 XK_Pointer_Drag3 = 0xFEF7 XK_Pointer_Drag4 = 0xFEF8 XK_Pointer_Drag5 = 0xFEFD XK_Pointer_EnableKeys = 0xFEF9 XK_Pointer_Accelerate = 0xFEFA XK_Pointer_DfltBtnNext = 0xFEFB XK_Pointer_DfltBtnPrev = 0xFEFC Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/000077500000000000000000000000001505160700500231555ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/ChangeLog000066400000000000000000000103771505160700500247370ustar00rootroot000000000000002006-07-22 Mike Grant * Xlib/protocol/display.py: (mggrant) Fix for 1219457 - flushing was blocking and waiting for a read operation. Added missing "import socket" per bug report #681511. Fix for bug:1098695 & 1098738. The "recv" variable was being used for more than one thing - renamed one. Changelog hasn't been maintained since 2002, but some of the more significant comments from cvs logs follow: * Xlib/protocol/request.py: (petli) Fix bugs in definition and method of GrabButton/Pointer 2002-02-22 Peter Liljenberg * event.py(CirculateNotify, CirculateRequest): These are identical, so subclass the common Circulate. 2002-02-13 Peter Liljenberg * rq.py (ValueList.parse_binary_value): Use = both for calcsize and unpacking. Caused problems on Alpha. 2002-02-11 Peter Liljenberg * request.py (GetWindowAttributes): Rename class to win_class. (AllocColorPlanes): Fix Pad(4) to Pad(8) in reply. * rq.py (ReplyLength): Add a reply length field, for completeness and easier unit test generation. 2002-02-10 Peter Liljenberg * rq.py (DictWrapper.__cmp__): Let DictWrapper compare with plain dictionaries. (Event.__init__): Set send_event to 0 when creating new events objects, and allow events to be compared. (Struct.parse_binary): Allow LengthFields to have a parse_value method. (OddLength.parse_value): Decode field. (String16.parse_binary_value): Handle OddLength fields. (TextElements8.parse_binary_value): Bugfix: return values instead of v. (String8.parse_binary_value): Parse String8 with no LengthOf field. 2002-02-09 Peter Liljenberg * rq.py (TextElements16): Bugfix: inherit TextElements8 instead of TextElements16. Found while preparing unit tests, whee. 2002-01-14 Peter Liljenberg * display.py (Display.parse_event_response): Fix bug reported by Ilpo Nyyssönen, whereby ReplyRequests which generates events (e.g. get_property with delete = 1) will get dropped when the event is received. 2001-12-14 Peter Liljenberg * display.py (Display.parse_event_response): * rq.py (Event.__init__): Fixed bug in event type decoding: bit 0-6 is the event type, and bit 7 is set if the event was sent by SendEvent. 2001-01-16 * event.py: Changed some class names so that they correspond exactly to the event type constants. Tue Jan 9 10:03:25 2001 Peter Liljenberg * display.py (Display.send_request): Fixed a call to append() with multiple arguments, something that modern Pythons don't allow. 2001-01-04 * rq.py: The fix for 64-bit platforms didn't work, and close scrutiny of structmodule.c shows why: it turns out that '=' translates into '<' or '>', the one the platform would use. This means B is one byte, H is two and L is four, and no extra alignment, always. '@', which is the default, selects native number of bytes, which on Alpha means that 'L' is eight bytes. Now the code goes to pains to ensure that '=' encoding is always used, so _now_ it should work on all platforms. Ahem. 2000-12-29 * rq.py: Optimizations: + replace calls to Field.get_name() with access to attribute name. (Struct.build_from_args): Fri Dec 29 17:05:02 2000 Peter Liljenberg * rq.py: Alpha forces us to probe how many bytes each struct code in 'bhil' represents, instead of being able to assume that b is 1, h is 2 and l is 4. 2000-12-21 * request.py (SetClipRectangles): Fixed typo (attribute was "rectangels"). 2000-12-20 * rq.py (DictWrapper.__setitem__), (DictWrapper.__delitem__), (DictWrapper.__setattr__), (DictWrapper.__delattr__): Add a few methods to the DictWrapper, to make sure that even if attributes are changed, all attributes can be found in the _data mapping. (ValueField.__init__): (Object.__init__): (ValueField.pack_value): (Set.__init__): Added a default parameter, so that structure elements with a default value can be omitted when calling build_from_args. Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/__init__.py000066400000000000000000000016701505160700500252720ustar00rootroot00000000000000# Xlib.protocol.__init__ -- glue for Xlib.protocol package # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA __all__ = [ 'display', 'event', 'request', 'rq', 'structs', ] Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/display.py000066400000000000000000001011051505160700500251720ustar00rootroot00000000000000# -*- coding: latin-1 -*- # # Xlib.protocol.display -- core display communication # # Copyright (C) 2000-2002 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Standard modules import errno import select import socket import struct import sys # Xlib modules from Xlib import error from Xlib.support import lock, connect # Xlib.protocol modules from . import rq, event # in Python 3, bytes are an actual array; in python 2, bytes are still # string-like, so in order to get an array element we need to call ord() if sys.version[0] >= '3': def _bytes_item(x): return x else: def _bytes_item(x): return ord(x) class Display: resource_classes = {} extension_major_opcodes = {} error_classes = error.xerror_class.copy() event_classes = event.event_class.copy() def __init__(self, display = None): name, host, displayno, screenno = connect.get_display(display) self.display_name = name self.default_screen = screenno self.socket = connect.get_socket(name, host, displayno) auth_name, auth_data = connect.get_auth(self.socket, name, host, displayno) # Internal structures for communication, grouped # by their function and locks # Socket error indicator, set when the socket is closed # in one way or another self.socket_error_lock = lock.allocate_lock() self.socket_error = None # Event queue self.event_queue_read_lock = lock.allocate_lock() self.event_queue_write_lock = lock.allocate_lock() self.event_queue = [] # Unsent request queue and sequence number counter self.request_queue_lock = lock.allocate_lock() self.request_serial = 1 self.request_queue = [] # Send-and-recieve loop, see function send_and_recive # for a detailed explanation self.send_recv_lock = lock.allocate_lock() self.send_active = 0 self.recv_active = 0 self.event_waiting = 0 self.event_wait_lock = lock.allocate_lock() self.request_waiting = 0 self.request_wait_lock = lock.allocate_lock() # Data used by the send-and-recieve loop self.sent_requests = [] self.request_length = 0 self.data_send = b'' self.data_recv = b'' self.data_sent_bytes = 0 # Resource ID structures self.resource_id_lock = lock.allocate_lock() self.resource_ids = {} self.last_resource_id = 0 # Use an default error handler, one which just prints the error self.error_handler = None # Right, now we're all set up for the connection setup # request with the server. # Figure out which endianess the hardware uses self.big_endian = struct.unpack('BB', struct.pack('H', 0x0100))[0] if self.big_endian: order = 0x42 else: order = 0x6c # Send connection setup r = ConnectionSetupRequest(self, byte_order = order, protocol_major = 11, protocol_minor = 0, auth_prot_name = auth_name, auth_prot_data = auth_data) # Did connection fail? if r.status != 1: raise error.DisplayConnectionError(self.display_name, r.reason) # Set up remaining info self.info = r self.default_screen = min(self.default_screen, len(self.info.roots) - 1) # # Public interface # def get_display_name(self): return self.display_name def get_default_screen(self): return self.default_screen def fileno(self): self.check_for_error() return self.socket.fileno() def next_event(self): self.check_for_error() # Main lock, so that only one thread at a time performs the # event waiting code. This at least guarantees that the first # thread calling next_event() will get the next event, although # no order is guaranteed among other threads calling next_event() # while the first is blocking. self.event_queue_read_lock.acquire() # Lock event queue, so we can check if it is empty self.event_queue_write_lock.acquire() # We have too loop until we get an event, as # we might be woken up when there is no event. while not self.event_queue: # Lock send_recv so no send_and_recieve # can start or stop while we're checking # whether there are one active. self.send_recv_lock.acquire() # Release event queue to allow an send_and_recv to # insert any now. self.event_queue_write_lock.release() # Call send_and_recv, which will return when # something has occured self.send_and_recv(event = 1) # Before looping around, lock the event queue against # modifications. self.event_queue_write_lock.acquire() # Whiew, we have an event! Remove it from # the event queue and relaese its write lock. event = self.event_queue[0] del self.event_queue[0] self.event_queue_write_lock.release() # Finally, allow any other threads which have called next_event() # while we were waiting to proceed. self.event_queue_read_lock.release() # And return the event! return event def pending_events(self): self.check_for_error() # Make a send_and_recv pass, receiving any events self.send_recv_lock.acquire() self.send_and_recv(recv = 1) # Lock the queue, get the event count, and unlock again. self.event_queue_write_lock.acquire() count = len(self.event_queue) self.event_queue_write_lock.release() return count def flush(self): self.check_for_error() self.send_recv_lock.acquire() self.send_and_recv(flush = 1) def close(self): self.flush() self.close_internal('client') def set_error_handler(self, handler): self.error_handler = handler def allocate_resource_id(self): """id = d.allocate_resource_id() Allocate a new X resource id number ID. Raises ResourceIDError if there are no free resource ids. """ self.resource_id_lock.acquire() try: i = self.last_resource_id while i in self.resource_ids: i = i + 1 if i > self.info.resource_id_mask: i = 0 if i == self.last_resource_id: raise error.ResourceIDError('out of resource ids') self.resource_ids[i] = None self.last_resource_id = i return self.info.resource_id_base | i finally: self.resource_id_lock.release() def free_resource_id(self, rid): """d.free_resource_id(rid) Free resource id RID. Attempts to free a resource id which isn't allocated by us are ignored. """ self.resource_id_lock.acquire() try: i = rid & self.info.resource_id_mask # Attempting to free a resource id outside our range if rid - i != self.info.resource_id_base: return None try: del self.resource_ids[i] except KeyError: pass finally: self.resource_id_lock.release() def get_resource_class(self, class_name, default = None): """class = d.get_resource_class(class_name, default = None) Return the class to be used for X resource objects of type CLASS_NAME, or DEFAULT if no such class is set. """ return self.resource_classes.get(class_name, default) def set_extension_major(self, extname, major): self.extension_major_opcodes[extname] = major def get_extension_major(self, extname): return self.extension_major_opcodes[extname] def add_extension_event(self, code, evt): self.event_classes[code] = evt def add_extension_error(self, code, err): self.error_classes[code] = err # # Private functions # def check_for_error(self): self.socket_error_lock.acquire() err = self.socket_error self.socket_error_lock.release() if err: raise err def send_request(self, request, wait_for_response): if self.socket_error: raise self.socket_error self.request_queue_lock.acquire() request._serial = self.request_serial self.request_serial = (self.request_serial + 1) % 65536 self.request_queue.append((request, wait_for_response)) qlen = len(self.request_queue) self.request_queue_lock.release() # if qlen > 10: # self.flush() def close_internal(self, whom): # Clear out data structures self.request_queue = None self.sent_requests = None self.event_queue = None self.data_send = None self.data_recv = None # Close the connection self.socket.close() # Set a connection closed indicator self.socket_error_lock.acquire() self.socket_error = error.ConnectionClosedError(whom) self.socket_error_lock.release() def send_and_recv(self, flush = None, event = None, request = None, recv = None): """send_and_recv(flush = None, event = None, request = None, recv = None) Perform I/O, or wait for some other thread to do it for us. send_recv_lock MUST be LOCKED when send_and_recv is called. It will be UNLOCKED at return. Exactly or one of the parameters flush, event, request and recv must be set to control the return condition. To attempt to send all requests in the queue, flush should be true. Will return immediately if another thread is already doing send_and_recv. To wait for an event to be recieved, event should be true. To wait for a response to a certain request (either an error or a response), request should be set the that request's serial number. To just read any pending data from the server, recv should be true. It is not guaranteed that the return condition has been fulfilled when the function returns, so the caller has to loop until it is finished. """ # We go to sleep if there is already a thread doing what we # want to do: # If flushing, we want to send # If waiting for a response to a request, we want to send # (to ensure that the request was sent - we alway recv # when we get to the main loop, but sending is the important # thing here) # If waiting for an event, we want to recv # If just trying to receive anything we can, we want to recv if (((flush or request is not None) and self.send_active) or ((event or recv) and self.recv_active)): # Signal that we are waiting for something. These locks # together with the *_waiting variables are used as # semaphores. When an event or a request response arrives, # it will zero the *_waiting and unlock the lock. The # locks will also be unlocked when an active send_and_recv # finishes to signal the other waiting threads that one of # them has to take over the send_and_recv function. # All this makes these locks and variables a part of the # send_and_recv control logic, and hence must be modified # only when we have the send_recv_lock locked. if event: wait_lock = self.event_wait_lock if not self.event_waiting: self.event_waiting = 1 wait_lock.acquire() elif request is not None: wait_lock = self.request_wait_lock if not self.request_waiting: self.request_waiting = 1 wait_lock.acquire() # Release send_recv, allowing a send_and_recive # to terminate or other threads to queue up self.send_recv_lock.release() # Return immediately if flushing, even if that # might mean that not necessarily all requests # have been sent. if flush or recv: return # Wait for something to happen, as the wait locks are # unlocked either when what we wait for has arrived (not # necessarily the exact object we're waiting for, though), # or when an active send_and_recv exits. # Release it immediately afterwards as we're only using # the lock for synchonization. Since we're not modifying # event_waiting or request_waiting here we don't have # to lock send_and_recv_lock. In fact, we can't do that # or we trigger a dead-lock. wait_lock.acquire() wait_lock.release() # Return to caller to let it check whether it has # got the data it was waiting for return # There's no thread doing what we need to do. Find out exactly # what to do # There must always be some thread recieving data, but it must not # necessarily be us if not self.recv_active: recieving = 1 self.recv_active = 1 else: recieving = 0 flush_bytes = None sending = 0 # Loop, recieving and sending data. while 1: # We might want to start sending data if sending or not self.send_active: # Turn all requests on request queue into binary form # and append them to self.data_send self.request_queue_lock.acquire() for req, wait in self.request_queue: self.data_send = self.data_send + req._binary if wait: self.sent_requests.append(req) del self.request_queue[:] self.request_queue_lock.release() # If there now is data to send, mark us as senders if self.data_send: self.send_active = 1 sending = 1 else: self.send_active = 0 sending = 0 # We've done all setup, so release the lock and start waiting # for the network to fire up self.send_recv_lock.release() # If we're flushing, figure out how many bytes we # have to send so that we're not caught in an interminable # loop if other threads continuously append requests. if flush and flush_bytes is None: flush_bytes = self.data_sent_bytes + len(self.data_send) try: # We're only checking for the socket to be writable # if we're the sending thread. We always check for it # to become readable: either we are the recieving thread # and should take care of the data, or the recieving thread # might finish recieving after having read the data if sending: writeset = [self.socket] else: writeset = [] # Timeout immediately if we're only checking for # something to read or if we're flushing, otherwise block if recv or flush: timeout = 0 else: timeout = None rs, ws, es = select.select([self.socket], writeset, [], timeout) # Ignore errors caused by a signal recieved while blocking. # All other errors are re-raised. except OSError as err: if err.errno != errno.EINTR: raise err # We must lock send_and_recv before we can loop to # the start of the loop self.send_recv_lock.acquire() continue # Socket is ready for sending data, send as much as possible. if ws: try: i = self.socket.send(self.data_send) except OSError as err: self.close_internal('server: %s' % err[1]) raise self.socket_error self.data_send = self.data_send[i:] self.data_sent_bytes = self.data_sent_bytes + i # There is data to read gotreq = 0 if rs: # We're the recieving thread, parse the data if recieving: try: bytes_recv = self.socket.recv(4096) except OSError as err: self.close_internal('server: %s' % err.strerror) raise self.socket_error if not bytes_recv: # Clear up, set a connection closed indicator and raise it self.close_internal('server') raise self.socket_error self.data_recv = self.data_recv + bytes_recv gotreq = self.parse_response(request) # Otherwise return, allowing the calling thread to figure # out if it has got the data it needs else: # We must be a sending thread if we're here, so reset # that indicator. self.send_recv_lock.acquire() self.send_active = 0 self.send_recv_lock.release() # And return to the caller return # There are three different end of send-recv-loop conditions. # However, we don't leave the loop immediately, instead we # try to send and recieve any data that might be left. We # do this by giving a timeout of 0 to select to poll # the socket. # When flushing: all requests have been sent if flush and flush_bytes >= self.data_sent_bytes: break # When waiting for an event: an event has been read if event and self.event_queue: break # When processing a certain request: got its reply if request is not None and gotreq: break # Always break if we just want to recieve as much as possible if recv: break # Else there's may still data which must be sent, or # we haven't got the data we waited for. Lock and loop self.send_recv_lock.acquire() # We have accomplished the callers request. # Record that there are now no active send_and_recv, # and wake up all waiting thread self.send_recv_lock.acquire() if sending: self.send_active = 0 if recieving: self.recv_active = 0 if self.event_waiting: self.event_waiting = 0 self.event_wait_lock.release() if self.request_waiting: self.request_waiting = 0 self.request_wait_lock.release() self.send_recv_lock.release() def parse_response(self, request): """Internal method. Parse data recieved from server. If REQUEST is not None true is returned if the request with that serial number was recieved, otherwise false is returned. If REQUEST is -1, we're parsing the server connection setup response. """ if request == -1: return self.parse_connection_setup() # Parse ordinary server response gotreq = 0 while 1: # Are we're waiting for additional data for a request response? if self.request_length: if len(self.data_recv) < self.request_length: return gotreq else: gotreq = self.parse_request_response(request) or gotreq # Every response is at least 32 bytes long, so don't bother # until we have recieved that much if len(self.data_recv) < 32: return gotreq # Check the first byte to find out what kind of response it is rtype = _bytes_item(self.data_recv[0]) # Error resposne if rtype == 0: gotreq = self.parse_error_response(request) or gotreq # Request response elif rtype == 1: # Set reply length, and loop around to see if # we have got the full response rlen = int(struct.unpack('=L', self.data_recv[4:8])[0]) self.request_length = 32 + rlen * 4 # Else event response else: self.parse_event_response(rtype) def parse_error_response(self, request): # Code is second byte code = _bytes_item(self.data_recv[1]) # Fetch error class estruct = self.error_classes.get(code, error.XError) e = estruct(self, self.data_recv[:32]) self.data_recv = self.data_recv[32:] # print 'recv Error:', e req = self.get_waiting_request(e.sequence_number) # Error for a request whose response we are waiting for, # or which have an error handler. However, if the error # handler indicates that it hasn't taken care of the # error, pass it on to the default error handler if req and req._set_error(e): # If this was a ReplyRequest, unlock any threads waiting # for a request to finish if isinstance(req, rq.ReplyRequest): self.send_recv_lock.acquire() if self.request_waiting: self.request_waiting = 0 self.request_wait_lock.release() self.send_recv_lock.release() return request == e.sequence_number # Else call the error handler else: if self.error_handler: rq.call_error_handler(self.error_handler, e, None) else: self.default_error_handler(e) return 0 def default_error_handler(self, err): sys.stderr.write('X protocol error:\n%s\n' % err) def parse_request_response(self, request): req = self.get_waiting_replyrequest() # Sequence number is always data[2:4] # Do sanity check before trying to parse the data sno = struct.unpack('=H', self.data_recv[2:4])[0] if sno != req._serial: raise RuntimeError("Expected reply for request %s, but got %s. Can't happen!" % (req._serial, sno)) req._parse_response(self.data_recv[:self.request_length]) # print 'recv Request:', req self.data_recv = self.data_recv[self.request_length:] self.request_length = 0 # Unlock any response waiting threads self.send_recv_lock.acquire() if self.request_waiting: self.request_waiting = 0 self.request_wait_lock.release() self.send_recv_lock.release() return req.sequence_number == request def parse_event_response(self, etype): # Skip bit 8 at lookup, that is set if this event came from an # SendEvent estruct = self.event_classes.get(etype & 0x7f, event.AnyEvent) e = estruct(display = self, binarydata = self.data_recv[:32]) self.data_recv = self.data_recv[32:] # Drop all requests having an error handler, # but which obviously succeded. # Decrement it by one, so that we don't remove the request # that generated these events, if there is such a one. # Bug reported by Ilpo Nyyssönen self.get_waiting_request((e.sequence_number - 1) % 65536) # print 'recv Event:', e # Insert the event into the queue self.event_queue_write_lock.acquire() self.event_queue.append(e) self.event_queue_write_lock.release() # Unlock any event waiting threads self.send_recv_lock.acquire() if self.event_waiting: self.event_waiting = 0 self.event_wait_lock.release() self.send_recv_lock.release() def get_waiting_request(self, sno): if not self.sent_requests: return None # Normalize sequence numbers, even if they have wrapped. # This ensures that # sno <= last_serial # and # self.sent_requests[0]._serial <= last_serial if self.sent_requests[0]._serial > self.request_serial: last_serial = self.request_serial + 65536 if sno < self.request_serial: sno = sno + 65536 else: last_serial = self.request_serial if sno > self.request_serial: sno = sno - 65536 # No matching events at all if sno < self.sent_requests[0]._serial: return None # Find last req <= sno req = None reqpos = len(self.sent_requests) adj = 0 last = 0 for i in range(0, len(self.sent_requests)): rno = self.sent_requests[i]._serial + adj # Did serial numbers just wrap around? if rno < last: adj = 65536 rno = rno + adj last = rno if sno == rno: req = self.sent_requests[i] reqpos = i + 1 break elif sno < rno: req = None reqpos = i break # Delete all request such as req <= sno del self.sent_requests[:reqpos] return req def get_waiting_replyrequest(self): for i in range(0, len(self.sent_requests)): if hasattr(self.sent_requests[i], '_reply'): req = self.sent_requests[i] del self.sent_requests[:i + 1] return req # Reply for an unknown request? No, that can't happen. else: raise RuntimeError("Request reply to unknown request. Can't happen!") def parse_connection_setup(self): """Internal function used to parse connection setup response. """ # Only the ConnectionSetupRequest has been sent so far r = self.sent_requests[0] while 1: # print 'data_send:', repr(self.data_send) # print 'data_recv:', repr(self.data_recv) if r._data: alen = r._data['additional_length'] * 4 # The full response haven't arrived yet if len(self.data_recv) < alen: return 0 # Connection failed or further authentication is needed. # Set reason to the reason string if r._data['status'] != 1: r._data['reason'] = self.data_recv[:r._data['reason_length']] # Else connection succeeded, parse the reply else: x, d = r._success_reply.parse_binary(self.data_recv[:alen], self, rawdict = 1) r._data.update(x) del self.sent_requests[0] self.data_recv = self.data_recv[alen:] return 1 else: # The base reply is 8 bytes long if len(self.data_recv) < 8: return 0 r._data, d = r._reply.parse_binary(self.data_recv[:8], self, rawdict = 1) self.data_recv = self.data_recv[8:] # Loop around to see if we have got the additional data # already PixmapFormat = rq.Struct( rq.Card8('depth'), rq.Card8('bits_per_pixel'), rq.Card8('scanline_pad'), rq.Pad(5) ) VisualType = rq.Struct ( rq.Card32('visual_id'), rq.Card8('visual_class'), rq.Card8('bits_per_rgb_value'), rq.Card16('colormap_entries'), rq.Card32('red_mask'), rq.Card32('green_mask'), rq.Card32('blue_mask'), rq.Pad(4) ) Depth = rq.Struct( rq.Card8('depth'), rq.Pad(1), rq.LengthOf('visuals', 2), rq.Pad(4), rq.List('visuals', VisualType) ) Screen = rq.Struct( rq.Window('root'), rq.Colormap('default_colormap'), rq.Card32('white_pixel'), rq.Card32('black_pixel'), rq.Card32('current_input_mask'), rq.Card16('width_in_pixels'), rq.Card16('height_in_pixels'), rq.Card16('width_in_mms'), rq.Card16('height_in_mms'), rq.Card16('min_installed_maps'), rq.Card16('max_installed_maps'), rq.Card32('root_visual'), rq.Card8('backing_store'), rq.Card8('save_unders'), rq.Card8('root_depth'), rq.LengthOf('allowed_depths', 1), rq.List('allowed_depths', Depth) ) class ConnectionSetupRequest(rq.GetAttrData): _request = rq.Struct( rq.Set('byte_order', 1, (0x42, 0x6c)), rq.Pad(1), rq.Card16('protocol_major'), rq.Card16('protocol_minor'), rq.LengthOf('auth_prot_name', 2), rq.LengthOf('auth_prot_data', 2), rq.Pad(2), rq.String8('auth_prot_name'), rq.String8('auth_prot_data') ) _reply = rq.Struct ( rq.Card8('status'), rq.Card8('reason_length'), rq.Card16('protocol_major'), rq.Card16('protocol_minor'), rq.Card16('additional_length') ) _success_reply = rq.Struct( rq.Card32('release_number'), rq.Card32('resource_id_base'), rq.Card32('resource_id_mask'), rq.Card32('motion_buffer_size'), rq.LengthOf('vendor', 2), rq.Card16('max_request_length'), rq.LengthOf('roots', 1), rq.LengthOf('pixmap_formats', 1), rq.Card8('image_byte_order'), rq.Card8('bitmap_format_bit_order'), rq.Card8('bitmap_format_scanline_unit'), rq.Card8('bitmap_format_scanline_pad'), rq.Card8('min_keycode'), rq.Card8('max_keycode'), rq.Pad(4), rq.String8('vendor'), rq.List('pixmap_formats', PixmapFormat), rq.List('roots', Screen), ) def __init__(self, display, *args, **keys): self._binary = self._request.to_binary(*args, **keys) self._data = None # Don't bother about locking, since no other threads have # access to the display yet display.request_queue.append((self, 1)) # However, we must lock send_and_recv, but we don't have # to loop. display.send_recv_lock.acquire() display.send_and_recv(request = -1) Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/event.py000066400000000000000000000360451505160700500246600ustar00rootroot00000000000000# Xlib.protocol.event -- definitions of core events # # Copyright (C) 2000-2002 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Xlib modules from Xlib import X # Xlib.protocol modules from Xlib.protocol import rq class AnyEvent(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('detail'), rq.Card16('sequence_number'), rq.FixedString('data', 28), ) class KeyButtonPointer(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('detail'), rq.Card16('sequence_number'), rq.Card32('time'), rq.Window('root'), rq.Window('window'), rq.Window('child', (X.NONE, )), rq.Int16('root_x'), rq.Int16('root_y'), rq.Int16('event_x'), rq.Int16('event_y'), rq.Card16('state'), rq.Card8('same_screen'), rq.Pad(1), ) class KeyPress(KeyButtonPointer): _code = X.KeyPress class KeyRelease(KeyButtonPointer): _code = X.KeyRelease class ButtonPress(KeyButtonPointer): _code = X.ButtonPress class ButtonRelease(KeyButtonPointer): _code = X.ButtonRelease class MotionNotify(KeyButtonPointer): _code = X.MotionNotify class EnterLeave(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('detail'), rq.Card16('sequence_number'), rq.Card32('time'), rq.Window('root'), rq.Window('window'), rq.Window('child', (X.NONE, )), rq.Int16('root_x'), rq.Int16('root_y'), rq.Int16('event_x'), rq.Int16('event_y'), rq.Card16('state'), rq.Card8('mode'), rq.Card8('flags'), ) class EnterNotify(EnterLeave): _code = X.EnterNotify class LeaveNotify(EnterLeave): _code = X.LeaveNotify class Focus(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Card8('detail'), rq.Card16('sequence_number'), rq.Window('window'), rq.Card8('mode'), rq.Pad(23), ) class FocusIn(Focus): _code = X.FocusIn class FocusOut(Focus): _code = X.FocusOut class Expose(rq.Event): _code = X.Expose _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('window'), rq.Card16('x'), rq.Card16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('count'), rq.Pad(14), ) class GraphicsExpose(rq.Event): _code = X.GraphicsExpose _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Drawable('drawable'), rq.Card16('x'), rq.Card16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('minor_event'), rq.Card16('count'), rq.Card8('major_event'), rq.Pad(11), ) class NoExpose(rq.Event): _code = X.NoExpose _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Drawable('window'), rq.Card16('minor_event'), rq.Card8('major_event'), rq.Pad(21), ) class VisibilityNotify(rq.Event): _code = X.VisibilityNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('window'), rq.Card8('state'), rq.Pad(23), ) class CreateNotify(rq.Event): _code = X.CreateNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('parent'), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('border_width'), rq.Card8('override'), rq.Pad(9), ) class DestroyNotify(rq.Event): _code = X.DestroyNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Pad(20), ) class UnmapNotify(rq.Event): _code = X.UnmapNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Card8('from_configure'), rq.Pad(19), ) class MapNotify(rq.Event): _code = X.MapNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Card8('override'), rq.Pad(19), ) class MapRequest(rq.Event): _code = X.MapRequest _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('parent'), rq.Window('window'), rq.Pad(20), ) class ReparentNotify(rq.Event): _code = X.ReparentNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Window('parent'), rq.Int16('x'), rq.Int16('y'), rq.Card8('override'), rq.Pad(11), ) class ConfigureNotify(rq.Event): _code = X.ConfigureNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Window('above_sibling', (X.NONE, )), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('border_width'), rq.Card8('override'), rq.Pad(5), ) class ConfigureRequest(rq.Event): _code = X.ConfigureRequest _fields = rq.Struct( rq.Card8('type'), rq.Card8('stack_mode'), rq.Card16('sequence_number'), rq.Window('parent'), rq.Window('window'), rq.Window('sibling', (X.NONE, )), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('border_width'), rq.Card16('value_mask'), rq.Pad(4), ) class GravityNotify(rq.Event): _code = X.GravityNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.Pad(16), ) class ResizeRequest(rq.Event): _code = X.ResizeRequest _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('window'), rq.Card16('width'), rq.Card16('height'), rq.Pad(20), ) class Circulate(rq.Event): _code = None _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('event'), rq.Window('window'), rq.Pad(4), rq.Card8('place'), rq.Pad(15), ) class CirculateNotify(Circulate): _code = X.CirculateNotify class CirculateRequest(Circulate): _code = X.CirculateRequest class PropertyNotify(rq.Event): _code = X.PropertyNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('window'), rq.Card32('atom'), rq.Card32('time'), rq.Card8('state'), rq.Pad(15), ) class SelectionClear(rq.Event): _code = X.SelectionClear _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Card32('time'), rq.Window('window'), rq.Card32('atom'), rq.Pad(16), ) class SelectionRequest(rq.Event): _code = X.SelectionRequest _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Card32('time'), rq.Window('owner'), rq.Window('requestor'), rq.Card32('selection'), rq.Card32('target'), rq.Card32('property'), rq.Pad(4), ) class SelectionNotify(rq.Event): _code = X.SelectionNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Card32('time'), rq.Window('requestor'), rq.Card32('selection'), rq.Card32('target'), rq.Card32('property'), rq.Pad(8), ) class ColormapNotify(rq.Event): _code = X.ColormapNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Window('window'), rq.Colormap('colormap', (X.NONE, )), rq.Card8('new'), rq.Card8('state'), rq.Pad(18), ) class MappingNotify(rq.Event): _code = X.MappingNotify _fields = rq.Struct( rq.Card8('type'), rq.Pad(1), rq.Card16('sequence_number'), rq.Card8('request'), rq.Card8('first_keycode'), rq.Card8('count'), rq.Pad(25), ) class ClientMessage(rq.Event): _code = X.ClientMessage _fields = rq.Struct( rq.Card8('type'), rq.Format('data', 1), rq.Card16('sequence_number'), rq.Window('window'), rq.Card32('client_type'), rq.FixedPropertyData('data', 20), ) class KeymapNotify(rq.Event): _code = X.KeymapNotify _fields = rq.Struct( rq.Card8('type'), rq.FixedList('data', 31, rq.Card8Obj, pad = 0) ) event_class = { X.KeyPress: KeyPress, X.KeyRelease: KeyRelease, X.ButtonPress: ButtonPress, X.ButtonRelease: ButtonRelease, X.MotionNotify: MotionNotify, X.EnterNotify: EnterNotify, X.LeaveNotify: LeaveNotify, X.FocusIn: FocusIn, X.FocusOut: FocusOut, X.KeymapNotify: KeymapNotify, X.Expose: Expose, X.GraphicsExpose: GraphicsExpose, X.NoExpose: NoExpose, X.VisibilityNotify: VisibilityNotify, X.CreateNotify: CreateNotify, X.DestroyNotify: DestroyNotify, X.UnmapNotify: UnmapNotify, X.MapNotify: MapNotify, X.MapRequest: MapRequest, X.ReparentNotify: ReparentNotify, X.ConfigureNotify: ConfigureNotify, X.ConfigureRequest: ConfigureRequest, X.GravityNotify: GravityNotify, X.ResizeRequest: ResizeRequest, X.CirculateNotify: CirculateNotify, X.CirculateRequest: CirculateRequest, X.PropertyNotify: PropertyNotify, X.SelectionClear: SelectionClear, X.SelectionRequest: SelectionRequest, X.SelectionNotify: SelectionNotify, X.ColormapNotify: ColormapNotify, X.ClientMessage: ClientMessage, X.MappingNotify: MappingNotify, } Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/request.py000066400000000000000000001346051505160700500252300ustar00rootroot00000000000000# Xlib.protocol.request -- definitions of core requests # # Copyright (C) 2000-2002 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Xlib modules from Xlib import X # Xlib.protocol modules from Xlib.protocol import rq, structs class CreateWindow(rq.Request): _request = rq.Struct( rq.Opcode(1), rq.Card8('depth'), rq.RequestLength(), rq.Window('wid'), rq.Window('parent'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('border_width'), rq.Set('window_class', 2, (X.CopyFromParent, X.InputOutput, X.InputOnly)), rq.Card32('visual'), structs.WindowValues('attrs'), ) class ChangeWindowAttributes(rq.Request): _request = rq.Struct( rq.Opcode(2), rq.Pad(1), rq.RequestLength(), rq.Window('window'), structs.WindowValues('attrs'), ) class GetWindowAttributes(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(3), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('backing_store'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('visual'), rq.Card16('win_class'), rq.Card8('bit_gravity'), rq.Card8('win_gravity'), rq.Card32('backing_bit_planes'), rq.Card32('backing_pixel'), rq.Card8('save_under'), rq.Card8('map_is_installed'), rq.Card8('map_state'), rq.Card8('override_redirect'), rq.Colormap('colormap', (X.NONE, )), rq.Card32('all_event_masks'), rq.Card32('your_event_mask'), rq.Card16('do_not_propagate_mask'), rq.Pad(2), ) class DestroyWindow(rq.Request): _request = rq.Struct( rq.Opcode(4), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class DestroySubWindows(rq.Request): _request = rq.Struct( rq.Opcode(5), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class ChangeSaveSet(rq.Request): _request = rq.Struct( rq.Opcode(6), rq.Set('mode', 1, (X.SetModeInsert, X.SetModeDelete)), rq.RequestLength(), rq.Window('window'), ) class ReparentWindow(rq.Request): _request = rq.Struct( rq.Opcode(7), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.Window('parent'), rq.Int16('x'), rq.Int16('y'), ) class MapWindow(rq.Request): _request = rq.Struct( rq.Opcode(8), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class MapSubwindows(rq.Request): _request = rq.Struct( rq.Opcode(9), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class UnmapWindow(rq.Request): _request = rq.Struct( rq.Opcode(10), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class UnmapSubwindows(rq.Request): _request = rq.Struct( rq.Opcode(11), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) class ConfigureWindow(rq.Request): _request = rq.Struct( rq.Opcode(12), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.ValueList( 'attrs', 2, 2, rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Int16('border_width'), rq.Window('sibling'), rq.Set('stack_mode', 1, (X.Above, X.Below, X.TopIf, X.BottomIf, X.Opposite)) ) ) class CirculateWindow(rq.Request): _request = rq.Struct( rq.Opcode(13), rq.Set('direction', 1, (X.RaiseLowest, X.LowerHighest)), rq.RequestLength(), rq.Window('window'), ) class GetGeometry(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(14), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable') ) _reply = rq.Struct ( rq.ReplyCode(), rq.Card8('depth'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('root'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card16('border_width'), rq.Pad(10) ) class QueryTree(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(15), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('root'), rq.Window('parent', (X.NONE, )), rq.LengthOf('children', 2), rq.Pad(14), rq.List('children', rq.WindowObj), ) class InternAtom(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(16), rq.Bool('only_if_exists'), rq.RequestLength(), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('atom'), rq.Pad(20), ) class GetAtomName(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(17), rq.Pad(1), rq.RequestLength(), rq.Card32('atom') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('name', 2), rq.Pad(22), rq.String8('name'), ) class ChangeProperty(rq.Request): _request = rq.Struct( rq.Opcode(18), rq.Set('mode', 1, (X.PropModeReplace, X.PropModePrepend, X.PropModeAppend)), rq.RequestLength(), rq.Window('window'), rq.Card32('property'), rq.Card32('type'), rq.Format('data', 1), rq.Pad(3), rq.LengthOf('data', 4), rq.PropertyData('data'), ) class DeleteProperty(rq.Request): _request = rq.Struct( rq.Opcode(19), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.Card32('property'), ) class GetProperty(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(20), rq.Bool('delete'), rq.RequestLength(), rq.Window('window'), rq.Card32('property'), rq.Card32('type'), rq.Card32('long_offset'), rq.Card32('long_length'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Format('value', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('property_type'), rq.Card32('bytes_after'), rq.LengthOf('value', 4), rq.Pad(12), rq.PropertyData('value'), ) class ListProperties(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(21), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('atoms', 2), rq.Pad(22), rq.List('atoms', rq.Card32Obj), ) class SetSelectionOwner(rq.Request): _request = rq.Struct( rq.Opcode(22), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.Card32('selection'), rq.Card32('time'), ) class GetSelectionOwner(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(23), rq.Pad(1), rq.RequestLength(), rq.Card32('selection') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('owner', (X.NONE, )), rq.Pad(20), ) class ConvertSelection(rq.Request): _request = rq.Struct( rq.Opcode(24), rq.Pad(1), rq.RequestLength(), rq.Window('requestor'), rq.Card32('selection'), rq.Card32('target'), rq.Card32('property'), rq.Card32('time'), ) class SendEvent(rq.Request): _request = rq.Struct( rq.Opcode(25), rq.Bool('propagate'), rq.RequestLength(), rq.Window('destination'), rq.Card32('event_mask'), rq.EventField('event'), ) class GrabPointer(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(26), rq.Bool('owner_events'), rq.RequestLength(), rq.Window('grab_window'), rq.Card16('event_mask'), rq.Set('pointer_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Set('keyboard_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Window('confine_to', (X.NONE, )), rq.Cursor('cursor', (X.NONE, )), rq.Card32('time'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), ) class UngrabPointer(rq.Request): _request = rq.Struct( rq.Opcode(27), rq.Pad(1), rq.RequestLength(), rq.Card32('time') ) class GrabButton(rq.Request): _request = rq.Struct( rq.Opcode(28), rq.Bool('owner_events'), rq.RequestLength(), rq.Window('grab_window'), rq.Card16('event_mask'), rq.Set('pointer_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Set('keyboard_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Window('confine_to', (X.NONE, )), rq.Cursor('cursor', (X.NONE, )), rq.Card8('button'), rq.Pad(1), rq.Card16('modifiers'), ) class UngrabButton(rq.Request): _request = rq.Struct( rq.Opcode(29), rq.Card8('button'), rq.RequestLength(), rq.Window('grab_window'), rq.Card16('modifiers'), rq.Pad(2), ) class ChangeActivePointerGrab(rq.Request): _request = rq.Struct( rq.Opcode(30), rq.Pad(1), rq.RequestLength(), rq.Cursor('cursor'), rq.Card32('time'), rq.Card16('event_mask'), rq.Pad(2), ) class GrabKeyboard(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(31), rq.Bool('owner_events'), rq.RequestLength(), rq.Window('grab_window'), rq.Card32('time'), rq.Set('pointer_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Set('keyboard_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), ) class UngrabKeyboard(rq.Request): _request = rq.Struct( rq.Opcode(32), rq.Pad(1), rq.RequestLength(), rq.Card32('time') ) class GrabKey(rq.Request): _request = rq.Struct( rq.Opcode(33), rq.Bool('owner_events'), rq.RequestLength(), rq.Window('grab_window'), rq.Card16('modifiers'), rq.Card8('key'), rq.Set('pointer_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Set('keyboard_mode', 1, (X.GrabModeSync, X.GrabModeAsync)), rq.Pad(3), ) class UngrabKey(rq.Request): _request = rq.Struct( rq.Opcode(34), rq.Card8('key'), rq.RequestLength(), rq.Window('grab_window'), rq.Card16('modifiers'), rq.Pad(2), ) class AllowEvents(rq.Request): _request = rq.Struct( rq.Opcode(35), rq.Set('mode', 1, (X.AsyncPointer, X.SyncPointer, X.ReplayPointer, X.AsyncKeyboard, X.SyncKeyboard, X.ReplayKeyboard, X.AsyncBoth, X.SyncBoth)), rq.RequestLength(), rq.Card32('time'), ) class GrabServer(rq.Request): _request = rq.Struct( rq.Opcode(36), rq.Pad(1), rq.RequestLength(), ) class UngrabServer(rq.Request): _request = rq.Struct( rq.Opcode(37), rq.Pad(1), rq.RequestLength(), ) class QueryPointer(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(38), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('same_screen'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('root'), rq.Window('child', (X.NONE, )), rq.Int16('root_x'), rq.Int16('root_y'), rq.Int16('win_x'), rq.Int16('win_y'), rq.Card16('mask'), rq.Pad(6), ) class GetMotionEvents(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(39), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.Card32('start'), rq.Card32('stop'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('events', 4), rq.Pad(20), rq.List('events', structs.TimeCoord), ) class TranslateCoords(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(40), rq.Pad(1), rq.RequestLength(), rq.Window('src_wid'), rq.Window('dst_wid'), rq.Int16('src_x'), rq.Int16('src_y'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('same_screen'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('child', (X.NONE, )), rq.Int16('x'), rq.Int16('y'), rq.Pad(16), ) class WarpPointer(rq.Request): _request = rq.Struct( rq.Opcode(41), rq.Pad(1), rq.RequestLength(), rq.Window('src_window'), rq.Window('dst_window'), rq.Int16('src_x'), rq.Int16('src_y'), rq.Card16('src_width'), rq.Card16('src_height'), rq.Int16('dst_x'), rq.Int16('dst_y'), ) class SetInputFocus(rq.Request): _request = rq.Struct( rq.Opcode(42), rq.Set('revert_to', 1, (X.RevertToNone, X.RevertToPointerRoot, X.RevertToParent)), rq.RequestLength(), rq.Window('focus'), rq.Card32('time'), ) class GetInputFocus(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(43), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('revert_to'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Window('focus', (X.NONE, X.PointerRoot)), rq.Pad(20), ) class QueryKeymap(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(44), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.FixedList('map', 32, rq.Card8Obj), ) class OpenFont(rq.Request): _request = rq.Struct( rq.Opcode(45), rq.Pad(1), rq.RequestLength(), rq.Font('fid'), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) class CloseFont(rq.Request): _request = rq.Struct( rq.Opcode(46), rq.Pad(1), rq.RequestLength(), rq.Font('font') ) class QueryFont(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(47), rq.Pad(1), rq.RequestLength(), rq.Fontable('font') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Object('min_bounds', structs.CharInfo), rq.Pad(4), rq.Object('max_bounds', structs.CharInfo), rq.Pad(4), rq.Card16('min_char_or_byte2'), rq.Card16('max_char_or_byte2'), rq.Card16('default_char'), rq.LengthOf('properties', 2), rq.Card8('draw_direction'), rq.Card8('min_byte1'), rq.Card8('max_byte1'), rq.Card8('all_chars_exist'), rq.Int16('font_ascent'), rq.Int16('font_descent'), rq.LengthOf('char_infos', 4), rq.List('properties', structs.FontProp), rq.List('char_infos', structs.CharInfo), ) class QueryTextExtents(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(48), rq.OddLength('string'), rq.RequestLength(), rq.Fontable('font'), rq.String16('string'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('draw_direction'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Int16('font_ascent'), rq.Int16('font_descent'), rq.Int16('overall_ascent'), rq.Int16('overall_descent'), rq.Int32('overall_width'), rq.Int32('overall_left'), rq.Int32('overall_right'), rq.Pad(4), ) class ListFonts(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(49), rq.Pad(1), rq.RequestLength(), rq.Card16('max_names'), rq.LengthOf('pattern', 2), rq.String8('pattern'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('fonts', 2), rq.Pad(22), rq.List('fonts', rq.Str), ) class ListFontsWithInfo(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(50), rq.Pad(1), rq.RequestLength(), rq.Card16('max_names'), rq.LengthOf('pattern', 2), rq.String8('pattern'), ) _reply = rq.Struct( rq.ReplyCode(), rq.LengthOf('name', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Object('min_bounds', structs.CharInfo), rq.Pad(4), rq.Object('max_bounds', structs.CharInfo), rq.Pad(4), rq.Card16('min_char_or_byte2'), rq.Card16('max_char_or_byte2'), rq.Card16('default_char'), rq.LengthOf('properties', 2), rq.Card8('draw_direction'), rq.Card8('min_byte1'), rq.Card8('max_byte1'), rq.Card8('all_chars_exist'), rq.Int16('font_ascent'), rq.Int16('font_descent'), rq.Card32('replies_hint'), rq.List('properties', structs.FontProp), rq.String8('name'), ) # Somebody must have smoked some really wicked weed when they # defined the ListFontsWithInfo request: # The server sends a reply for _each_ matching font... # It then sends a special reply (name length == 0) to indicate # that there are no more fonts in the reply. # This means that we have to do some special parsing to see if # we have got the end-of-reply reply. If we haven't, we # have to reinsert the request in the front of the # display.sent_request queue to catch the next response. # Bastards. def __init__(self, *args, **keys): self._fonts = [] ReplyRequest.__init__(*(self, ) + args, **keys) def _parse_response(self, data): if ord(data[1]) == 0: self._response_lock.acquire() self._data = self._fonts del self._fonts self._response_lock.release() return r, d = self._reply.parse_binary(data) self._fonts.append(r) self._display.sent_requests.insert(0, self) # Override the default __getattr__, since it isn't usable for # the list reply. Instead provide a __getitem__ and a __len__. def __getattr__(self, attr): raise AttributeError(attr) def __getitem__(self, item): return self._data[item] def __len__(self): return len(self._data) class SetFontPath(rq.Request): _request = rq.Struct( rq.Opcode(51), rq.Pad(1), rq.RequestLength(), rq.LengthOf('path', 2), rq.Pad(2), rq.List('path', rq.Str), ) class GetFontPath(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(52), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('paths', 2), rq.Pad(22), rq.List('paths', rq.Str), ) class CreatePixmap(rq.Request): _request = rq.Struct( rq.Opcode(53), rq.Card8('depth'), rq.RequestLength(), rq.Pixmap('pid'), rq.Drawable('drawable'), rq.Card16('width'), rq.Card16('height'), ) class FreePixmap(rq.Request): _request = rq.Struct( rq.Opcode(54), rq.Pad(1), rq.RequestLength(), rq.Pixmap('pixmap') ) class CreateGC(rq.Request): _request = rq.Struct( rq.Opcode(55), rq.Pad(1), rq.RequestLength(), rq.GC('cid'), rq.Drawable('drawable'), structs.GCValues('attrs'), ) class ChangeGC(rq.Request): _request = rq.Struct( rq.Opcode(56), rq.Pad(1), rq.RequestLength(), rq.GC('gc'), structs.GCValues('attrs'), ) class CopyGC(rq.Request): _request = rq.Struct( rq.Opcode(57), rq.Pad(1), rq.RequestLength(), rq.GC('src_gc'), rq.GC('dst_gc'), rq.Card32('mask'), ) class SetDashes(rq.Request): _request = rq.Struct( rq.Opcode(58), rq.Pad(1), rq.RequestLength(), rq.GC('gc'), rq.Card16('dash_offset'), rq.LengthOf('dashes', 2), rq.List('dashes', rq.Card8Obj), ) class SetClipRectangles(rq.Request): _request = rq.Struct( rq.Opcode(59), rq.Set('ordering', 1, (X.Unsorted, X.YSorted, X.YXSorted, X.YXBanded)), rq.RequestLength(), rq.GC('gc'), rq.Int16('x_origin'), rq.Int16('y_origin'), rq.List('rectangles', structs.Rectangle), ) class FreeGC(rq.Request): _request = rq.Struct( rq.Opcode(60), rq.Pad(1), rq.RequestLength(), rq.GC('gc') ) class ClearArea(rq.Request): _request = rq.Struct( rq.Opcode(61), rq.Bool('exposures'), rq.RequestLength(), rq.Window('window'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), ) class CopyArea(rq.Request): _request = rq.Struct( rq.Opcode(62), rq.Pad(1), rq.RequestLength(), rq.Drawable('src_drawable'), rq.Drawable('dst_drawable'), rq.GC('gc'), rq.Int16('src_x'), rq.Int16('src_y'), rq.Int16('dst_x'), rq.Int16('dst_y'), rq.Card16('width'), rq.Card16('height'), ) class CopyPlane(rq.Request): _request = rq.Struct( rq.Opcode(63), rq.Pad(1), rq.RequestLength(), rq.Drawable('src_drawable'), rq.Drawable('dst_drawable'), rq.GC('gc'), rq.Int16('src_x'), rq.Int16('src_y'), rq.Int16('dst_x'), rq.Int16('dst_y'), rq.Card16('width'), rq.Card16('height'), rq.Card32('bit_plane'), ) class PolyPoint(rq.Request): _request = rq.Struct( rq.Opcode(64), rq.Set('coord_mode', 1, (X.CoordModeOrigin, X.CoordModePrevious)), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('points', structs.Point), ) class PolyLine(rq.Request): _request = rq.Struct( rq.Opcode(65), rq.Set('coord_mode', 1, (X.CoordModeOrigin, X.CoordModePrevious)), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('points', structs.Point), ) class PolySegment(rq.Request): _request = rq.Struct( rq.Opcode(66), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('segments', structs.Segment), ) class PolyRectangle(rq.Request): _request = rq.Struct( rq.Opcode(67), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('rectangles', structs.Rectangle), ) class PolyArc(rq.Request): _request = rq.Struct( rq.Opcode(68), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('arcs', structs.Arc), ) class FillPoly(rq.Request): _request = rq.Struct( rq.Opcode(69), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Set('shape', 1, (X.Complex, X.Nonconvex, X.Convex)), rq.Set('coord_mode', 1, (X.CoordModeOrigin, X.CoordModePrevious)), rq.Pad(2), rq.List('points', structs.Point), ) class PolyFillRectangle(rq.Request): _request = rq.Struct( rq.Opcode(70), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('rectangles', structs.Rectangle), ) class PolyFillArc(rq.Request): _request = rq.Struct( rq.Opcode(71), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.List('arcs', structs.Arc), ) class PutImage(rq.Request): _request = rq.Struct( rq.Opcode(72), rq.Set('format', 1, (X.XYBitmap, X.XYPixmap, X.ZPixmap)), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Card16('width'), rq.Card16('height'), rq.Int16('dst_x'), rq.Int16('dst_y'), rq.Card8('left_pad'), rq.Card8('depth'), rq.Pad(2), rq.String8('data'), ) class GetImage(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(73), rq.Set('format', 1, (X.XYPixmap, X.ZPixmap)), rq.RequestLength(), rq.Drawable('drawable'), rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Card32('plane_mask'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('depth'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('visual'), rq.Pad(20), rq.String8('data'), ) class PolyText8(rq.Request): _request = rq.Struct( rq.Opcode(74), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Int16('x'), rq.Int16('y'), rq.TextElements8('items'), ) class PolyText16(rq.Request): _request = rq.Struct( rq.Opcode(75), rq.Pad(1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Int16('x'), rq.Int16('y'), rq.TextElements16('items'), ) class ImageText8(rq.Request): _request = rq.Struct( rq.Opcode(76), rq.LengthOf('string', 1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Int16('x'), rq.Int16('y'), rq.String8('string'), ) class ImageText16(rq.Request): _request = rq.Struct( rq.Opcode(77), rq.LengthOf('string', 1), rq.RequestLength(), rq.Drawable('drawable'), rq.GC('gc'), rq.Int16('x'), rq.Int16('y'), rq.String16('string'), ) class CreateColormap(rq.Request): _request = rq.Struct( rq.Opcode(78), rq.Set('alloc', 1, (X.AllocNone, X.AllocAll)), rq.RequestLength(), rq.Colormap('mid'), rq.Window('window'), rq.Card32('visual'), ) class FreeColormap(rq.Request): _request = rq.Struct( rq.Opcode(79), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap') ) class CopyColormapAndFree(rq.Request): _request = rq.Struct( rq.Opcode(80), rq.Pad(1), rq.RequestLength(), rq.Colormap('mid'), rq.Colormap('src_cmap'), ) class InstallColormap(rq.Request): _request = rq.Struct( rq.Opcode(81), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap') ) class UninstallColormap(rq.Request): _request = rq.Struct( rq.Opcode(82), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap') ) class ListInstalledColormaps(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(83), rq.Pad(1), rq.RequestLength(), rq.Window('window') ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('cmaps', 2), rq.Pad(22), rq.List('cmaps', rq.ColormapObj), ) class AllocColor(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(84), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.Card16('red'), rq.Card16('green'), rq.Card16('blue'), rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('red'), rq.Card16('green'), rq.Card16('blue'), rq.Pad(2), rq.Card32('pixel'), rq.Pad(12), ) class AllocNamedColor(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(85), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('pixel'), rq.Card16('exact_red'), rq.Card16('exact_green'), rq.Card16('exact_blue'), rq.Card16('screen_red'), rq.Card16('screen_green'), rq.Card16('screen_blue'), rq.Pad(8), ) class AllocColorCells(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(86), rq.Bool('contiguous'), rq.RequestLength(), rq.Colormap('cmap'), rq.Card16('colors'), rq.Card16('planes'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('pixels', 2), rq.LengthOf('masks', 2), rq.Pad(20), rq.List('pixels', rq.Card32Obj), rq.List('masks', rq.Card32Obj), ) class AllocColorPlanes(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(87), rq.Bool('contiguous'), rq.RequestLength(), rq.Colormap('cmap'), rq.Card16('colors'), rq.Card16('red'), rq.Card16('green'), rq.Card16('blue'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('pixels', 2), rq.Pad(2), rq.Card32('red_mask'), rq.Card32('green_mask'), rq.Card32('blue_mask'), rq.Pad(8), rq.List('pixels', rq.Card32Obj), ) class FreeColors(rq.Request): _request = rq.Struct( rq.Opcode(88), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.Card32('plane_mask'), rq.List('pixels', rq.Card32Obj), ) class StoreColors(rq.Request): _request = rq.Struct( rq.Opcode(89), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.List('items', structs.ColorItem), ) class StoreNamedColor(rq.Request): _request = rq.Struct( rq.Opcode(90), rq.Card8('flags'), rq.RequestLength(), rq.Colormap('cmap'), rq.Card32('pixel'), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) class QueryColors(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(91), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.List('pixels', rq.Card32Obj), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('colors', 2), rq.Pad(22), rq.List('colors', structs.RGB), ) class LookupColor(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(92), rq.Pad(1), rq.RequestLength(), rq.Colormap('cmap'), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('exact_red'), rq.Card16('exact_green'), rq.Card16('exact_blue'), rq.Card16('screen_red'), rq.Card16('screen_green'), rq.Card16('screen_blue'), rq.Pad(12), ) class CreateCursor(rq.Request): _request = rq.Struct( rq.Opcode(93), rq.Pad(1), rq.RequestLength(), rq.Cursor('cid'), rq.Pixmap('source'), rq.Pixmap('mask'), rq.Card16('fore_red'), rq.Card16('fore_green'), rq.Card16('fore_blue'), rq.Card16('back_red'), rq.Card16('back_green'), rq.Card16('back_blue'), rq.Card16('x'), rq.Card16('y'), ) class CreateGlyphCursor(rq.Request): _request = rq.Struct( rq.Opcode(94), rq.Pad(1), rq.RequestLength(), rq.Cursor('cid'), rq.Font('source'), rq.Font('mask'), rq.Card16('source_char'), rq.Card16('mask_char'), rq.Card16('fore_red'), rq.Card16('fore_green'), rq.Card16('fore_blue'), rq.Card16('back_red'), rq.Card16('back_green'), rq.Card16('back_blue'), ) class FreeCursor(rq.Request): _request = rq.Struct( rq.Opcode(95), rq.Pad(1), rq.RequestLength(), rq.Cursor('cursor') ) class RecolorCursor(rq.Request): _request = rq.Struct( rq.Opcode(96), rq.Pad(1), rq.RequestLength(), rq.Cursor('cursor'), rq.Card16('fore_red'), rq.Card16('fore_green'), rq.Card16('fore_blue'), rq.Card16('back_red'), rq.Card16('back_green'), rq.Card16('back_blue'), ) class QueryBestSize(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(97), rq.Set('item_class', 1, (X.CursorShape, X.TileShape, X.StippleShape)), rq.RequestLength(), rq.Drawable('drawable'), rq.Card16('width'), rq.Card16('height'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('width'), rq.Card16('height'), rq.Pad(20), ) class QueryExtension(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(98), rq.Pad(1), rq.RequestLength(), rq.LengthOf('name', 2), rq.Pad(2), rq.String8('name'), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card8('present'), rq.Card8('major_opcode'), rq.Card8('first_event'), rq.Card8('first_error'), rq.Pad(20), ) class ListExtensions(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(99), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.LengthOf('names', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), rq.List('names', rq.Str), ) class ChangeKeyboardMapping(rq.Request): _request = rq.Struct( rq.Opcode(100), rq.LengthOf('keysyms', 1), rq.RequestLength(), rq.Card8('first_keycode'), rq.Format('keysyms', 1), rq.Pad(2), rq.KeyboardMapping('keysyms'), ) class GetKeyboardMapping(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(101), rq.Pad(1), rq.RequestLength(), rq.Card8('first_keycode'), rq.Card8('count'), rq.Pad(2), ) _reply = rq.Struct( rq.ReplyCode(), rq.Format('keysyms', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), rq.KeyboardMapping('keysyms'), ) class ChangeKeyboardControl(rq.Request): _request = rq.Struct( rq.Opcode(102), rq.Pad(1), rq.RequestLength(), rq.ValueList( 'attrs', 4, 0, rq.Int8('key_click_percent'), rq.Int8('bell_percent'), rq.Int16('bell_pitch'), rq.Int16('bell_duration'), rq.Card8('led'), rq.Set('led_mode', 1, (X.LedModeOff, X.LedModeOn)), rq.Card8('key'), rq.Set('auto_repeat_mode', 1, (X.AutoRepeatModeOff, X.AutoRepeatModeOn, X.AutoRepeatModeDefault)) ) ) class GetKeyboardControl(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(103), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('global_auto_repeat'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card32('led_mask'), rq.Card8('key_click_percent'), rq.Card8('bell_percent'), rq.Card16('bell_pitch'), rq.Card16('bell_duration'), rq.Pad(2), rq.FixedList('auto_repeats', 32, rq.Card8Obj), ) class Bell(rq.Request): _request = rq.Struct( rq.Opcode(104), rq.Int8('percent'), rq.RequestLength(), ) class ChangePointerControl(rq.Request): _request = rq.Struct( rq.Opcode(105), rq.Pad(1), rq.RequestLength(), rq.Int16('accel_num'), rq.Int16('accel_denum'), rq.Int16('threshold'), rq.Bool('do_accel'), rq.Bool('do_thresh'), ) class GetPointerControl(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(106), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('accel_num'), rq.Card16('accel_denom'), rq.Card16('threshold'), rq.Pad(18), ) class SetScreenSaver(rq.Request): _request = rq.Struct( rq.Opcode(107), rq.Pad(1), rq.RequestLength(), rq.Int16('timeout'), rq.Int16('interval'), rq.Set('prefer_blank', 1, (X.DontPreferBlanking, X.PreferBlanking, X.DefaultBlanking)), rq.Set('allow_exposures', 1, (X.DontAllowExposures, X.AllowExposures, X.DefaultExposures)), rq.Pad(2), ) class GetScreenSaver(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(108), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Card16('timeout'), rq.Card16('interval'), rq.Card8('prefer_blanking'), rq.Card8('allow_exposures'), rq.Pad(18), ) class ChangeHosts(rq.Request): _request = rq.Struct( rq.Opcode(109), rq.Set('mode', 1, (X.HostInsert, X.HostDelete)), rq.RequestLength(), rq.Set('host_family', 1, (X.FamilyInternet, X.FamilyDECnet, X.FamilyChaos)), rq.Pad(1), rq.LengthOf('host', 2), rq.List('host', rq.Card8Obj) ) class ListHosts(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(110), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('mode'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('hosts', 2), rq.Pad(22), rq.List('hosts', structs.Host), ) class SetAccessControl(rq.Request): _request = rq.Struct( rq.Opcode(111), rq.Set('mode', 1, (X.DisableAccess, X.EnableAccess)), rq.RequestLength(), ) class SetCloseDownMode(rq.Request): _request = rq.Struct( rq.Opcode(112), rq.Set('mode', 1, (X.DestroyAll, X.RetainPermanent, X.RetainTemporary)), rq.RequestLength(), ) class KillClient(rq.Request): _request = rq.Struct( rq.Opcode(113), rq.Pad(1), rq.RequestLength(), rq.Resource('resource') ) class RotateProperties(rq.Request): _request = rq.Struct( rq.Opcode(114), rq.Pad(1), rq.RequestLength(), rq.Window('window'), rq.LengthOf('properties', 2), rq.Int16('delta'), rq.List('properties', rq.Card32Obj), ) class ForceScreenSaver(rq.Request): _request = rq.Struct( rq.Opcode(115), rq.Set('mode', 1, (X.ScreenSaverReset, X.ScreenSaverActive)), rq.RequestLength(), ) class SetPointerMapping(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(116), rq.LengthOf('map', 1), rq.RequestLength(), rq.List('map', rq.Card8Obj), ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), ) class GetPointerMapping(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(117), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.LengthOf('map', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), rq.List('map', rq.Card8Obj), ) class SetModifierMapping(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(118), rq.Format('keycodes', 1), rq.RequestLength(), rq.ModifierMapping('keycodes') ) _reply = rq.Struct( rq.ReplyCode(), rq.Card8('status'), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), ) class GetModifierMapping(rq.ReplyRequest): _request = rq.Struct( rq.Opcode(119), rq.Pad(1), rq.RequestLength(), ) _reply = rq.Struct( rq.ReplyCode(), rq.Format('keycodes', 1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.Pad(24), rq.ModifierMapping('keycodes') ) class NoOperation(rq.Request): _request = rq.Struct( rq.Opcode(127), rq.Pad(1), rq.RequestLength(), ) major_codes = { 1: CreateWindow, 2: ChangeWindowAttributes, 3: GetWindowAttributes, 4: DestroyWindow, 5: DestroySubWindows, 6: ChangeSaveSet, 7: ReparentWindow, 8: MapWindow, 9: MapSubwindows, 10: UnmapWindow, 11: UnmapSubwindows, 12: ConfigureWindow, 13: CirculateWindow, 14: GetGeometry, 15: QueryTree, 16: InternAtom, 17: GetAtomName, 18: ChangeProperty, 19: DeleteProperty, 20: GetProperty, 21: ListProperties, 22: SetSelectionOwner, 23: GetSelectionOwner, 24: ConvertSelection, 25: SendEvent, 26: GrabPointer, 27: UngrabPointer, 28: GrabButton, 29: UngrabButton, 30: ChangeActivePointerGrab, 31: GrabKeyboard, 32: UngrabKeyboard, 33: GrabKey, 34: UngrabKey, 35: AllowEvents, 36: GrabServer, 37: UngrabServer, 38: QueryPointer, 39: GetMotionEvents, 40: TranslateCoords, 41: WarpPointer, 42: SetInputFocus, 43: GetInputFocus, 44: QueryKeymap, 45: OpenFont, 46: CloseFont, 47: QueryFont, 48: QueryTextExtents, 49: ListFonts, 50: ListFontsWithInfo, 51: SetFontPath, 52: GetFontPath, 53: CreatePixmap, 54: FreePixmap, 55: CreateGC, 56: ChangeGC, 57: CopyGC, 58: SetDashes, 59: SetClipRectangles, 60: FreeGC, 61: ClearArea, 62: CopyArea, 63: CopyPlane, 64: PolyPoint, 65: PolyLine, 66: PolySegment, 67: PolyRectangle, 68: PolyArc, 69: FillPoly, 70: PolyFillRectangle, 71: PolyFillArc, 72: PutImage, 73: GetImage, 74: PolyText8, 75: PolyText16, 76: ImageText8, 77: ImageText16, 78: CreateColormap, 79: FreeColormap, 80: CopyColormapAndFree, 81: InstallColormap, 82: UninstallColormap, 83: ListInstalledColormaps, 84: AllocColor, 85: AllocNamedColor, 86: AllocColorCells, 87: AllocColorPlanes, 88: FreeColors, 89: StoreColors, 90: StoreNamedColor, 91: QueryColors, 92: LookupColor, 93: CreateCursor, 94: CreateGlyphCursor, 95: FreeCursor, 96: RecolorCursor, 97: QueryBestSize, 98: QueryExtension, 99: ListExtensions, 100: ChangeKeyboardMapping, 101: GetKeyboardMapping, 102: ChangeKeyboardControl, 103: GetKeyboardControl, 104: Bell, 105: ChangePointerControl, 106: GetPointerControl, 107: SetScreenSaver, 108: GetScreenSaver, 109: ChangeHosts, 110: ListHosts, 111: SetAccessControl, 112: SetCloseDownMode, 113: KillClient, 114: RotateProperties, 115: ForceScreenSaver, 116: SetPointerMapping, 117: GetPointerMapping, 118: SetModifierMapping, 119: GetModifierMapping, 127: NoOperation, } Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/rq.py000066400000000000000000001324511505160700500241570ustar00rootroot00000000000000# Xlib.protocol.rq -- structure primitives for request, events and errors # # Copyright (C) 2000-2002 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from array import array import struct import sys import traceback import types # Xlib modules from Xlib import X from Xlib.support import lock _PY3 = sys.version[0] >= '3' # in Python 3, bytes are an actual array; in python 2, bytes are still # string-like, so in order to get an array element we need to call ord() if _PY3: def _bytes_item(x): return x else: def _bytes_item(x): return ord(x) class BadDataError(Exception): pass # These are struct codes, we know their byte sizes signed_codes = { 1: 'b', 2: 'h', 4: 'l' } unsigned_codes = { 1: 'B', 2: 'H', 4: 'L' } # Unfortunately, we don't know the array sizes of B, H and L, since # these use the underlying architecture's size for a char, short and # long. Therefore we probe for their sizes, and additionally create # a mapping that translates from struct codes to array codes. # # Bleah. array_unsigned_codes = { } struct_to_array_codes = { } for c in 'bhil': size = array(c).itemsize array_unsigned_codes[size] = c.upper() try: struct_to_array_codes[signed_codes[size]] = c struct_to_array_codes[unsigned_codes[size]] = c.upper() except KeyError: pass # print array_unsigned_codes, struct_to_array_codes class Field: """Field objects represent the data fields of a Struct. Field objects must have the following attributes: name -- the field name, or None structcode -- the struct codes representing this field structvalues -- the number of values encodes by structcode Additionally, these attributes should either be None or real methods: check_value -- check a value before it is converted to binary parse_value -- parse a value after it has been converted from binary If one of these attributes are None, no check or additional parsings will be done one values when converting to or from binary form. Otherwise, the methods should have the following behaviour: newval = check_value(val) Check that VAL is legal when converting to binary form. The value can also be converted to another Python value. In any case, return the possibly new value. NEWVAL should be a single Python value if structvalues is 1, a tuple of structvalues elements otherwise. newval = parse_value(val, display) VAL is an unpacked Python value, which now can be further refined. DISPLAY is the current Display object. Return the new value. VAL will be a single value if structvalues is 1, a tuple of structvalues elements otherwise. If `structcode' is None the Field must have the method f.parse_binary_value() instead. See its documentation string for details. """ name = None default = None structcode = None structvalues = 0 check_value = None parse_value = None keyword_args = 0 def __init__(self): pass def parse_binary_value(self, data, display, length, format): """value, remaindata = f.parse_binary_value(data, display, length, format) Decode a value for this field from the binary string DATA. If there are a LengthField and/or a FormatField connected to this field, their values will be LENGTH and FORMAT, respectively. If there are no such fields the parameters will be None. DISPLAY is the display involved, which is really only used by the Resource fields. The decoded value is returned as VALUE, and the remaining part of DATA shold be returned as REMAINDATA. """ raise RuntimeError('Neither structcode or parse_binary_value provided for %s' % self) class Pad(Field): def __init__(self, size): self.size = size self.value = b'\0' * size self.structcode = '%dx' % size self.structvalues = 0 class ConstantField(Field): def __init__(self, value): self.value = value class Opcode(ConstantField): structcode = 'B' structvalues = 1 class ReplyCode(ConstantField): structcode = 'B' structvalues = 1 def __init__(self): self.value = 1 class LengthField(Field): """A LengthField stores the length of some other Field whose size may vary, e.g. List and String8. Its name should be the same as the name of the field whose size it stores. The lf.get_binary_value() method of LengthFields is not used, instead a lf.get_binary_length() should be provided. Unless LengthField.get_binary_length() is overridden in child classes, there should also be a lf.calc_length(). """ structcode = 'L' structvalues = 1 def calc_length(self, length): """newlen = lf.calc_length(length) Return a new length NEWLEN based on the provided LENGTH. """ return length class TotalLengthField(LengthField): pass class RequestLength(TotalLengthField): structcode = 'H' structvalues = 1 def calc_length(self, length): return length // 4 class ReplyLength(TotalLengthField): structcode = 'L' structvalues = 1 def calc_length(self, length): return (length - 32) // 4 class LengthOf(LengthField): def __init__(self, name, size): self.name = name self.structcode = unsigned_codes[size] class OddLength(LengthField): structcode = 'B' structvalues = 1 def __init__(self, name): self.name = name def calc_length(self, length): return length % 2 def parse_value(self, value, display): if value == 0: return 'even' else: return 'odd' class FormatField(Field): """A FormatField encodes the format of some other field, in a manner similar to LengthFields. The ff.get_binary_value() method is not used, replaced by ff.get_binary_format(). """ structvalues = 1 def __init__(self, name, size): self.name = name self.structcode = unsigned_codes[size] Format = FormatField class ValueField(Field): def __init__(self, name, default = None): self.name = name self.default = default class Int8(ValueField): structcode = 'b' structvalues = 1 class Int16(ValueField): structcode = 'h' structvalues = 1 class Int32(ValueField): structcode = 'l' structvalues = 1 class Card8(ValueField): structcode = 'B' structvalues = 1 class Card16(ValueField): structcode = 'H' structvalues = 1 class Card32(ValueField): structcode = 'L' structvalues = 1 class Resource(Card32): cast_function = '__resource__' class_name = 'resource' def __init__(self, name, codes = (), default = None): Card32.__init__(self, name, default) self.codes = codes def check_value(self, value): try: return getattr(value, self.cast_function)() except AttributeError: return value def parse_value(self, value, display): # if not display: # return value if value in self.codes: return value c = display.get_resource_class(self.class_name) if c: return c(display, value) else: return value class Window(Resource): cast_function = '__window__' class_name = 'window' class Pixmap(Resource): cast_function = '__pixmap__' class_name = 'pixmap' class Drawable(Resource): cast_function = '__drawable__' class_name = 'drawable' class Fontable(Resource): cast_function = '__fontable__' class_name = 'fontable' class Font(Resource): cast_function = '__font__' class_name = 'font' class GC(Resource): cast_function = '__gc__' class_name = 'gc' class Colormap(Resource): cast_function = '__colormap__' class_name = 'colormap' class Cursor(Resource): cast_function = '__cursor__' class_name = 'cursor' class Bool(ValueField): structvalues = 1 structcode = 'B' def check_value(self, value): return not not value class Set(ValueField): structvalues = 1 def __init__(self, name, size, values, default = None): ValueField.__init__(self, name, default) self.structcode = unsigned_codes[size] self.values = values def check_value(self, val): if val not in self.values: raise ValueError('field %s: argument %s not in %s' % (self.name, val, self.values)) return val class Gravity(Set): def __init__(self, name): Set.__init__(self, name, 1, (X.ForgetGravity, X.StaticGravity, X.NorthWestGravity, X.NorthGravity, X.NorthEastGravity, X.WestGravity, X.CenterGravity, X.EastGravity, X.SouthWestGravity, X.SouthGravity, X.SouthEastGravity)) class FixedString(ValueField): structvalues = 1 def __init__(self, name, size): ValueField.__init__(self, name) self.structcode = '%ds' % size class String8(ValueField): structcode = None def __init__(self, name, pad = 1): ValueField.__init__(self, name) self.pad = pad def pack_value(self, val): slen = len(val) if _PY3 and type(val) is str: val = val.encode('UTF-8') if self.pad: return val + b'\0' * ((4 - slen % 4) % 4), slen, None else: return val, slen, None def parse_binary_value(self, data, display, length, format): if length is None: try: return data.decode('UTF-8'), b'' except UnicodeDecodeError: return data, b'' if self.pad: slen = length + ((4 - length % 4) % 4) else: slen = length s = data[:length] try: s = s.decode('UTF-8') except UnicodeDecodeError: pass # return as bytes return s, data[slen:] class String16(ValueField): structcode = None def __init__(self, name, pad = 1): ValueField.__init__(self, name) self.pad = pad def pack_value(self, val): # Convert 8-byte string into 16-byte list if type(val) is str: val = [ord(c) for c in val] slen = len(val) if self.pad: pad = b'\0\0' * (slen % 2) else: pad = b'' return (struct.pack(*('>' + 'H' * slen, ) + tuple(val)) + pad, slen, None) def parse_binary_value(self, data, display, length, format): if length == 'odd': length = len(data) // 2 - 1 elif length == 'even': length = len(data) // 2 if self.pad: slen = length + (length % 2) else: slen = length return (struct.unpack('>' + 'H' * length, data[:length * 2]), data[slen * 2:]) class List(ValueField): """The List, FixedList and Object fields store compound data objects. The type of data objects must be provided as an object with the following attributes and methods: ... """ structcode = None def __init__(self, name, type, pad = 1): ValueField.__init__(self, name) self.type = type self.pad = pad def parse_binary_value(self, data, display, length, format): if length is None: ret = [] if self.type.structcode is None: while data: val, data = self.type.parse_binary(data, display) ret.append(val) else: scode = '=' + self.type.structcode slen = struct.calcsize(scode) pos = 0 while pos + slen <= len(data): v = struct.unpack(scode, data[pos: pos + slen]) if self.type.structvalues == 1: v = v[0] if self.type.parse_value is None: ret.append(v) else: ret.append(self.type.parse_value(v, display)) pos = pos + slen data = data[pos:] else: ret = [None] * int(length) if self.type.structcode is None: for i in range(length): ret[i], data = self.type.parse_binary(data, display) else: scode = '=' + self.type.structcode slen = struct.calcsize(scode) pos = 0 for i in range(0, length): # FIXME: remove try..except try: v = struct.unpack(scode, data[pos: pos + slen]) except Exception: v = b'\x00\x00\x00\x00' if self.type.structvalues == 1: v = v[0] if self.type.parse_value is None: ret[i] = v else: ret[i] = self.type.parse_value(v, display) pos = pos + slen data = data[pos:] if self.pad: data = data[len(data) % 4:] return ret, data def pack_value(self, val): # Single-char values, we'll assume that means integer lists. if self.type.structcode and len(self.type.structcode) == 1: data = array(struct_to_array_codes[self.type.structcode], val).tobytes() else: data = [] for v in val: data.append(self.type.pack_value(v)) data = b''.join(data) if self.pad: dlen = len(data) data = data + b'\0' * ((4 - dlen % 4) % 4) return data, len(val), None class FixedList(List): def __init__(self, name, size, type, pad = 1): List.__init__(self, name, type, pad) self.size = size def parse_binary_value(self, data, display, length, format): return List.parse_binary_value(self, data, display, self.size, format) def pack_value(self, val): if len(val) != self.size: raise BadDataError('length mismatch for FixedList %s' % self.name) return List.pack_value(self, val) class Object(ValueField): structcode = None def __init__(self, name, type, default = None): ValueField.__init__(self, name, default) self.type = type self.structcode = self.type.structcode self.structvalues = self.type.structvalues def parse_binary_value(self, data, display, length, format): if self.type.structcode is None: return self.type.parse_binary(data, display) else: scode = '=' + self.type.structcode slen = struct.calcsize(scode) v = struct.unpack(scode, data[:slen]) if self.type.structvalues == 1: v = v[0] if self.type.parse_value is not None: v = self.type.parse_value(v, display) return v, data[slen:] def parse_value(self, val, display): if self.type.parse_value is None: return val else: return self.type.parse_value(val, display) def pack_value(self, val): # Single-char values, we'll assume that mean an integer if self.type.structcode and len(self.type.structcode) == 1: return struct.pack('=' + self.type.structcode, val), None, None else: return self.type.pack_value(val) def check_value(self, val): if self.type.structcode is None: return val if type(val) is tuple: return val if type(val) is dict: data = val elif isinstance(val, DictWrapper): data = val._data else: raise TypeError('Object value must be tuple, dictionary or DictWrapper: %s' % val) vals = [] for f in self.type.fields: if f.name: vals.append(data[f.name]) return vals class PropertyData(ValueField): structcode = None def parse_binary_value(self, data, display, length, format): if length is None: length = len(data) // (format // 8) else: length = int(length) if format == 0: ret = None return ret, data elif format == 8: ret = (8, data[:length]) data = data[length + ((4 - length % 4) % 4):] elif format == 16: ret = (16, array(array_unsigned_codes[2], data[:2 * length])) data = data[2 * (length + length % 2):] elif format == 32: ret = (32, array(array_unsigned_codes[4], data[:4 * length])) data = data[4 * length:] if type(ret[1]) is bytes: try: ret = (ret[0], ret[1].decode('UTF-8')) except UnicodeDecodeError: pass # return as bytes return ret, data def pack_value(self, value): fmt, val = value if fmt not in (8, 16, 32): raise BadDataError('Invalid property data format %d' % fmt) if _PY3 and type(val) is str: val = val.encode('UTF-8') if type(val) is bytes: size = fmt // 8 vlen = len(val) if vlen % size: vlen = vlen - vlen % size data = val[:vlen] else: data = val dlen = vlen // size else: if type(val) is tuple: val = list(val) size = fmt // 8 data = array(array_unsigned_codes[size], val).tobytes() dlen = len(val) dl = len(data) data = data + b'\0' * ((4 - dl % 4) % 4) return data, dlen, fmt class FixedPropertyData(PropertyData): def __init__(self, name, size): PropertyData.__init__(self, name) self.size = size def parse_binary_value(self, data, display, length, format): return PropertyData.parse_binary_value(self, data, display, self.size // (format // 8), format) def pack_value(self, value): data, dlen, fmt = PropertyData.pack_value(self, value) if len(data) != self.size: raise BadDataError('Wrong data length for FixedPropertyData: %s' % (value, )) return data, dlen, fmt class ValueList(Field): structcode = None keyword_args = 1 default = 'usekeywords' def __init__(self, name, mask, pad, *fields): self.name = name self.maskcode = '=%s%dx' % (unsigned_codes[mask], pad) self.maskcodelen = struct.calcsize(self.maskcode) self.fields = [] flag = 1 for f in fields: if f.name: self.fields.append((f, flag)) flag = flag << 1 def pack_value(self, arg, keys): mask = 0 data = b'' if arg == self.default: arg = keys for field, flag in self.fields: if field.name in arg: mask = mask | flag val = arg[field.name] if field.check_value is not None: val = field.check_value(val) d = struct.pack('=' + field.structcode, val) data = data + d + b'\0' * (4 - len(d)) return struct.pack(self.maskcode, mask) + data, None, None def parse_binary_value(self, data, display, length, format): r = {} mask = int(struct.unpack(self.maskcode, data[:self.maskcodelen])[0]) data = data[self.maskcodelen:] for field, flag in self.fields: if mask & flag: if field.structcode: vals = struct.unpack('=' + field.structcode, data[:struct.calcsize('=' + field.structcode)]) if field.structvalues == 1: vals = vals[0] if field.parse_value is not None: vals = field.parse_value(vals, display) else: vals, d = field.parse_binary_value(data[:4], display, None, None) r[field.name] = vals data = data[4:] return DictWrapper(r), data class KeyboardMapping(ValueField): structcode = None def parse_binary_value(self, data, display, length, format): if length is None: dlen = len(data) else: dlen = 4 * length * format a = array(array_unsigned_codes[4], data[:dlen]) ret = [] for i in range(0, len(a), format): ret.append(a[i : i + format]) return ret, data[dlen:] def pack_value(self, value): keycodes = 0 for v in value: keycodes = max(keycodes, len(v)) a = array(array_unsigned_codes[4]) for v in value: for k in v: a.append(k) for i in range(len(v), keycodes): a.append(X.NoSymbol) return a.tobytes(), len(value), keycodes class ModifierMapping(ValueField): structcode = None def parse_binary_value(self, data, display, length, format): a = array(array_unsigned_codes[1], data[:8 * format]) ret = [] for i in range(0, 8): ret.append(a[i * format : (i + 1) * format]) return ret, data[8 * format:] def pack_value(self, value): if len(value) != 8: raise BadDataError('ModifierMapping list should have eight elements') keycodes = 0 for v in value: keycodes = max(keycodes, len(v)) a = array(array_unsigned_codes[1]) for v in value: for k in v: a.append(k) for i in range(len(v), keycodes): a.append(0) return a.tobytes(), len(value), keycodes class EventField(ValueField): structcode = None def pack_value(self, value): if not isinstance(value, Event): raise BadDataError('%s is not an Event for field %s' % (value, self.name)) return value._binary, None, None def parse_binary_value(self, data, display, length, format): from Xlib.protocol import event estruct = display.event_classes.get(_bytes_item(data[0]) & 0x7f, event.AnyEvent) return estruct(display = display, binarydata = data[:32]), data[32:] # # Objects usable for List and FixedList fields. # Struct is also usable. # class ScalarObj: def __init__(self, code): self.structcode = code self.structvalues = 1 self.parse_value = None Card8Obj = ScalarObj('B') Card16Obj = ScalarObj('H') Card32Obj = ScalarObj('L') class ResourceObj: structcode = 'L' structvalues = 1 def __init__(self, class_name): self.class_name = class_name def parse_value(self, value, display): # if not display: # return value c = display.get_resource_class(self.class_name) if c: return c(display, value) else: return value WindowObj = ResourceObj('window') ColormapObj = ResourceObj('colormap') class StrClass: structcode = None def pack_value(self, val): if type(val) is not bytes: val = val.encode('UTF-8') if _PY3: val = bytes([len(val)]) + val else: val = chr(len(val)) + val return val def parse_binary(self, data, display): slen = _bytes_item(data[0]) + 1 s = data[1:slen] try: s = s.decode('UTF-8') except UnicodeDecodeError: pass # return as bytes return s, data[slen:] Str = StrClass() class Struct: """Struct objects represents a binary data structure. It can contain both fields with static and dynamic sizes. However, all static fields must appear before all dynamic fields. Fields are represented by various subclasses of the abstract base class Field. The fields of a structure are given as arguments when instantiating a Struct object. Struct objects have two public methods: to_binary() -- build a binary representation of the structure with the values given as arguments parse_binary() -- convert a binary (string) representation into a Python dictionary or object. These functions will be generated dynamically for each Struct object to make conversion as fast as possible. They are generated the first time the methods are called. """ def __init__(self, *fields): self.fields = fields # Structures for to_binary, parse_value and parse_binary self.static_codes = '=' self.static_values = 0 self.static_fields = [] self.static_size = None self.var_fields = [] for f in self.fields: # Append structcode if there is one and we haven't # got any varsize fields yet. if f.structcode is not None: assert not self.var_fields self.static_codes = self.static_codes + f.structcode # Only store fields with values if f.structvalues > 0: self.static_fields.append(f) self.static_values = self.static_values + f.structvalues # If we have got one varsize field, all the rest must # also be varsize fields. else: self.var_fields.append(f) self.static_size = struct.calcsize(self.static_codes) if self.var_fields: self.structcode = None self.structvalues = 0 else: self.structcode = self.static_codes[1:] self.structvalues = self.static_values # These functions get called only once, as they will override # themselves with dynamically created functions in the Struct # object def to_binary(self, *varargs, **keys): """data = s.to_binary(...) Convert Python values into the binary representation. The arguments will be all value fields with names, in the order given when the Struct object was instantiated. With one exception: fields with default arguments will be last. Returns the binary representation as the string DATA. """ code = '' total_length = str(self.static_size) joins = [] args = [] defargs = [] kwarg = 0 # First pack all varfields so their lengths and formats are # available when we pack their static LengthFields and # FormatFields i = 0 for f in self.var_fields: if f.keyword_args: kwarg = 1 kw = ', _keyword_args' else: kw = '' # Call pack_value method for each field, storing # the return values for later use code = code + (' _%(name)s, _%(name)s_length, _%(name)s_format' ' = self.var_fields[%(fno)d].pack_value(%(name)s%(kw)s)\n' % { 'name': f.name, 'fno': i, 'kw': kw }) total_length = total_length + ' + len(_%s)' % f.name joins.append('_%s' % f.name) i = i + 1 # Construct argument list for struct.pack call, packing all # static fields. First argument is the structcode, the # remaining are values. pack_args = ['"%s"' % self.static_codes] i = 0 for f in self.static_fields: if isinstance(f, LengthField): # If this is a total length field, insert # the calculated field value here if isinstance(f, TotalLengthField): if self.var_fields: pack_args.append('self.static_fields[%d].calc_length(%s)' % (i, total_length)) else: pack_args.append(str(f.calc_length(self.static_size))) else: pack_args.append('self.static_fields[%d].calc_length(_%s_length)' % (i, f.name)) # Format field, just insert the value we got previously elif isinstance(f, FormatField): pack_args.append('_%s_format' % f.name) # A constant field, insert its value directly elif isinstance(f, ConstantField): pack_args.append(str(f.value)) # Value fields else: if f.structvalues == 1: # If there's a value check/convert function, call it if f.check_value is not None: pack_args.append('self.static_fields[%d].check_value(%s)' % (i, f.name)) # Else just use the argument as provided else: pack_args.append(f.name) # Multivalue field. Handled like single valuefield, # but the value are tuple unpacked into seperate arguments # which are appended to pack_args else: a = [] for j in range(f.structvalues): a.append('_%s_%d' % (f.name, j)) if f.check_value is not None: code = code + (' %s = self.static_fields[%d].check_value(%s)\n' % (', '.join(a), i, f.name)) else: code = code + ' %s = %s\n' % (', '.join(a), f.name) pack_args = pack_args + a # Add field to argument list if f.name: if f.default is None: args.append(f.name) else: defargs.append('%s = %s' % (f.name, repr(f.default))) i = i + 1 # Construct call to struct.pack pack = 'struct.pack(%s)' % ', '.join(pack_args) # If there are any varfields, we append the packed strings to build # the resulting binary value if self.var_fields: code = code + ' return %s + %s\n' % (pack, ' + '.join(joins)) # If there's only static fields, return the packed value else: code = code + ' return %s\n' % pack # Add all varsize fields to argument list. We do it here # to ensure that they appear after the static fields. for f in self.var_fields: if f.name: if f.default is None: args.append(f.name) else: defargs.append('%s = %s' % (f.name, repr(f.default))) args = args + defargs if kwarg: args.append('**_keyword_args') # Add function header code = 'def to_binary(self, %s):\n' % ', '.join(args) + code # self._pack_code = code # print # print code # print # Finally, compile function by evaluating it. This will store # the function in the local variable to_binary, thanks to the # def: line. Convert it into a instance metod bound to self, # and store it in self. # Unfortunately, this creates a circular reference. However, # Structs are not really created dynamically so the potential # memory leak isn't that serious. Besides, Python 2.0 has # real garbage collect. g = globals().copy() exec(code, g) self.to_binary = types.MethodType(g['to_binary'], self) # Finally call it manually return self.to_binary(*varargs, **keys) def pack_value(self, value): """ This function allows Struct objects to be used in List and Object fields. Each item represents the arguments to pass to to_binary, either a tuple, a dictionary or a DictWrapper. """ if type(value) is tuple: return self.to_binary(*value, **{}) elif type(value) is dict: return self.to_binary(*(), **value) elif isinstance(value, DictWrapper): return self.to_binary(*(), **value._data) else: raise BadDataError('%s is not a tuple or a list' % (value)) def parse_value(self, val, display, rawdict = 0): """This function is used by List and Object fields to convert Struct objects with no var_fields into Python values. """ code = ('def parse_value(self, val, display, rawdict = 0):\n' ' ret = {}\n') vno = 0 fno = 0 for f in self.static_fields: # Fields without names should be ignored, and there should # not be any length or format fields if this function # ever gets called. (If there were such fields, there should # be a matching field in var_fields and then parse_binary # would have been called instead. if not f.name: pass elif isinstance(f, LengthField): pass elif isinstance(f, FormatField): pass # Value fields else: # Get the index or range in val representing this field. if f.structvalues == 1: vrange = str(vno) else: vrange = '%d:%d' % (vno, vno + f.structvalues) # If this field has a parse_value method, call it, otherwise # use the unpacked value as is. if f.parse_value is None: code = code + ' ret["%s"] = val[%s]\n' % (f.name, vrange) else: code = code + (' ret["%s"] = self.static_fields[%d].' 'parse_value(val[%s], display)\n' % (f.name, fno, vrange)) fno = fno + 1 vno = vno + f.structvalues code = code + ' if not rawdict: return DictWrapper(ret)\n' code = code + ' return ret\n' # print # print code # print # Finally, compile function as for to_binary. g = globals().copy() exec(code, g) self.parse_value = types.MethodType(g['parse_value'], self) # Call it manually return self.parse_value(val, display, rawdict) def parse_binary(self, data, display, rawdict = 0): """values, remdata = s.parse_binary(data, display, rawdict = 0) Convert a binary representation of the structure into Python values. DATA is a string or a buffer containing the binary data. DISPLAY should be a Xlib.protocol.display.Display object if there are any Resource fields or Lists with ResourceObjs. The Python values are returned as VALUES. If RAWDICT is true, a Python dictionary is returned, where the keys are field names and the values are the corresponding Python value. If RAWDICT is false, a DictWrapper will be returned where all fields are available as attributes. REMDATA are the remaining binary data, unused by the Struct object. """ code = ('def parse_binary(self, data, display, rawdict = 0):\n' ' ret = {}\n' ' val = struct.unpack("%s", data[:%d])\n' % (self.static_codes, self.static_size)) lengths = {} formats = {} vno = 0 fno = 0 for f in self.static_fields: # Fields without name should be ignored. This is typically # pad and constant fields if not f.name: pass # Store index in val for Length and Format fields, to be used # when treating varfields. elif isinstance(f, LengthField): if f.parse_value is None: lengths[f.name] = 'val[%d]' % vno else: lengths[f.name] = ('self.static_fields[%d].' 'parse_value(val[%d], display)' % (fno, vno)) elif isinstance(f, FormatField): formats[f.name] = 'val[%d]' % vno # Treat value fields the same was as in parse_value. else: if f.structvalues == 1: vrange = str(vno) else: vrange = '%d:%d' % (vno, vno + f.structvalues) if f.parse_value is None: code = code + ' ret["%s"] = val[%s]\n' % (f.name, vrange) else: code = code + (' ret["%s"] = self.static_fields[%d].' 'parse_value(val[%s], display)\n' % (f.name, fno, vrange)) fno = fno + 1 vno = vno + f.structvalues code = code + ' data = data[%d:]\n' % self.static_size # Call parse_binary_value for each var_field, passing the # length and format values from the unpacked val. fno = 0 for f in self.var_fields: code = code + (' ret["%s"], data = ' 'self.var_fields[%d].parse_binary_value' '(data, display, %s, %s)\n' % (f.name, fno, lengths.get(f.name, 'None'), formats.get(f.name, 'None'))) fno = fno + 1 code = code + ' if not rawdict: ret = DictWrapper(ret)\n' code = code + ' return ret, data\n' # print # print code # print # Finally, compile function as for to_binary. g = globals().copy() exec(code, g) self.parse_binary = types.MethodType(g['parse_binary'], self) # Call it manually return self.parse_binary(data, display, rawdict) class TextElements8(ValueField): string_textitem = Struct( LengthOf('string', 1), Int8('delta'), String8('string', pad = 0) ) def pack_value(self, value): data = b'' args = {} for v in value: # Let values be simple strings, meaning a delta of 0 if _PY3 and type(v) is str: v = v.encode('UTF-8') if type(v) is bytes: v = (0, v) # A tuple, it should be (delta, string) # Encode it as one or more textitems if type(v) in (tuple, dict) or \ isinstance(v, DictWrapper): if type(v) is tuple: delta, s = v else: delta = v['delta'] s = v['string'] while delta or s: args['delta'] = delta args['string'] = s[:254] data = data + self.string_textitem.to_binary(*(), **args) delta = 0 s = s[254:] # Else an integer, i.e. a font change else: # Use fontable cast function if instance if hasattr(v, '__fontable__'): v = v.__fontable__() data = data + struct.pack('>BL', 255, v) # Pad out to four byte length dlen = len(data) return data + b'\0' * ((4 - dlen % 4) % 4), None, None def parse_binary_value(self, data, display, length, format): values = [] while 1: if len(data) < 2: break # font change if _bytes_item(data[0]) == 255: values.append(struct.unpack('>L', data[1:5])[0]) data = data[5:] # skip null strings elif _bytes_item(data[0]) == 0 and _bytes_item(data[1]) == 0: data = data[2:] # string with delta else: v, data = self.string_textitem.parse_binary(data, display) values.append(v) return values, b'' class TextElements16(TextElements8): string_textitem = Struct( LengthOf('string', 1), Int8('delta'), String16('string', pad = 0) ) class GetAttrData: def __getattr__(self, attr): try: if self._data: return self._data[attr] else: raise AttributeError(attr) except KeyError: raise AttributeError(attr) class DictWrapper(GetAttrData): def __init__(self, dict): self.__dict__['_data'] = dict def __getitem__(self, key): return self._data[key] def __setitem__(self, key, value): self._data[key] = value def __delitem__(self, key): del self._data[key] def __setattr__(self, key, value): self._data[key] = value def __delattr__(self, key): del self._data[key] def __str__(self): return str(self._data) def __repr__(self): return '%s(%s)' % (self.__class__, repr(self._data)) def __eq__(self, other): if isinstance(other, DictWrapper): return self._data == other._data else: return self._data == other def __ne__(self, other): return not self.__eq__(other) class Request: def __init__(self, display, onerror = None, *args, **keys): self._errorhandler = onerror self._binary = self._request.to_binary(*args, **keys) self._serial = None display.send_request(self, onerror is not None) def _set_error(self, error): if self._errorhandler is not None: return call_error_handler(self._errorhandler, error, self) else: return 0 class ReplyRequest(GetAttrData): def __init__(self, display, defer = 0, *args, **keys): self._display = display self._binary = self._request.to_binary(*args, **keys) self._serial = None self._data = None self._error = None self._response_lock = lock.allocate_lock() self._display.send_request(self, 1) if not defer: self.reply() def reply(self): # Send request and wait for reply if we hasn't # already got one. This means that reply() can safely # be called more than one time. self._response_lock.acquire() while self._data is None and self._error is None: self._display.send_recv_lock.acquire() self._response_lock.release() self._display.send_and_recv(request = self._serial) self._response_lock.acquire() self._response_lock.release() self._display = None # If error has been set, raise it if self._error: raise self._error def _parse_response(self, data): self._response_lock.acquire() self._data, d = self._reply.parse_binary(data, self._display, rawdict = 1) self._response_lock.release() def _set_error(self, error): self._response_lock.acquire() self._error = error self._response_lock.release() return 1 def __repr__(self): return '<%s serial = %s, data = %s, error = %s>' % (self.__class__, self._serial, self._data, self._error) class Event(GetAttrData): def __init__(self, binarydata = None, display = None, **keys): if binarydata: self._binary = binarydata self._data, data = self._fields.parse_binary(binarydata, display, rawdict = 1) # split event type into type and send_event bit self._data['send_event'] = not not self._data['type'] & 0x80 self._data['type'] = self._data['type'] & 0x7f else: if self._code: keys['type'] = self._code keys['sequence_number'] = 0 self._binary = self._fields.to_binary(*(), **keys) keys['send_event'] = 0 self._data = keys def __repr__(self): kwlist = [] for kw, val in self._data.items(): if kw == 'send_event': continue if kw == 'type' and self._data['send_event']: val = val | 0x80 kwlist.append('%s = %s' % (kw, repr(val))) kws = ', '.join(kwlist) return '%s(%s)' % (self.__class__, kws) def __eq__(self, other): if isinstance(other, Event): return self._data == other._data else: return cmp(self._data, other) def call_error_handler(handler, error, request): try: return handler(error, request) except: sys.stderr.write('Exception raised by error handler.\n') traceback.print_exc() return 0 Nagstamon-master/Nagstamon/thirdparty/Xlib/protocol/structs.py000066400000000000000000000123371505160700500252440ustar00rootroot00000000000000# Xlib.protocol.structs -- some common request structures # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # Xlib modules from Xlib import X # Xlib.protocol modules from Xlib.protocol import rq def WindowValues(arg): return rq.ValueList( arg, 4, 0, rq.Pixmap('background_pixmap'), rq.Card32('background_pixel'), rq.Pixmap('border_pixmap'), rq.Card32('border_pixel'), rq.Gravity('bit_gravity'), rq.Gravity('win_gravity'), rq.Set('backing_store', 1, (X.NotUseful, X.WhenMapped, X.Always)), rq.Card32('backing_planes'), rq.Card32('backing_pixel'), rq.Bool('override_redirect'), rq.Bool('save_under'), rq.Card32('event_mask'), rq.Card32('do_not_propagate_mask'), rq.Colormap('colormap'), rq.Cursor('cursor'), ) def GCValues(arg): return rq.ValueList( arg, 4, 0, rq.Set('function', 1, (X.GXclear, X.GXand, X.GXandReverse, X.GXcopy, X.GXandInverted, X.GXnoop, X.GXxor, X.GXor, X.GXnor, X.GXequiv, X.GXinvert, X.GXorReverse, X.GXcopyInverted, X.GXorInverted, X.GXnand, X.GXset)), rq.Card32('plane_mask'), rq.Card32('foreground'), rq.Card32('background'), rq.Card16('line_width'), rq.Set('line_style', 1, (X.LineSolid, X.LineOnOffDash, X.LineDoubleDash)), rq.Set('cap_style', 1, (X.CapNotLast, X.CapButt, X.CapRound, X.CapProjecting)), rq.Set('join_style', 1, (X.JoinMiter, X.JoinRound, X.JoinBevel)), rq.Set('fill_style', 1, (X.FillSolid, X.FillTiled, X.FillStippled, X.FillOpaqueStippled)), rq.Set('fill_rule', 1, (X.EvenOddRule, X.WindingRule)), rq.Pixmap('tile'), rq.Pixmap('stipple'), rq.Int16('tile_stipple_x_origin'), rq.Int16('tile_stipple_y_origin'), rq.Font('font'), rq.Set('subwindow_mode', 1, (X.ClipByChildren, X.IncludeInferiors)), rq.Bool('graphics_exposures'), rq.Int16('clip_x_origin'), rq.Int16('clip_y_origin'), rq.Pixmap('clip_mask'), rq.Card16('dash_offset'), rq.Card8('dashes'), rq.Set('arc_mode', 1, (X.ArcChord, X.ArcPieSlice)) ) TimeCoord = rq.Struct( rq.Card32('time'), rq.Int16('x'), rq.Int16('y'), ) Host = rq.Struct( rq.Set('family', 1, (X.FamilyInternet, X.FamilyDECnet, X.FamilyChaos)), rq.Pad(1), rq.LengthOf('name', 2), rq.List('name', rq.Card8Obj) ) CharInfo = rq.Struct( rq.Int16('left_side_bearing'), rq.Int16('right_side_bearing'), rq.Int16('character_width'), rq.Int16('ascent'), rq.Int16('descent'), rq.Card16('attributes'), ) FontProp = rq.Struct( rq.Card32('name'), rq.Card32('value'), ) ColorItem = rq.Struct( rq.Card32('pixel'), rq.Card16('red'), rq.Card16('green'), rq.Card16('blue'), rq.Card8('flags'), rq.Pad(1), ) RGB = rq.Struct( rq.Card16('red'), rq.Card16('green'), rq.Card16('blue'), rq.Pad(2), ) Point = rq.Struct( rq.Int16('x'), rq.Int16('y'), ) Segment = rq.Struct( rq.Int16('x1'), rq.Int16('y1'), rq.Int16('x2'), rq.Int16('y2'), ) Rectangle = rq.Struct( rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), ) Arc = rq.Struct( rq.Int16('x'), rq.Int16('y'), rq.Card16('width'), rq.Card16('height'), rq.Int16('angle1'), rq.Int16('angle2'), ) Nagstamon-master/Nagstamon/thirdparty/Xlib/rdb.py000066400000000000000000000466731505160700500224550ustar00rootroot00000000000000# Xlib.rdb -- X resource database implementation # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # See end of file for an explanation of the algorithm and # data structures used. # Standard modules import functools import re import sys # Xlib modules from Xlib.support import lock # Set up a few regexpes for parsing string representation of resources comment_re = re.compile(r'^\s*!') resource_spec_re = re.compile(r'^\s*([-_a-zA-Z0-9?.*]+)\s*:\s*(.*)$') value_escape_re = re.compile('\\\\([ \tn\\\\]|[0-7]{3,3})') resource_parts_re = re.compile(r'([.*]+)') # Constants used for determining which match is best NAME_MATCH = 0 CLASS_MATCH = 2 WILD_MATCH = 4 MATCH_SKIP = 6 # Option error class class OptionError(Exception): pass class ResourceDB: def __init__(self, file = None, string = None, resources = None): self.db = {} self.lock = lock.allocate_lock() if file is not None: self.insert_file(file) if string is not None: self.insert_string(string) if resources is not None: self.insert_resources(resources) def insert_file(self, file): """insert_file(file) Load resources entries from FILE, and insert them into the database. FILE can be a filename (a string)or a file object. """ if type(file) is str: file = open(file, 'r') self.insert_string(file.read()) def insert_string(self, data): """insert_string(data) Insert the resources entries in the string DATA into the database. """ # First split string into lines lines = data.split('\n') while lines: line = lines[0] del lines[0] # Skip empty line if not line: continue # Skip comments if comment_re.match(line): continue # Handle continued lines while line[-1] == '\\': if lines: line = line[:-1] + lines[0] del lines[0] else: line = line[:-1] break # Split line into resource and value m = resource_spec_re.match(line) # Bad line, just ignore it silently if not m: continue res, value = m.group(1, 2) # Convert all escape sequences in value splits = value_escape_re.split(value) for i in range(1, len(splits), 2): s = splits[i] if len(s) == 3: splits[i] = chr(int(s, 8)) elif s == 'n': splits[i] = '\n' # strip the last value part to get rid of any # unescaped blanks splits[-1] = splits[-1].rstrip() value = ''.join(splits) self.insert(res, value) def insert_resources(self, resources): """insert_resources(resources) Insert all resources entries in the list RESOURCES into the database. Each element in RESOURCES should be a tuple: (resource, value) Where RESOURCE is a string and VALUE can be any Python value. """ for res, value in resources: self.insert(res, value) def insert(self, resource, value): """insert(resource, value) Insert a resource entry into the database. RESOURCE is a string and VALUE can be any Python value. """ # Split res into components and bindings parts = resource_parts_re.split(resource) # If the last part is empty, this is an invalid resource # which we simply ignore if parts[-1] == '': return self.lock.acquire() db = self.db for i in range(1, len(parts), 2): # Create a new mapping/value group if parts[i - 1] not in db: db[parts[i - 1]] = ({}, {}) # Use second mapping if a loose binding, first otherwise if '*' in parts[i]: db = db[parts[i - 1]][1] else: db = db[parts[i - 1]][0] # Insert value into the derived db if parts[-1] in db: db[parts[-1]] = db[parts[-1]][:2] + (value, ) else: db[parts[-1]] = ({}, {}, value) self.lock.release() def __getitem__(self, nc): """db[name, class] Return the value matching the resource identified by NAME and CLASS. If no match is found, KeyError is raised. """ name, cls = nc # Split name and class into their parts namep = name.split('.') clsp = cls.split('.') # It is an error for name and class to have different number # of parts if len(namep) != len(clsp): raise ValueError('Different number of parts in resource name/class: %s/%s' % (name, cls)) complen = len(namep) matches = [] # Lock database and wrap the lookup code in a try-finally # block to make sure that it is unlocked. self.lock.acquire() try: # Precedence order: name -> class -> ? if namep[0] in self.db: bin_insert(matches, _Match((NAME_MATCH, ), self.db[namep[0]])) if clsp[0] in self.db: bin_insert(matches, _Match((CLASS_MATCH, ), self.db[clsp[0]])) if '?' in self.db: bin_insert(matches, _Match((WILD_MATCH, ), self.db['?'])) # Special case for the unlikely event that the resource # only has one component if complen == 1 and matches: x = matches[0] if x.final(complen): return x.value() else: raise KeyError((name, cls)) # Special case for resources which begins with a loose # binding, e.g. '*foo.bar' if '' in self.db: bin_insert(matches, _Match((), self.db[''][1])) # Now iterate over all components until we find the best match. # For each component, we choose the best partial match among # the mappings by applying these rules in order: # Rule 1: If the current group contains a match for the # name, class or '?', we drop all previously found loose # binding mappings. # Rule 2: A matching name has precedence over a matching # class, which in turn has precedence over '?'. # Rule 3: Tight bindings have precedence over loose # bindings. while matches: # Work on the first element == the best current match x = matches[0] del matches[0] # print 'path: ', x.path # if x.skip: # print 'skip: ', x.db # else: # print 'group: ', x.group # print i = x.match_length() for part, score in ((namep[i], NAME_MATCH), (clsp[i], CLASS_MATCH), ('?', WILD_MATCH)): # Attempt to find a match in x match = x.match(part, score) if match: # Hey, we actually found a value! if match.final(complen): return match.value() # Else just insert the new match else: bin_insert(matches, match) # Generate a new loose match match = x.skip_match(complen) if match: bin_insert(matches, match) # Oh well, nothing matched raise KeyError((name, cls)) finally: self.lock.release() def get(self, res, cls, default = None): """get(name, class [, default]) Return the value matching the resource identified by NAME and CLASS. If no match is found, DEFAULT is returned, or None if DEFAULT isn't specified. """ try: return self[(res, cls)] except KeyError: return default def update(self, db): """update(db) Update this database with all resources entries in the resource database DB. """ self.lock.acquire() update_db(self.db, db.db) self.lock.release() def output(self): """output() Return the resource database in text representation. """ self.lock.acquire() text = output_db('', self.db) self.lock.release() return text def getopt(self, name, argv, opts): """getopt(name, argv, opts) Parse X command line options, inserting the recognised options into the resource database. NAME is the application name, and will be prepended to all specifiers. ARGV is the list of command line arguments, typically sys.argv[1:]. OPTS is a mapping of options to resource specifiers. The key is the option flag (with leading -), and the value is an instance of some Option subclass: NoArg(specifier, value): set resource to value. IsArg(specifier): set resource to option itself SepArg(specifier): value is next argument ResArg: resource and value in next argument SkipArg: ignore this option and next argument SkipLine: ignore rest of arguments SkipNArgs(count): ignore this option and count arguments The remaining, non-option, oparguments is returned. rdb.OptionError is raised if there is an error in the argument list. """ while argv and argv[0] and argv[0][0] == '-': try: argv = opts[argv[0]].parse(name, self, argv) except KeyError: raise OptionError('unknown option: %s' % argv[0]) except IndexError: raise OptionError('missing argument to option: %s' % argv[0]) return argv @functools.total_ordering class _Match: def __init__(self, path, dbs): self.path = path if type(dbs) is tuple: self.skip = 0 self.group = dbs else: self.skip = 1 self.db = dbs def __eq__(self, other): return self.path == other.path def __lt__(self, other): return self.path < other.path def match_length(self): return len(self.path) def match(self, part, score): if self.skip: if part in self.db: return _Match(self.path + (score, ), self.db[part]) else: return None else: if part in self.group[0]: return _Match(self.path + (score, ), self.group[0][part]) elif part in self.group[1]: return _Match(self.path + (score + 1, ), self.group[1][part]) else: return None def skip_match(self, complen): # Can't make another skip if we have run out of components if len(self.path) + 1 >= complen: return None # If this already is a skip match, clone a new one if self.skip: if self.db: return _Match(self.path + (MATCH_SKIP, ), self.db) else: return None # Only generate a skip match if the loose binding mapping # is non-empty elif self.group[1]: return _Match(self.path + (MATCH_SKIP, ), self.group[1]) # This is a dead end match else: return None def final(self, complen): if not self.skip and len(self.path) == complen and len(self.group) > 2: return 1 else: return 0 def value(self): return self.group[2] # # Helper function for ResourceDB.__getitem__() # def bin_insert(list, element): """bin_insert(list, element) Insert ELEMENT into LIST. LIST must be sorted, and ELEMENT will be inserted to that LIST remains sorted. If LIST already contains ELEMENT, it will not be duplicated. """ if not list: list.append(element) return lower = 0 upper = len(list) - 1 while lower <= upper: center = (lower + upper) // 2 if element < list[center]: upper = center - 1 elif element > list[center]: lower = center + 1 elif element == list[center]: return if element < list[upper]: list.insert(upper, element) elif element > list[upper]: list.insert(upper + 1, element) # # Helper functions for ResourceDB.update() # def update_db(dest, src): for comp, group in src.items(): # DEST already contains this component, update it if comp in dest: # Update tight and loose binding databases update_db(dest[comp][0], group[0]) update_db(dest[comp][1], group[1]) # If a value has been set in SRC, update # value in DEST if len(group) > 2: dest[comp] = dest[comp][:2] + group[2:] # COMP not in src, make a deep copy else: dest[comp] = copy_group(group) def copy_group(group): return (copy_db(group[0]), copy_db(group[1])) + group[2:] def copy_db(db): newdb = {} for comp, group in db.items(): newdb[comp] = copy_group(group) return newdb # # Helper functions for output # def output_db(prefix, db): res = '' for comp, group in db.items(): # There's a value for this component if len(group) > 2: res = res + '%s%s: %s\n' % (prefix, comp, output_escape(group[2])) # Output tight and loose bindings res = res + output_db(prefix + comp + '.', group[0]) res = res + output_db(prefix + comp + '*', group[1]) return res def output_escape(value): value = str(value) if not value: return value for char, esc in (('\\', '\\\\'), ('\000', '\\000'), ('\n', '\\n')): value = value.replace(char, esc) # If first or last character is space or tab, escape them. if value[0] in ' \t': value = '\\' + value if value[-1] in ' \t' and value[-2:-1] != '\\': value = value[:-1] + '\\' + value[-1] return value # # Option type definitions # class Option: def __init__(self): pass def parse(self, name, db, args): pass class NoArg(Option): """Value is provided to constructor.""" def __init__(self, specifier, value): self.specifier = specifier self.value = value def parse(self, name, db, args): db.insert(name + self.specifier, self.value) return args[1:] class IsArg(Option): """Value is the option string itself.""" def __init__(self, specifier): self.specifier = specifier def parse(self, name, db, args): db.insert(name + self.specifier, args[0]) return args[1:] class SepArg(Option): """Value is the next argument.""" def __init__(self, specifier): self.specifier = specifier def parse(self, name, db, args): db.insert(name + self.specifier, args[1]) return args[2:] class ResArgClass(Option): """Resource and value in the next argument.""" def parse(self, name, db, args): db.insert_string(args[1]) return args[2:] ResArg = ResArgClass() class SkipArgClass(Option): """Ignore this option and next argument.""" def parse(self, name, db, args): return args[2:] SkipArg = SkipArgClass() class SkipLineClass(Option): """Ignore rest of the arguments.""" def parse(self, name, db, args): return [] SkipLine = SkipLineClass() class SkipNArgs(Option): """Ignore this option and the next COUNT arguments.""" def __init__(self, count): self.count = count def parse(self, name, db, args): return args[1 + self.count:] def get_display_opts(options, argv = sys.argv): """display, name, db, args = get_display_opts(options, [argv]) Parse X OPTIONS from ARGV (or sys.argv if not provided). Connect to the display specified by a *.display resource if one is set, or to the default X display otherwise. Extract the RESOURCE_MANAGER property and insert all resources from ARGV. The four return values are: DISPLAY -- the display object NAME -- the application name (the filname of ARGV[0]) DB -- the created resource database ARGS -- any remaining arguments """ from Xlib import display, Xatom import os name = os.path.splitext(os.path.basename(argv[0]))[0] optdb = ResourceDB() leftargv = optdb.getopt(name, argv[1:], options) dname = optdb.get(name + '.display', name + '.Display', None) d = display.Display(dname) rdbstring = d.screen(0).root.get_full_property(Xatom.RESOURCE_MANAGER, Xatom.STRING) if rdbstring: data = rdbstring.value else: data = None db = ResourceDB(string = data) db.update(optdb) return d, name, db, leftargv # Common X options stdopts = {'-bg': SepArg('*background'), '-background': SepArg('*background'), '-fg': SepArg('*foreground'), '-foreground': SepArg('*foreground'), '-fn': SepArg('*font'), '-font': SepArg('*font'), '-name': SepArg('.name'), '-title': SepArg('.title'), '-synchronous': NoArg('*synchronous', 'on'), '-xrm': ResArg, '-display': SepArg('.display'), '-d': SepArg('.display'), } # Notes on the implementation: # Resource names are split into their components, and each component # is stored in a mapping. The value for a component is a tuple of two # or three elements: # (tightmapping, loosemapping [, value]) # tightmapping contains the next components which are connected with a # tight binding (.). loosemapping contains the ones connected with # loose binding (*). If value is present, then this component is the # last component for some resource which that value. # The top level components are stored in the mapping r.db, where r is # the resource object. # Example: Inserting "foo.bar*gazonk: yep" into an otherwise empty # resource database would give the folliwing structure: # { 'foo': ( { 'bar': ( { }, # { 'gazonk': ( { }, # { }, # 'yep') # } # ) # }, # {}) # } Nagstamon-master/Nagstamon/thirdparty/Xlib/support/000077500000000000000000000000001505160700500230305ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/support/__init__.py000066400000000000000000000017011505160700500251400ustar00rootroot00000000000000# Xlib.support.__init__ -- support code package # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA __all__ = [ 'lock', 'connect' # The platform specific modules should not be listed here ] Nagstamon-master/Nagstamon/thirdparty/Xlib/support/connect.py000066400000000000000000000054201505160700500250340ustar00rootroot00000000000000# Xlib.support.connect -- OS-independent display connection functions # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys # List the modules which contain the corresponding functions _display_mods = { 'OpenVMS': 'vms_connect', } _default_display_mod = 'unix_connect' _socket_mods = { 'OpenVMS': 'vms_connect' } _default_socket_mod = 'unix_connect' _auth_mods = { 'OpenVMS': 'vms_connect' } _default_auth_mod = 'unix_connect' # Figure out which OS we're using. # sys.platform is either "OS-ARCH" or just "OS". platform = sys.platform.split('-')[0] def get_display(display): """dname, host, dno, screen = get_display(display) Parse DISPLAY into its components. If DISPLAY is None, use the default display. The return values are: DNAME -- the full display name (string) HOST -- the host name (string, possibly empty) DNO -- display number (integer) SCREEN -- default screen number (integer) """ modname = _display_mods.get(platform, _default_display_mod) mod = __import__(modname, globals(),level=1) return mod.get_display(display) def get_socket(dname, host, dno): """socket = get_socket(dname, host, dno) Connect to the display specified by DNAME, HOST and DNO, which are the corresponding values from a previous call to get_display(). Return SOCKET, a new socket object connected to the X server. """ modname = _socket_mods.get(platform, _default_socket_mod) mod = __import__(modname, globals(),level=1) return mod.get_socket(dname, host, dno) def get_auth(sock, dname, host, dno): """auth_name, auth_data = get_auth(sock, dname, host, dno) Return authentication data for the display on the other side of SOCK, which was opened with DNAME, HOST and DNO. Return AUTH_NAME and AUTH_DATA, two strings to be used in the connection setup request. """ modname = _auth_mods.get(platform, _default_auth_mod) mod = __import__(modname, globals(),level=1) return mod.get_auth(sock, dname, host, dno) Nagstamon-master/Nagstamon/thirdparty/Xlib/support/lock.py000066400000000000000000000030041505160700500243270ustar00rootroot00000000000000# Xlib.support.lock -- allocate a lock # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA class _DummyLock: def __init__(self): # This might be nerdy, but by assigning methods like this # instead of defining them all, we create a single bound # method object once instead of one each time one of the # methods is called. # This gives some speed improvements which should reduce the # impact of the threading infrastructure in the regular code, # when not using threading. self.acquire = self.release = self.locked = self.__noop def __noop(self, *args): return # More optimisations: we use a single lock for all lock instances _dummy_lock = _DummyLock() def allocate_lock(): return _dummy_lock Nagstamon-master/Nagstamon/thirdparty/Xlib/support/unix_connect.py000066400000000000000000000120561505160700500261020ustar00rootroot00000000000000# Xlib.support.unix_connect -- Unix-type display connection functions # # Copyright (C) 2000,2002 Peter Liljenberg # Copyright (C) 2013 LiuLang # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import fcntl import os import platform import re import socket F_SETFD = fcntl.F_SETFD FD_CLOEXEC = fcntl.FD_CLOEXEC from Xlib import error, xauth uname = platform.uname() if (uname[0] == 'Darwin') and ([int(x) for x in uname[2].split('.')] >= [9, 0]): display_re = re.compile(r'^([-a-zA-Z0-9._/]*):([0-9]+)(\.([0-9]+))?$') else: display_re = re.compile(r'^([-a-zA-Z0-9._]*):([0-9]+)(\.([0-9]+))?$') def get_display(display): # Use $DISPLAY if display isn't provided if display is None: display = os.environ.get('DISPLAY', '') m = display_re.match(display) if not m: raise error.DisplayNameError(display) name = display host = m.group(1) if host == 'unix': host = '' dno = int(m.group(2)) screen = m.group(4) if screen: screen = int(screen) else: screen = 0 return name, host, dno, screen def get_socket(dname, host, dno): try: # Darwin funky socket if (uname[0] == 'Darwin') and host and host.startswith('/tmp/'): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect(dname) # If hostname (or IP) is provided, use TCP socket elif host: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, 6000 + dno)) # Else use Unix socket else: s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.connect('/tmp/.X11-unix/X%d' % dno) except OSError as val: raise error.DisplayConnectionError(dname, str(val)) # Make sure that the connection isn't inherited in child processes fcntl.fcntl(s.fileno(), F_SETFD, FD_CLOEXEC) return s def new_get_auth(sock, dname, host, dno): # Translate socket address into the xauth domain if (uname[0] == 'Darwin') and host and host.startswith('/tmp/'): family = xauth.FamilyLocal addr = socket.gethostname() elif host: family = xauth.FamilyInternet # Convert the prettyprinted IP number into 4-octet string. # Sometimes these modules are too damn smart... octets = sock.getpeername()[0].split('.') addr = ''.join(map(lambda x: chr(int(x)), octets)) else: family = xauth.FamilyLocal addr = socket.gethostname() au = xauth.Xauthority() while 1: try: return au.get_best_auth(family, addr, dno) except error.XNoAuthError: pass # We need to do this to handle ssh's X forwarding. It sets # $DISPLAY to localhost:10, but stores the xauth cookie as if # DISPLAY was :10. Hence, if localhost and not found, try # again as a Unix socket. if family == xauth.FamilyInternet and addr == '\x7f\x00\x00\x01': family = xauth.FamilyLocal addr = socket.gethostname() else: return '', '' def old_get_auth(sock, dname, host, dno): # Find authorization cookie auth_name = auth_data = '' try: # We could parse .Xauthority, but xauth is simpler # although more inefficient data = os.popen('xauth list %s 2>/dev/null' % dname).read() # If there's a cookie, it is of the format # DISPLAY SCHEME COOKIE # We're interested in the two last parts for the # connection establishment lines = data.split('\n') if len(lines) >= 1: parts = lines[0].split(None, 2) if len(parts) == 3: auth_name = parts[1] hexauth = parts[2] auth = '' # Translate hexcode into binary for i in range(0, len(hexauth), 2): auth = auth + chr(int(hexauth[i:i+2], 16)) auth_data = auth except os.error: pass if not auth_data and host == 'localhost': # 127.0.0.1 counts as FamilyLocal, not FamilyInternet # See Xtransutil.c:ConvertAddress. # There might be more ways to spell 127.0.0.1 but # 'localhost', yet this code fixes a the case of # OpenSSH tunneling X. return get_auth('unix:%d' % dno, 'unix', dno) return auth_name, auth_data get_auth = new_get_auth Nagstamon-master/Nagstamon/thirdparty/Xlib/support/vms_connect.py000066400000000000000000000040151505160700500257200ustar00rootroot00000000000000# Xlib.support.vms_connect -- VMS-type display connection functions # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import re import socket from Xlib import error display_re = re.compile(r'^([-a-zA-Z0-9._]*):([0-9]+)(\.([0-9]+))?$') def get_display(display): # Use dummy display if none is set. We really should # check DECW$DISPLAY instead, but that has to wait if display is None: return ':0.0', 'localhost', 0, 0 m = display_re.match(display) if not m: raise error.DisplayNameError(display) name = display # Always return a host, since we don't have AF_UNIX sockets host = m.group(1) if not host: host = 'localhost' dno = int(m.group(2)) screen = m.group(4) if screen: screen = int(screen) else: screen = 0 return name, host, dno, screen def get_socket(dname, host, dno): try: # Always use TCP/IP sockets. Later it would be nice to # be able to use DECNET och LOCAL connections. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, 6000 + dno)) except OSError as val: raise error.DisplayConnectionError(dname, str(val)) return s def get_auth(sock, dname, host, dno): # VMS doesn't have xauth return '', '' Nagstamon-master/Nagstamon/thirdparty/Xlib/threaded.py000066400000000000000000000022201505160700500234420ustar00rootroot00000000000000# Xlib.threaded -- Import this module to enable threading # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA try: # Python 3 import _thread as thread except ImportError: # Python 2 import thread # We change the allocate_lock function in Xlib.support.lock to # return a basic Python lock, instead of the default dummy lock from Xlib.support import lock lock.allocate_lock = thread.allocate_lock Nagstamon-master/Nagstamon/thirdparty/Xlib/xauth.py000066400000000000000000000101611505160700500230160ustar00rootroot00000000000000# Xlib.xauth -- ~/.Xauthority access # # Copyright (C) 2000 Peter Liljenberg # Copyright (C) 2013 LiuLang # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import os import struct from Xlib import X, error FamilyInternet = X.FamilyInternet FamilyDECnet = X.FamilyDECnet FamilyChaos = X.FamilyChaos FamilyLocal = 256 class Xauthority: def __init__(self, filename = None): if filename is None: filename = os.environ.get('XAUTHORITY') if filename is None: try: filename = os.path.join(os.environ['HOME'], '.Xauthority') except KeyError: raise error.XauthError( '$HOME not set, cannot find ~/.Xauthority') try: raw = open(filename, 'rb').read() except OSError as err: raise error.XauthError('~/.Xauthority: %s' % err) self.entries = [] # entry format (all shorts in big-endian) # short family; # short addrlen; # char addr[addrlen]; # short numlen; # char num[numlen]; # short namelen; # char name[namelen]; # short datalen; # char data[datalen]; n = 0 try: while n < len(raw): family, = struct.unpack('>H', raw[n:n+2]) n = n + 2 length, = struct.unpack('>H', raw[n:n+2]) n = n + length + 2 addr = raw[n - length : n] length, = struct.unpack('>H', raw[n:n+2]) n = n + length + 2 num = raw[n - length : n] length, = struct.unpack('>H', raw[n:n+2]) n = n + length + 2 name = raw[n - length : n] length, = struct.unpack('>H', raw[n:n+2]) n = n + length + 2 data = raw[n - length : n] if len(data) != length: break self.entries.append((family, addr, num, name, data, )) except struct.error as e: print("Xlib.xauth: warning, failed to parse part of xauthority file (%s), aborting all further parsing" % filename) if len(self.entries) == 0: print("Xlib.xauth: warning, no xauthority details available") # raise an error? this should get partially caught by the XNoAuthError in get_best_auth.. def __len__(self): return len(self.entries) def __getitem__(self, i): return self.entries[i] def get_best_auth(self, family, address, dispno, types = ( b"MIT-MAGIC-COOKIE-1", )): """Find an authentication entry matching FAMILY, ADDRESS and DISPNO. The name of the auth scheme must match one of the names in TYPES. If several entries match, the first scheme in TYPES will be choosen. If an entry is found, the tuple (name, data) is returned, otherwise XNoAuthError is raised. """ num = str(dispno).encode() address = address.encode() matches = {} for efam, eaddr, enum, ename, edata in self.entries: if efam == family and eaddr == address and num == enum: matches[ename] = edata for t in types: try: return (t, matches[t]) except KeyError: pass raise error.XNoAuthError((family, address, dispno)) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/000077500000000000000000000000001505160700500227525ustar00rootroot00000000000000Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/__init__.py000066400000000000000000000017151505160700500250670ustar00rootroot00000000000000# Xlib.xobject.__init__ -- glue for Xlib.xobject package # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA __all__ = [ 'colormap', 'cursor', 'drawable', 'fontable', 'icccm', 'resource', ] Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/colormap.py000066400000000000000000000131501505160700500251400ustar00rootroot00000000000000# Xlib.xobject.colormap -- colormap object # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import re from Xlib import error from Xlib.protocol import request from Xlib.xobject import resource rgb_res = [ re.compile(r'\Argb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})\Z'), re.compile(r'\A#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\Z'), re.compile(r'\A#([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])\Z'), re.compile(r'\A#([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])\Z'), re.compile(r'\A#([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])\Z'), ] class Colormap(resource.Resource): __colormap__ = resource.Resource.__resource__ def free(self, onerror = None): request.FreeColormap(display = self.display, onerror = onerror, cmap = self.id) self.display.free_resource_id(self.id) def copy_colormap_and_free(self, scr_cmap): mid = self.display.allocate_resource_id() request.CopyColormapAndFree(display = self.display, mid = mid, src_cmap = src_cmap) cls = self.display.get_resource_class('colormap', Colormap) return cls(self.display, mid, owner = 1) def install_colormap(self, onerror = None): request.InstallColormap(display = self.display, onerror = onerror, cmap = self.id) def uninstall_colormap(self, onerror = None): request.UninstallColormap(display = self.display, onerror = onerror, cmap = self.id) def alloc_color(self, red, green, blue): return request.AllocColor(display = self.display, cmap = self.id, red = red, green = green, blue = blue) def alloc_named_color(self, name): for r in rgb_res: m = r.match(name) if m: rs = m.group(1) r = int(rs + '0' * (4 - len(rs)), 16) gs = m.group(2) g = int(gs + '0' * (4 - len(gs)), 16) bs = m.group(3) b = int(bs + '0' * (4 - len(bs)), 16) return self.alloc_color(r, g, b) try: return request.AllocNamedColor(display = self.display, cmap = self.id, name = name) except error.BadName: return None def alloc_color_cells(self, contiguous, colors, planes): return request.AllocColorCells(display = self.display, contiguous = contiguous, cmap = self.id, colors = colors, planes = planes) def alloc_color_planes(self, contiguous, colors, red, green, blue): return request.AllocColorPlanes(display = self.display, contiguous = contiguous, cmap = self.id, colors = colors, red = red, green = green, blue = blue) def free_colors(self, pixels, plane_mask, onerror = None): request.FreeColors(display = self.display, onerror = onerror, cmap = self.id, plane_mask = plane_mask, pixels = pixels) def store_colors(self, items, onerror = None): request.StoreColors(display = self.display, onerror = onerror, cmap = self.id, items = items) def store_named_color(self, name, pixel, flags, onerror = None): request.StoreNamedColor(display = self.display, onerror = onerror, flags = flags, cmap = self.id, pixel = pixel, name = name) def query_colors(self, pixels): r = request.QueryColors(display = self.display, cmap = self.id, pixels = pixels) return r.colors def lookup_color(self, name): return request.LookupColor(display = self.display, cmap = self.id, name = name) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/cursor.py000066400000000000000000000034611505160700500246450ustar00rootroot00000000000000# Xlib.xobject.cursor -- cursor object # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib.protocol import request from Xlib.xobject import resource class Cursor(resource.Resource): __cursor__ = resource.Resource.__resource__ def free(self, onerror = None): request.FreeCursor(display = self.display, onerror = onerror, cursor = self.id) self.display.free_resource_id(self.id) def recolor(self, f_rgb, b_rgb, onerror = None): back_red, back_green, back_blue = b_rgb fore_red, fore_green, fore_blue = f_rgb request.RecolorCursor(display = self.display, onerror = onerror, cursor = self.id, fore_red = fore_red, fore_green = fore_green, fore_blue = fore_blue, back_red = back_red, back_green = back_green, back_blue = back_blue) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/drawable.py000066400000000000000000001007561505160700500251160ustar00rootroot00000000000000# Xlib.xobject.drawable -- drawable objects (window and pixmap) # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib import X, Xatom, Xutil from Xlib.protocol import request, rq # Other X resource objects from Xlib.xobject import resource, colormap, cursor, fontable, icccm class Drawable(resource.Resource): __drawable__ = resource.Resource.__resource__ def get_geometry(self): return request.GetGeometry(display = self.display, drawable = self) def create_pixmap(self, width, height, depth): pid = self.display.allocate_resource_id() request.CreatePixmap(display = self.display, depth = depth, pid = pid, drawable = self.id, width = width, height = height) cls = self.display.get_resource_class('pixmap', Pixmap) return cls(self.display, pid, owner = 1) def create_gc(self, **keys): cid = self.display.allocate_resource_id() request.CreateGC(display = self.display, cid = cid, drawable = self.id, attrs = keys) cls = self.display.get_resource_class('gc', fontable.GC) return cls(self.display, cid, owner = 1) def copy_area(self, gc, src_drawable, src_x, src_y, width, height, dst_x, dst_y, onerror = None): request.CopyArea(display = self.display, onerror = onerror, src_drawable = src_drawable, dst_drawable = self.id, gc = gc, src_x = src_x, src_y = src_y, dst_x = dst_x, dst_y = dst_y, width = width, height = height) def copy_plane(self, gc, src_drawable, src_x, src_y, width, height, dst_x, dst_y, bit_plane, onerror = None): request.CopyPlane(display = self.display, onerror = onerror, src_drawable = src_drawable, dst_drawable = self.id, gc = gc, src_x = src_x, src_y = src_y, dst_x = dst_x, dst_y = dst_y, width = width, height = height, bit_plane = bit_plane) def poly_point(self, gc, coord_mode, points, onerror = None): request.PolyPoint(display = self.display, onerror = onerror, coord_mode = coord_mode, drawable = self.id, gc = gc, points = points) def point(self, gc, x, y, onerror = None): request.PolyPoint(display = self.display, onerror = onerror, coord_mode = X.CoordModeOrigin, drawable = self.id, gc = gc, points = [(x, y)]) def poly_line(self, gc, coord_mode, points, onerror = None): request.PolyLine(display = self.display, onerror = onerror, coord_mode = coord_mode, drawable = self.id, gc = gc, points = points) def line(self, gc, x1, y1, x2, y2, onerror = None): request.PolySegment(display = self.display, onerror = onerror, drawable = self.id, gc = gc, segments = [(x1, y1, x2, y2)]) def poly_segment(self, gc, segments, onerror = None): request.PolySegment(display = self.display, onerror = onerror, drawable = self.id, gc = gc, segments = segments) def poly_rectangle(self, gc, rectangles, onerror = None): request.PolyRectangle(display = self.display, onerror = onerror, drawable = self.id, gc = gc, rectangles = rectangles) def rectangle(self, gc, x, y, width, height, onerror = None): request.PolyRectangle(display = self.display, onerror = onerror, drawable = self.id, gc = gc, rectangles = [(x, y, width, height)]) def poly_arc(self, gc, arcs, onerror = None): request.PolyArc(display = self.display, onerror = onerror, drawable = self.id, gc = gc, arcs = arcs) def arc(self, gc, x, y, width, height, angle1, angle2, onerror = None): request.PolyArc(display = self.display, onerror = onerror, drawable = self.id, gc = gc, arcs = [(x, y, width, height, angle1, angle2)]) def fill_poly(self, gc, shape, coord_mode, points, onerror = None): request.FillPoly(display = self.display, onerror = onerror, shape = shape, coord_mode = coord_mode, drawable = self.id, gc = gc, points = points) def poly_fill_rectangle(self, gc, rectangles, onerror = None): request.PolyFillRectangle(display = self.display, onerror = onerror, drawable = self.id, gc = gc, rectangles = rectangles) def fill_rectangle(self, gc, x, y, width, height, onerror = None): request.PolyFillRectangle(display = self.display, onerror = onerror, drawable = self.id, gc = gc, rectangles = [(x, y, width, height)]) def poly_fill_arc(self, gc, arcs, onerror = None): request.PolyFillArc(display = self.display, onerror = onerror, drawable = self.id, gc = gc, arcs = arcs) def fill_arc(self, gc, x, y, width, height, angle1, angle2, onerror = None): request.PolyFillArc(display = self.display, onerror = onerror, drawable = self.id, gc = gc, arcs = [(x, y, width, height, angle1, angle2)]) def put_image(self, gc, x, y, width, height, format, depth, left_pad, data, onerror = None): request.PutImage(display = self.display, onerror = onerror, format = format, drawable = self.id, gc = gc, width = width, height = height, dst_x = x, dst_y = y, left_pad = left_pad, depth = depth, data = data) # Trivial little method for putting PIL images. Will break on anything # but depth 1 or 24... def put_pil_image(self, gc, x, y, image, onerror = None): width, height = image.size if image.mode == '1': format = X.XYBitmap depth = 1 if self.display.info.bitmap_format_bit_order == 0: rawmode = '1;R' else: rawmode = '1' pad = self.display.info.bitmap_format_scanline_pad stride = roundup(width, pad) >> 3 elif image.mode == 'RGB': format = X.ZPixmap depth = 24 if self.display.info.image_byte_order == 0: rawmode = 'BGRX' else: rawmode = 'RGBX' pad = self.display.info.bitmap_format_scanline_pad unit = self.display.info.bitmap_format_scanline_unit stride = roundup(width * unit, pad) >> 3 else: raise ValueError('Unknown data format') maxlen = (self.display.info.max_request_length << 2) \ - request.PutImage._request.static_size split = maxlen // stride x1 = 0 x2 = width y1 = 0 while y1 < height: h = min(height, split) if h < height: subimage = image.crop((x1, y1, x2, y1 + h)) else: subimage = image w, h = subimage.size data = subimage.tostring("raw", rawmode, stride, 0) self.put_image(gc, x, y, w, h, format, depth, 0, data) y1 = y1 + h y = y + h def get_image(self, x, y, width, height, format, plane_mask): return request.GetImage(display = self.display, format = format, drawable = self.id, x = x, y = y, width = width, height = height, plane_mask = plane_mask) def draw_text(self, gc, x, y, text, onerror = None): request.PolyText8(display = self.display, onerror = onerror, drawable = self.id, gc = gc, x = x, y = y, items = [text]) def poly_text(self, gc, x, y, items, onerror = None): request.PolyText8(display = self.display, onerror = onerror, drawable = self.id, gc = gc, x = x, y = y, items = items) def poly_text_16(self, gc, x, y, items, onerror = None): request.PolyText16(display = self.display, onerror = onerror, drawable = self.id, gc = gc, x = x, y = y, items = items) def image_text(self, gc, x, y, string, onerror = None): request.ImageText8(display = self.display, onerror = onerror, drawable = self.id, gc = gc, x = x, y = y, string = string) def image_text_16(self, gc, x, y, string, onerror = None): request.ImageText16(display = self.display, onerror = onerror, drawable = self.id, gc = gc, x = x, y = y, string = string) def query_best_size(self, item_class, width, height): return request.QueryBestSize(display = self.display, item_class = item_class, drawable = self.id, width = width, height = height) class Window(Drawable): __window__ = resource.Resource.__resource__ def create_window(self, x, y, width, height, border_width, depth, window_class = X.CopyFromParent, visual = X.CopyFromParent, onerror = None, **keys): wid = self.display.allocate_resource_id() request.CreateWindow(display = self.display, onerror = onerror, depth = depth, wid = wid, parent = self.id, x = x, y = y, width = width, height = height, border_width = border_width, window_class = window_class, visual = visual, attrs = keys) cls = self.display.get_resource_class('window', Window) return cls(self.display, wid, owner = 1) def change_attributes(self, onerror = None, **keys): request.ChangeWindowAttributes(display = self.display, onerror = onerror, window = self.id, attrs = keys) def get_attributes(self): return request.GetWindowAttributes(display = self.display, window = self.id) def destroy(self, onerror = None): request.DestroyWindow(display = self.display, onerror = onerror, window = self.id) self.display.free_resource_id(self.id) def destroy_sub_windows(self, onerror = None): request.DestroySubWindows(display = self.display, onerror = onerror, window = self.id) def change_save_set(self, mode, onerror = None): request.ChangeSaveSet(display = self.display, onerror = onerror, mode = mode, window = self.id) def reparent(self, parent, x, y, onerror = None): request.ReparentWindow(display = self.display, onerror = onerror, window = self.id, parent = parent, x = x, y = y) def map(self, onerror = None): request.MapWindow(display = self.display, onerror = onerror, window = self.id) def map_sub_windows(self, onerror = None): request.MapSubwindows(display = self.display, onerror = onerror, window = self.id) def unmap(self, onerror = None): request.UnmapWindow(display = self.display, onerror = onerror, window = self.id) def unmap_sub_windows(self, onerror = None): request.UnmapSubwindows(display = self.display, onerror = onerror, window = self.id) def configure(self, onerror = None, **keys): request.ConfigureWindow(display = self.display, onerror = onerror, window = self.id, attrs = keys) def circulate(self, direction, onerror = None): request.CirculateWindow(display = self.display, onerror = onerror, direction = direction, window = self.id) def raise_window(self, onerror = None): """alias for raising the window to the top - as in XRaiseWindow""" self.configure(onerror, stack_mode = X.Above) def query_tree(self): return request.QueryTree(display = self.display, window = self.id) def change_property(self, property, type, format, data, mode = X.PropModeReplace, onerror = None): request.ChangeProperty(display = self.display, onerror = onerror, mode = mode, window = self.id, property = property, type = type, data = (format, data)) def delete_property(self, property, onerror = None): request.DeleteProperty(display = self.display, onerror = onerror, window = self.id, property = property) def get_property(self, property, type, offset, length, delete = 0): r = request.GetProperty(display = self.display, delete = delete, window = self.id, property = property, type = type, long_offset = offset, long_length = length) if r.property_type: fmt, value = r.value r.format = fmt r.value = value return r else: return None def get_full_property(self, property, type, sizehint = 10): prop = self.get_property(property, type, 0, sizehint) if prop: val = prop.value if prop.bytes_after: prop = self.get_property(property, type, sizehint, prop.bytes_after // 4 + 1) val = val + prop.value prop.value = val return prop else: return None def list_properties(self): r = request.ListProperties(display = self.display, window = self.id) return r.atoms def set_selection_owner(self, selection, time, onerror = None): request.SetSelectionOwner(display = self.display, onerror = onerror, window = self.id, selection = selection, time = time) def convert_selection(self, selection, target, property, time, onerror = None): request.ConvertSelection(display = self.display, onerror = onerror, requestor = self.id, selection = selection, target = target, property = property, time = time) def send_event(self, event, event_mask = 0, propagate = 0, onerror = None): request.SendEvent(display = self.display, onerror = onerror, propagate = propagate, destination = self.id, event_mask = event_mask, event = event) def grab_pointer(self, owner_events, event_mask, pointer_mode, keyboard_mode, confine_to, cursor, time): r = request.GrabPointer(display = self.display, owner_events = owner_events, grab_window = self.id, event_mask = event_mask, pointer_mode = pointer_mode, keyboard_mode = keyboard_mode, confine_to = confine_to, cursor = cursor, time = time) return r.status def grab_button(self, button, modifiers, owner_events, event_mask, pointer_mode, keyboard_mode, confine_to, cursor, onerror = None): request.GrabButton(display = self.display, onerror = onerror, owner_events = owner_events, grab_window = self.id, event_mask = event_mask, pointer_mode = pointer_mode, keyboard_mode = keyboard_mode, confine_to = confine_to, cursor = cursor, button = button, modifiers = modifiers) def ungrab_button(self, button, modifiers, onerror = None): request.UngrabButton(display = self.display, onerror = onerror, button = button, grab_window = self.id, modifiers = modifiers) def grab_keyboard(self, owner_events, pointer_mode, keyboard_mode, time): r = request.GrabKeyboard(display = self.display, owner_events = owner_events, grab_window = self.id, time = time, pointer_mode = pointer_mode, keyboard_mode = keyboard_mode) return r.status def grab_key(self, key, modifiers, owner_events, pointer_mode, keyboard_mode, onerror = None): request.GrabKey(display = self.display, onerror = onerror, owner_events = owner_events, grab_window = self.id, modifiers = modifiers, key = key, pointer_mode = pointer_mode, keyboard_mode = keyboard_mode) def ungrab_key(self, key, modifiers, onerror = None): request.UngrabKey(display = self.display, onerror = onerror, key = key, grab_window = self.id, modifiers = modifiers) def query_pointer(self): return request.QueryPointer(display = self.display, window = self.id) def get_motion_events(self, start, stop): r = request.GetMotionEvents(display = self.display, window = self.id, start = start, stop = stop) return r.events def translate_coords(self, src_window, src_x, src_y): return request.TranslateCoords(display = self.display, src_wid = src_window, dst_wid = self.id, src_x = src_x, src_y = src_y) def warp_pointer(self, x, y, src_window = 0, src_x = 0, src_y = 0, src_width = 0, src_height = 0, onerror = None): request.WarpPointer(display = self.display, onerror = onerror, src_window = src_window, dst_window = self.id, src_x = src_x, src_y = src_y, src_width = src_width, src_height = src_height, dst_x = x, dst_y = y) def set_input_focus(self, revert_to, time, onerror = None): request.SetInputFocus(display = self.display, onerror = onerror, revert_to = revert_to, focus = self.id, time = time) def clear_area(self, x = 0, y = 0, width = 0, height = 0, exposures = 0, onerror = None): request.ClearArea(display = self.display, onerror = onerror, exposures = exposures, window = self.id, x = x, y = y, width = width, height = height) def create_colormap(self, visual, alloc): mid = self.display.allocate_resource_id() request.CreateColormap(display = self.display, alloc = alloc, mid = mid, window = self.id, visual = visual) cls = self.display.get_resource_class('colormap', colormap.Colormap) return cls(self.display, mid, owner = 1) def list_installed_colormaps(self): r = request.ListInstalledColormaps(display = self.display, window = self.id) return r.cmaps def rotate_properties(self, properties, delta, onerror = None): request.RotateProperties(display = self.display, onerror = onerror, window = self.id, delta = delta, properties = properties) def set_wm_name(self, name, onerror = None): self.change_property(Xatom.WM_NAME, Xatom.STRING, 8, name, onerror = onerror) def get_wm_name(self): d = self.get_full_property(Xatom.WM_NAME, Xatom.STRING) if d is None or d.format != 8: return None else: return d.value def set_wm_icon_name(self, name, onerror = None): self.change_property(Xatom.WM_ICON_NAME, Xatom.STRING, 8, name, onerror = onerror) def get_wm_icon_name(self): d = self.get_full_property(Xatom.WM_ICON_NAME, Xatom.STRING) if d is None or d.format != 8: return None else: return d.value def set_wm_class(self, inst, cls, onerror = None): self.change_property(Xatom.WM_CLASS, Xatom.STRING, 8, '%s\0%s\0' % (inst, cls), onerror = onerror) def get_wm_class(self): d = self.get_full_property(Xatom.WM_CLASS, Xatom.STRING) if d is None or d.format != 8: return None else: parts = d.value.split('\0') if len(parts) < 2: return None else: return parts[0], parts[1] def set_wm_transient_for(self, window, onerror = None): self.change_property(Xatom.WM_TRANSIENT_FOR, Xatom.WINDOW, 32, window.id, onerror = onerror) def get_wm_transient_for(self): d = self.get_property(Xatom.WM_TRANSIENT_FOR, Xatom.WINDOW, 0, 1) if d is None or d.format != 32 or len(d.value) < 1: return None else: cls = self.display.get_resource_class('window', Window) return cls(self.display, d.value[0]) def set_wm_protocols(self, protocols, onerror = None): self.change_property(self.display.get_atom('WM_PROTOCOLS'), Xatom.ATOM, 32, protocols, onerror = onerror) def get_wm_protocols(self): d = self.get_full_property(self.display.get_atom('WM_PROTOCOLS'), Xatom.ATOM) if d is None or d.format != 32: return [] else: return d.value def set_wm_colormap_windows(self, windows, onerror = None): self.change_property(self.display.get_atom('WM_COLORMAP_WINDOWS'), Xatom.WINDOW, 32, [w.id for w in windows], onerror = onerror) def get_wm_colormap_windows(self): d = self.get_full_property(self.display.get_atom('WM_COLORMAP_WINDOWS'), Xatom.WINDOW) if d is None or d.format != 32: return [] else: cls = self.display.get_resource_class('window', Window) return list(map(lambda i, d = self.display, c = cls: c(d, i), d.value)) def set_wm_client_machine(self, name, onerror = None): self.change_property(Xatom.WM_CLIENT_MACHINE, Xatom.STRING, 8, name, onerror = onerror) def get_wm_client_machine(self): d = self.get_full_property(Xatom.WM_CLIENT_MACHINE, Xatom.STRING) if d is None or d.format != 8: return None else: return d.value def set_wm_normal_hints(self, hints = {}, onerror = None, **keys): self._set_struct_prop(Xatom.WM_NORMAL_HINTS, Xatom.WM_SIZE_HINTS, icccm.WMNormalHints, hints, keys, onerror) def get_wm_normal_hints(self): return self._get_struct_prop(Xatom.WM_NORMAL_HINTS, Xatom.WM_SIZE_HINTS, icccm.WMNormalHints) def set_wm_hints(self, hints = {}, onerror = None, **keys): self._set_struct_prop(Xatom.WM_HINTS, Xatom.WM_HINTS, icccm.WMHints, hints, keys, onerror) def get_wm_hints(self): return self._get_struct_prop(Xatom.WM_HINTS, Xatom.WM_HINTS, icccm.WMHints) def set_wm_state(self, hints = {}, onerror = None, **keys): atom = self.display.get_atom('WM_STATE') self._set_struct_prop(atom, atom, icccm.WMState, hints, keys, onerror) def get_wm_state(self): atom = self.display.get_atom('WM_STATE') return self._get_struct_prop(atom, atom, icccm.WMState) def set_wm_icon_size(self, hints = {}, onerror = None, **keys): self._set_struct_prop(Xatom.WM_ICON_SIZE, Xatom.WM_ICON_SIZE, icccm.WMIconSize, hints, keys, onerror) def get_wm_icon_size(self): return self._get_struct_prop(Xatom.WM_ICON_SIZE, Xatom.WM_ICON_SIZE, icccm.WMIconSize) # Helper function for getting structured properties. # pname and ptype are atoms, and pstruct is a Struct object. # Returns a DictWrapper, or None def _get_struct_prop(self, pname, ptype, pstruct): r = self.get_property(pname, ptype, 0, pstruct.static_size // 4) if r and r.format == 32: value = r.value.tostring() if len(value) == pstruct.static_size: return pstruct.parse_binary(value, self.display)[0] return None # Helper function for setting structured properties. # pname and ptype are atoms, and pstruct is a Struct object. # hints is a mapping or a DictWrapper, keys is a mapping. keys # will be modified. onerror is the error handler. def _set_struct_prop(self, pname, ptype, pstruct, hints, keys, onerror): if isinstance(hints, rq.DictWrapper): keys.update(hints._data) else: keys.update(hints) value = pstruct.to_binary(*(), **keys) self.change_property(pname, ptype, 32, value, onerror = onerror) class Pixmap(Drawable): __pixmap__ = resource.Resource.__resource__ def free(self, onerror = None): request.FreePixmap(display = self.display, onerror = onerror, pixmap = self.id) self.display.free_resource_id(self.id) def create_cursor(self, mask, f_rgb, b_rgb, x, y): fore_red, fore_green, fore_blue = f_rgb back_red, back_green, back_blue = b_rgb cid = self.display.allocate_resource_id() request.CreateCursor(display = self.display, cid = cid, source = self.id, mask = mask, fore_red = fore_red, fore_green = fore_green, fore_blue = fore_blue, back_red = back_red, back_green = back_green, back_blue = back_blue, x = x, y = y) cls = self.display.get_resource_class('cursor', cursor.Cursor) return cls(self.display, cid, owner = 1) def roundup(value, unit): return (value + (unit - 1)) & ~(unit - 1) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/fontable.py000066400000000000000000000101711505160700500251160ustar00rootroot00000000000000# Xlib.xobject.fontable -- fontable objects (GC, font) # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib.protocol import request from Xlib.xobject import resource, cursor class Fontable(resource.Resource): __fontable__ = resource.Resource.__resource__ def query(self): return request.QueryFont(display = self.display, font = self.id) def query_text_extents(self, string): return request.QueryTextExtents(display = self.display, font = self.id, string = string) class GC(Fontable): __gc__ = resource.Resource.__resource__ def change(self, onerror = None, **keys): request.ChangeGC(display = self.display, onerror = onerror, gc = self.id, attrs = keys) def copy(self, src_gc, mask, onerror = None): request.CopyGC(display = self.display, onerror = onerror, src_gc = src_gc, dst_gc = self.id, mask = mask) def set_dashes(self, offset, dashes, onerror = None): request.SetDashes(display = self.display, onerror = onerror, gc = self.id, dash_offset = offset, dashes = dashes) def set_clip_rectangles(self, x_origin, y_origin, rectangles, ordering, onerror = None): request.SetClipRectangles(display = self.display, onerror = onerror, ordering = ordering, gc = self.id, x_origin = x_origin, y_origin = y_origin, rectangles = rectangles) def free(self, onerror = None): request.FreeGC(display = self.display, onerror = onerror, gc = self.id) self.display.free_resource_id(self.id) class Font(Fontable): __font__ = resource.Resource.__resource__ def close(self, onerror = None): request.CloseFont(display = self.display, onerror = onerror, font = self.id) self.display.free_resource_id(self.id) def create_glyph_cursor(self, mask, source_char, mask_char, f_rgb, b_rgb): fore_red, fore_green, fore_blue = f_rgb back_red, back_green, back_blue = b_rgb cid = self.display.allocate_resource_id() request.CreateGlyphCursor(display = self.display, cid = cid, source = self.id, mask = mask, source_char = source_char, mask_char = mask_char, fore_red = fore_red, fore_green = fore_green, fore_blue = fore_blue, back_red = back_red, back_green = back_green, back_blue = back_blue) cls = self.display.get_resource_class('cursor', cursor.Cursor) return cls(self.display, cid, owner = 1) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/icccm.py000066400000000000000000000064471505160700500244150ustar00rootroot00000000000000# Xlib.xobject.icccm -- ICCCM structures # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib import X, Xutil from Xlib.protocol import rq Aspect = rq.Struct( rq.Int32('num'), rq.Int32('denum') ) WMNormalHints = rq.Struct( rq.Card32('flags'), rq.Pad(16), rq.Int32('min_width', default = 0), rq.Int32('min_height', default = 0), rq.Int32('max_width', default = 0), rq.Int32('max_height', default = 0), rq.Int32('width_inc', default = 0), rq.Int32('height_inc', default = 0), rq.Object('min_aspect', Aspect, default = (0, 0)), rq.Object('max_aspect', Aspect, default = (0, 0)), rq.Int32('base_width', default = 0), rq.Int32('base_height', default = 0), rq.Int32('win_gravity', default = 0), ) WMHints = rq.Struct( rq.Card32('flags'), rq.Card32('input', default = 0), rq.Set('initial_state', 4, # withdrawn is totally bogus according to # ICCCM, but some window managers seem to # use this value to identify dockapps. # Oh well. ( Xutil.WithdrawnState, Xutil.NormalState, Xutil.IconicState ), default = Xutil.NormalState), rq.Pixmap('icon_pixmap', default = 0), rq.Window('icon_window', default = 0), rq.Int32('icon_x', default = 0), rq.Int32('icon_y', default = 0), rq.Pixmap('icon_mask', default = 0), rq.Window('window_group', default = 0), ) WMState = rq.Struct( rq.Set('state', 4, ( Xutil.WithdrawnState, Xutil.NormalState, Xutil.IconicState )), rq.Window('icon', ( X.NONE, )), ) WMIconSize = rq.Struct( rq.Card32('min_width'), rq.Card32('min_height'), rq.Card32('max_width'), rq.Card32('max_height'), rq.Card32('width_inc'), rq.Card32('height_inc'), ) Nagstamon-master/Nagstamon/thirdparty/Xlib/xobject/resource.py000066400000000000000000000033441505160700500251570ustar00rootroot00000000000000# Xlib.xobject.resource -- any X resource object # # Copyright (C) 2000 Peter Liljenberg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA from Xlib.protocol import request class Resource: def __init__(self, display, rid, owner = 0): self.display = display self.id = rid self.owner = owner def __resource__(self): return self.id def __eq__(self, obj): if isinstance(obj, Resource): if self.display == obj.display: return self.id == obj.id else: return self.display == obj.display else: return id(self) == id(obj) def __hash__(self): return int(self.id) def __str__(self): return '%s(0x%08x)' % (self.__class__, self.id) def __repr__(self): return '<%s 0x%08x>' % (self.__class__, self.id) def kill_client(self, onerror = None): request.KillClient(display = self.display, onerror = onerror, resource = self.id) Nagstamon-master/Nagstamon/thirdparty/__init__.py000066400000000000000000000015731505160700500225350ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """Module thirdparty""" Nagstamon-master/Nagstamon/thirdparty/ewmh.py000066400000000000000000000330331505160700500217320ustar00rootroot00000000000000"""This module intends to provide an implementation of Extended Window Manager Hints, based on the Xlib modules for python. See the freedesktop.org `specification `_ for more information. """ from Xlib import display, X, protocol, Xatom import time class EWMH: """This class provides the ability to get and set properties defined by the EWMH spec. Each property can be accessed in two ways. For example, to get the active window:: win = ewmh.getActiveWindow() # or: win = ewmh.getProperty('_NET_ACTIVE_WINDOW') Similarly, to set the active window:: ewmh.setActiveWindow(myWindow) # or: ewmh.setProperty('_NET_ACTIVE_WINDOW', myWindow) When a property is written, don't forget to really send the notification by flushing requests:: ewmh.display.flush() :param _display: the display to use. If not given, Xlib.display.Display() is used. :param root: the root window to use. If not given, self.display.screen().root is used.""" NET_WM_WINDOW_TYPES = ( '_NET_WM_WINDOW_TYPE_DESKTOP', '_NET_WM_WINDOW_TYPE_DOCK', '_NET_WM_WINDOW_TYPE_TOOLBAR', '_NET_WM_WINDOW_TYPE_MENU', '_NET_WM_WINDOW_TYPE_UTILITY', '_NET_WM_WINDOW_TYPE_SPLASH', '_NET_WM_WINDOW_TYPE_DIALOG', '_NET_WM_WINDOW_TYPE_DROPDOWN_MENU', '_NET_WM_WINDOW_TYPE_POPUP_MENU', '_NET_WM_WINDOW_TYPE_NOTIFICATION', '_NET_WM_WINDOW_TYPE_COMBO', '_NET_WM_WINDOW_TYPE_DND', '_NET_WM_WINDOW_TYPE_NORMAL') """List of strings representing all known window types.""" NET_WM_ACTIONS = ( '_NET_WM_ACTION_MOVE', '_NET_WM_ACTION_RESIZE', '_NET_WM_ACTION_MINIMIZE', '_NET_WM_ACTION_SHADE', '_NET_WM_ACTION_STICK', '_NET_WM_ACTION_MAXIMIZE_HORZ', '_NET_WM_ACTION_MAXIMIZE_VERT', '_NET_WM_ACTION_FULLSCREEN', '_NET_WM_ACTION_CHANGE_DESKTOP', '_NET_WM_ACTION_CLOSE', '_NET_WM_ACTION_ABOVE', '_NET_WM_ACTION_BELOW') """List of strings representing all known window actions.""" NET_WM_STATES = ( '_NET_WM_STATE_MODAL', '_NET_WM_STATE_STICKY', '_NET_WM_STATE_MAXIMIZED_VERT', '_NET_WM_STATE_MAXIMIZED_HORZ', '_NET_WM_STATE_SHADED', '_NET_WM_STATE_SKIP_TASKBAR', '_NET_WM_STATE_SKIP_PAGER', '_NET_WM_STATE_HIDDEN', '_NET_WM_STATE_FULLSCREEN','_NET_WM_STATE_ABOVE', '_NET_WM_STATE_BELOW', '_NET_WM_STATE_DEMANDS_ATTENTION') """List of strings representing all known window states.""" def __init__(self, _display=None, root = None): self.display = _display or display.Display() self.root = root or self.display.screen().root self.__getAttrs = { '_NET_CLIENT_LIST': self.getClientList, '_NET_CLIENT_LIST_STACKING': self.getClientListStacking, '_NET_NUMBER_OF_DESKTOPS': self.getNumberOfDesktops, '_NET_DESKTOP_GEOMETRY': self.getDesktopGeometry, '_NET_DESKTOP_VIEWPORT': self.getDesktopViewPort, '_NET_CURRENT_DESKTOP': self.getCurrentDesktop, '_NET_ACTIVE_WINDOW': self.getActiveWindow, '_NET_WORKAREA': self.getWorkArea, '_NET_SHOWING_DESKTOP': self.getShowingDesktop, '_NET_WM_NAME': self.getWmName, '_NET_WM_VISIBLE_NAME': self.getWmVisibleName, '_NET_WM_DESKTOP': self.getWmDesktop, '_NET_WM_WINDOW_TYPE': self.getWmWindowType, '_NET_WM_STATE': self.getWmState, '_NET_WM_ALLOWED_ACTIONS': self.getWmAllowedActions, '_NET_WM_PID': self.getWmPid, } self.__setAttrs = { '_NET_NUMBER_OF_DESKTOPS': self.setNumberOfDesktops, '_NET_DESKTOP_GEOMETRY': self.setDesktopGeometry, '_NET_DESKTOP_VIEWPORT': self.setDesktopViewport, '_NET_CURRENT_DESKTOP': self.setCurrentDesktop, '_NET_ACTIVE_WINDOW': self.setActiveWindow, '_NET_SHOWING_DESKTOP': self.setShowingDesktop, '_NET_CLOSE_WINDOW': self.setCloseWindow, '_NET_MOVERESIZE_WINDOW': self.setMoveResizeWindow, '_NET_WM_NAME': self.setWmName, '_NET_WM_VISIBLE_NAME': self.setWmVisibleName, '_NET_WM_DESKTOP': self.setWmDesktop, '_NET_WM_STATE': self.setWmState, } # ------------------------ setters properties ------------------------ def setNumberOfDesktops(self, nb): """Set the number of desktops (property _NET_NUMBER_OF_DESKTOPS). :param nb: the number of desired desktops""" self._setProperty('_NET_NUMBER_OF_DESKTOPS', [nb]) def setDesktopGeometry(self, w, h): """Set the desktop geometry (property _NET_DESKTOP_GEOMETRY) :param w: desktop width :param h: desktop height""" self._setProperty('_NET_DESKTOP_GEOMETRY', [w, h]) def setDesktopViewport(self, w, h): """Set the viewport size of the current desktop (property _NET_DESKTOP_VIEWPORT) :param w: desktop width :param h: desktop height""" self._setProperty('_NET_DESKTOP_VIEWPORT', [w, h]) def setCurrentDesktop(self, i): """Set the current desktop (property _NET_CURRENT_DESKTOP). :param i: the desired desktop number""" self._setProperty('_NET_CURRENT_DESKTOP', [i, X.CurrentTime]) def setActiveWindow(self, win): """Set the given window active (property _NET_ACTIVE_WINDOW) :param win: the window object""" self._setProperty('_NET_ACTIVE_WINDOW', [1, X.CurrentTime, win.id], win) def setShowingDesktop(self, show): """Set/unset the mode Showing desktop (property _NET_SHOWING_DESKTOP) :param show: 1 to set the desktop mode, else 0""" self._setProperty('_NET_SHOWING_DESKTOP', [show]) def setCloseWindow(self, win): """Colse the given window (property _NET_CLOSE_WINDOW) :param win: the window object""" self._setProperty('_NET_CLOSE_WINDOW', [int(time.mktime(time.localtime())), 1], win) def setWmName(self, win, name): """Set the property _NET_WM_NAME :param win: the window object :param name: desired name""" self._setProperty('_NET_WM_NAME', name, win) def setWmVisibleName(self, win, name): """Set the property _NET_WM_VISIBLE_NAME :param win: the window object :param name: desired visible name""" self._setProperty('_NET_WM_VISIBLE_NAME', name, win) def setWmDesktop(self, win, i): """Move the window to the desired desktop by changing the property _NET_WM_DESKTOP. :param win: the window object :param i: desired desktop number""" self._setProperty('_NET_WM_DESKTOP', [i, 1], win) def setMoveResizeWindow(self, win, gravity=0, x=None, y=None, w=None, h=None): """Set the property _NET_MOVERESIZE_WINDOW to move or resize the given window. Flags are automatically calculated if x, y, w or h are defined. :param win: the window object :param gravity: gravity (one of the Xlib.X.*Gravity constant or 0) :param x: int or None :param y: int or None :param w: int or None :param h: int or None""" gravity_flags = gravity | 0b0000100000000000 # indicate source (application) if x is None: x = 0 else: gravity_flags = gravity_flags | 0b0000010000000000 # indicate presence of x if y is None: y = 0 else: gravity_flags = gravity_flags | 0b0000001000000000 # indicate presence of y if w is None: w = 0 else: gravity_flags = gravity_flags | 0b0000000100000000 # indicate presence of w if h is None: h = 0 else: gravity_flags = gravity_flags | 0b0000000010000000 # indicate presence of h self._setProperty('_NET_MOVERESIZE_WINDOW', [gravity_flags, x, y, w, h], win) def setWmState(self, win, action, state, state2=0): """Set/unset one or two state(s) for the given window (property _NET_WM_STATE). :param win: the window object :param action: 0 to remove, 1 to add or 2 to toggle state(s) :param state: a state :type state: int or str (see :attr:`NET_WM_STATES`) :param state2: a state or 0 :type state2: int or str (see :attr:`NET_WM_STATES`)""" if type(state) != int: state = self.display.get_atom(state, 1) if type(state2) != int: state2 = self.display.get_atom(state2, 1) self._setProperty('_NET_WM_STATE', [action, state, state2, 1], win) # ------------------------ getters properties ------------------------ def getClientList(self): """Get the list of windows maintained by the window manager for the property _NET_CLIENT_LIST. :return: list of Window objects""" return map(self._createWindow, self._getProperty('_NET_CLIENT_LIST')) def getClientListStacking(self): """Get the list of windows maintained by the window manager for the property _NET_CLIENT_LIST_STACKING. :return: list of Window objects""" return map(self._createWindow, self._getProperty('_NET_CLIENT_LIST_STACKING')) def getNumberOfDesktops(self): """Get the number of desktops (property _NET_NUMBER_OF_DESKTOPS). :return: int""" return self._getProperty('_NET_NUMBER_OF_DESKTOPS')[0] def getDesktopGeometry(self): """Get the desktop geometry (property _NET_DESKTOP_GEOMETRY) as an array of two integers [width, height]. :return: [int, int]""" return self._getProperty('_NET_DESKTOP_GEOMETRY') def getDesktopViewPort(self): """Get the current viewports of each desktop as a list of [x, y] representing the top left corner (property _NET_DESKTOP_VIEWPORT). :return: list of [int, int]""" return self._getProperty('_NET_DESKTOP_VIEWPORT') def getCurrentDesktop(self): """Get the current desktop number (property _NET_CURRENT_DESKTOP) :return: int""" return self._getProperty('_NET_CURRENT_DESKTOP')[0] def getActiveWindow(self): """Get the current active (toplevel) window or None (property _NET_ACTIVE_WINDOW) :return: Window object or None""" active_window = self._getProperty('_NET_ACTIVE_WINDOW') if active_window == None: return None return self._createWindow(active_window[0]) def getWorkArea(self): """Get the work area for each desktop (property _NET_WORKAREA) as a list of [x, y, width, height] :return: a list of [int, int, int, int]""" return self._getProperty('_NET_WORKAREA') def getShowingDesktop(self): """Get the value of "showing the desktop" mode of the window manager (property _NET_SHOWING_DESKTOP). 1 means the mode is activated, and 0 means deactivated. :return: int""" return self._getProperty('_NET_SHOWING_DESKTOP')[0] def getWmName(self, win): """Get the property _NET_WM_NAME for the given window as a string. :param win: the window object :return: str""" return self._getProperty('_NET_WM_NAME', win) def getWmVisibleName(self, win): """Get the property _NET_WM_VISIBLE_NAME for the given window as a string. :param win: the window object :return: str""" return self._getProperty('_NET_WM_VISIBLE_NAME', win) def getWmDesktop(self, win): """Get the current desktop number of the given window (property _NET_WM_DESKTOP). :param win: the window object :return: int""" return self._getProperty('_NET_WM_DESKTOP', win)[0] def getWmWindowType(self, win, str=False): """Get the list of window types of the given window (property _NET_WM_WINDOW_TYPE). :param win: the window object :param str: True to get a list of string types instead of int :return: list of (int|str)""" types = self._getProperty('_NET_WM_WINDOW_TYPE', win) if not str: return types return map(self._getAtomName, wtypes) def getWmState(self, win, str=False): """Get the list of states of the given window (property _NET_WM_STATE). :param win: the window object :param str: True to get a list of string states instead of int :return: list of (int|str)""" states = self._getProperty('_NET_WM_STATE', win) if not str: return states return map(self._getAtomName, states) def getWmAllowedActions(self, win, str=False): """Get the list of allowed actions for the given window (property _NET_WM_ALLOWED_ACTIONS). :param win: the window object :param str: True to get a list of string allowed actions instead of int :return: list of (int|str)""" wAllowedActions = self._getProperty('_NET_WM_ALLOWED_ACTIONS', win) if not str: return wAllowedActions return map(self._getAtomName, wAllowedActions) def getWmPid(self, win): """Get the pid of the application associated to the given window (property _NET_WM_PID) :param win: the window object""" return self._getProperty('_NET_WM_PID', win)[0] def _getProperty(self, _type, win=None): if not win: win = self.root atom = win.get_full_property(self.display.get_atom(_type), X.AnyPropertyType) if atom: return atom.value def _setProperty(self, _type, data, win=None, mask=None): """Send a ClientMessage event to the root window""" if not win: win = self.root if type(data) is str: dataSize = 8 else: data = (data+[0]*(5-len(data)))[:5] dataSize = 32 ev = protocol.event.ClientMessage(window=win, client_type=self.display.get_atom(_type), data=(dataSize, data)) if not mask: mask = (X.SubstructureRedirectMask|X.SubstructureNotifyMask) self.root.send_event(ev, event_mask=mask) def _getAtomName(self, atom): try: return self.display.get_atom_name(atom) except: return 'UNKNOWN' def _createWindow(self, wId): if not wId: return None return self.display.create_resource_object('window', wId) def getReadableProperties(self): """Get all the readable properties' names""" return self.__getAttrs.keys() def getProperty(self, prop, *args, **kwargs): """Get the value of a property. See the corresponding method for the required arguments. For example, for the property _NET_WM_STATE, look for :meth:`getWmState`""" f = self.__getAttrs.get(prop) if not f: raise KeyError('Unknow readable property: %s' % prop) return f(self, *args, **kwargs) def getWritableProperties(self): """Get all the writable properties names""" return self.__setAttrs.keys() def setProperty(self, prop, *args, **kwargs): """Set the value of a property by sending an event on the root window. See the corresponding method for the required arguments. For example, for the property _NET_WM_STATE, look for :meth:`setWmState`""" f = self.__setAttrs.get(prop) if not f: raise KeyError('Unknow writable property: %s' % prop) f(self, *args, **kwargs) Nagstamon-master/Nagstamon/thirdparty/sensu_api.py000066400000000000000000000264321505160700500227650ustar00rootroot00000000000000# This is part of Sangoma's pysensu library found here: https://github.com/sangoma/pysensu # and licensed under the MIT License # # The MIT License (MIT) # Copyright (c) 2015 Sangoma Technologies # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of # the Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import json import logging import requests from requests.auth import HTTPBasicAuth logger = logging.getLogger(__name__) class SensuAPIException(Exception): pass class SensuAPI: def __init__(self, url_base, username=None, password=None, verify=None): self._url_base = url_base self._session = requests.Session() self._session.headers.update({ 'User-Agent': 'PySensu Client v0.9.0' }) self._session.verify = verify self.good_status = (200, 201, 202, 204) if username and password: self._session.auth = HTTPBasicAuth(username, password) elif password and not username: self._session.headers.update({ 'Authorization': 'token {}'.format(password) }) def _request(self, method, path, **kwargs): url = '{}{}'.format(self._url_base, path) logger.debug('{} -> {} with {}'.format(method, url, kwargs)) if method == 'GET': resp = self._session.get(url, **kwargs) elif method == 'POST': resp = self._session.post(url, **kwargs) elif method == 'PUT': resp = self._session.put(url, **kwargs) elif method == 'DELETE': resp = self._session.delete(url, **kwargs) else: raise SensuAPIException( 'Method {} not implemented'.format(method) ) if resp.status_code in self.good_status: logger.debug('{}: {}'.format( resp.status_code, ''.join(resp.text.split('\n'))[0:80] )) return resp elif resp.status_code == 400: logger.error('{}: {}'.format( resp.status_code, resp.text )) raise SensuAPIException('API returned "Bad Request"') else: logger.warning('{}: {}'.format( resp.status_code, resp.text )) raise SensuAPIException('API bad response {}: {}'.format(resp.status_code, resp.text)) """ Clients ops """ def get_clients(self, limit=None, offset=None): """ Returns a list of clients. """ data = {} if limit: data['limit'] = limit if offset: data['offset'] = offset result = self._request('GET', '/clients', data=json.dumps(data)) return result.json() def get_client_data(self, client): """ Returns a client. """ data = self._request('GET', '/clients/{}'.format(client)) return data.json() def get_client_history(self, client): """ Returns the history for a client. """ data = self._request('GET', '/clients/{}/history'.format(client)) return data.json() def delete_client(self, client): """ Removes a client, resolving its current events. (delayed action) """ self._request('DELETE', '/clients/{}'.format(client)) return True """ Events ops """ def get_events(self): """ Returns the list of current events. """ data = self._request('GET', '/events') return data.json() def get_all_client_events(self, client): """ Returns the list of current events for a given client. """ data = self._request('GET', '/events/{}'.format(client)) return data.json() def get_event(self, client, check): """ Returns an event for a given client & check name. """ data = self._request('GET', '/events/{}/{}'.format(client, check)) return data.json() def delete_event(self, client, check): """ Resolves an event for a given check on a given client. (delayed action) """ self._request('DELETE', '/events/{}/{}'.format(client, check)) return True def post_event(self, client, check): """ Resolves an event. (delayed action) """ self._request('POST', '/resolve', json={'client': client, 'check': check}) return True """ Checks ops """ def get_checks(self): """ Returns the list of checks. """ data = self._request('GET', '/checks') return data.json() def get_check(self, check): """ Returns a check. """ data = self._request('GET', '/checks/{}'.format(check)) return data.json() def post_check_request(self, check, subscribers, dc=None): """ Issues a check execution request. """ data = { 'check': check, 'subscribers': [subscribers] } if dc is not None: data['dc'] = dc self._request('POST', '/request', json=data) return True """ Silenced API ops """ def get_silenced(self, limit=None, offset=None): """ Returns a list of silence entries. """ data = {} if limit: data['limit'] = limit if offset: data['offset'] = offset result = self._request('GET', '/silenced', json=data) return result.json() def post_silence_request(self, kwargs): """ Create a silence entry. """ self._request('POST', '/silenced', json=kwargs) return True def clear_silence(self, kwargs): """ Clear a silence entry. """ self._request('POST', '/silenced/clear', json=kwargs) return True """ Aggregates ops """ def get_aggregates(self): """ Returns the list of named aggregates. """ data = self._request('GET', '/aggregates') return data.json() def get_aggregate_check(self, check, age=None): """ Returns the list of aggregates for a given check """ data = {} if age: data['max_age'] = age result = self._request('GET', '/aggregates/{}'.format(check), json=data) return result.json() def delete_aggregate(self, check): """ Deletes all aggregate data for a named aggregate """ self._request('DELETE', '/aggregates/{}'.format(check)) return True """ Status ops """ def get_info(self): """ Returns information on the API. """ data = self._request('GET', '/info') return data.json() def get_health(self, consumers=2, messages=100): """ Returns health information on transport & Redis connections. """ data = {'consumers': consumers, 'messages': messages} try: self._request('GET', '/health', json=data) return True except SensuAPIException: return False """ Results ops """ def get_all_client_results(self): """ Returns the list of results. """ data = self._request('GET', '/results') return data.json() def get_results(self, client): """ Returns a result. """ data = self._request('GET', '/results/{}'.format(client)) return data.json() def get_result(self, client, check): """ Returns an event for a given client & result name. """ data = self._request('GET', '/results/{}/{}'.format(client, check)) return data.json() def delete_result(self, client, check): """ Deletes an check result data for a given check on a given client. """ self._request('DELETE', '/results/{}/{}'.format(client, check)) return True def post_result_data(self, client, check, output, status): """ Posts check result data. """ data = { 'source': client, 'name': check, 'output': output, 'status': status, } self._request('POST', '/results', json=data) return True """ Stashes ops """ def get_stashes(self): """ Returns a list of stashes. """ data = self._request('GET', '/stashes') return data.json() def create_stash(self, payload, path=None): """ Create a stash. (JSON document) """ if path: self._request('POST', '/stashes/{}'.format(path), json=payload) else: self._request('POST', '/stashes', json=payload) return True def delete_stash(self, path): """ Delete a stash. (JSON document) """ self._request('DELETE', '/stashes/{}'.format(path)) return True """ Subscriptions ops (not directly in the Sensu API) """ def get_subscriptions(self, nodes=[]): """ Returns all the channels where (optionally specified) nodes are subscribed """ if len(nodes) > 0: data = [node for node in self.get_clients() if node['name'] in nodes] else: data = self.get_clients() channels = [] for client in data: if 'subscriptions' in client: if isinstance(client['subscriptions'], list): for channel in client['subscriptions']: if channel not in channels: channels.append(channel) else: if client['subscriptions'] not in channels: channels.append(client['subscriptions']) return channels def get_subscriptions_channel(self, search_channel): """ Return all the nodes that are subscribed to the specified channel """ data = self.get_clients() clients = [] for client in data: if 'subscriptions' in client: if isinstance(client['subscriptions'], list): if search_channel in client['subscriptions']: clients.append(client['name']) else: if search_channel == client['subscriptions']: clients.append(client['name']) return clients Nagstamon-master/Nagstamon/thirdparty/sensugo_api.py000066400000000000000000000046171505160700500233140ustar00rootroot00000000000000import json import logging import requests from requests.auth import HTTPBasicAuth logger = logging.getLogger(__name__) class SensuGoAPIException(Exception): pass class SensuGoAPI: _base_api_url = '' _refresh_token = '' _session = None GOOD_RESPONSE_CODES = (200, 201, 202, 204) SEVERITY_CHECK_STATUS = { 0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN' } def __init__(self, base_api_url): self._base_api_url = base_api_url self._session = requests.Session() def auth(self, username, password, verify): response = self._session.get( f'{self._base_api_url}/auth', verify=verify, auth=HTTPBasicAuth(username, password)) self._update_local_tokens(response) def _refresh_access_token(self): response = self._session.post( f'{self._base_api_url}/auth/token', json={'refresh_token': self._refresh_token}) self._update_local_tokens(response) def _update_local_tokens(self, response): if response.status_code in SensuGoAPI.GOOD_RESPONSE_CODES: access_token = response.json()['access_token'] self._session.headers.update({ 'Authorization': f'Bearer {access_token}'}) self._refresh_token = response.json()['refresh_token'] else: logger.error(f'Response code: {response.status_code} {response.text}') raise SensuGoAPIException('API returned bad request') def get_all_events(self): self._refresh_access_token() response = self._session.get(f'{self._base_api_url}/api/core/v2/events') return (response.status_code, response.json()) def has_acquired_token(self): return self._refresh_token != '' @staticmethod def parse_check_status(status_code): status = SensuGoAPI.SEVERITY_CHECK_STATUS[3] if status_code in SensuGoAPI.SEVERITY_CHECK_STATUS: status = SensuGoAPI.SEVERITY_CHECK_STATUS[status_code] return status def create_or_update_silence(self, kwargs): namespace = kwargs['metadata']['namespace'] check = kwargs['metadata']['name'] silence_api = f'/api/core/v2/namespaces/{namespace}/silenced/{check}' self._session.put(self._base_api_url + silence_api, json=kwargs) Nagstamon-master/Nagstamon/thirdparty/zenoss_api.py000066400000000000000000000142441505160700500231470ustar00rootroot00000000000000īģŋ#!/usr/bin/python # port from the zenstamon zenoss_api.py code into nagstamon: # Zenoss-4.x JSON API Example (python) # Copyright (C) 2016 Jake Murphy Far Edge Technology # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA # NOTES: # Currently experimental version, only viewing and acknowledging works at present. import os import base64 import zlib import hashlib import logging import string import sys import urllib import re from collections import deque try: # Separate module or Python <2.6 import simplejson as json except ImportError: # Python >=2.6 import json ZENOSS_INSTANCE = '' ZENOSS_USERNAME = '' ZENOSS_PASSWORD = '' ROUTERS = {'MessagingRouter': 'messaging', 'EventsRouter': 'evconsole', 'ProcessRouter': 'process', 'ServiceRouter': 'service', 'DeviceRouter': 'device', 'NetworkRouter': 'network', 'TemplateRouter': 'template', 'DetailNavRouter': 'detailnav', 'ReportRouter': 'report', 'MibRouter': 'mib', 'ZenPackRouter': 'zenpack'} class ZenossAPI: def __init__(self, debug=False, Server=None): """ Initialise the API connection, log in, and store authentication cookie """ self.set_config_data(Server) self.urlOpener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor()) if debug: self.urlOpener.add_handler(urllib.request.HTTPHandler(debuglevel=1)) self.reqCount = 1 self._login() #login to the zenoss system def _login(self): # Construct POST parameters and submit login. loginParams = urllib.parse.urlencode(dict( __ac_name=self.ZENOSS_USERNAME, __ac_password=self.ZENOSS_PASSWORD, submitted='true', came_from=self.ZENOSS_INSTANCE + '/zport/dmd')).encode('ascii') self.urlOpener.open(self.ZENOSS_INSTANCE + '/zport/acl_users/cookieAuthHelper/login', loginParams) def set_config_data(self, Server): if True: self.ZENOSS_INSTANCE = 'http://' + Server.server_url + ':' + Server.server_port self.ZENOSS_USERNAME = Server.username self.ZENOSS_PASSWORD = Server.password else: pass def _router_request(self, router, method, data=[]): if router not in ROUTERS: raise Exception('Router "' + router + '" not available.') # Contruct a standard URL request for API calls req = urllib.request.Request(self.ZENOSS_INSTANCE + '/zport/dmd/' + ROUTERS[router] + '_router') # NOTE: Content-type MUST be set to 'application/json' for these requests req.add_header('Content-type', 'application/json; charset=utf-8') # Convert the request parameters into JSON reqData = json.dumps([dict( tid=self.reqCount, type='rpc', data=data, method=method, action=router)]).encode('ascii') # Increment the request count ('tid'). More important if sending multiple # calls in a single request self.reqCount += 1 # Submit the request and convert the returned JSON to objects try: response = self.urlOpener.open(req, reqData) except: self._login() return {'result': {'events': []}} #return an empty object if response.code != 200: raise ZenossAPIException("HTTP ERROR %s: %s" % (response.status, response.reason)) try: jsondata = response.read() except: raise ZenossAPIException("Couldn't read response data") if len(jsondata) == 0: raise ZenossAPIException("Received zero json data") try: jsonobj = json.loads(jsondata.decode('utf-8')) except ValueError as msg: self._login() return {'result': {'events': []}} #return an 'empty' json object on failure return jsonobj ''' The API ''' def get_event(self, device=None, component=None, eventClass=None): data = dict(start=0, limit=500, dir='ASC', sort='severity') data['uid'] = '/zport/dmd' data['sort'] = 'device' data['keys'] = ['eventState', 'severity', 'device', 'component', 'eventClass', 'message', 'firstTime', 'lastTime', 'count', 'DevicePriority', 'evid', 'eventClassKey'] data['params'] = dict(severity=[5, 4, 3], eventState=[0, 1], tags=[]) if device: data['params']['device'] = device if component: data['params']['component'] = component if eventClass: data['params']['eventClass'] = eventClass return self._router_request('EventsRouter', 'query', [data])['result'] def set_event_ack(self, evids): data = dict(limit=100) data['evids'] = [evids] return self._router_request('EventsRouter', 'acknowledge', [data])['result'] def remove_event_ack(self, evids): data = dict(limit=100) data['evids'] = [evids] return self._router_request('EventsRouter', 'reopen', [data])['result'] def remove_event(self, evids): data = dict(limit=100) data['evids'] = [evids] return self._router_request('EventsRouter', 'close', [data])['result'] class ZenossAPIException(Exception): """ generic zenoss api exception """ passNagstamon-master/README.md000066400000000000000000000031341505160700500155550ustar00rootroot00000000000000Nagstamon ========= Nagstamon is a status monitor for the desktop. It connects to multiple Nagios, Icinga, Opsview, Centreon, Op5 Monitor/Ninja, Checkmk Multisite, Thruk and monitos monitoring servers. Experimental support is provided for Zabbix, Zenoss and Livestatus monitors. It resides in systray, as a floating statusbar or fullscreen at the desktop showing a brief summary of critical, warning, unknown, unreachable and down hosts and services. It pops up a detailed status overview when being touched by the mouse pointer. Connections to displayed hosts and services are easily established by context menu via SSH, RDP, VNC or any self defined actions. Users can be notified by sound. Hosts and services can be filtered by category and regular expressions. It is inspired by Nagios Checker for Firefox – just without an open Firefox window all the time to monitor the network. Nagstamon is released under the GPLv2 and free to use and modify. Nagstamon is written in Python 3 and uses the Qt 5/6 GUI toolkit which makes it very portable. It has been tested successfully on latest Ubuntu, Debian, Windows, NetBSD, OpenBSD, FreeBSD and MacOS X. It works with GNOME, KDE, Windows and macOS desktops. Successfully tested monitors include: - Nagios 1.x, 2.x, 3.x and 4.x - Icinga 1.2+ and 2.3+ - Opsview 5+ - Centreon 2.3+ - Op5 Monitor 7+ - Checkmk/Multisite 1.1.10+ - Thruk 1.5.0+ - monitos 4.4+ - Prometheus - Alertmanager - Zabbix 2.2+ - Livestatus – experimental - Zenoss – experimental - monitos 3 - experimental - SNAG-View3 - experimental See https://nagstamon.de for further information. Nagstamon-master/SECURITY.md000066400000000000000000000005501505160700500160660ustar00rootroot00000000000000# Security Policy ## Supported Versions Versions currently being supported with security updates: | Version | Supported | | ------- | ------------------ | | 3.8.x | :white_check_mark: | | < 3.8.x | :x: | | 3.9.x | :white_check_mark: | ## Reporting a Vulnerability If you found a vulnerability send it to security@nagstamon.de. Nagstamon-master/TODO.md000066400000000000000000000024361505160700500153710ustar00rootroot00000000000000# Temporary file for tracking TODOs regarding The Big Split - [x] change font like in settings.py - [ ] check_servers.check() - [x] show_macos_dock_icon_if_necessary should be called elsewhere - [x] hide_macos_dock_icon_if_necessary should be called elsewhere - [ ] 'new' -> 'create' - [ ] statuswindow.is_shown_timestamp -= 1 - [x] normal-light activated server in settings dialog when opening settings - was like this before already - [x] spaceless comments - [x] shrink statuswindow when less alerts being active - [ ] debug_queue not in config.py? - [ ] check if statuswindow.update() works in macOS - [ ] check if 'submit check result' still works - [ ] check if no multiple flashing in statusbar is triggered - [x] systray icon menu Windows - [x] switch from full screen to other mode - [x] hover over treeview rows leads to indent changes? - [x] flickering statuswindow when dialog is opened - [ ] position of statuswindow is not centered in relation to statusbar - somehow lagging behind? - [x] starting to move the popped up statuswindow leads to loosing the statusbar - [ ] something weird happened to the CSS in treeview - depending on OS? - [x] build RPM fails - [x] basically working OSes - [x] Linux - [x] Ubuntu - [x] Fedora - [x] Pyinstaller] - [x] Windows - [x] macOS Nagstamon-master/build/000077500000000000000000000000001505160700500153745ustar00rootroot00000000000000Nagstamon-master/build/build.py000066400000000000000000000271161505160700500170540ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import glob import gzip import os, os.path from os import environ from pathlib import Path import platform import shutil import subprocess import sys import zipfile CURRENT_DIR = os.getcwd() NAGSTAMON_DIR = os.path.normpath('{0}{1}..{1}'.format(CURRENT_DIR, os.sep)) sys.path.insert(1, NAGSTAMON_DIR) SCRIPTS_DIR = '{0}{1}scripts-{2}.{3}'.format(CURRENT_DIR, os.sep, sys.version_info.major, sys.version_info.minor) # has to be imported here after NAGSTAMON_DIR was wadded to sys.path from Nagstamon.config import AppInfo from Nagstamon.helpers import get_distro VERSION = AppInfo.VERSION ARCH_WINDOWS = platform.architecture()[0][0:2] ARCH_WINDOWS_OPTS = {'32': ('win32', 'win32', '', 'x86'), '64': ('win-amd64', 'amd64', '(X86)', 'x64compatible')} ARCH_MACOS = platform.machine() ARCH_MACOS_NAMES = {'x86_64': 'Intel', 'arm64': 'ARM'} PYTHON_VERSION = '{0}.{1}'.format(sys.version_info[0], sys.version_info[1]) DIST_NAME, DIST_VERSION, DIST_ID = get_distro() # depending on debug build or not a console window will be shown or not if len(sys.argv) > 1 and sys.argv[1] == 'debug': DEBUG = True # create console window with pyinstaller to get some output GUI_MODE = '--console' # add '_debug' to name of zip file FILENAME_SUFFIX = '_debug' else: DEBUG = False # no console window via pyinstaller GUI_MODE = '--windowed' # also no need for filename suffix FILENAME_SUFFIX = '' # when run by GitHub Actions with PFX and password as environment variables # signing will be done SIGNING = False if 'WIN_SIGNING_CERT_BASE64' in environ \ and 'WIN_SIGNING_PASSWORD' in environ: SIGNING = True def zip_manpage(): # workaround for manpage gzipping bug in bdist_rpm os.chdir(NAGSTAMON_DIR) man = open('Nagstamon/resources/nagstamon.1', 'rb') mangz = gzip.open('Nagstamon/resources/nagstamon.1.gz', 'wb') mangz.writelines(man) mangz.close() man.close() def package_windows(): """ execute steps necessary for compilation of Windows binaries and setup.exe """ # InnoSetup does not like VersionInfoVersion with letters, only 0.0.0.0 schemed numbers if 'alpha' in VERSION.lower() or 'beta' in VERSION.lower() or 'rc' in VERSION.lower() or '-' in VERSION.lower(): VERSION_IS = VERSION.replace('alpha', '').replace('beta', '').replace('rc', '').replace('-', '.').replace('..', '.') VERSION_IS = VERSION_IS.split('.') version_segments = list() for part in VERSION_IS: if len(part) < 4: version_segments.append(part) else: version_segments.append(part[0:4]) version_segments.append(part[4:]) VERSION_IS = '.'.join(version_segments) else: VERSION_IS = VERSION # old-school formatstrings needed for old Debian build base distro jessie and its old python ISCC = r'{0}{1}Inno Setup 6{1}iscc.exe'.format(os.environ['PROGRAMFILES{0}'.format(ARCH_WINDOWS_OPTS[ARCH_WINDOWS][2])], os.sep) DIR_BUILD_EXE = '{0}{1}dist{1}Nagstamon'.format(CURRENT_DIR, os.sep, ARCH_WINDOWS_OPTS[ARCH_WINDOWS][0], PYTHON_VERSION) DIR_BUILD_NAGSTAMON = '{0}{1}dist{1}Nagstamon-{2}-win{3}{4}'.format(CURRENT_DIR, os.sep, VERSION, ARCH_WINDOWS, FILENAME_SUFFIX) FILE_ZIP = '{0}.zip'.format(DIR_BUILD_NAGSTAMON) # clean older binaries for file in (DIR_BUILD_EXE, DIR_BUILD_NAGSTAMON, FILE_ZIP): if os.path.exists(file): try: shutil.rmtree(file) except: os.remove(file) # pyinstaller seems also to be installed not in \Scripts folder - if so, try without path pyinstaller_path = f'{sys.base_prefix}\\Scripts\\pyinstaller' if not Path(pyinstaller_path).exists(): pyinstaller_path = 'pyinstaller' subprocess.call([pyinstaller_path, '--noconfirm', '--add-data=..\\Nagstamon/resources;resources', '--icon=..\\Nagstamon\\resources\\nagstamon.ico', '--name=Nagstamon', '--hidden-import=PyQt6.uic.plugins', '--hidden-import=win32timezone', GUI_MODE, '..\\nagstamon.py'], shell=True) if SIGNING: # environment variables will be used by powershell script for signing subprocess.run(['pwsh.exe', './windows/code_signing.ps1', 'build/Nagstamon/*.exe']) # rename output os.rename(DIR_BUILD_EXE, DIR_BUILD_NAGSTAMON) # create simple batch file for debugging if DEBUG: # got to Nagstamon build directory with Nagstamon.exe os.chdir(DIR_BUILD_NAGSTAMON) batch_file = Path('nagstamon-debug.bat') # cmd /k keeps the console window open to get some debug output batch_file.write_text('set \n' 'cmd /k nagstamon.exe') # after cleaning start zipping and setup.exe-building - go back to original directory os.chdir(CURRENT_DIR) # create .zip file if os.path.exists('{0}{1}dist'.format(CURRENT_DIR, os.sep)): os.chdir('{0}{1}dist'.format(CURRENT_DIR, os.sep)) zip_archive = zipfile.ZipFile(FILE_ZIP, mode='w', compression=zipfile.ZIP_DEFLATED) zip_archive.write(os.path.basename(DIR_BUILD_NAGSTAMON)) for root, dirs, files in os.walk(os.path.basename(DIR_BUILD_NAGSTAMON)): for file in files: zip_archive.write('{0}{1}{2}'.format(root, os.sep, file)) zip_archive.close() if not DEBUG: # for some reason out of nowhere the old path SetupIconFile={#resources}\nagstamon.ico # does not work anymore, so the icon gets copied into the path referenced to # as SourceDir in the ISS file shutil.copyfile(f'{NAGSTAMON_DIR}{os.sep}Nagstamon{os.sep}resources{os.sep}nagstamon.ico', f'{DIR_BUILD_NAGSTAMON}{os.sep}nagstamon.ico') # execute InnoSetup with many variables set by ISCC.EXE outside .iss file result = subprocess.call([ISCC, r'/Dsource={0}'.format(DIR_BUILD_NAGSTAMON), r'/Dversion_is={0}'.format(VERSION_IS), r'/Dversion={0}'.format(VERSION), r'/Darch={0}'.format(ARCH_WINDOWS), r'/Darchs_allowed={0}'.format(ARCH_WINDOWS_OPTS[ARCH_WINDOWS][3]), r'/O{0}{1}dist'.format(CURRENT_DIR, os.sep), r'{0}{1}windows{1}nagstamon.iss'.format(CURRENT_DIR, os.sep)], shell=True) if result > 0: sys.exit(result) if SIGNING: # environment variables will be used by powershell script for signing subprocess.run(['pwsh.exe', '../windows/code_signing.ps1', '*.exe']) def package_macos(): """ execute steps necessary for compilation of MacOS X binaries and .dmg file """ # can't pass --version to pyinstaller in spec mode, so export as env var os.environ['NAGSTAMON_VERSION'] = VERSION # create one-file .app bundle by pyinstaller subprocess.call(['pyinstaller --noconfirm macos/nagstamon.spec'], shell=True) # create staging DMG folder for later compressing of DMG shutil.rmtree(f'Nagstamon_{VERSION}_Staging_DMG/', ignore_errors=True) # move app bundle folder shutil.move('dist/Nagstamon.app', f'Nagstamon_{VERSION}_Staging_DMG/Nagstamon.app') # copy icon to staging folder shutil.copy('../Nagstamon/resources/nagstamon.ico', 'nagstamon.ico'.format(VERSION)) # cleanup before new images get created for dmg_file in glob.iglob('*.dmg'): os.unlink(dmg_file) # create dmg file with create-dmg insttaled via brew subprocess.call([f'create-dmg ' f'--volname "Nagstamon {VERSION}" ' f'--volicon "nagstamon.ico" ' f'--window-pos 400 300 ' f'--window-size 600 320 ' f'--icon-size 100 ' f'--icon "Nagstamon.app" 175 110 ' f'--hide-extension "Nagstamon.app" ' f'--app-drop-link 425 110 ' f'"dist/Nagstamon-{VERSION}-{ARCH_MACOS_NAMES[ARCH_MACOS]}.dmg" ' f'Nagstamon_{VERSION}_Staging_DMG/' ], shell=True) def package_linux_deb(): shutil.rmtree(SCRIPTS_DIR, ignore_errors=True) shutil.rmtree('{0}{1}.pybuild'.format(CURRENT_DIR, os.sep), ignore_errors=True) shutil.rmtree('{0}{1}debian'.format(NAGSTAMON_DIR, os.sep), ignore_errors=True) os.chdir(NAGSTAMON_DIR) # masquerade .py file as .py-less shutil.copyfile('nagstamon.py', 'nagstamon') shutil.copytree('{0}{1}debian{1}'.format(CURRENT_DIR, os.sep), '{0}{1}debian'.format(NAGSTAMON_DIR, os.sep)) # just in case some Windows commit converted linebreaks # for debian_file in glob.iglob('debian/*'): # subprocess.call(['dos2unix', f'{debian_file}']) os.chmod(f'{CURRENT_DIR}/debian/rules', 0o755) subprocess.call(['fakeroot', 'debian/rules', 'build']) subprocess.call(['fakeroot', 'debian/rules', 'binary']) # copy .deb file to current directory for debian_package in glob.iglob('../nagstamon*.deb'): shutil.move(debian_package, CURRENT_DIR) def package_linux_rpm(): """ create .rpm file via setup.py bdist_rpm - most settings are in setup.py """ os.chdir(NAGSTAMON_DIR) # masquerade .py file as .py-less shutil.copyfile('nagstamon.py', 'nagstamon') # run setup.py for rpm creation subprocess.call(['python3', 'setup.py', 'bdist_rpm'], shell=False) current_dir = Path(CURRENT_DIR) for file in current_dir.iterdir(): if VERSION.replace('-', '.') in file.name and ('noarch' in file.name or 'src' in file.name): for file_type in ['noarch', 'src']: if file_type in file.name: file.replace(file.parent / Path(file.name.replace(f'{file_type}.rpm', f'{DIST_NAME}{DIST_VERSION}.{file_type}.rpm'))) DISTS = { 'debian': package_linux_deb, 'ubuntu': package_linux_deb, 'linuxmint': package_linux_deb, 'fedora': package_linux_rpm, 'rhel': package_linux_rpm } if __name__ == '__main__': if platform.system() == 'Windows': package_windows() elif platform.system() == 'Darwin': package_macos() else: dist = get_distro()[0] if dist in DISTS: zip_manpage() DISTS[dist]() else: print('Your system is not supported for automated build yet') Nagstamon-master/build/debian/000077500000000000000000000000001505160700500166165ustar00rootroot00000000000000Nagstamon-master/build/debian/README.source000066400000000000000000000035071505160700500210020ustar00rootroot00000000000000This package uses quilt to manage all modifications to the upstream source. Changes are stored in the source package as diffs in debian/patches and applied during the build. To configure quilt to use debian/patches instead of patches, you want either to export QUILT_PATCHES=debian/patches in your environment or use this snippet in your ~/.quiltrc: for where in ./ ../ ../../ ../../../ ../../../../ ../../../../../; do if [ -e ${where}debian/rules -a -d ${where}debian/patches ]; then export QUILT_PATCHES=debian/patches fi done To get the fully patched source after unpacking the source package, cd to the root level of the source package and run: quilt push -a The last patch listed in debian/patches/series will become the current patch. To add a new set of changes, first run quilt push -a, and then run: quilt new where is a descriptive name for the patch, used as the filename in debian/patches. Then, for every file that will be modified by this patch, run: quilt add before editing those files. You must tell quilt with quilt add what files will be part of the patch before making changes or quilt will not work properly. After editing the files, run: quilt refresh to save the results as a patch. Alternately, if you already have an external patch and you just want to add it to the build system, run quilt push -a and then: quilt import -P /path/to/patch quilt push -a (add -p 0 to quilt import if needed). as above is the filename to use in debian/patches. The last quilt push -a will apply the patch to make sure it works properly. To remove an existing patch from the list of patches that will be applied, run: quilt delete You may need to run quilt pop -a to unapply patches first before running this command. Nagstamon-master/build/debian/changelog000066400000000000000000000456061505160700500205030ustar00rootroot00000000000000nagstamon (3.17-20250821) unstable; urgency=low * New upstream -- Henri Wahl Thu, 21 Aug 2025 08:00:00 +0200 nagstamon (3.16.2) stable; urgency=low * New upstream - fix sound problem - fix IncingaDBWeb -- Henri Wahl Mon, 21 Oct 2024 08:00:00 +0200 nagstamon (3.16.1) stable; urgency=low * New upstream - fix ridiculous Windows 11 appearance - fix Zabbix all information copy action -- Henri Wahl Sun, 20 Oct 2024 08:00:00 +0200 nagstamon (3.16.0) stable; urgency=low * New upstream - Apple Silicon native support - fixes for Centreon - fixes for Checkmk - fixes for Icinga2 - fixes for IcingaDBWeb - fixes for Zabbix - fixes for pyinstaller - updated Qt6 -- Henri Wahl Thu, 10 Oct 2024 08:00:00 +0200 nagstamon (3.14.0) stable; urgency=low * New upstream - improved Wayland support - improved proxy support - added Opsview hashtag filtering and can_change_only option - fixes for Alertmanager - fixes for Centreon - fixes for Icinga - fixes for Opsview - fixes for Zabbix - added support for registering version in Windows - added support for using system certificates in Windows -- Henri Wahl Sun, 25 Feb 2024 08:00:00 +0200 nagstamon (3.12.0) stable; urgency=low * New upstream - added option to hide dock icon on macOS when using systray - added fallback GSSAPI usage when no Kerberos is available - added usage of local certificate store on Windows - added codesigning for Windows builds - added support for RHEL9 - added Windows debugging version - fixed Wayland support without using EWMH - fixes for Icinga - fixes for Zabbix - fixes for Centreon - fixes for Opsview - fixes for Monitos - improved build pipeline on GitHub Actions -- Henri Wahl Sat, 06 May 2023 08:00:00 +0200 nagstamon (3.10.1) stable; urgency=low * New upstream - fixes Centreon flapping bug - fixes Kubuntu 20.04 not starting bug -- Henri Wahl Fri, 04 Nov 2022 08:00:00 +0100 nagstamon (3.10.0) stable; urgency=low * New upstream - upgrade to Qt6 GUI framework - updates for latest Centreon - added bearer authentication - added IcingaDB support - extended Zabbix support - fixes for GUI - fixes for TLS - fixes for Checkmk -- Henri Wahl Thu, 27 Oct 2022 08:00:00 +0100 nagstamon (3.8.0) stable; urgency=low * New upstream - added alertmanager acknownledgment - added ECP authentication - added Zabbix 5.4+ support - Checkmk 2.0 fixes - Thruk fixes - Zabbix fixes - dependency updates -- Henri Wahl Mon, 15 Nov 2021 19:00:00 +0100 nagstamon (3.6.0) stable; urgency=low * New upstream - added Prometheus support - added SensuGo support - added support for SOCKS5 proxy - added display of macOS version - added Thruk autologin - update for Centreon 20.10 - fixes for Zabbix - fix for IcingaWeb2 - fix for Nagios/Icinga login with invalid credentials - fixes Thruk login - fixes context menu bug - fix for HighDPI - fix for distro detection -- Henri Wahl Sun, 05 Apr 2021 17:00:00 +0100 nagstamon (3.4.1) stable; urgency=low * New upstream - Centreon fix for acknowledgement when using autologin key - IcingaWeb2 fix support older setups - fix popup in systray mode on Windows - improved mode switching - better support for high resolution displays - build support for Python 3.8 - improved Windows installer -- Henri Wahl Mon, 27 Jan 2020 10:00:00 +0100 nagstamon (3.4) stable; urgency=low * New upstream - added multiple item selection and actions execution - added notification for OK state - added darkmode for macOS - added option to copy service to clipboard - added support for custom CA cert for Sensu/Uchiwa - added support for IcingaWeb2 expire time - added server encoding detection - updated components - fixed crash when selecting downtime on down host - fixed settings filenames troubles by url-encoding them - fixed pyinstaller onefile issues on macOS - fixed Kerberos issues on macOS - fixed Cinnamon systray/popup issue - Centreon fixes - Checkmk fixes - IcingaWeb2 fixes - Monitos4x fixes - op5Monitor fixes - Sensu fixes -- Henri Wahl Wed, 25 Dec 2019 10:00:00 +0100 nagstamon (3.2.1) stable; urgency=low * New upstream - fixed macOS system tray icon crash -- Henri Wahl Mon, 7 Jan 2019 10:00:00 +0100 nagstamon (3.2) stable; urgency=low * New upstream - new Zabbix backend - added Sensu/Uchiwa changes - fixed Opsview/Nagios duration counter - fixed Centreon new versions - fixed non-closing floating statusbar - fixed certain custom notification action bugs - fixed certain keyring bugs - fixed macOS Retina support -- Henri Wahl Wed, 19 Dec 2018 22:00:00 +0100 nagstamon (3.0.1) stable; urgency=low * New upstream - fixes for Zabbix - fixed filters for Op5 - fixes for update detection and self-signed certificates -- Henri Wahl Tue, 19 Sep 2017 16:00:00 +0200 nagstamon (3.0) stable; urgency=low * New upstream - added Kerberos authentification - added increased security by taking TLS seriously - added window mode - added large OK label for fullscreen and window mode - added click-somewhere-to-close-statuswindow mode - added custom views to Check_MK - added monitor type Monitos3 - added monitor type SNAG-View 3 - added monitor type Sensu - fixes statuswindow garbage when changing mode - fixed filename bug - fixed check for .Xauthority file - fixes for Centreon - fixes for Check_MK 1.4.0 - fixes for Zabbix - switched to pyinstaller -- Henri Wahl Wed, 13 Sep 2017 16:00:00 +0200 nagstamon (2.0.1) stable; urgency=low * New upstream - Major Centreon bug making it useless - Icinga version check fix - Thruk login fix - EWMH initialization change - Systrayicon left mouse click without context menu - DBus crash workaround -- Henri Wahl Thu, 20 Oct 2016 08:00:00 +0200 nagstamon (2.0) stable; urgency=low * New upstream - Based on Qt 5 it now comes with a better integrated look-and-feel – especially remarkable on MacOS - Partly simplified design - Less clutter in setting dialogs - Runs on latest Windows and MacOS - Uses QT 5 multimedia which means native sound on Linux and MacOS - Uses only SVG graphics – allows changing colors even in systray icon - Customizable font and font size - Adjust to dark or light desktop theme - Action allowing to copy host/service information to clipboard - Added ‘Archive Event’ action for Check_MK monitors - Additionally supports IcingaWeb2 - Updated Opsview and Centreon to support latest monitor server versions - Experimental support for Livestatus and Zenoss - New build script based on cx-Freeze for Windows and MacOS - Native 64 bit version for Windows - No or less memory leaks, especially in Windows - Make sure only one instance per config is running -- Henri Wahl Mon, 29 Aug 2016 16:00:00 +0200 nagstamon (1.0.1) stable; urgency=low * New upstream - added option to disable system keyring storage to prevent crashes - reverted default sorting order to "Descending" - fixed too narrow fullscreen display - fixed vanishing Nagstamon submenu in Ubuntu Appindicator -- Henri Wahl Mon, 22 Sep 2014 9:00:00 +0200 nagstamon (1.0) stable; urgency=low * New upstream - added custom event notification with custom commands - added highlighting of new events - added storage of passwords in OS keyring - added optional tooltip for full status information - added support for applying custom actions to specific monitor only - added copy buttons for servers and actions dialogs - added stopping notification if event already vanished - added support for Op5Monitor 6.3 instead of Ninja - added experimental Zabbix support - added automatic refreshing after acknowledging - added permanent hamburger menu - unified layout of dialogs - various Check_MK improvements - fixed old regression not-staying-on-top-bug - fixed Check_MK-Recheck-DOS-bug - fixed pop window size calculation on multiple screens - fixed following popup window on multiple screens - fixed hiding dialogs in MacOSX - fixed ugly statusbar font in MacOSX - fixed use of changed colors - fixed non-ascending default sort order - fixed Opsview downtime dialog - fixed sometimes not working context menu - fixed some GUI glitches - fixed password saving bug - fixed Centreon language inconsistencies - fixed regression Umlaut bug -- Henri Wahl Mon, 28 Jul 2014 09:30:00 +0200 nagstamon (1.0rc2) unstable; urgency=low * New upstream - added automatic refreshing after acknowledging - added permanent hamburger menu - unified layout of dialogs - fixed some GUI glitches - fixed password saving bug - fixed Centreon language inconsistencies - fixed regression Umlaut bug -- Henri Wahl Tue, 08 Jul 2014 11:00:00 +0200 nagstamon (1.0rc1) unstable; urgency=low * New upstream - added custom event notification with custom commands - added highlighting of new events - added storage of passwords in OS keyring - added optional tooltip for full status information - added support for applying custom actions to specific monitor only - added copy buttons for servers and actions dialogs - added stopping notification if event already vanished - added support for Op5Monitor 6.3 instead of Ninja - added experimental Zabbix support - fixed old regression not-staying-on-top-bug - fixed Check_MK-Recheck-DOS-bug - fixed pop window size calculation on multiple screens - fixed following popup window on multiple screens - fixed hiding dialogs in MacOSX - fixed ugly statusbar font in MacOSX - fixed use of changed colors - fixed non-ascending default sort order - fixed Opsview downtime dialog - fixed sometimes not working context menu - various Check_MK improvements -- Henri Wahl Tue, 24 Jun 2014 11:00:00 +0200 nagstamon (0.9.11) stable; urgency=low * New upstream - added Ubuntu AppIndicator support - added libnotify desktop notification support - added Centreon criticality support - fixed broken authentication dialog - fixed wrong OK state for Nagios and Icinga - fixed Correct-Statusbar-Position-O-Matic - fixed some Thruk issues - fixed popup resizing artefact - fixed some server edit dialog bugs - fixed missing auth field in Icinga when credentials are wrong - fixed quoting URLs for browser actions -- Henri Wahl Wed, 11 Sep 2013 09:00:00 +0200 nagstamon (0.9.11rc1) unstable; urgency=low * New upstream - added Ubuntu AppIndicator support - added libnotify desktop notification support - added Centreon criticality support - fixed broken authentication dialog - fixed wrong OK state for Nagios and Icinga - fixed Correct-Statusbar-Position-O-Matic - fixed some Thruk issues - fixed popup resizing artefact - fixed some server edit dialog bugs - fixed missing auth field in Icinga when credentials are wrong -- Henri Wahl Mon, 29 Jul 2013 10:35:00 +0200 nagstamon (0.9.10) stable; urgency=low * New upstream - added fullscreen option - added Thruk support - added Check_MK cookie-based auth - added new Centreon autologin option - added configurable default sort order - added filter for hosts in hard/soft state for Nagios, Icinga, Opsview and Centreon - added $STATUS-INFO$ variable for custom actions - added audio alarms also in fullscreen mode - improved update interval set in seconds instead minutes - improved Icinga JSON support - improved Centreon 2.4 xml/broker support - improved Nagios 3.4 pagination support - improved nicer GTK theme Murrine on MacOSX - fixed security bug - fixed some memory leaks - fixed superfluous passive icon for Check_MK - fixed blocking of shutdown/reboot on MacOSX - fixed saving converted pre 0.9.9 config immediately - fixed statusbar position when offscreen - fixed some GUI issues - fixed update detection -- Henri Wahl Wed, 11 Jul 2013 11:07:13 +0200 nagstamon (0.9.10rc2) unstable; urgency=low * New upstream - audio alarms also in fullscreen mode - adjust x0 y0 position of statusbar when offscreen - save converted pre 0.9.9 config immediately -- Henri Wahl Tue, 09 Jul 2013 14:25:00 +0200 nagstamon (0.9.10rc1) unstable; urgency=low * New upstream - added fullscreen option - added Thruk support - added Check_MK cookie-based auth - added new Centreon autologin option - added configurable default sort order - added filter for hosts in hard/soft state for Nagios, Icinga, Opsview and Centreon - added $STATUS-INFO$ variable for custom actions - update interval set in seconds instead minutes - improved Icinga JSON support - improved Centreon 2.4 xml/broker support - improved Nagios 3.4 pagination support - uses nicer GTK theme Murrine on MacOSX - fixed some memory leaks - fixed superfluous passive icon for Check_MK - fixed blocking of shutdown/reboot on MacOSX - fixed some GUI issues - fixed update detection -- Henri Wahl Wed, 03 Jul 2013 10:25:00 +0200 nagstamon (0.9.9.1-1) stable; urgency=low * New upstream - added custom actions in context menu - added reauthentication in case of authenticaton problems - changed configuration file to configuration directory (default: ~/.nagstamon) - added filter for flapping hosts and services - added history button for monitors - added shortcut to filter settings in popup window - improved keyboard usage in acknowledge/downtime/submit dialogs - fixed bug in Icinga acknowledgement - fixed bug in Check_MK Multisite sorting - fixed some Check_MK Multisite UTF trouble - fixed some GUI artefacts when resizing popup window -- Henri Wahl Fri, 13 Apr 2012 11:25:00 +0200 nagstamon (0.9.8.1-1) stable; urgency=low * New upstream - added customizable acknowledge/downtime/submit-result defaults - added regexp filter for status information column - added option to connect to hosts via its monitor hostname without HTTP overhead - added ability to keep status detail popup open despite hovering away - added option to change offset between popup window and systray icon to avoid partly hidden popup - fixed some popup artefacts - fixed various bugs with acknowledgement flags (persistent/sticky/notification), now they are actually working - fixed some issues when running on MacOS X -- Henri Wahl Wed, 10 Oct 2011 14:49:00 +0200 nagstamon (0.9.7.1-1) stable; urgency=low * New upstream - hot fix for broken Centreon support - sf.net bug 3309166 -- Henri Wahl Fri, 30 May 2011 12:01:00 +0200 nagstamon (0.9.7-1) stable; urgency=low * New upstream - on some servers now context menu allows submitting check results for hosts and services - added filter for services on acknowledged hosts - added icons for "passiveonly" and "flapping" hosts and services - fix for uneditable text entry fields in settings dialog - sf.net bug 3300873 - fix for not working filter "services on hosts in maintenance" - sf.net bug 3299790 - fix for soft state detection in Centreon - sf.net bug 3303861 - fix for not filtered services which should have been filtered - sf.net bug 3308008 -- Henri Wahl Fri, 27 May 2011 16:01:00 +0200 nagstamon (0.9.6.1-1) stable; urgency=low * fix for sf.net bug 3298321 - displaying error when all is OK -- Henri Wahl Fri, 06 May 2011 16:01:00 +0200 nagstamon (0.9.6-1) stable; urgency=low * New upstreeam release - improved, full Ninja support - rewritten filtering mechanism allows new features - displaying icons in status overview popup indicating states "acknowledged" and "scheduled downtime" - added option to play notification sounds more than once - small UI improvements - uses BeautifulSoup instead of lxml - uses GTK UI Builder instead of glade - as always: bugfixes -- Henri Wahl Fri, 06 May 2011 16:01:00 +0200 nagstamon (0.9.5-1) stable; urgency=low * New upstream release - added op5 Ninja support - added Check_MK Multisite support - improved Icinga support (compatibility with Icinga 1.3) - improved Centreon support (compatible with Centreon 2.1) - added sortable columns in status overview - added customizable colors - better debugging and error messages - password must not be stored in config file - major memory leak closed, various bugs fixed -- Henri Wahl Tue, 05 Apr 2011 13:23:00 +0200 nagstamon (0.9.4-1) stable; urgency=low * New upstream release (Closes: #582977) * removed debian/manpages * renamed debian/nagstamon.install to debian/install * debian/patches - removed settings_glade - removed setup_patch -- Carl Chenet Sat, 19 Jun 2010 12:46:42 +0200 nagstamon (0.9.3-2) stable; urgency=low * debian/patches/default_search - disable the default search for newer versions (Closes: #585928) * debian/patches/series - added default_search -- Carl Chenet Tue, 15 Jun 2010 00:41:22 +0200 nagstamon (0.9.3-1) stable; urgency=low * New upstream release * Switching to 3.0 source format * debian/patches - added settings_glade patch to close #2998035 upstream bug - added setup_patch to remove an absolute link for the upstream manpage * debian/control - added quilt in Build-Depends - added the support of Opsview servers in the long description * debian/nagstamon.manpages - switched to the file provided by the upstream * debian/nagstamon.desktop - commenting the OnlyShowIn directive -- Carl Chenet Sun, 23 May 2010 12:47:11 +0200 nagstamon (0.9.2-2) stable; urgency=low * debian/control - Added a mandatory runtime missing dependency python-pkg-resources. - Fixed a typo in the long message. -- Carl Chenet Wed, 24 Mar 2010 23:18:21 +0100 nagstamon (0.9.2-1) stable; urgency=low * Initial release. (Closes: #534842) -- Carl Chenet Mon, 22 Feb 2010 14:16:44 +0100 Nagstamon-master/build/debian/control000066400000000000000000000031601505160700500202210ustar00rootroot00000000000000Source: nagstamon Section: utils Priority: optional Maintainer: Python Applications Packaging Team Uploaders: Carl Chenet Build-Depends: dbus, debhelper-compat (= 13), dh-python, dh-sequence-python3, quilt (>= 0.63) Build-Depends-Indep: python3-all (>= 3.4), python3-keyring, python3-setuptools, python3-psutil, Standards-Version: 4.6.0 Homepage: https://nagstamon.de Vcs-Git: git://github.com/HenriWahl/Nagstamon.git Vcs-Browser: https://codeload.github.com/HenriWahl/Nagstamon/zip/master Package: nagstamon Architecture: all Depends: ${python3:Depends}, ${misc:Depends}, libqt6multimedia6 | libqt5multimedia5-plugins , python3-bs4, python3-dbus.mainloop.pyqt6 | python3-dbus.mainloop.pyqt5, python3-lxml, python3-packaging, python3-pkg-resources, python3-pyqt6 | python3-pyqt5, pyqt6-dev-tools | pyqt5-dev-tools, python3-pyqt6.qtmultimedia | python3-pyqt5.qtmultimedia, python3-pyqt6.qtsvg | python3-pyqt5.qtsvg, python3-requests, python3-requests-kerberos, python3-psutil, python3-keyring, python3-ewmh, python3-arrow, python3-tzlocal Recommends: python3-secretstorage Description: Nagios status monitor which takes place in systray or on desktop Nagstamon is a Nagios status monitor which takes place in systray or on desktop (GNOME, KDE) as floating statusbar to inform you in realtime about the status of your Nagios and some of its derivatives monitored network. It allows to connect to multiple Nagios, Icinga, Opsview, Centreon, Op5Monitor, Checkmk Multisite, Thruk, Zabbix and Zenoss servers. Events could be handled by instant access to failed hosts/services. Nagstamon-master/build/debian/copyright000066400000000000000000000023151505160700500205520ustar00rootroot00000000000000This package was debianized by Carl Chenet on Mon, 22 Feb 2010 00:00:15 +0200. It was downloaded from http://sourceforge.net/projects/nagstamon/ Upstream Author: Henri Wahl Copyright: Copyright Š 2008-2011 Henri Wahl License: This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. On Debian systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL-2'. The Debian packaging is: Copyright Š 2010 Carl Chenet and is also under the same license described above. Nagstamon-master/build/debian/install000066400000000000000000000000631505160700500202060ustar00rootroot00000000000000debian/nagstamon.desktop usr/share/applications Nagstamon-master/build/debian/nagstamon.desktop000066400000000000000000000002271505160700500222010ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Nagstamon Comment=Nagios status monitor Icon=nagstamon Exec=nagstamon Terminal=false Categories=System;Monitor; Nagstamon-master/build/debian/patches/000077500000000000000000000000001505160700500202455ustar00rootroot00000000000000Nagstamon-master/build/debian/patches/default_search000066400000000000000000000014111505160700500231360ustar00rootroot00000000000000# Description: disable the default search for newer version # Author: Carl Chenet Index: nagstamon_0.9.3/Nagstamon/nagstamonConfig.py =================================================================== --- nagstamon_0.9.3.orig/Nagstamon/nagstamonConfig.py 2010-06-15 00:38:16.000000000 +0200 +++ nagstamon_0.9.3/Nagstamon/nagstamonConfig.py 2010-06-15 00:38:39.000000000 +0200 @@ -39,7 +39,7 @@ self.connect_by_dns_yes = True self.connect_by_dns_no = False self.debug_mode = False - self.check_for_new_version = True + self.check_for_new_version = False self.notification = True self.notification_flashing = True # because of nonexistent windows systray popup support I'll let it be now Nagstamon-master/build/debian/patches/series000066400000000000000000000000171505160700500214600ustar00rootroot00000000000000default_search Nagstamon-master/build/debian/rules000077500000000000000000000001041505160700500176710ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with python3 --buildsystem=pybuild Nagstamon-master/build/debian/source/000077500000000000000000000000001505160700500201165ustar00rootroot00000000000000Nagstamon-master/build/debian/source/format000066400000000000000000000000141505160700500213240ustar00rootroot000000000000003.0 (quilt) Nagstamon-master/build/debian/watch000066400000000000000000000000721505160700500176460ustar00rootroot00000000000000version=3 http://sf.net/nagstamon/nagstamon_(.+)\.tar\.gz Nagstamon-master/build/docker/000077500000000000000000000000001505160700500166435ustar00rootroot00000000000000Nagstamon-master/build/docker/Dockerfile-debian000066400000000000000000000017511505160700500220610ustar00rootroot00000000000000FROM debian:11 LABEL maintainer=henri@nagstamon.de RUN apt -y update RUN apt -y install apt-utils # python3-pysocks in debian:8 becomes python3-socks in later versions RUN apt -y install debhelper \ dh-python \ dos2unix \ fakeroot \ git \ make \ libqt5multimedia5-plugins \ python3-bs4 \ python3-dateutil \ python3-dbus.mainloop.pyqt5 \ python3-keyring \ python3-lxml \ python3-pkg-resources \ python3-psutil \ python3-pyqt5 \ python3-pyqt5.qtsvg \ python3-pyqt5.qtmultimedia \ python3-requests \ python3-requests-kerberos \ python3-setuptools \ python3-socks CMD cd /nagstamon/build && \ /usr/bin/python3 build.pyNagstamon-master/build/docker/Dockerfile-fedora-40000066400000000000000000000015441505160700500223200ustar00rootroot00000000000000FROM fedora:40 LABEL maintainer=henri@nagstamon.de RUN dnf -y upgrade RUN dnf -y install createrepo_c \ desktop-file-utils \ git \ python3 \ python3-beautifulsoup4 \ python3-cryptography \ python3-dateutil \ python3-devel \ python3-keyring \ python3-lxml \ python3-psutil \ python3-pyqt6 \ python3-pyqt6-devel \ python3-requests \ python3-requests-kerberos \ python3-SecretStorage \ python3-setuptools \ qt6-qtsvg \ qt6-qtmultimedia \ rpm-build CMD cd /nagstamon/build && \ /usr/bin/python3 build.py Nagstamon-master/build/docker/Dockerfile-fedora-41000066400000000000000000000023501505160700500223150ustar00rootroot00000000000000FROM fedora:41 LABEL maintainer=henri@nagstamon.de # Because it happened that fedora:42 image actually was a fedora:43 image but stayed so # due to caching the image I add some random characters to force a rebuild of the image if needed: # 1 2 1 2 keep it on... # avoid Cisco OpenH264 plugin causing upgrade failure RUN dnf config-manager setopt fedora-cisco-openh264.enabled=false # Upgrade first RUN dnf -y upgrade # Install dependencies RUN dnf -y install createrepo_c \ desktop-file-utils \ git \ python3 \ python3-beautifulsoup4 \ python3-cryptography \ python3-dateutil \ python3-devel \ python3-keyring \ python3-lxml \ python3-psutil \ python3-pyqt6 \ python3-pyqt6-devel \ python3-requests \ python3-requests-kerberos \ python3-SecretStorage \ python3-setuptools \ qt6-qtsvg \ qt6-qtmultimedia \ rpm-build # Finally build CMD cd /nagstamon/build && \ /usr/bin/python3 build.py Nagstamon-master/build/docker/Dockerfile-fedora-42000066400000000000000000000030451505160700500223200ustar00rootroot00000000000000FROM fedora:42 LABEL maintainer=henri@nagstamon.de # Because it happened that fedora:42 image actually was a fedora:43 image but stayed so # due to caching the image I add some random characters to force a rebuild of the image if needed: # 1 2 1 2 keep it on.... # avoid Cisco OpenH264 plugin causing upgrade failure RUN dnf config-manager setopt fedora-cisco-openh264.enabled=false # Upgrade first RUN dnf -y upgrade # Install dependencies RUN dnf -y install createrepo_c \ desktop-file-utils \ git \ python3 \ python3-beautifulsoup4 \ python3-cryptography \ python3-dateutil \ python3-devel \ python3-keyring \ python3-lxml \ python3-psutil \ python3-pyqt6 \ python3-pyqt6-devel \ python3-requests \ python3-requests-kerberos \ python3-SecretStorage \ python3-setuptools \ qt6-qtsvg \ qt6-qtmultimedia \ rpm-build # Existing /usr/sbin/python3 causes problems with RPM package installation # /usr/sbin is a symlink to /usr/bin - deleting it causes next problem # with ldconfig, which therefore symlinked into /usr/sbin RUN rm -rf /usr/sbin RUN mkdir /usr/sbin # needed for RPM build RUN ln -s /usr/bin/ldconfig /usr/sbin/ldconfig # Finally build CMD cd /nagstamon/build && \ /usr/bin/python3 build.py Nagstamon-master/build/docker/Dockerfile-fedora-43000066400000000000000000000030451505160700500223210ustar00rootroot00000000000000FROM fedora:43 LABEL maintainer=henri@nagstamon.de # Because it happened that fedora:42 image actually was a fedora:43 image but stayed so # due to caching the image I add some random characters to force a rebuild of the image if needed: # 1 2 1 2 keep it on.... # avoid Cisco OpenH264 plugin causing upgrade failure RUN dnf config-manager setopt fedora-cisco-openh264.enabled=false # Upgrade first RUN dnf -y upgrade # Install dependencies RUN dnf -y install createrepo_c \ desktop-file-utils \ git \ python3 \ python3-beautifulsoup4 \ python3-cryptography \ python3-dateutil \ python3-devel \ python3-keyring \ python3-lxml \ python3-psutil \ python3-pyqt6 \ python3-pyqt6-devel \ python3-requests \ python3-requests-kerberos \ python3-SecretStorage \ python3-setuptools \ qt6-qtsvg \ qt6-qtmultimedia \ rpm-build # Existing /usr/sbin/python3 causes problems with RPM package installation # /usr/sbin is a symlink to /usr/bin - deleting it causes next problem # with ldconfig, which therefore symlinked into /usr/sbin RUN rm -rf /usr/sbin RUN mkdir /usr/sbin # needed for RPM build RUN ln -s /usr/bin/ldconfig /usr/sbin/ldconfig # Finally build CMD cd /nagstamon/build && \ /usr/bin/python3 build.py Nagstamon-master/build/docker/Dockerfile-linux-pyinstaller000066400000000000000000000020161505160700500243350ustar00rootroot00000000000000FROM fedora:42 LABEL maintainer=henri@nagstamon.de ARG VERSION=0.0.0 # Because it happened that fedora:42 image actually was a fedora:43 image but stayed so # due to caching the image I add some random characters to force a rebuild of the image if needed: # 1 2 1 2 keep it on... # Upgrade first RUN dnf -y upgrade # Needed packages for installation and partly building of Python modules RUN dnf -y install dbus-devel \ gcc \ glib2-devel \ krb5-devel \ python3 \ python3-devel \ python3-virtualenv # Finally build CMD cd /nagstamon && \ virtualenv venv && \ . venv/bin/activate && \ pip3 install -r build/requirements/linux.txt && \ pip3 install pyinstaller && \ pyinstaller --add-data="Nagstamon/resources:Nagstamon/resources" --noconfirm --name=Nagstamon --onefile --collect-submodules gssapi.raw nagstamon.py && \ mv /nagstamon/dist/Nagstamon /nagstamon/dist/nagstamon-${VERSION}-linux-x86_64Nagstamon-master/build/docker/Dockerfile-rhel-9000066400000000000000000000020751505160700500217370ustar00rootroot00000000000000FROM rockylinux:9 LABEL maintainer=henri@nagstamon.de RUN dnf -y upgrade RUN dnf -y install 'dnf-command(config-manager)' \ epel-release && \ crb enable && \ dnf -y install createrepo_c \ desktop-file-utils \ git \ python3 \ python3-beautifulsoup4 \ python3-cryptography \ python3-dateutil \ python3-devel \ python3-keyring \ python3-lxml \ python3-psutil \ python3-qt5 \ python3-qt5-devel \ python3-requests \ python3-requests-kerberos \ python3-SecretStorage \ qt5-qtsvg \ qt5-qtmultimedia \ rpm-build # ugly workaround for legacy Qt5 on RHEL < 10 CMD cd /nagstamon/build && \ sed -i s/pyqt6/pyqt5/g redhat/nagstamon.spec && \ sed -i s/qt6/qt5/g redhat/nagstamon.spec && \ /usr/bin/python3 build.py Nagstamon-master/build/docker/Dockerfile-test000066400000000000000000000006321505160700500216130ustar00rootroot00000000000000ARG VERSION FROM python:${VERSION} LABEL maintainer=henri@nagstamon.de ARG REQUIREMENTS RUN echo "${REQUIREMENTS}" RUN apt -y update RUN apt -y install apt-utils RUN apt -y install libdbus-1-dev libkrb5-dev RUN python -m pip install --upgrade pip RUN pip install pytest pylint wheel RUN echo ${REQUIREMENTS} | base64 --decode > requirements.txt RUN cat requirements.txt RUN pip install -r requirements.txtNagstamon-master/build/macos/000077500000000000000000000000001505160700500164765ustar00rootroot00000000000000Nagstamon-master/build/macos/nagstamon.spec000066400000000000000000000027361505160700500213510ustar00rootroot00000000000000# -*- mode: python ; coding: utf-8 -*- import os block_cipher = None a = Analysis(['../../nagstamon.py'], pathex=[], binaries=[], datas=[('../../Nagstamon/resources', 'Nagstamon/resources')], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='Nagstamon', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, codesign_identity=None, entitlements_file=None, icon='../../Nagstamon/resources/nagstamon.icns') # LSUIElement in info_plist hides the icon in dock app = BUNDLE(exe, name='Nagstamon.app', icon='../../Nagstamon/resources/nagstamon.icns', bundle_identifier='de.nagstamon', version=os.environ['NAGSTAMON_VERSION'], info_plist={ 'NSRequiresAquaSystemAppearance': False, 'LSBackgroundOnly': False, 'LSUIElement': True }) Nagstamon-master/build/redhat/000077500000000000000000000000001505160700500166435ustar00rootroot00000000000000Nagstamon-master/build/redhat/nagstamon.desktop000066400000000000000000000003341505160700500222250ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Nagstamon Comment=Nagios status monitor Icon=nagstamon Exec=nagstamon Terminal=false Categories=System;Monitor; StartupNotify=true GenericName=Nagios status monitor for the desktop Nagstamon-master/build/redhat/nagstamon.spec000066400000000000000000000050761505160700500215160ustar00rootroot00000000000000%global gitdate 20160602 %global commit 7139844d1a8109ba45f03601293ab70050b7dc94 %global shortcommit %(c=%{commit}; echo ${c:0:7}) Name: nagstamon Version: 3.10.0 Release: 0.1.%{gitdate}git%{shortcommit}%{?dist} Summary: Nagios status monitor for desktop License: GPLv2+ URL: https://nagstamon.de Source0: https://github.com/HenriWahl/Nagstamon/archive/%{commit}/nagstamon-%{commit}.tar.gz BuildArch: noarch BuildRequires: python3-devel BuildRequires: python3-pyqt5-devel BuildRequires: desktop-file-utils Requires: python3 Requires: python3-arrow Requires: python3-beautifulsoup4 Requires: python3-cryptography Requires: python3-dateutil Requires: python3-dbus Requires: python3-keyring Requires: python3-lxml Requires: python3-packaging Requires: python3-psutil Requires: python3-pysocks Requires: python3-pyqt5 Requires: python3-requests Requires: python3-requests-kerberos Requires: python3-SecretStorage Requires: python3-tzlocal Requires: qt5-qtsvg Requires: qt5-qtmultimedia %description Nagstamon is a Nagios status monitor which takes place in system tray or on desktop (GNOME, KDE, Windows) as floating status bar to inform you in real-time about the status of your Nagios and derivatives monitored network. It allows to connect to multiple Nagios, Icinga, Opsview, Op5Monitor, Checkmk/Multisite, Centreon and Thruk servers. %prep %setup -qn Nagstamon-%{commit} %build %{__python3} setup.py build %install %{__python3} setup.py install --single-version-externally-managed -O1 --root=%{buildroot} #Provide directory to install icon for desktop file mkdir -p %{buildroot}%{_datadir}/pixmaps #Copy icon to pixmaps directory cp Nagstamon/resources/%{name}.svg %{buildroot}%{_datadir}/pixmaps/%{name}.svg #Remove execute bit from icon chmod -x %{buildroot}%{_datadir}/pixmaps/%{name}.svg #Remove the file extension for convenience mv %{buildroot}%{_bindir}/%{name}.py %{buildroot}%{_bindir}/%{name} desktop-file-install --dir %{buildroot}/%{_datadir}/applications\ --delete-original\ --set-icon=%{name}.svg\ %{buildroot}%{python3_sitelib}/Nagstamon/resources/%{name}.desktop %files %doc ChangeLog %license COPYRIGHT LICENSE %{_datadir}/pixmaps/%{name}.svg %{_datadir}/applications/%{name}.desktop %{python3_sitelib}/Nagstamon/ %{_bindir}/%{name} %{_mandir}/man1/%{name}.1* %{python3_sitelib}/%{name}*.egg-info %changelog * Sun Jun 05 2016 Momcilo Medic 2.0-0.1.20160602git7139844 - Initial .spec file Nagstamon-master/build/requirements/000077500000000000000000000000001505160700500201175ustar00rootroot00000000000000Nagstamon-master/build/requirements/linux.txt000066400000000000000000000002411505160700500220140ustar00rootroot00000000000000arrow beautifulsoup4 dbus-python keyring lxml packaging psutil pyqt6 pyqt6-qt6 pysocks python-dateutil requests requests-kerberos requests-ecp setuptools tzlocalNagstamon-master/build/requirements/macos.txt000066400000000000000000000004151505160700500217620ustar00rootroot00000000000000arrow beautifulsoup4 # problems since 24.x? keyring==23.13.1 lxml packaging psutil pyinstaller pyobjc-framework-ApplicationServices pyqt6==6.9.1 pyqt6-qt6==6.9.1 pysocks python-dateutil requests requests-ecp # gssapi instead kerberos requests-gssapi setuptools tzlocal Nagstamon-master/build/requirements/windows.txt000066400000000000000000000005231505160700500223520ustar00rootroot00000000000000arrow beautifulsoup4 keyring lxml pip-system-certs packaging psutil pyinstaller pypiwin32 pyqt6==6.9.1 pyqt6-qt6==6.9.1 pysocks python-dateutil requests requests-ecp # maybe requests-gssapi might become usable with https://github.com/pythongssapi/python-gssapi/issues/244#issuecomment-827404287 requests-kerberos tzlocal setuptools wheel Nagstamon-master/build/windows/000077500000000000000000000000001505160700500170665ustar00rootroot00000000000000Nagstamon-master/build/windows/code_signing.ps1000066400000000000000000000007661505160700500221540ustar00rootroot00000000000000# get file to be signed from first argument $file = $args[0] # decode base64 PFX from environment variable $cert_buffer = [System.Convert]::FromBase64String($env:WIN_SIGNING_CERT_BASE64) # open cert from PFX with password $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New($cert_buffer, $env:WIN_SIGNING_PASSWORD) # finally sign the given file Set-AuthenticodeSignature -HashAlgorithm SHA256 -Certificate $cert -TimestampServer http://timestamp.sectigo.com -FilePath $fileNagstamon-master/build/windows/nagstamon.iss000066400000000000000000000031531505160700500215770ustar00rootroot00000000000000[Setup] AppName=Nagstamon AppVerName=Nagstamon {#version_is} AppVersion={#version_is} AppPublisher=Henri Wahl DefaultDirName={commonpf}\Nagstamon DefaultGroupName=Nagstamon AlwaysUsePersonalGroup=false ShowLanguageDialog=no SetupIconFile=nagstamon.ico UsePreviousGroup=false OutputBaseFilename=Nagstamon-{#version}-win{#arch}_setup UninstallDisplayIcon={app}\_internal\resources\nagstamon.ico UsePreviousAppDir=false AppID={{44F7CFFB-4776-4DA4-9930-A07178069517} UninstallRestartComputer=false VersionInfoVersion={#version_is} VersionInfoCopyright=Henri Wahl VersionInfoProductName=Nagstamon VersionInfoProductVersion={#version_is} InternalCompressLevel=max Compression=lzma SolidCompression=true SourceDir={#source} ArchitecturesAllowed={#archs_allowed} ArchitecturesInstallIn64BitMode=x64compatible CloseApplications=no WizardStyle=modern [Icons] Name: {group}\Nagstamon; Filename: {app}\nagstamon.exe; WorkingDir: {app}; IconFilename: {app}\_internal\resources\nagstamon.ico; IconIndex: 0 Name: {commonstartup}\Nagstamon; Filename: {app}\nagstamon.exe; WorkingDir: {app}; IconFilename: {app}\_internal\resources\nagstamon.ico; IconIndex: 0 [Files] Source: "*"; DestDir: {app}; Flags: recursesubdirs createallsubdirs ignoreversion; BeforeInstall: KillRunningNagstamon() [Tasks] Name: RunAfterInstall; Description: Run Nagstamon after installation [Run] Filename: {app}\nagstamon.exe; Flags: shellexec skipifsilent nowait; Tasks: RunAfterInstall [Code] procedure KillRunningNagstamon(); var ReturnCode: Integer; begin Exec(ExpandConstant('taskkill.exe'), '/f /t /im nagstamon.exe', '', SW_HIDE, ewWaitUntilTerminated, ReturnCode); end; Nagstamon-master/nagstamon.py000066400000000000000000000043471505160700500166460ustar00rootroot00000000000000#!/usr/bin/env python3 # encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import sys import socket # fix/patch for https://bugs.launchpad.net/ubuntu/+source/nagstamon/+bug/732544 socket.setdefaulttimeout(30) try: if __name__ == '__main__': from Nagstamon.config import (conf, OS, OS_WINDOWS) # according to https://gitlab.com/alelec/pip-system-certs/-/issues/7#note_1066992053 if OS == OS_WINDOWS: import pip_system_certs.wrapt_requests from Nagstamon.helpers import lock_config_folder # Acquire the lock if not lock_config_folder(conf.configdir): print('An instance is already running this config ({})'.format(conf.configdir)) sys.exit(1) # get GUI from Nagstamon.qui import (app, check_version, statuswindow) from Nagstamon.qui.helpers import check_servers # ask for help if no servers are configured check_servers.check() # show and resize the status window statuswindow.show() if not conf.fullscreen: statuswindow.adjustSize() if conf.check_for_new_version is True: check_version.check(start_mode=True, parent=statuswindow) sys.exit(app.exec()) except Exception as err: import traceback traceback.print_exc(file=sys.stdout) Nagstamon-master/setup.py000066400000000000000000000166551505160700500160240ustar00rootroot00000000000000#!/usr/bin/env python3 # # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2025 Henri Wahl et al. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA import os.path from pathlib import Path import platform import sys from Nagstamon.config import AppInfo, \ OS from Nagstamon.helpers import get_distro # dummy debug queue for compiling debug_queue = list() NAME = AppInfo.NAME # make name lowercase for Linux/Unix if OS not in ['Windows', 'Darwin']: if OS == 'Linux': DIST, DIST_VERSION, DIST_NAME = get_distro() # platform.dist() returns "('', '', '')" on FreeBSD elif OS == 'FreeBSD': DIST, DIST_VERSION, DIST_NAME = ('', '', '') # platform.dist() does not exist on NetBSD elif OS == 'NetBSD': DIST, DIST_VERSION, DIST_NAME = ('', '', '') else: DIST, DIST_VERSION, DIST_NAME = platform.dist() NAME = NAME.lower() else: DIST = "" VERSION = AppInfo.VERSION.replace('-', '.') NAGSTAMON_SCRIPT = 'nagstamon.py' from setuptools import setup os_dependent_include_files = ['Nagstamon/resources'] if os.path.exists('nagstamon'): NAGSTAMON_SCRIPT = 'nagstamon' CLASSIFIERS = ['Intended Audience :: System Administrators', 'Development Status :: 5 - Production/Stable', 'Environment :: Win32 (MS Windows)', 'Environment :: X11 Applications', 'Environment :: MacOS X', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Operating System :: POSIX', 'Natural Language :: English', 'Programming Language :: Python', 'Topic :: System :: Monitoring', 'Topic :: System :: Networking :: Monitoring'] # Dependencies are automatically detected, but it might need # fine tuning. build_exe_options = dict(packages=['PyQt6.QtNetwork', 'keyring.backends.kwallet', 'keyring.backends.OS_X', 'keyring.backends.SecretService', 'keyring.backends.Windows'], include_files=os_dependent_include_files, include_msvcr=True, excludes=[]) bdist_mac_options = dict(iconfile='Nagstamon/resources/nagstamon.icns', custom_info_plist='Nagstamon/resources/Info.plist') bdist_dmg_options = dict(volume_label='{0} {1}'.format(NAME, VERSION), applications_shortcut=False) # get paths of modules aka packages dynamically current_directory = Path().cwd() nagstamon_directory = current_directory / 'Nagstamon' packages_directories = [x.absolute().relative_to(current_directory) for x in list(nagstamon_directory.glob('**')) if '__pycache__' not in x.parts and x.is_dir()] packages = [str(x).replace('/', '.') for x in packages_directories] # older Fedora needs Qt5 if OS not in ['Windows', 'Darwin']: if DIST.lower() == 'fedora' and int(DIST_VERSION) < 36 or \ DIST.lower() == 'rhel' and int(DIST_VERSION) <= 9: bdist_rpm_options = dict(requires='python3 ' 'python3-arrow ' 'python3-beautifulsoup4 ' 'python3-cryptography ' 'python3-dateutil ' 'python3-keyring ' 'python3-lxml ' 'python3-packaging ' 'python3-psutil ' 'python3-pysocks ' 'python3-qt5 ' 'python3-requests ' 'python3-requests-kerberos ' 'python3-SecretStorage ' 'python3-tzlocal ' 'qt5-qtmultimedia ' 'qt5-qtsvg ', dist_dir='./build') else: bdist_rpm_options = dict(requires='python3 ' 'python3-arrow ' 'python3-beautifulsoup4 ' 'python3-cryptography ' 'python3-dateutil ' 'python3-keyring ' 'python3-lxml ' 'python3-psutil ' 'python3-pysocks ' 'python3-pyqt6 ' 'python3-requests ' 'python3-requests-kerberos ' 'python3-SecretStorage ' 'python3-tzlocal ' 'qt6-qtmultimedia ' 'qt6-qtsvg ', dist_dir='./build') setup(name=NAME, version=VERSION, license='GPL-2.0', description='Nagios status monitor for desktop', long_description='Nagstamon is a Nagios status monitor which takes place in systray or on desktop (GNOME, KDE, Windows) as floating statusbar to inform you in realtime about the status of your Nagios and derivatives monitored network. It allows to connect to multiple Nagios, Icinga, Opsview, Op5Monitor, Checkmk/Multisite, Centreon and Thruk servers.', classifiers=CLASSIFIERS, author='Henri Wahl', author_email='henri@nagstamon.de', url='https://nagstamon.de', download_url='https://nagstamon.de/download', scripts=[NAGSTAMON_SCRIPT], packages=packages, package_dir={'Nagstamon': 'Nagstamon'}, package_data={'Nagstamon': ['resources/*.*', 'resources/qui/*', 'resources/LICENSE', 'resources/CREDITS']}, data_files=[(f'{sys.prefix}/share/man/man1', ['Nagstamon/resources/nagstamon.1.gz']), (f'{sys.prefix}/share/pixmaps', ['Nagstamon/resources/nagstamon.svg']), (f'{sys.prefix}/share/applications', ['Nagstamon/resources/nagstamon.desktop'])], options=dict(build_exe=build_exe_options, bdist_mac=bdist_mac_options, bdist_dmg=bdist_dmg_options, bdist_rpm=bdist_rpm_options) ) Nagstamon-master/tests/000077500000000000000000000000001505160700500154375ustar00rootroot00000000000000Nagstamon-master/tests/test_alertmanager.py000066400000000000000000000216341505160700500215200ustar00rootroot00000000000000import json from pylint import lint import unittest from Nagstamon.Servers.Alertmanager import AlertmanagerServer conf = {} conf['debug_mode'] = True class test_alertmanager(unittest.TestCase): def test_lint_with_pylint(self): with self.assertRaises(SystemExit) as cm: lint.Run(['Nagstamon/Servers/Alertmanager']) self.assertEqual(cm.exception.code, 0) def test_unit_alert_suppressed(self): with open('tests/test_alertmanager_suppressed.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = 'instance,pod_name,namespace' test_class.map_to_servicename = 'alertname' test_class.map_to_status_information = 'message,summary,description' test_class.map_to_unknwon = '' test_class.map_to_critical = '' test_class.map_to_warning = '' test_class.map_to_ok = '' test_result = test_class._process_alert(data) self.assertEqual(test_result['attempt'], 'suppressed') self.assertEqual(test_result['acknowledged'], True) self.assertEqual(test_result['scheduled_downtime'], True) self.assertEqual(test_result['host'], '127.0.0.1') self.assertEqual(test_result['name'], 'Error') self.assertEqual(test_result['server'], '') self.assertEqual(test_result['status'], 'WARNING') self.assertEqual(test_result['labels'], {"alertname":"Error","device":"murpel","endpoint":"metrics","instance":"127.0.0.1:9100","job":"node-exporter","namespace":"monitoring","pod":"monitoring-prometheus-node-exporter-4711","prometheus":"monitoring/monitoring-prometheus-oper-prometheus","service":"monitoring-prometheus-node-exporter","severity":"warning"}) self.assertEqual(test_result['generatorURL'], 'http://localhost') self.assertEqual(test_result['fingerprint'], '0ef7c4bd7a504b8d') self.assertEqual(test_result['status_information'], 'Network interface "murpel" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711') def test_unit_alert_skipped(self): with open('tests/test_alertmanager_skipped.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = 'instance,pod_name,namespace' test_class.map_to_servicename = 'alertname' test_class.map_to_status_information = 'message,summary,description' test_result = test_class._process_alert(data) self.assertEqual(test_result, False) def test_unit_alert_warning(self): with open('tests/test_alertmanager_warning.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = 'instance,pod_name,namespace' test_class.map_to_servicename = 'alertname' test_class.map_to_status_information = 'message,summary,description' test_class.map_to_unknwon = '' test_class.map_to_critical = '' test_class.map_to_warning = '' test_class.map_to_ok = '' test_result = test_class._process_alert(data) self.assertEqual(test_result['attempt'], 'active') self.assertEqual(test_result['acknowledged'], False) self.assertEqual(test_result['scheduled_downtime'], False) self.assertEqual(test_result['host'], 'unknown') self.assertEqual(test_result['name'], 'TargetDown') self.assertEqual(test_result['server'], '') self.assertEqual(test_result['status'], 'WARNING') self.assertEqual(test_result['labels'], {"alertname": "TargetDown","job": "kubelet","prometheus": "monitoring/monitoring-prometheus-oper-prometheus","severity": "warning"}) self.assertEqual(test_result['generatorURL'], 'http://localhost') self.assertEqual(test_result['fingerprint'], '7be970c6e97b95c9') self.assertEqual(test_result['status_information'], '66.6% of the kubelet targets are down.') def test_unit_alert_critical(self): with open('tests/test_alertmanager_critical.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = 'instance,pod_name,namespace' test_class.map_to_servicename = 'alertname' test_class.map_to_status_information = 'message,summary,description' test_class.map_to_unknwon = '' test_class.map_to_critical = '' test_class.map_to_warning = '' test_class.map_to_ok = '' test_result = test_class._process_alert(data) self.assertEqual(test_result['attempt'], 'active') self.assertEqual(test_result['acknowledged'], False) self.assertEqual(test_result['scheduled_downtime'], False) self.assertEqual(test_result['host'], '127.0.0.1') self.assertEqual(test_result['name'], 'Error') self.assertEqual(test_result['server'], '') self.assertEqual(test_result['status'], 'ERROR') self.assertEqual(test_result['labels'], {"alertname":"Error","device":"murpel","endpoint":"metrics","instance":"127.0.0.1:9100","job":"node-exporter","namespace":"monitoring","pod":"monitoring-prometheus-node-exporter-4711","prometheus":"monitoring/monitoring-prometheus-oper-prometheus","service":"monitoring-prometheus-node-exporter","severity":"error"}) self.assertEqual(test_result['generatorURL'], 'http://localhost') self.assertEqual(test_result['fingerprint'], '0ef7c4bd7a504b8d') self.assertEqual(test_result['status_information'], 'Network interface "murpel" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711') def test_unit_alert_critical_with_empty_maps(self): with open('tests/test_alertmanager_critical.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = '' test_class.map_to_servicename = '' test_class.map_to_status_information = '' test_class.map_to_unknwon = '' test_class.map_to_critical = '' test_class.map_to_warning = '' test_class.map_to_ok = '' test_result = test_class._process_alert(data) self.assertEqual(test_result['attempt'], 'active') self.assertEqual(test_result['acknowledged'], False) self.assertEqual(test_result['scheduled_downtime'], False) self.assertEqual(test_result['host'], 'unknown') self.assertEqual(test_result['name'], 'unknown') self.assertEqual(test_result['server'], '') self.assertEqual(test_result['status'], 'ERROR') self.assertEqual(test_result['labels'], {"alertname":"Error","device":"murpel","endpoint":"metrics","instance":"127.0.0.1:9100","job":"node-exporter","namespace":"monitoring","pod":"monitoring-prometheus-node-exporter-4711","prometheus":"monitoring/monitoring-prometheus-oper-prometheus","service":"monitoring-prometheus-node-exporter","severity":"error"}) self.assertEqual(test_result['generatorURL'], 'http://localhost') self.assertEqual(test_result['fingerprint'], '0ef7c4bd7a504b8d') self.assertEqual(test_result['status_information'], '') def test_unit_alert_custom_severity_critical(self): with open('tests/test_alertmanager_custom_severity.json') as json_file: data = json.load(json_file) test_class = AlertmanagerServer() test_class.map_to_hostname = 'instance,pod_name,namespace' test_class.map_to_servicename = 'alertname' test_class.map_to_status_information = 'message,summary,description' test_class.map_to_unknwon = 'unknown' test_class.map_to_critical = 'error,rocketchat' test_class.map_to_warning = 'warning' test_class.map_to_ok = 'ok' test_result = test_class._process_alert(data) self.assertEqual(test_result['attempt'], 'active') self.assertEqual(test_result['acknowledged'], False) self.assertEqual(test_result['scheduled_downtime'], False) self.assertEqual(test_result['host'], '127.0.0.1') self.assertEqual(test_result['name'], 'Error') self.assertEqual(test_result['server'], '') self.assertEqual(test_result['status'], 'CRITICAL') self.assertEqual(test_result['labels'], {"alertname":"Error","device":"murpel","endpoint":"metrics","instance":"127.0.0.1:9100","job":"node-exporter","namespace":"monitoring","pod":"monitoring-prometheus-node-exporter-4711","prometheus":"monitoring/monitoring-prometheus-oper-prometheus","service":"monitoring-prometheus-node-exporter","severity":"rocketchat"}) self.assertEqual(test_result['generatorURL'], 'http://localhost') self.assertEqual(test_result['fingerprint'], '0ef7c4bd7a504b8d') self.assertEqual(test_result['status_information'], 'Network interface "murpel" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711') if __name__ == '__main__': unittest.main() Nagstamon-master/tests/test_alertmanager_critical.json000066400000000000000000000017311505160700500237070ustar00rootroot00000000000000{ "annotations": { "message": "Network interface \"murpel\" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711" }, "endsAt": "1970-01-01T00:00:12.345Z", "fingerprint": "0ef7c4bd7a504b8d", "receivers": [ { "name": "null" } ], "startsAt": "1970-01-01T00:00:01.345Z", "status": { "inhibitedBy": [], "silencedBy": [], "state": "active" }, "updatedAt": "1970-01-01T00:00:12.345Z", "generatorURL": "http://localhost", "labels": { "alertname": "Error", "device": "murpel", "endpoint": "metrics", "instance": "127.0.0.1:9100", "job": "node-exporter", "namespace": "monitoring", "pod": "monitoring-prometheus-node-exporter-4711", "prometheus": "monitoring/monitoring-prometheus-oper-prometheus", "service": "monitoring-prometheus-node-exporter", "severity": "error" } }Nagstamon-master/tests/test_alertmanager_custom_severity.json000066400000000000000000000017361505160700500253660ustar00rootroot00000000000000{ "annotations": { "message": "Network interface \"murpel\" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711" }, "endsAt": "1970-01-01T00:00:12.345Z", "fingerprint": "0ef7c4bd7a504b8d", "receivers": [ { "name": "null" } ], "startsAt": "1970-01-01T00:00:01.345Z", "status": { "inhibitedBy": [], "silencedBy": [], "state": "active" }, "updatedAt": "1970-01-01T00:00:12.345Z", "generatorURL": "http://localhost", "labels": { "alertname": "Error", "device": "murpel", "endpoint": "metrics", "instance": "127.0.0.1:9100", "job": "node-exporter", "namespace": "monitoring", "pod": "monitoring-prometheus-node-exporter-4711", "prometheus": "monitoring/monitoring-prometheus-oper-prometheus", "service": "monitoring-prometheus-node-exporter", "severity": "rocketchat" } }Nagstamon-master/tests/test_alertmanager_skipped.json000066400000000000000000000016171505160700500235570ustar00rootroot00000000000000{ "labels": { "alertname": "Watchdog", "prometheus": "monitoring/monitoring-prometheus-oper-prometheus", "severity": "none" }, "annotations": { "message": "This is an alert meant to ensure that the entire alerting pipeline is functional.\nThis alert is always firing, therefore it should always be firing in Alertmanager\nand always fire against a receiver. There are integrations with various notification\nmechanisms that send a notification when this alert is not firing. For example the\n\"DeadMansSnitch\" integration in PagerDuty.\n" }, "startsAt": "1970-01-01T00:00:00.520032135Z", "endsAt": "1970-01-01T00:00:00.520032135Z", "generatorURL": "http://localhost", "status": { "state": "active", "silencedBy": [], "inhibitedBy": [] }, "receivers": [ "null" ], "fingerprint": "1b43ca4565de75d2" }Nagstamon-master/tests/test_alertmanager_suppressed.json000066400000000000000000000020331505160700500243060ustar00rootroot00000000000000{ "annotations": { "message": "Network interface \"murpel\" showing errors on node-exporter monitoring/monitoring-prometheus-node-exporter-4711" }, "endsAt": "1970-01-01T00:00:12.345Z", "fingerprint": "0ef7c4bd7a504b8d", "receivers": [ { "name": "null" } ], "startsAt": "1970-01-01T00:00:01.345Z", "status": { "inhibitedBy": [], "silencedBy": [ "bb043288-42a0-4315-8bae-15cde1d7e239" ], "state": "suppressed" }, "updatedAt": "1970-01-01T00:00:12.345Z", "generatorURL": "http://localhost", "labels": { "alertname": "Error", "device": "murpel", "endpoint": "metrics", "instance": "127.0.0.1:9100", "job": "node-exporter", "namespace": "monitoring", "pod": "monitoring-prometheus-node-exporter-4711", "prometheus": "monitoring/monitoring-prometheus-oper-prometheus", "service": "monitoring-prometheus-node-exporter", "severity": "warning" } }Nagstamon-master/tests/test_alertmanager_warning.json000066400000000000000000000012111505160700500235530ustar00rootroot00000000000000{ "labels": { "alertname": "TargetDown", "job": "kubelet", "prometheus": "monitoring/monitoring-prometheus-oper-prometheus", "severity": "warning" }, "annotations": { "message": "66.6% of the kubelet targets are down." }, "startsAt": "1970-01-01T00:00:00.520032135Z", "endsAt": "1970-01-01T00:00:00.520032135Z", "updatedAt": "1970-01-01T00:00:00.520032135Z", "generatorURL": "http://localhost", "status": { "state": "active", "silencedBy": [], "inhibitedBy": [] }, "receivers": [ "null" ], "fingerprint": "7be970c6e97b95c9" }