pax_global_header00006660000000000000000000000064144061355660014524gustar00rootroot0000000000000052 comment=435248b73cdc080c5b9fdd1c5915d29ea9e57827 linux-nvme-nvme-stas-a8026bb/000077500000000000000000000000001440613556600161475ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/.coveragerc.in000066400000000000000000000014551440613556600207020ustar00rootroot00000000000000# .coveragerc to control coverage.py for combined stafd/stacd coverage [run] data_file = coverage/nvme-stas parallel=True concurrency=thread [report] omit = /usr/* */test/test-*.py subprojects/libnvme/* # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Coverage cannot cover code running in threads def in_thread_exec # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise RuntimeError # Don't complain if non-runnable code isn't run: pass if 0: if __name__ *== *__main__ *: sys\.exit\(\) sys\.exit\(f?'.+\) # ImportError is usually OK because there will be a workaround import. except ImportError skip_empty = True [html] directory = coverage title = nvme-stas coverage report linux-nvme-nvme-stas-a8026bb/.dockerignore000066400000000000000000000000241440613556600206170ustar00rootroot00000000000000.git .github .build linux-nvme-nvme-stas-a8026bb/.github/000077500000000000000000000000001440613556600175075ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/.github/dependabot.yml000066400000000000000000000003231440613556600223350ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly"linux-nvme-nvme-stas-a8026bb/.github/workflows/000077500000000000000000000000001440613556600215445ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/.github/workflows/docker-publish.yml000066400000000000000000000036231440613556600252060ustar00rootroot00000000000000name: Docker # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. on: push: branches: [ main ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: branches: [ main ] workflow_dispatch: env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: docker-publish: if: ${{ !github.event.act }} # skip during local actions testing runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v3 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@507c2f2dc502c992ad446e3d7a5dfbe311567a96 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} linux-nvme-nvme-stas-a8026bb/.github/workflows/docker-test.yml000066400000000000000000000031211440613556600245100ustar00rootroot00000000000000name: Test on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: docker-run: if: ${{ !github.event.act }} # skip during local actions testing runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install requirements # make sure nvme-cli installed (we need it for /etc/nvme/hostnqn and /etc/nvme/hostid) run: sudo apt-get install --yes --quiet nvme-cli - name: Load Kernel drivers run: sudo modprobe -v nvme-fabrics - name: Fix D-BUS run: | sed 's/@STAFD_DBUS_NAME@/org.nvmexpress.staf/g' etc/dbus-1/system.d/org.nvmexpress.staf.in.conf | sudo tee /usr/share/dbus-1/system.d/org.nvmexpress.staf.conf sed 's/@STACD_DBUS_NAME@/org.nvmexpress.stac/g' etc/dbus-1/system.d/org.nvmexpress.stac.in.conf | sudo tee /usr/share/dbus-1/system.d/org.nvmexpress.stac.conf sudo systemctl reload dbus.service - name: Build & Start containers run: docker-compose -f "docker-compose.yml" up --detach --build - name: Run tests run: | docker-compose ps docker-compose exec -T stafd stafctl ls docker-compose exec -T stafd stafctl status docker-compose exec -T stacd stacctl ls docker-compose exec -T stacd stacctl status docker-compose logs - name: Logs if: failure() run: | docker-compose ps || true docker-compose logs || true - name: Stop containers if: always() run: docker-compose -f "docker-compose.yml" down linux-nvme-nvme-stas-a8026bb/.github/workflows/meson-test.yml000066400000000000000000000052621440613556600243720ustar00rootroot00000000000000name: Meson on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: meson-build: runs-on: ubuntu-latest steps: - name: "CHECKOUT: nvme-stas" uses: actions/checkout@v3 - name: "INSTALL: Overall dependencies" run: | sudo apt-mark hold grub-efi-amd64-signed # Workaround for upstream issue sudo apt-get update --yes --quiet sudo apt-get upgrade --yes --quiet sudo apt-get install --yes --quiet python3-pip cmake iproute2 sudo python3 -m pip install --upgrade pip sudo python3 -m pip install --upgrade wheel meson ninja - name: "INSTALL: nvme-stas dependencies" run: | sudo apt-get install --yes --quiet docbook-xml sudo apt-get install --yes --quiet docbook-xsl sudo apt-get install --yes --quiet xsltproc sudo apt-get install --yes --quiet libglib2.0-dev sudo apt-get install --yes --quiet libgirepository1.0-dev sudo apt-get install --yes --quiet libsystemd-dev sudo apt-get install --yes --quiet python3-systemd sudo apt-get install --yes --quiet python3-pyudev sudo apt-get install --yes --quiet python3-lxml python3 -m pip install --upgrade dasbus pylint pyflakes PyGObject python3 -m pip install --upgrade vermin pyfakefs importlib-resources - name: "INSTALL: libnvme" run: | sudo apt-get install --yes --quiet swig libjson-c-dev meson subprojects download meson setup .build subprojects/libnvme -Dlibdbus=disabled -Dopenssl=disabled -Dbuildtype=release -Dprefix=/usr -Dpython=true ninja -C .build sudo meson install -C .build - name: "CONFIG: PYTHONPATH" run: | echo "PYTHONPATH=.build:.build/subprojects/libnvme:/usr/lib/python3/dist-packages/" >> $GITHUB_ENV - name: "TEST: nvme-stas" uses: BSFishy/meson-build@v1.0.3 with: action: test directory: .build setup-options: -Dman=true -Dhtml=true options: --print-errorlogs --suite nvme-stas # Preserve meson's log file on failure - uses: actions/upload-artifact@v3 if: failure() with: name: "Linux_Meson_Testlog" path: .build/meson-logs/* - name: "Generate coverage report" run: | python3 -m pip install pytest python3 -m pip install pytest-cov PYTHONPATH=.build:.build/subprojects/libnvme:/usr/lib/python3/dist-packages/ pytest --cov=./staslib --cov-report=xml test/test-*.py - uses: codecov/codecov-action@v3 with: fail_ci_if_error: falselinux-nvme-nvme-stas-a8026bb/.github/workflows/pylint.yml000066400000000000000000000063431440613556600236140ustar00rootroot00000000000000name: Linters on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: docker-lint: if: ${{ !github.event.act }} # skip during local actions testing runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - uses: hadolint/hadolint-action@v3.1.0 with: recursive: true ignore: DL3041 python-lint: runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - name: "CHECKOUT: nvme-stas" uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} # - name: "UPGRADE: existing packages" # run: | # sudo apt-get update --yes --quiet || true # sudo apt-get upgrade --yes --quiet || true - name: "INSTALL: additional packages" run: | sudo apt-get install --yes --quiet python3-pip || true sudo apt-get install --yes --quiet cmake || true sudo apt-get install --yes --quiet libgirepository1.0-dev || true sudo apt-get install --yes --quiet libsystemd-dev || true sudo apt-get install --yes --quiet python3-systemd || true sudo python3 -m pip install --upgrade pip sudo python3 -m pip install --upgrade wheel sudo python3 -m pip install --upgrade meson sudo python3 -m pip install --upgrade ninja python3 -m pip install --upgrade dasbus python3 -m pip install --upgrade pylint python3 -m pip install --upgrade pyflakes python3 -m pip install --upgrade PyGObject python3 -m pip install --upgrade lxml python3 -m pip install --upgrade pyudev - name: "BUILD: libnvme" run: | sudo apt-get install --yes --quiet swig libjson-c-dev || true meson subprojects download meson setup builddir subprojects/libnvme -Dlibdbus=disabled -Dopenssl=disabled -Dbuildtype=release -Dprefix=/usr -Dpython=true ninja -C builddir sudo meson install -C builddir - name: Set PYTHONPATH run: | echo "PYTHONPATH=builddir:builddir/subprojects/libnvme:/usr/lib/python3/dist-packages/" >> $GITHUB_ENV - name: Show test environment run: | echo -e "Build Directory:\n$(ls -laF builddir)" python3 -VV python3 -m site python3 -m pylint --version echo "pyflakes $(python3 -m pyflakes --version)" - name: Pylint run: | python3 -m pylint --rcfile=test/pylint.rc *.py staslib - name: Pyflakes if: always() run: | python3 -m pyflakes *.py staslib python-black: if: ${{ !github.event.act }} # skip during local actions testing name: python-black formatter runs-on: ubuntu-latest steps: - name: "CHECKOUT: nvme-stas" uses: actions/checkout@v3 - name: "BLACK" uses: psf/black@stable with: options: "--check --diff --color --line-length 120 --skip-string-normalization --extend-exclude (subprojects|debian|.build)" src: "." linux-nvme-nvme-stas-a8026bb/.gitignore000066400000000000000000000002171440613556600201370ustar00rootroot00000000000000.build obj-x86_64-linux-gnu # DEBs Artifacts redhat-linux-build # RPMs Artifacts __pycache__ subprojects/* !subprojects/*.wrap .vscode linux-nvme-nvme-stas-a8026bb/.readthedocs.yaml000066400000000000000000000012661440613556600214030ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # version: 2 python: system_packages: true build: os: ubuntu-22.04 tools: python: "3" apt_packages: - meson - python3-lxml - docbook-xsl - xsltproc - pandoc jobs: post_install: - pip3 install lxml pre_build: - meson .build -Dreadthedocs=true || cat .build/meson-logs/meson-log.txt - ninja -C .build sphinx: configuration: .build/doc/readthedocs/conf.py linux-nvme-nvme-stas-a8026bb/CONTRIBUTING.md000066400000000000000000000046541440613556600204110ustar00rootroot00000000000000# Contributing guidelines Thanks for contributing to this project. We'd like to get your feedback and suggestions. ## Issues Bugs, feature requests, or issues must be reported through GitHub's "[Issues](https://github.com/linux-nvme/nvme-stas/issues)". Make sure there is not an existing open issue (or recently closed) for the same problem or feature. Include all pertinent info: environment, nvme-stas version, how to reproduce, expected result, etc. ## Contribution process All contributions should be made through pull requests. Before submitting make sure that you followed the coding style (below) and you ran and passed the unit tests. ### How to submit contributions 1. Fork the repo 2. Make changes. Try to split you changes into distinct commits and avoid making big reformatting as it makes it harder to review the changes. 3. If possible, add unit tests for new features. 4. Run `make black` to make sure the changes conform to coding styles. See [Coding styles]() below. 5. Run `make test` and make sure all tests pass. 6. Commit to your fork with descriptive message and use the "`--signoff, -s`" option 7. Send the pull request 8. Check for failures in the automated CI output. 9. Be involved in the conversation (if any). ## Coding style nvme-stas uses [black](https://black.readthedocs.io/en/stable/), [pylint](https://pylint.readthedocs.io/en/latest/), and [pyflakes](https://pypi.org/project/pyflakes/) to check that the code meets minimum style requirements. We use `black` with the following options. ```bash black --diff --color --line-length 120 --skip-string-normalization [file or directory] ``` You can also use this convenience make command: ``` make black ``` ## Minimum Python version required nvme-stas must be able to run with Python 3.6. Code changes cannot use Python features not supported by Python 3.6. The only exception is for Python scripts used during the build phase (e.g. scripts to generate the documentation) or test scripts. Those scripts can follow Python 3.8 syntax. nvme-stas uses [vermin](https://pypi.org/project/vermin/) to verify that the code submitted complies with the minimum version required. Vermin gets executed as part of the tests (see `make test` below). ## Unit tests Unit tests can be run with this command: ```bash make test ``` This command not only runs the unit tests, but also pylint, pyflakes, and vermin. Make sure that these programs are installed otherwise the tests will be skipped. linux-nvme-nvme-stas-a8026bb/DISTROS.md000066400000000000000000000556421440613556600176340ustar00rootroot00000000000000# Notes to Linux distributors This document contains information about the packaging of nvme-stas. ## Compile-time dependencies nvme-stas is a Python 3 project and does not require compile-time libraries per se. However, we use the meson build system for installation and testing. | Library / Program | Purpose | Mandatory / Optional | | ----------------- | ------------------------------------------------- | -------------------- | | meson | Project configuration, installation, and testing. | Mandatory | ## Unit tests dependencies nvme-stas provides static code analysis (pylint, pyflakes), which can be run with "`meson test`". | Library / Program | Purpose | Mandatory / Optional | | ----------------- | ------------------------------------------------------------ | -------------------- | | pylint | Static code analysis | Optional | | python3-pyflakes | Static code analysis | Optional | | python3-pyfakefs | Static code analysis | Optional | | vermin | Check that code meets minimum Python version requirement (3.6) | Optional | ## Run-time dependencies Python 3.6 is the minimum version required to run nvme-stas. nvme-stas is built on top of libnvme, which is used to interact with the kernel's NVMe driver (i.e. `drivers/nvme/host/`). To support all the features of nvme-stas, several changes to the Linux kernel are required. nvme-stas can also operate with older kernels, but with limited functionality. Kernel 5.18 provides all the features needed by nvme-stas. nvme-stas can also work with older kernels that include back-ported changes to the NVMe driver. The next table shows different features that were added to the NVMe driver and in which version of the Linux kernel they were added (the list of git patches can be found in addendum). Note that the ability to query the NVMe driver to determine what options it supports was added in 5.17. This is needed if nvme-stas is to make the right decision on whether a feature is supported. Otherwise, nvme-stas can only rely on the kernel version to decide what is supported. This can greatly limit the features supported on back-ported kernels. | Feature | Introduced in kernel version | | ------------------------------------------------------------ | ---------------------------- | | **`host-iface` option** - Ability to force TCP connections over a specific interface. Needed for zeroconf provisioning. | 5.14 | | **TP8013 Support** - Discovery Controller (DC) Unique NQN. Allow the creation of connections to DC with a NQN other than the default `nqn.2014-08.org.nvmexpress.discovery` | 5.16 | | **Query supported options** - Allow user-space applications to query which options the NVMe driver supports | 5.17 | | **TP8010 Support** - Ability for a Host to register with a Discovery Controller. This version of the kernel introduces a new event to indicate to user-space apps (e.g. nvme-stas) when a connection to a DC is restored. This is used to trigger a re-registration of the host. This kernel also exposes the DC Type (dctype) attribute through the sysfs, which is needed to determine whether registration is supported. | 5.18 | | - Print actual source IP address (`src_addr`) through sysfs "address" attr. This is needed to verify that TCP connections were made on the right interface.
- Consider also `host_iface` when checking IP options.
- Send a rediscover uevent when a persistent discovery controller reconnects. | 6.1 | nvme-stas also depends on the following run-time libraries and modules. Note that versions listed are the versions that were tested with at the time the code was developed. | Package / Module | Min version | stafd | stacd | How to determine the currently installed version | | ---------------------------------------------------------- | ----------- | ------------- | ------------- | ------------------------------------------------------------ | | python3 | 3.6 | **Mandatory** | **Mandatory** | `python3 --version`
`nvme-stas` requires Python 3.6 as a minimum. | | python3-dasbus | 1.6 | **Mandatory** | **Mandatory** | pip list \| grep dasbus | | python3-pyudev | 0.22.0 | **Mandatory** | **Mandatory** | `python3 -c 'import pyudev; print(f"{pyudev.__version__}")'` | | python3-systemd | 240 | **Mandatory** | **Mandatory** | `systemd --version` | | python3-gi (Debian) OR python3-gobject (Fedora) | 3.36.0 | **Mandatory** | **Mandatory** | `python3 -c 'import gi; print(f"{gi.__version__}")'` | | nvme-tcp (kernel module) | 5.18 * | **Mandatory** | **Mandatory** | N/A | | dbus-daemon | 1.12.2 | **Mandatory** | **Mandatory** | `dbus-daemon --version` | | avahi-daemon | 0.7 | **Mandatory** | Not required | `avahi-daemon --version` | | python3-libnvme | 1.3 | **Mandatory** | **Mandatory** | `python3 -c 'import libnvme; print(f"{libnvme.__version__}")'` | | importlib.resources.files() OR importlib_resources.files() | *** | Optional | Optional | `importlib.resources.files()` was introduced in Python 3.9 and backported to earlier versions as `importlib_resources.files()`. If neither modules can be found, `nvme-stas` will default to using the less efficient `pkg_resources.resource_string()` instead. When `nvme-stas` is no longer required to support Python 3.6 and is allowed a minimum of 3.9 or later, only `importlib.resources.files()` will be required. | * Kernel 5.18 provides full functionality. nvme-stas can work with older kernels, but with limited functionality, unless the kernels contain back-ported features (see Addendum for the list of kernel patches that could be back-ported to an older kernel). ## Things to do post installation ### D-Bus configuration We install D-Bus configuration files under `/usr/share/dbus-1/system.d`. One needs to run **`systemctl reload dbus-broker.service`** (Fedora) OR **`systemctl reload dbus.service`** (SuSE, Debian) for the new configuration to take effect. ### Configuration shared with `libnvme` and `nvme-cli` `stafd` and `stacd` use the `libnvme` library to interact with the Linux kernel. And `libnvme` as well as `nvme-cli` rely on two configuration files, `/etc/nvme/hostnqn` and `/etc/nvme/hostid`, to retrieve the Host NQN and ID respectively. These files should be created post installation with the help of the `stadadm` utility. Here's an example for Debian-based systems: ``` if [ "$1" = "configure" ]; then if [ ! -d "/etc/nvme" ] mkdir /etc/nvme fi if [ ! -s "/etc/nvme/hostnqn" ]; then stasadm hostnqn -f /etc/nvme/hostnqn fi if [ ! -s "/etc/nvme/hostid" ]; then stasadm hostid -f /etc/nvme/hostid fi fi ``` The utility program `stasadm` gets installed with `nvme-stas`. `stasadm` also manages the creation (and updating) of `/etc/stas/sys.conf`, the `nvme-stas` system configuration file. ### Configuration specific to nvme-stas The [README](./README.md) file defines the following three configuration files: - `/etc/stas/sys.conf` - `/etc/stas/stafd.conf` - `/etc/stas/stacd.conf` Care should be taken during upgrades to preserve customer configuration and not simply overwrite it. The process to migrate the configuration data and the list of parameters to migrate is still to be defined. ### Enabling and starting the daemons Lastly, the two daemons, `stafd` and `stacd`, should be enabled (e.g. `systemctl enable stafd.service stacd.service`) and started (e.g. `systemctl start stafd.service stacd.service`). # Compatibility between nvme-stas and nvme-cli Udev rules are installed along with `nvme-cli` (e.g. `/usr/lib/udev/rules.d/70-nvmf-autoconnect.rules`). These udev rules allow `nvme-cli` to perform tasks similar to those performed by `nvme-stas`. However, the udev rules in `nvme-cli` version 2.1.2 and prior drop the `host-iface` parameter when making TCP connections to I/O controllers. `nvme-stas`, on the other hand, always makes sure that TCP connections to I/O controllers are made over the right interface using the `host-iface` parameter. We essentially have a race condition because `nvme-stas` and `nvme-cli` react to the same kernel events. Both try to perform the same task in parallel, which is to connect to I/O controllers. Because `nvme-stas` is written in Python and the udevd daemon (i.e. the process running the udev rules) in C, `nvme-stas` usually loses the race and TCP connections are made by the udev rules without specifying the `host-iface`. To remedy to this problem, `nvme-stas` disables `nvme-cli` udev rules and assumes the tasks performed by the udev rules. This way, only one process will take action on kernel events eliminating any race conditions. This also ensure that the right `host-iface` is used when making TCP connections. # Addendum ## Kernel patches for nvme-stas 1.x Here's the list of kernel patches (added in kernels 5.14 to 5.18) that will enable all features of nvme-stas. ``` commit e3448b134426741902b6e2c07cbaf5f66cfd2ebc Author: Martin Belanger Date: Tue Feb 8 14:18:02 2022 -0500 nvme: Expose cntrltype and dctype through sysfs TP8010 introduces the Discovery Controller Type attribute (dctype). The dctype is returned in the response to the Identify command. This patch exposes the dctype through the sysfs. Since the dctype depends on the Controller Type (cntrltype), another attribute of the Identify response, the patch also exposes the cntrltype as well. The dctype will only be displayed for discovery controllers. A note about the naming of this attribute: Although TP8010 calls this attribute the Discovery Controller Type, note that the dctype is now part of the response to the Identify command for all controller types. I/O, Discovery, and Admin controllers all share the same Identify response PDU structure. Non-discovery controllers as well as pre-TP8010 discovery controllers will continue to set this field to 0 (which has always been the default for reserved bytes). Per TP8010, the value 0 now means "Discovery controller type is not reported" instead of "Reserved". One could argue that this definition is correct even for non-discovery controllers, and by extension, exposing it in the sysfs for non-discovery controllers is appropriate. Signed-off-by: Martin Belanger commit 68c483a105ce7107f1cf8e1ed6c2c2abb5baa551 Author: Martin Belanger Date: Thu Feb 3 16:04:29 2022 -0500 nvme: send uevent on connection up When connectivity with a controller is lost, the driver will keep trying to reconnect once every 10 sec. When connection is restored, user-space apps need to be informed so that they can take proper action. For example, TP8010 introduces the DIM PDU, which is used to register with a discovery controller (DC). The DIM PDU is sent from user-space. The DIM PDU must be sent every time a connection is established with a DC. Therefore, the kernel must tell user-space apps when connection is restored so that registration can happen. The uevent sent is a "change" uevent with environmental data set to: "NVME_EVENT=connected". Signed-off-by: Martin Belanger Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg Reviewed-by: Chaitanya Kulkarni commit f18ee3d988157ebcadc9b7e5fd34811938f50223 Author: Hannes Reinecke Date: Tue Dec 7 14:55:49 2021 +0100 nvme-fabrics: print out valid arguments when reading from /dev/nvme-fabrics Currently applications have a hard time figuring out which nvme-over-fabrics arguments are supported for any given kernel; the ioctl will return an error code on failure, and the application has to guess whether this was due to an invalid argument or due to a connection or controller error. With this patch applications can read a list of supported arguments by simply reading from /dev/nvme-fabrics, allowing them to validate the connection string. Signed-off-by: Hannes Reinecke Reviewed-by: Chaitanya Kulkarni Signed-off-by: Christoph Hellwig commit e5ea42faa773c6a6bb5d9e9f5c2cc808940b5a55 Author: Hannes Reinecke Date: Wed Sep 22 08:35:25 2021 +0200 nvme: display correct subsystem NQN With discovery controllers supporting unique subsystem NQNs the actual subsystem NQN might be different from that one passed in via the connect args. So add a helper to display the resulting subsystem NQN. Signed-off-by: Hannes Reinecke Reviewed-by: Chaitanya Kulkarni Signed-off-by: Christoph Hellwig commit 20e8b689c9088027b7495ffd6f80812c11ecc872 Author: Hannes Reinecke Date: Wed Sep 22 08:35:24 2021 +0200 nvme: Add connect option 'discovery' Add a connect option 'discovery' to specify that the connection should be made to a discovery controller, not a normal I/O controller. With discovery controllers supporting unique subsystem NQNs we cannot easily distinguish by the subsystem NQN if this should be a discovery connection, but we need this information to blank out options not supported by discovery controllers. Signed-off-by: Hannes Reinecke Reviewed-by: Chaitanya Kulkarni Signed-off-by: Christoph Hellwig commit 954ae16681f6bdf684f016ca626329302a38e177 Author: Hannes Reinecke Date: Wed Sep 22 08:35:23 2021 +0200 nvme: expose subsystem type in sysfs attribute 'subsystype' With unique discovery controller NQNs we cannot distinguish the subsystem type by the NQN alone, but need to check the subsystem type, too. So expose the subsystem type in a new sysfs attribute 'subsystype'. Signed-off-by: Hannes Reinecke Reviewed-by: Chaitanya Kulkarni Signed-off-by: Christoph Hellwig commit 3ede8f72a9a2825efca23a3552e80a1202ea88fd Author: Martin Belanger Date: Thu May 20 15:09:34 2021 -0400 nvme-tcp: allow selecting the network interface for connections In our application, we need a way to force TCP connections to go out a specific IP interface instead of letting Linux select the interface based on the routing tables. Add the 'host-iface' option to allow specifying the interface to use. When the option host-iface is specified, the driver uses the specified interface to set the option SO_BINDTODEVICE on the TCP socket before connecting. This new option is needed in addtion to the existing host-traddr for the following reasons: Specifying an IP interface by its associated IP address is less intuitive than specifying the actual interface name and, in some cases, simply doesn't work. That's because the association between interfaces and IP addresses is not predictable. IP addresses can be changed or can change by themselves over time (e.g. DHCP). Interface names are predictable [1] and will persist over time. Consider the following configuration. 1: lo: mtu 65536 qdisc noqueue state ... link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 100.0.0.100/24 scope global lo valid_lft forever preferred_lft forever 2: enp0s3: mtu 1500 qdisc ... link/ether 08:00:27:21:65:ec brd ff:ff:ff:ff:ff:ff inet 100.0.0.100/24 scope global enp0s3 valid_lft forever preferred_lft forever 3: enp0s8: mtu 1500 qdisc ... link/ether 08:00:27:4f:95:5c brd ff:ff:ff:ff:ff:ff inet 100.0.0.100/24 scope global enp0s8 valid_lft forever preferred_lft forever The above is a VM that I configured with the same IP address (100.0.0.100) on all interfaces. Doing a reverse lookup to identify the unique interface associated with 100.0.0.100 does not work here. And this is why the option host_iface is required. I understand that the above config does not represent a standard host system, but I'm using this to prove a point: "We can never know how users will configure their systems". By te way, The above configuration is perfectly fine by Linux. The current TCP implementation for host_traddr performs a bind()-before-connect(). This is a common construct to set the source IP address on a TCP socket before connecting. This has no effect on how Linux selects the interface for the connection. That's because Linux uses the Weak End System model as described in RFC1122 [2]. On the other hand, setting the Source IP Address has benefits and should be supported by linux-nvme. In fact, setting the Source IP Address is a mandatory FedGov requirement (e.g. connection to a RADIUS/TACACS+ server). Consider the following configuration. $ ip addr list dev enp0s8 3: enp0s8: mtu 1500 qdisc ... link/ether 08:00:27:4f:95:5c brd ff:ff:ff:ff:ff:ff inet 192.168.56.101/24 brd 192.168.56.255 scope global enp0s8 valid_lft 426sec preferred_lft 426sec inet 192.168.56.102/24 scope global secondary enp0s8 valid_lft forever preferred_lft forever inet 192.168.56.103/24 scope global secondary enp0s8 valid_lft forever preferred_lft forever inet 192.168.56.104/24 scope global secondary enp0s8 valid_lft forever preferred_lft forever Here we can see that several addresses are associated with interface enp0s8. By default, Linux always selects the default IP address, 192.168.56.101, as the source address when connecting over interface enp0s8. Some users, however, want the ability to specify a different source address (e.g., 192.168.56.102, 192.168.56.103, ...). The option host_traddr can be used as-is to perform this function. In conclusion, I believe that we need 2 options for TCP connections. One that can be used to specify an interface (host-iface). And one that can be used to set the source address (host-traddr). Users should be allowed to use one or the other, or both, or none. Of course, the documentation for host_traddr will need some clarification. It should state that when used for TCP connection, this option only sets the source address. And the documentation for host_iface should say that this option is only available for TCP connections. References: [1] https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/ [2] https://tools.ietf.org/html/rfc1122 Tested both IPv4 and IPv6 connections. Signed-off-by: Martin Belanger Reviewed-by: Sagi Grimberg Reviewed-by: Hannes Reinecke Signed-off-by: Christoph Hellwig ``` ## Kernel patches for nvme-stas 2.x These patches are not essential for nvme-stas 2.x, but they allow nvme-stas to operate better. ``` commit f46ef9e87c9e8941b7acee45611c7c6a322592bb Author: Sagi Grimberg Date: Thu Sep 22 11:15:37 2022 +0300 nvme: send a rediscover uevent when a persistent discovery controller reconnects When a discovery controller is disconnected, no AENs will arrive to notify the host about discovery log change events. In order to solve this, send a uevent notification when a persistent discovery controller reconnects. We add a new ctrl flag NVME_CTRL_STARTED_ONCE that will be set on the first start, and consecutive calls will find it set, and send the event to userspace if the controller is a discovery controller. Upon the event reception, userspace will re-read the discovery log page and will act upon changes as it sees fit. Signed-off-by: Sagi Grimberg Reviewed-by: Daniel Wagner Reviewed-by: James Smart Signed-off-by: Christoph Hellwig commit 02c57a82c0081141abc19150beab48ef47f97f18 (tag: nvme-6.1-2022-09-20) Author: Martin Belanger Date: Wed Sep 7 08:27:37 2022 -0400 nvme-tcp: print actual source IP address through sysfs "address" attr TCP transport relies on the routing table to determine which source address and interface to use when making a connection. Currently, there is no way to tell from userspace where a connection was made. This patch exposes the actual source address using a new field named "src_addr=" in the "address" attribute. This is needed to diagnose and identify connectivity issues. With the source address we can infer the interface associated with each connection. This was tested with nvme-cli 2.0 to verify it does not have any adverse effect. The new "src_addr=" field will simply be displayed in the output of the "list-subsys" or "list -v" commands as shown here. $ nvme list-subsys nvme-subsys0 - NQN=nqn.2014-08.org.nvmexpress.discovery \ +- nvme0 tcp traddr=192.168.56.1,trsvcid=8009,src_addr=192.168.56.101 live Signed-off-by: Martin Belanger Reviewed-by: Sagi Grimberg Reviewed-by: Chaitanya Kulkarni Signed-off-by: Christoph Hellwig commit 4cde03d82e2d0056d20fd5af6a264c7f5e6a3e76 Author: Daniel Wagner Date: Fri Jul 29 16:26:30 2022 +0200 nvme: consider also host_iface when checking ip options It's perfectly fine to use the same traddr and trsvcid more than once as long we use different host interface. This is used in setups where the host has more than one interface but the target exposes only one traddr/trsvcid combination. Use the same acceptance rules for host_iface as we have for host_traddr. Signed-off-by: Daniel Wagner Reviewed-by: Chao Leng Signed-off-by: Christoph Hellwig ``` linux-nvme-nvme-stas-a8026bb/Dockerfile000066400000000000000000000006051440613556600201420ustar00rootroot00000000000000FROM fedora:37 WORKDIR /root # first line for nvme-stas # second line for libnvme RUN dnf install -y python3-dasbus python3-pyudev python3-systemd python3-gobject meson \ git gcc g++ cmake openssl-devel libuuid-devel json-c-devel swig python-devel meson && dnf clean all COPY . . RUN meson .build && ninja -C .build && meson install -C .build ENTRYPOINT ["python3"] linux-nvme-nvme-stas-a8026bb/LICENSE000066400000000000000000000261061440613556600171610ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 STFS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. linux-nvme-nvme-stas-a8026bb/Makefile000066400000000000000000000063571440613556600176220ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # .DEFAULT_GOAL := stas BUILD-DIR := .build DEB-PKG-DIR := ${BUILD-DIR}/deb-pkg RPM-BUILDROOT-DIR := ${BUILD-DIR}/rpmbuild ifneq (,$(strip $(filter $(MAKECMDGOALS),rpm deb dist))) XTRA-MESON-OPTS := --wrap-mode=nodownload endif .PHONY: update-subprojects update-subprojects: meson subprojects update ${BUILD-DIR}: BUILD_DIR=${BUILD-DIR} ./configure ${XTRA-MESON-OPTS} @echo "Configuration located in: $@" @echo "-------------------------------------------------------" .PHONY: stas stas: ${BUILD-DIR} ninja -C ${BUILD-DIR} .PHONY: clean clean: ifneq ("$(wildcard ${BUILD-DIR})","") ninja -C ${BUILD-DIR} -t clean endif .PHONY: purge purge: ifneq ("$(wildcard ${BUILD-DIR})","") rm -rf ${BUILD-DIR} endif .PHONY: install install: stas sudo meson $@ -C ${BUILD-DIR} --skip-subprojects .PHONY: uninstall uninstall: ${BUILD-DIR} sudo ninja -C ${BUILD-DIR} uninstall .PHONY: dist dist: stas meson $@ -C ${BUILD-DIR} --formats gztar .PHONY: test test: stas meson $@ -C ${BUILD-DIR} --suite nvme-stas .PHONY: loc loc: @cloc --by-file --exclude-dir=${BUILD-DIR},doc,subprojects,test,utils,debian,obj-x86_64-linux-gnu,.github --exclude-lang=Markdown,"NAnt script",XML,"Bourne Again Shell",make,"Bourne Shell",Meson,YAML,XSLT . .PHONY: loc-full loc-full: @cloc --by-file --exclude-dir=${BUILD-DIR},subprojects,debian,obj-x86_64-linux-gnu,.github . .PHONY: black black: black --diff --color --line-length 120 --skip-string-normalization --extend-exclude="(subprojects|debian|.build)" . # Coverage requirements: # pip install coverage .PHONY: coverage coverage: stas cd ${BUILD-DIR} && ./coverage.sh ################################################################################ # Debian (*.deb) # Use "DEB_BUILD_OPTIONS=nocheck make deb" to skip unit testing. # This requires: sudo apt install -y debhelper dh-python ifeq (deb,$(strip $(MAKECMDGOALS))) ifneq (SUCCESS,$(shell dpkg -s debhelper dh-python > /dev/null 2>&1 && echo "SUCCESS" || echo "FAIL")) $(error Missing packages. Run -> "sudo apt install -y debhelper dh-python") endif endif .PHONY: deb deb: ${BUILD-DIR} mkdir -p ${DEB-PKG-DIR} dpkg-buildpackage -us -uc @mv ../nvme-stas_*.deb ${DEB-PKG-DIR} @mv ../nvme-stas_*.buildinfo ${DEB-PKG-DIR} @mv ../nvme-stas_*.changes ${DEB-PKG-DIR} @mv ../nvme-stas_*.dsc ${DEB-PKG-DIR} @mv ../nvme-stas_*.tar.gz ${DEB-PKG-DIR} @echo "=======================================================" @echo "Debian packages located in: ${DEB-PKG-DIR}/" ################################################################################ # RedHat (*.rpm) ${BUILD-DIR}/nvme-stas.spec: ${BUILD-DIR} nvme-stas.spec.in meson --wrap-mode=nodownload --reconfigure ${BUILD-DIR} ${RPM-BUILDROOT-DIR}: ${BUILD-DIR}/nvme-stas.spec rpmbuild -ba $< --build-in-place --clean --nocheck --define "_topdir $(abspath ${BUILD-DIR}/rpmbuild)" @echo "=======================================================" @echo "RPM packages located in: ${RPM-BUILDROOT-DIR}/" .PHONY: rpm rpm: ${RPM-BUILDROOT-DIR} linux-nvme-nvme-stas-a8026bb/NEWS.md000066400000000000000000000313241440613556600172500ustar00rootroot00000000000000# STorage Appliance Services (STAS) ## Changes with release 2.2.1 Added a few more unit and coverage tests. Fixed the following bugs. Bug fixes: * Fix errors with some debug commands (e.g. `stafctl ls --detailed`) * Fix setting controller DHCHAP key (this requires [corresponding changes in libnvme](https://github.com/linux-nvme/libnvme/pull/597)) ## Changes with release 2.2 Support for in-band authentication. ## Changes with release 2.1.3 This release is all about `udev rules`. As explained in [DISTROS.md](./DISTROS.md), `nvme-stas` and `nvme-cli` compete for the same kernel events (a.k.a. uevents or udev events). Those are events generated by the kernel related to Discovery Controller (DC) state changes. For example, an AEN indicating a change of Discovery Log Page (DLP), or an event indicating that the the connection to a DC was restored (event = `connected` or `rediscover`), which means that the DLP needs to be refreshed and connections to controllers listed in the DLP need to be updated. When both `nvme-stas` and `nvme-cli` are allowed to react and process these events, we have a race condition where both processes try to perform the same connections at the same time. Since the kernel will not allow duplicate connections, then one process will get an error. This is not a real problem since the connection does succeed, but the kernel will log an error and this can be irritating to users. We tried different ways to fix this issue. The simplest was to disable the `udev rules` installed by `nvme-cli`. This prevents `nvme-cli` from reacting to udev events and only `nvme-stas` gets to process the events. The downside to this is that `nvme-stas` only expects udev events from DCs that it manages. If a DC connection is made outside of `nvme-stas` (e.g. using `nvme-cli`) and `nvme-stas` receives an event for that DC, it won't know what to do with it and will simply ignore it. To solve this issue, and to eliminate the race condition, this release of `nvme-stas` includes changes that allows `nvme-stas` to react and process events even for DCs that are not managed by `nvme-stas`. In that case, `nvme-stas` invokes `nvme-cli's` standard event handler. While `nvme-stas` is running, `nvme-cli's` `udev rules` will be disabled and all event handling will be performed by `nvme-stas`. `nvme-cli's` `udev rules` are restored when `nvme-stas` is stopped. With this change we no longer need to provide the configuration parameter `udev-rule=[enabled|disabled]` in `stacd.conf`. This parameter is therefore deprecated. This release also adds the "[black](https://github.com/psf/black)" code formatter to the GitHub actions. From now on, code submitted as a pull request with GitHub must comply to black's code format. A new command, `make black`, has been added to allow users to verify their code before submitting a pull request. ## Changes with release 2.1.2 * Bug fixes: * Add support for RoCE and iWARP protocols in mDNS TXT field (i.e. `p=roce`, `p=iwarp`) * Add `_nvme-disc._udp` to the list of supported mDNS service types (stype) ## Changes with release 2.1.1 * Bug fixes: * Fix handling of unresponsive zeroconf-discovered Discovery Controllers. Sometimes we could have a timeout during twice as long as normal. * Set default value of legacy "[Global] persistent-connections=false" * Add `ControllerTerminator` entity to deal with potential (rare) cases where Connect/Disconnect operations could be performed in reverse order. * Add more unit tests * Increase code coverage * Improve name resolution algorithm * Set udev event priority to high (for faster handling) ## Changes with release 2.1 * Bug fixes: * Immediately remove existing connection to Discovery Controllers (DC) discovered through zeroconf (mDNS) when added to `exclude=` in `stafd.conf`. Previously, adding DCs to `exclude=` would only take effect on new connections and would not apply to existing connections. * When handling "key=value" pairs in the TXT field from Avahi, "keys" need to be case insensitive. * Strip spaces from Discovery Log Page Entries (DLPE). Some DCs may append extra spaces to DLPEs (e.g. IP addresses with trailing spaces). The kernel driver does not expect extra spaces and therefore they need to be removed. * In `stafd.conf` and `stacd.conf`, added new configuration parameters to provide parity with `nvme-cli`: * `nr-io-queues`, `nr-write-queues`, `nr-poll-queues`, `queue-size`, `reconnect-delay`, `ctrl-loss-tmo`, `duplicate-connect`, `disable-sqflow` * Changes to `stafd.conf`: * Move `persistent-connections` from the `[Global]` section to a new section named `[Discovery controller connection management]`. `persistent-connections` will still be recognized from the `[Global]` section, but will be deprecated over time. * Add new configuration parameter `zeroconf-connections-persistence` to section `[Discovery controller connection management]`. This parameter allows one to age Discovery Controllers discovered through zeroconf (mDNS) when they are no longer reachable and should be purged from the configuration. * Added more configuration validation to identify invalid Sections and Options in configuration files (`stafd.conf` and `stacd.conf`). * Improve dependencies in meson build environment so that missing subprojects won't prevent distros from packaging the `nvme-stas` (i.e. needed when invoking meson with the `--wrap-mode=nodownload` option) * Improve Read-The-Docs documentation format. ## Changes with release 2.0 Because of incompatibilities between 1.1.6 and 1.2 (ref. `sticky-connections`), it was decided to skip release 1.2 and have a 2.0 release instead. Release 2.0 contains everything listed in 1.2 (below) plus the following: * Add support for PLEO - Port-Local Entries Only, see TP8010. * Add new configuration parameter to stafd.conf: `pleo=[enabled|disabled]` * This requires `libnvme` 1.2 or later although nvme-stas can still operate with 1.1 (but PLEO will not be supported). * Although `blacklist=` is deprecated, keep supporting it for a while. * Target `udev-rule=` at TCP connections only. * Read-the-docs will now build directly from source (instead of using a possibly stale copy) * More unit tests were added * Refactored the code that handles pyudev events in an effort to fix spurious lost events. ## ~~Changes with release 1.2~~ (never released - use 2.0 instead) - In `stacd.conf`, add a new configuration section, `[I/O controller connection management]`. - This is to replace `sticky-connections` by `disconnect-scope` and `disconnect-trtypes`, which is needed so that hosts can better react to Fabric Zoning changes at the CDC. - Add `connect-attempts-on-ncc` to control how stacd will react to the NCC bit (Not Connected to CDC). - When the host's symbolic name is changed in `sys.conf`, allow re-issuing the DIM command (register with DC) on a `reload` signal (`systemctl reload stafd`). - Replace `blacklist=` by `exclude=` is `stafd.conf` and `stacd.conf`. Warning: this may create an incompatibility for people that were using `blacklist=`. They will need to manually migrate their configuration files. - Change `TID.__eq__()` and `TID.__ne__()` to recognize a TID object even when the `host-iface` is not set. This is to fix system audits where `nvme-stas` would not recognize connections made by `nvme-cli`. The TID object, or Transport ID, contains all the parameters needed to establish a connection with a controller, e.g. (`trtype`, `traddr`, `trsvcid`, `nqn`, `host-traddr`, and `host-iface`). `nvme-stas` can scan the `sysfs` (`/sys/class/nvme/`) to find exiting NVMe connections. It relies on the `address` and other attributes for that. For example the attribute `/sys/class/nvme/nvme0/address` may contain something like: `traddr=192.168.56.1,trsvcid=8009,host_iface=enp0s8`. `nvme-stas` always specify the `host-iface` when making connections but `nvme-cli` typically does not. Instead, `nvme-cli` relies on the routing table to select the interface. This creates a discrepancy between the `address` attribute of connections made by `nvme-cli` and those made by `nvme-stas` (i.e. `host_iface=` is missing for `nvme-cli` connections). And this results in `nvme-stas` not being able to recognize connections made by `nvme-cli`. Two solutions have been proposed to workaround this problem: - First, a short term solution changes `TID.__eq__()` and `TID.__ne__()` so that the `host-iface` has a lesser weight when comparing two TIDs. This way, the TID of a connection created by `nvme-cli` can be compared to the TID of a connection made with `nvme-stas` and still result in a match. The downside to this approach is that a connection made with `nvme-cli` that is going over the wrong interface (e.g. bad routing table entry), will now be accepted by `nvme-stas` as a valid connection. - Second, a long term solution that involves a change to the kernel NVMe driver will allow being able to determine the host interface for any NVMe connections, even those made without specifying the `host-iface` parameter. The kernel driver will now expose the source address of all NVMe connections through the `sysfs`. This will be identified by the key=value pair "`src-addr=[ip-address]`" in the `address` attribute. And from the source address one can infer the actual host interface. This actually will solve the shortcomings of the "short term" solution discussed above. Unfortunately, it may take several months before this kernel addition is available in a stock Distribution OS. So, the short term solution will need to suffice for now. ## Changes with release 1.1.6 - Fix issues with I/O controller connection audits - Eliminate pcie devices from list of I/O controller connections to audit - Add soaking timer to workaround race condition between kernel and user-space applications on "add" uevents. When the kernel adds a new nvme device (e.g. `/dev/nvme7`) and sends a "add" uevent to notify user-space applications, the attributes associated with that device (e.g. `/sys/class/nvme/nvme7/cntrltype`) may not be fully initialized which can lead `stacd` to dismiss a device that should get audited. - Make `sticky-connections=enabled` the default (see `stacd.conf`) ## Changes with release 1.1.5 - Fix issues introduced in 1.1.3 when enabling Fibre Channel (FC) support. - Eliminate pcie devices from discovery log pages. When enabling FC, pcie was accidentally enabled as well. - Fix I/O controller scan and detect algorithm. Again, while adding support for FC, the I/O scan & detect algorithm was modified, but we accidentally made it detect Discovery Controllers as well as I/O controllers. ## ~~Changes with release 1.1.4~~ USE 1.1.5 INSTEAD. - Fix issues for Fibre Channel (FC) support. - Add TESTING.md ## Changes with release 1.1.3 **stacd**: Add I/O controller connection audits. Audits are enabled when the configuration parameter "`sticky-connections`" is disabled. **stafd**: Preserve and Reload last known configuration on restarts. This is for warm restarts of the `stafd` daemon. This does not apply to system reboots (cold restarts). This is needed to avoid deleting I/O controller (IOC) connections by mistake when restarting `stafd`. It prevents momentarily losing previously acquired Discovery Log Page Entries (DLPE). Since `stacd` relies on acquired DLPEs to determine which connection should be created or deleted, it's important that the list of DLPEs survives a `stafd` restart. Eventually, after `stafd` has restarted and reconnected to all Discovery Controllers (DC), the list will get refreshed and the DLPE cache will get updated. And as the cache gets updated, `stacd` will be able to determine which connections should remain and which one should get deleted. **`stafd`/`stacd`**: Fixed crash caused by `stafd`/`stacd` calling the wrong callback function during the normal disconnect of a controller. There are two callback functions that can be called after a controller is disconnected, but one of them must only be called on a final disconnect just before the process (`stafd` or `stacd`) exits. The wrong callback was being called on a normal disconnect, which led the process to think it was shutting down. ## ~~Changes with release 1.1.2~~ USE 1.1.3 INSTEAD. stacd: Bug fix. Check that self._cfg_soak_tmr is not None before dereferencing it. ## Changes with release 1.1.1 Make `sticky-connections=disabled` the default (see `stacd.conf`) ## Changes with release 1.1 - Add `udev-rule` configuration parameter to `stacd.conf`. - Add `sticky-connections` configuration parameter to `stacd.conf`. - Add coverage testing (`make coverage`) - Add `make uninstall` - To `README.md`, add mDNS troubleshooting section. ## Changes with release 1.0.1 - Install staslib as pure python package instead of arch-specific. ## Changes with release 1.0 - First public release following TP8009 / TP8010 ratification and publication. ## Changes with release 0.1: - Initial release linux-nvme-nvme-stas-a8026bb/README.md000066400000000000000000000366011440613556600174340ustar00rootroot00000000000000# STorage Appliance Services (STAS) ![Build](https://github.com/linux-nvme/nvme-stas/actions/workflows/meson-test.yml/badge.svg) ![GitHub](https://img.shields.io/github/license/linux-nvme/nvme-stas) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Release](https://img.shields.io/github/v/release/linux-nvme/nvme-stas?include_prereleases&style=)](https://github.com/linux-nvme/nvme-stas/releases) [![GitHub commits](https://img.shields.io/github/commits-since/linux-nvme/nvme-stas/latest.svg)](https://GitHub.com/linux-nvme/nvme-stas/commit/) [![Read the Docs](https://img.shields.io/readthedocs/nvme-stas)](https://nvme-stas.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/linux-nvme/nvme-stas/branch/main/graph/badge.svg)](https://codecov.io/gh/linux-nvme/nvme-stas) [![Minimum Python Version](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) What does nvme-stas provide? - A Central Discovery Controller (CDC) client for Linux - Asynchronous Event Notifications (AEN) handling - Automated NVMe subsystem connection controls - Error handling and reporting - Automatic (zeroconf) and Manual configuration ## Overview STAS is composed of two services, STAF and STAC, running on the Host computer. **STAF** - **STorage Appliance Finder**. The tasks performed by STAF include: - Register with the Avahi daemon for service type `_nvme-disc._tcp`. This allows STAF to locate Central or Direct Discovery Controllers (CDC, DDC) with zero-touch provisioning (ZTP). STAF also allows users to manually enter CDCs and DDCs in a configuration file (`/etc/stas/stafd.conf`) when users prefer not to use ZTP. - Connect to discovered or configured CDCs or DDCs. - Retrieve the list of storage subsystems using the "get log page" command. - Maintain a cache of the discovered storage subsystems. - Provide a D-Bus interface where 3rd party applications can retrieve the data about the Discovery Controller connections (e.g. log pages). **STAC** - **STorage Appliance Connector**. The tasks performed by STAC include: - Read the list of storage subsystems from STAF over D-Bus. - Similar to STAF, STAC can also read a list of storage subsystems to connect to from a configuration file. - Set up the I/O controller connections. - Provide a D-Bus interface where 3rd party applications can retrieve data about the I/O controller connections. ![Definition](./doc/images/STAF-STAC-libnvme.png) ## Design **`stafd`** and **`stacd`** use the [GLib main loop](https://docs.gtk.org/glib/main-loop.html). The [GLib](https://docs.gtk.org/glib/index.html) Python module provides several low-level building blocks that are needed by **`stafd`** and **`stacd`**. In addition, many Python modules "play nice" with GLib such as `dasbus` and `pyudev`. GLib also provides additional components such as timers, signal handlers, and much more. **`stafd`** connects to the `avahi-daemon`, which it uses to detect Central Discovery Controllers (CDC) and Direct Discovery Controllers (DDC). When Discovery Controllers (DC) are found with Avahi's help, **`stafd`** uses `libnvme` to set up persistent connections and retrieve the discovery log pages. ## Daemonization **`stafd`** and **`stacd`** are managed as `systemd` services. The following operations are supported (here showing only `stafd`, but the same operations apply to `stacd`): - `systemctl start stafd`. Start daemon. - `systemctl stop stafd`. Stop daemon. The `SIGTERM` signal is used to tell the daemon to stop. - `systemctl restart stafd`. Effectively a `stop` + `start`. - `systemctl reload stafd`. Reload configuration. This is done in real time without restarting the daemon. The `SIGHUP` signal is used to tell the daemon to reload its configuration file. ## Configuration As stated before, **`stafd`** can automatically locate discovery controllers with the help of Avahi and connect to them, and **`stacd`** can automatically set up the I/O connections to discovered storage subsystems. However, **`stafd`** and **`stacd`** can also operate in a non-automatic mode based on manually entered configuration. In other words, discovery controllers and/or storage subsystems can be entered manually. This is to provide customers with more flexibility. The configuration for each daemon is found in **`/etc/stas/stafd.conf`** and **`/etc/stas/stacd.conf`** respectively. The configuration files also provide additional parameters, such as log-level attributes used mainly for debugging purposes. The following configuration files are defined: | File | Consumer | Purpose | | ---------------------- | ----------------- | ------------------------------------------------------------ | | `/etc/stas/sys.conf` | `stafd` + `stacd` | Contains system-wide (i.e. host) configuration such as the Host NQN, the Host ID, and the Host Symbolic Name. Changes to this file can be made manually or with the help of the `stasadm` utility as described in the previous section.

For example, `stasadm hostnqn -f /etc/nvme/hostnqn` writes the Host NQN to the file `/etc/nvme/hostnqn`, but also adds an entry to `/etc/stas/sys.conf` to indicate where the Host NQN has been saved.

This gives nvme-stas the flexibility of defining its own Host parameters or to use the same parameters defined by `libnvme` and `nvme-cli`. | | `/etc/stas/stafd.conf` | `stafd` | Contains configuration specific to `stafd`. Discovery controllers can be manually added or excluded in this file. | | `/etc/stas/stacd.conf` | `stacd` | Contains configuration specific to `stacd`. I/O controllers can be manually added or excluded in this file. | ## D-Bus interface The interface to **`stafd`** and **`stacd`** is D-Bus. This allows other programs, such as **`stafctl`** and **`stacctl`**, to communicate with the daemons. This also provides third parties the ability to write their own applications that can interact with **`stafd`** and **`stacd`**. For example, someone could decide to write a GUI where they would display the discovery controllers as well as the all the discovery log pages in a "pretty" window. The next table provides info about the two D-Bus interfaces. | Component | D-Bus address | | --------- | ------------------------------ | | `stafd` | **`org.nvmexpress.staf.conf`** | | `stacd` | **`org.nvmexpress.stac.conf`** | ## Companion programs: `stafctl` and `stacctl` **`stafctl`** and **`stacctl`** are utilities that allow users to interact with **`stafd`** and **`stacd`** respectively. This is a model used by several programs, such as `systemctl` with `systemd`. At a minimum, these utilities provide debug tools, but they could also provide some configuration capabilities (TBD). ## Packages **`stafd`** and **`stacd`** as well as their companion programs **`stafctl`** and **`stacctl`** are released together in a package called "**`nvme-stas`**" for **ST**orage **A**pplicance **S**ervices (e.g. `stas-1.0.0-1.x86_64.rpm` or `stas_1.0.0_amd64.deb`). ## Dependencies **`stafd`**/**`stacd`** require Linux kernel 5.14 or later. The following packages must be installed to use **`stafd`**/**`stacd`** **Debian packages (tested on Ubuntu 20.04):** ```bash sudo apt-get install -y python3-pyudev python3-systemd python3-gi sudo apt-get install -y python3-dasbus # Ubuntu 22.04 OR: sudo pip3 install dasbus # Ubuntu 20.04 ``` **RPM packages (tested on Fedora 34..35 and SLES15):** ```bash sudo dnf install -y python3-dasbus python3-pyudev python3-systemd python3-gobject ``` # STAF - STorage Appliance Finder | Component | Description | | --------------- | -------------------------------------------------------- | | **`/usr/sbin/stafd`** | A daemon that finds (discovers) NVMe storage appliances. | | **`/usr/bin/stafctl`** | A companion shell utility for `stafd`. | | **`/etc/stas/stafd.conf`** | Configuration file | ## stafd configuration file The configuration file is named `/etc/stas/stafd.conf`. This file contains configuration parameters for the **`stafd`** daemon. One of the things you may want to configure is the IP address of the discovery controller(s) you want **`stafd`** to connect to. The configuration file contains a description of all the parameters that can be configured. ## Service discovery with Avahi **`stafd`** can automatically find and set up connections to Discovery Controllers. To do this, **`stafd`** registers with the [Avahi](https://www.avahi.org/), the mDNS/DNS-SD (Service Discovery) daemon. Discovery Controllers that advertise themselves with service type `_nvme-disc._tcp` will be recognized by Avahi, which will inform **`stafd`**. ### Not receiving mDNS packets? If **`stafd`** is not detecting any discovery controllers through Avahi, it could simply be that the mDNS packets are being suppressed by your firewall. If you know for a fact that the discovery controllers are advertizing themselves with mDNS packets, make sure that the Avahi daemon is receiving them as follows: ```bash avahi-browse -t -r _nvme-disc._tcp ``` If you're not seeing anything, then check whether your firewall allows mDNS packets. ### Why is Avahi failing to discover services on some interfaces? Linux limits the number of multicast group memberships that a host can belong to. The default is 20. For Avahi to monitor mDNS (multicast DNS) packets on all interfaces, the host computer must be able to register one multicast group per interface. This can be physical or logical interfaces. For example, configuring 10 VLANs on a physical interface increases the total number of interfaces by 10. If the total number of interfaces is greater than the limit of 20, then Avahi won't be able to monitor all interfaces. The limit can be changed by configuring the variable **`igmp_max_memberships`**. This variable is defined [here](https://sysctl-explorer.net/net/ipv4/igmp_max_memberships/) in the kernel documentation. And this [StackExchange page](https://unix.stackexchange.com/questions/23832/is-there-a-way-to-increase-the-20-multicast-group-limit-per-socket) describes how one can increase the limit. # STAC - STorage Appliance Connector | File name | Description | | -------------------------- | -------------------------------------------------- | | **`/usr/sbin/stacd`** | A daemon that connects to NVMe storage appliances. | | **`/usr/bin/stacctl`** | A companion shell utility for `stacd`. | | **`/etc/stas/stacd.conf`** | Configuration file | ## stacd configuration file The configuration file is named `/etc/stas/stacd.conf`. In this file you can configure storage appliances that **`stacd`** will connect to. By default, **`stacd`** uses information (log pages) collected from **`stafd`** to connect to storage appliances. However, you can also manually enter IP addresses of storage appliances in this file. # System configuration A host must be provided with a Host NQN and a Host ID. `nvme-stas` will not run without these two mandatory configuration parameters. To follow in the footsteps of `nvme-cli` and `libnvme`, `nvme-stas` will use the same Host NQN and ID that `nvme-cli` and `libnvme` use by default. In other words, `nvme-stas` will read the Host NQN and ID from these two files by default: 1. `/etc/nvme/hostnqn` 2. `/etc/nvme/hostid` Using the same configuration files will ensure consistency between `nvme-stas`, `nvme-cli`, and `libnvme`. On the other hand, `nvme-stas` can operate with a different Host NQN and/or ID. In that case, one can specify them in `/etc/stas/sys.conf`. A new optional configuration parameters introduced in TP8010, the Host Symbolic Name, can also be specified in `/etc/stas/sys.conf`. The schema/documentation for `/etc/stas/sys.conf` can be found [`/etc/stas/sys.conf.doc`](./etc/stas/sys.conf.doc). # Build, install, unit tests STAS uses the `meson` build system. Since STAS is a Python project, there is no code to build. However, the code needs to be installed using `meson`. Unit tests can also be run with `meson`. ## Using meson Invoke `meson` to configure the project: ```bash meson .build ``` The command `meson .build` need only be called once. This analyzes the project and the host computer to determine if all the necessary tools and dependencies are available. The result is saved to the directory named `.build`. To compile the code: ```bash cd .build ninja ``` To install / uninstall the code: ```bash cd .build meson install ninja uninstall ``` To run the unit tests: ```bash cd .build meson test ``` For more information about testing, please refer to: [TESTING.md](./TESTING.md) ## Alternate approach using Good-ole make Recognizing that many people are not familiar with `meson`, we're providing a second way to install the code using the more familiar `configure` script combined with a `make`. ```bash ./configure make ``` This performs the same operations as the meson approach described above. The `configure` script is automatically invoked when running `make` by itself. | make command | Description | | -------------------- | :----------------------------------------------------------- | | **`make`** | Invoke the `.configure` script and build the code. | | **`make install`** | Install the code. Requires root privileges (you will be asked to enter your password). | | **`make uninstall`** | Uninstall the code. Requires root privileges (you will be asked to enter your password). | | **`make test`** | Run the unit tests | | **`make clean`** | Clean build artifacts, but does not remove the meson's configuration. That is, the configuration in `.build` is preserved. | | **`make purge`** | Remove all build artifacts including the `.build` directory. | ## Compiling and running nvme-stas in a docker container Use published image (optional) ```bash docker pull ghcr.io/linux-nvme/nvme-stas:main ``` Build your own image (optional) ```bash docker-compose up --build ``` Run services using docker-compose like this ```bash docker-compose up ``` Run companion programs **`stafctl`** and **`stacctl`** like this ```bash docker-compose exec stafd stafctl ls docker-compose exec stafd stafctl status docker-compose exec stacd stacctl ls docker-compose exec stacd stacctl status ``` dependencies: dbus, avahi. ## Generating man and html pages nvme-stas uses the following programs to generate the documentation. These can be installed as shown in the "dependencies" section below. - `xsltproc` - Used to convert DocBook XML notation to "man pages" and "html pages". - `gdbus-codegen` - Used to convert D-Bus IDL to DocBook XML notation. ### Dependencies The following packages must be installed to generate the documentation **Debian packages (tested on Ubuntu 20.04):** ```bash sudo apt-get install -y docbook-xml docbook-xsl xsltproc libglib2.0-dev ``` **RPM packages (tested on Fedora 34..35 and SLES15):** ```bash sudo dnf install -y docbook-style-xsl libxslt glib2-devel ``` ### Configuring and building the man and html pages By default, the documentation is not built. You need to run the `configure` as follows to tell meson that you want to build the documentation. You may need to first purge any previous configuration. ```bash make purge ./configure -Dman=true -Dhtml=true make ``` ## Generating RPM and/or DEB packages ```bash make rpm make deb ``` linux-nvme-nvme-stas-a8026bb/TESTING.md000066400000000000000000000204251440613556600176110ustar00rootroot00000000000000--- author: Martin Belanger title: Testing nvme-stas --- # Overview For quick an easy testing, it's possible to run a storage subsystem simulator using the `nvmet` driver. This is how most of the testing was done during `nvme-stas` development. The main feature that cannot be tested this way is mDNS discovery. There are two ways to run the tests. - The first one involves starting all the components manually and using the nvmet driver as the storage appliance. - The second one is fully automated and can be invoked simply by running `make coverage`. [toc] # Manual testing using the nvmet driver A script is provided (`utils/nvmet/nvmet.py`) to simplify the configuration of the `nvmet` driver. The script comes with a companion configuration file (`utils/nvmet/nvmet.conf`). The configuration file is where you configure the port(s) and subsystem(s) to create. The default configuration will create 3 subsystems under port 1. This is mapped to the local IPv6 loopback address (`::1`). Since nvmet doesn't provide a mDNS responder, you will need to manually configure `stafd` (`/etc/stas/stafd.conf`) so that it connects to the DDC that the nvmet driver creates by adding the DDC's address under the `[Controllers]` section. For example: ```bash [Controllers] controller=transport=tcp;traddr=localhost ``` ## Monitoring While testing it's a good idea to follow the journal in real time to see how `stafd` and `stacd` are performing. In a terminal (e.g. `bash`) do: ```bash $ sudo journalctl --system --full -o short-precise --follow ``` You probably don't really need all these options, but they will give you full view of the messages with a millisecond time resolution. I personally define an alias `alias j='sudo journalctl --system --full -o short-precise'` and then I need only invoke `j -f`. Or even better, I add my user to the `systemd-journal` group so that I don't have to use `sudo` to see system-level log messages (Ref: [systemd-journal.service](https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html#Access%20Control)). ## Test startup Here's a step-by-step guide to start `stafd` and `stacd` and connect to the `nvmet` driver. Open a second terminal and enter the following commands (these commands assume that nvme-stas will be cloned under `~/work/nvme-stas` referred to as `$STAS_DIR`): ### Clone nvme-stas (if not done already) ```bash $ mkdir ~/work $ cd ~/work $ git clone https://github.com/linux-nvme/nvme-stas.git $ STAS_DIR=~/work/nvme-stas ``` ### Build and install nvme-stas ```bash $ cd $STAS_DIR $ make install ``` ### Create a post-install script Create an executable shell script (call it `stas-config.sh`) with the following contents. These are post-installation configuration steps required every time `nvme-stas` is reinstalled. Place the script in a directory that is in the search `$PATH` so that it can be invoked easily. ```bash #!/usr/bin/env bash ##################################################################### # Must run daemon-reload after installing nvme-stas sudo systemctl daemon-reload ##################################################################### # Make sure Host NQN and ID are configured if [ ! -d "/etc/nvme" ]; then sudo mkdir /etc/nvme fi if [ ! -s /etc/nvme/hostnqn ]; then sudo stasadm hostnqn -f /etc/nvme/hostnqn fi if [ ! -s /etc/nvme/hostid ]; then sudo stasadm hostid -f /etc/nvme/hostid fi ##################################################################### # Edit /etc/stas/stafd.conf to enable tracing and add the local # nvmet driver as the Discovery Controller to connect to. FILES="stafd.conf stacd.conf" for file in ${FILES}; do sudo sed -i '/^#tron=false/a tron=true' /etc/stas/${file} done sudo sed -i '/^#controller=$/a controller=transport=tcp;traddr=localhost' /etc/stas/stafd.conf ``` ### Run the post-install script ```bash $ stas-config.sh ``` ### Start the nvmet driver ```bash $ cd $STAS_DIR/utils/nvmet $ sudo ./nvmet.py create ``` ### Start stafd and stacd ```bash $ sudo systemctl start stafd stacd ``` ## So, is it running yet? You should have seen `stafd` and `stacd` starting in the first terminal where `journalctl` is following the system log. At this point `stafd` should have connected to the `nvmet` discovery controller and retrieved the discovery log page entries (DLPE). And `stacd` should have retrieved the DLPEs from `stafd` and connected to the 3 subsystems defined in `nvmet.conf`. This can be confirmed as follows: ```bash $ stafctl ls [{'device': 'nvme0', 'host-iface': '', 'host-traddr': '', 'subsysnqn': 'nqn.2014-08.org.nvmexpress.discovery', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}] ``` And: ```bash $ stacctl ls [{'device': 'nvme1', 'host-iface': '', 'host-traddr': '', 'subsysnqn': 'klingons', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'device': 'nvme2', 'host-iface': '', 'host-traddr': '', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'device': 'nvme3', 'host-iface': '', 'host-traddr': '', 'subsysnqn': 'starfleet', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}] ``` You can also use `nvme-cli` to list the connections. For example: `nvme list -v`. ## Generating Asynchronous Event Notifications (AEN) You can use the `nvmet.py` script to simulate the removal of a subsystem, which results in an AEN being sent to indicate a "Change of Discovery Log Page". Here's how: ```bash $ cd $STAS_DIR/utils/nvmet $ sudo ./nvmet.py unlink -p 1 -s klingons ``` Observe what happens in the journal. `stafd` will receive the AEN and update the DLPEs by performing a Get Discovery Log Page command. And `stacd` will disconnect from the "`klingons`" subsystem (use `stacctl ls` to confirm). Then, add the subsystem back as follows: ```bash $ sudo ./nvmet.py link -p 1 -s klingons ``` **NOTE**: I know, "`klingons`" is not a valid NQN, but it sure is easier to remember and to type than a valid NQN. Fortunately, the `nvmet` driver doesn't care what the actual subsystem's NQN looks like. :smile: ## Stopping nvmet ```bash $ cd $STAS_DIR/utils/nvmet $ sudo ./nvmet.py clean ``` # Automated testing using the coverage test This requires the [Python coverage package](https://coverage.readthedocs.io/en/6.4.1/), which can be installed as follows: ```bash $ sudo pip install coverage ``` Note that this test cannot be run while `stafd` and `stacd` are running. Make sure to stop `stafd` and `stacd` if they are running (`systemctl stop [stafd|stacd]`). You may also need to mask those services (`systemctl mask [stafd|stacd]`) if coverage fails to start. To run the coverage test, from the root of the `nvme-stas` git repo: ```bash $ make coverage ``` This will start `stafd`, `stacd`, and the `nvmet` target. At the end, if all goes well, you should get an output similar to this: ```bash Name Stmts Miss Cover ---------------------------------------- stacctl 53 0 100% stacd 190 3 98% stafctl 75 0 100% stafd 246 21 91% staslib/avahi.py 185 19 90% staslib/defs.py 22 0 100% staslib/stas.py 858 51 94% staslib/version.py 31 0 100% ---------------------------------------- TOTAL 1660 94 94% ``` Note that the Python coverage package has trouble tracking code executed in threads. And since nvme-stas uses threads, some of the code will not be accounted for (in other words, you'll never get 100% coverage). Also note, that some of the code (e.g. explicit registration per TP8010) only gets executed when connected to a CDC (not a DDC). So, depending on your environment you will most likely get different coverage result. The above test was done on a system where mDNS discovery with a CDC was available, which provides more coverage than using the `nvmet` driver alone. An HTML output is also available where you can click on each file and which lines of code got executed and which ones were missed. In your web browser, simply type `file:///[$STAS_DIR]/.build/coverage/index.html` (you must replace `[$STAS_DIR]` by the actual location of the nvme-stas repo where `make coverage` was run) . You should get something like this: ![](./doc/images/Coverage.png)linux-nvme-nvme-stas-a8026bb/configure000077500000000000000000000006721440613556600200630ustar00rootroot00000000000000#!/bin/bash -e # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # BUILD_DIR="${BUILD_DIR:-.build}" if [ ! -d ${BUILD_DIR} ]; then exec meson ${BUILD_DIR} "$@" else exec meson configure ${BUILD_DIR} "$@" fi linux-nvme-nvme-stas-a8026bb/coverage.sh.in000077500000000000000000000426341440613556600207170ustar00rootroot00000000000000#!/usr/bin/env bash PRIMARY_GRP=$( id -ng ) PRIMARY_USR=$( id -nu ) PYTHON_PATH=.:./subprojects/libnvme AVAHI_PUBLISHER=mdns_publisher.service file=/tmp/stafd.conf.XXXXXX stafd_conf_fname=$(mktemp $file) file=/tmp/stacd.conf.XXXXXX stacd_conf_fname=$(mktemp $file) CYAN="[1;36m" RED="[1;31m" YELLOW="[1;33m" NORMAL="[0m" log() { msg="$1" printf "%b%s%s%b[0m\n" "\0033" ${CYAN} "${msg}" "\0033" sudo logger -t COVERAGE -i "@@@@@ " -p warning -- "${msg}" } log_file_contents() { rc=$1 file=$2 if [ $rc -eq 0 ]; then color=${NORMAL} level="info" else color=${YELLOW} level="error" fi while IFS= read -r line; do msg=" ${line}" printf "%b%s%s%b[0m\n" "\0033" ${color} "${msg}" "\0033" sudo logger -t COVERAGE -i "@@@@@ " -p ${level} -- "${msg}" done < ${file} } systemctl-exists() { unit="$1" [ $(systemctl list-unit-files "${unit}" | wc -l) -gt 3 ] } sd_stop() { app="$1" unit="${app}"-cov.service if systemctl-exists "${unit}" >/dev/null 2>&1; then log "Stop ${app}" sudo systemctl stop "${unit}" >/tmp/output.txt 2>&1 if [ -s /tmp/output.txt ]; then log_file_contents $? /tmp/output.txt else printf " sudo systemctl stop %s\n" "${unit}" fi sudo systemctl reset-failed "${unit}" >/dev/null 2>&1 printf "\n" sleep 1 fi } sd_start() { app="$1" dbus="$2" conf="$3" unit="${app}"-cov.service if [ -z "${conf}" ]; then cmd="${app} --syslog" else cmd="${app} --syslog -f ${conf}" fi RUNTIME_DIRECTORY=/tmp/${app} rm -rf ${RUNTIME_DIRECTORY} mkdir ${RUNTIME_DIRECTORY} # Clear previous failure status (if any) sudo systemctl reset-failed "${unit}" >/dev/null 2>&1 log "Start ${app}" sudo systemd-run --unit="${unit}" --working-directory=. --property=Type=dbus --property=BusName="${dbus}" --property="SyslogIdentifier=${app}" --property="ExecReload=/bin/kill -HUP \$MAINPID" --setenv=PYTHONPATH=${PYTHON_PATH} --setenv=RUNTIME_DIRECTORY=${RUNTIME_DIRECTORY} coverage run --rcfile=.coveragerc ${cmd} >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt printf "\n" sleep 1 } sd_restart() { app="$1" unit="${app}"-cov.service if systemctl is-active "${unit}" >/dev/null 2>&1; then log "Restart ${app}" sudo systemctl restart "${unit}" && printf "systemctl restart %s\n" "${unit}" >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt sleep 1 else msg="Cannot restart ${app}, which is not currently running." printf "%b%s%s%b[0m\n\n" "\0033" ${RED} "${msg}" "\0033" fi printf "\n" } reload_cfg() { app="$1" unit="${app}"-cov.service log "Reload config ${app}" sudo systemctl reload "${unit}" && printf "systemctl reload %s\n" "${unit}" >/tmp/output.txt 2>&1 #pid=$( systemctl show --property MainPID --value "${unit}" ) #sudo kill -HUP "${pid}" >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt printf "\n" sleep 1 } run_unit_test() { input=$@ if [ "$1" == "sudo" ]; then shift COVERAGE="sudo coverage" else COVERAGE="coverage" fi test=$@ log "Run unit test: ${input}" PYTHONPATH=${PYTHON_PATH} ${COVERAGE} run --rcfile=.coveragerc ../test/${test} >/dev/null 2>&1 } run_cmd_coverage() { input=$@ if [ "$1" == "sudo" ]; then shift COVERAGE="sudo coverage" else COVERAGE="coverage" fi cmd="$@" log "Invoke: ${input}" ${COVERAGE} run --rcfile=.coveragerc ${cmd} >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt printf "\n" } run_cmd() { cmd="$@" ${cmd} >/tmp/output.txt 2>&1 if [ -s /tmp/output.txt ]; then log_file_contents $? /tmp/output.txt else printf " %s\n" "${cmd}" fi } prerun_setup() { if [ ! -d coverage ]; then mkdir coverage fi for file in staf stac; do if [ ! -f "/usr/share/dbus-1/system.d/org.nvmexpress.${file}.conf" -a \ ! -f "/etc/dbus-1/system.d/org.nvmexpress.${file}.conf" ]; then log "hardlink /etc/dbus-1/system.d/org.nvmexpress.${file}.conf -> @BUILD_DIR@/etc/dbus-1/system.d/org.nvmexpress.${file}.conf" sudo ln @BUILD_DIR@/etc/dbus-1/system.d/org.nvmexpress.${file}.conf /etc/dbus-1/system.d/org.nvmexpress.${file}.conf if [ $? -ne 0 ]; then log "hardlink failed" exit 1 fi fi done sudo systemctl reload dbus.service } postrun_cleanup() { sd_stop "stafd" sd_stop "stacd" log "Stop nvmet" sudo ../utils/nvmet/nvmet.py clean >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt printf "\n" log "nvme disconnect-all" run_cmd sudo nvme disconnect-all printf "\n" log "Remove ${stafd_conf_fname} and ${stacd_conf_fname}" rm "${stafd_conf_fname}" rm "${stacd_conf_fname}" printf "\n" for file in staf stac; do if [ -f "/etc/dbus-1/system.d/org.nvmexpress.${file}.conf" ]; then if [ "$(stat -c %h -- "/etc/dbus-1/system.d/org.nvmexpress.${file}.conf")" -gt 1 ]; then log "Remove hardlink /etc/dbus-1/system.d/org.nvmexpress.${file}.conf" sudo rm "/etc/dbus-1/system.d/org.nvmexpress.${file}.conf" fi fi done sudo systemctl reload dbus.service sudo systemctl unmask avahi-daemon.service sudo systemctl unmask avahi-daemon.socket sudo systemctl start avahi-daemon.service sudo systemctl start avahi-daemon.socket sudo systemctl stop ${AVAHI_PUBLISHER} >/dev/null 2>&1 sudo systemctl reset-failed ${AVAHI_PUBLISHER} >/dev/null 2>&1 log "All done!!!" log "FINISHED-FINISHED-FINISHED-FINISHED-FINISHED-FINISHED-FINISHED-FINISHED" } trap postrun_cleanup EXIT trap postrun_cleanup SIGINT ################################################################################ ################################################################################ ################################################################################ log "START-START-START-START-START-START-START-START-START-START-START-START" if systemctl is-active stafd.service >/dev/null 2>&1 || systemctl is-active stacd.service >/dev/null 2>&1; then msg="Stopping because stafd and/or stacd is/are currently running." printf "%b%s%s%b[0m\n" "\0033" ${RED} "${msg}" "\0033" exit 1 fi prerun_setup #******************************************************************************* # Load nvme kernel module log "modprobe nvme_tcp" run_cmd sudo /usr/sbin/modprobe nvme_tcp log "nvme disconnect-all" run_cmd sudo nvme disconnect-all printf "\n" sd_stop stafd # make sure it's not running already sd_stop stacd # make sure it's not running already #******************************************************************************* # Create a dummy config file for stafd log "Create dummy config file ${stafd_conf_fname}" cat > "${stafd_conf_fname}" <<'EOF' [Global] tron = true ip-family = ipv6 johnny = be-good queue-size = 2000000 reconnect-delay = NaN ctrl-loss-tmo = 10 disable-sqflow = true [Discovery controller connection management] persistent-connections = false zeroconf-connections-persistence = -1 [Hello] hello = bye EOF log_file_contents 0 "${stafd_conf_fname}" printf "\n" #******************************************************************************* # Create a dummy config file for stacd log "Create dummy config file ${stacd_conf_fname}" cat > "${stacd_conf_fname}" <<'EOF' [Global] tron = true kato = 10 nr-io-queues = 4 nr-write-queues = NaN nr-poll-queues = NaN queue-size = 2000000 reconnect-delay = 1 ctrl-loss-tmo = 1 disable-sqflow = true [I/O controller connection management] disconnect-scope = blah-blah disconnect-trtypes = boing-boing EOF log_file_contents 0 "${stacd_conf_fname}" printf "\n" log "Stop & Mask Avahi daemon" run_cmd sudo systemctl stop avahi-daemon.service run_cmd sudo systemctl stop avahi-daemon.socket run_cmd sudo systemctl mask avahi-daemon.service run_cmd sudo systemctl mask avahi-daemon.socket printf "\n" sleep 1 log ">>>>>>>>>>>>>>>>>>>>> Marker [1] <<<<<<<<<<<<<<<<<<<<<" printf "\n" run_cmd_coverage stafctl ls run_cmd_coverage stafctl invalid-command run_cmd_coverage stacctl ls run_cmd_coverage stacctl invalid-command #******************************************************************************* # Start nvme target simulator log "Start nvmet" sudo ../utils/nvmet/nvmet.py clean >/dev/null 2>&1 sudo ../utils/nvmet/nvmet.py create -f ../utils/nvmet/nvmet.conf >/tmp/output.txt 2>&1 log_file_contents $? /tmp/output.txt printf "\n" sleep 2 log ">>>>>>>>>>>>>>>>>>>>> Marker [2] <<<<<<<<<<<<<<<<<<<<<" printf "\n" #******************************************************************************* # Start stafd and stacd sd_start "stafd" "@STAFD_DBUS_NAME@" "${stafd_conf_fname}" sd_start "stacd" "@STACD_DBUS_NAME@" "${stacd_conf_fname}" sleep 2 run_cmd_coverage stafctl status reload_cfg "stafd" sleep 1 log "Restart Avahi daemon" run_cmd sudo systemctl unmask avahi-daemon.socket run_cmd sudo systemctl unmask avahi-daemon.service run_cmd sudo systemctl start avahi-daemon.socket run_cmd sudo systemctl start avahi-daemon.service printf "\n" sleep 2 log ">>>>>>>>>>>>>>>>>>>>> Marker [3] <<<<<<<<<<<<<<<<<<<<<" printf "\n" log "Change stafd config [1]:" cat > "${stafd_conf_fname}" <<'EOF' [Global] tron = true [Discovery controller connection management] persistent-connections = false zeroconf-connections-persistence = 0.5 [Service Discovery] zeroconf = enabled EOF log_file_contents 0 "${stafd_conf_fname}" printf "\n" reload_cfg "stafd" sleep 1 log "Change stafd config [2]:" cat > "${stafd_conf_fname}" <<'EOF' [Global] tron = true ip-family = ipv4 queue-size = 2000000 reconnect-delay = 1 ctrl-loss-tmo = 1 disable-sqflow = true pleo = disable [Discovery controller connection management] persistent-connections = false zeroconf-connections-persistence = 1:01 [Controllers] controller = transport = tcp ; traddr = localhost ; ; ; kato=31; dhchap-ctrl-secret=not-so-secret controller=transport=tcp;traddr=1.1.1.1 controller=transport=tcp;traddr=100.100.100.100 controller=transport=tcp;traddr=2607:f8b0:4002:c2c::71 exclude=transport=tcp;traddr=1.1.1.1 EOF log_file_contents 0 "${stafd_conf_fname}" printf "\n" reload_cfg "stafd" sleep 5 log "Change stacd config [1]:" cat > "${stacd_conf_fname}" <<'EOF' [Global] tron=true nr-io-queues=4 nr-write-queues=4 queue-size=2000000 reconnect-delay=1 ctrl-loss-tmo=1 disable-sqflow=true [I/O controller connection management] disconnect-scope=all-connections-matching-disconnect-trtypes disconnect-trtypes=tcp+rdma EOF log_file_contents 0 "${stacd_conf_fname}" printf "\n" reload_cfg "stacd" sleep 5 log ">>>>>>>>>>>>>>>>>>>>> Marker [4] <<<<<<<<<<<<<<<<<<<<<" printf "\n" run_cmd_coverage stafctl status #******************************************************************************* # Fake mDNS packets from a CDC log "Start Avahi publisher" run_cmd sudo systemctl stop ${AVAHI_PUBLISHER} run_cmd sudo systemctl reset-failed ${AVAHI_PUBLISHER} run_cmd sudo systemd-run --unit=${AVAHI_PUBLISHER} --working-directory=. avahi-publish -s SFSS _nvme-disc._tcp 8009 "p=tcp" printf "\n" sleep 1 #******************************************************************************* run_cmd_coverage stafd --version run_cmd_coverage stacd --version #******************************************************************************* # Stimulate D-Bus activity run_cmd_coverage sudo stafctl --version run_cmd_coverage sudo stafctl blah run_cmd_coverage sudo stafctl troff run_cmd_coverage stafctl status run_cmd_coverage sudo stafctl tron run_cmd_coverage stafctl ls -d run_cmd_coverage stafctl adlp -d run_cmd_coverage stafctl dlp -t tcp -a ::1 -s 8009 run_cmd_coverage sudo stacctl --version run_cmd_coverage sudo stacctl blah run_cmd_coverage sudo stacctl troff run_cmd_coverage stacctl status run_cmd_coverage sudo stacctl tron run_cmd_coverage stacctl ls -d log ">>>>>>>>>>>>>>>>>>>>> Marker [5] <<<<<<<<<<<<<<<<<<<<<" printf "\n" #******************************************************************************* # Stimulate AENs activity by removing/restoring namespaces log "Remove namespace: klingons" run_cmd sudo ../utils/nvmet/nvmet.py unlink -p 1 -s klingons printf "\n" sleep 2 run_cmd_coverage stacctl ls log "Restore namespace: klingons" run_cmd sudo ../utils/nvmet/nvmet.py link -p 1 -s klingons printf "\n" sleep 2 run_cmd_coverage stacctl ls log ">>>>>>>>>>>>>>>>>>>>> Marker [6] <<<<<<<<<<<<<<<<<<<<<" printf "\n" #******************************************************************************* # Stop Avahi Publisher log "Stop Avahi publisher" run_cmd sudo systemctl stop ${AVAHI_PUBLISHER} printf "\n" sleep 1 #******************************************************************************* log "Restart Avahi publisher" run_cmd sudo systemd-run --unit=${AVAHI_PUBLISHER} --working-directory=. avahi-publish -s SFSS _nvme-disc._tcp 8009 "p=tcp" printf "\n" sleep 2 log ">>>>>>>>>>>>>>>>>>>>> Marker [7] <<<<<<<<<<<<<<<<<<<<<" printf "\n" #******************************************************************************* # Make config changes for stafd log "Change stafd config [3]:" cat > "${stafd_conf_fname}" <<'EOF' [Global] tron = true queue-size = 2000000 reconnect-delay = 1 ctrl-loss-tmo = 1 disable-sqflow = true [Discovery controller connection management] persistent-connections=false zeroconf-connections-persistence=0.5 [Service Discovery] zeroconf=disabled EOF log_file_contents 0 "${stafd_conf_fname}" printf "\n" reload_cfg "stafd" sleep 3 #******************************************************************************* # Make more config changes for stafd log "Change stafd config [4]:" cat > "${stafd_conf_fname}" <<'EOF' [Global] tron=true queue-size=2000000 reconnect-delay=1 ctrl-loss-tmo=0 disable-sqflow=true ip-family=ipv6 [Discovery controller connection management] persistent-connections=false zeroconf-connections-persistence=0 [Controllers] controller=transport=tcp;traddr=localhost;trsvcid=8009 controller=transport=tcp;traddr=abracadabra controller=transport=tcp;traddr=google.com controller= controller=trsvcid controller=transport=rdma;traddr=!@#$ controller=transport=fc;traddr=21:00:00:00:00:00:00:00;host-traddr=20:00:00:00:00:00:00:00 controller=transport=XM;traddr=2.2.2.2 controller=transport=tcp;traddr=555.555.555.555 EOF log_file_contents 0 "${stafd_conf_fname}" printf "\n" log ">>>>>>>>>>>>>>>>>>>>> Marker [8] <<<<<<<<<<<<<<<<<<<<<" printf "\n" reload_cfg "stafd" sleep 2 #******************************************************************************* # Stop Avahi Publisher log "Stop Avahi publisher" run_cmd sudo systemctl stop ${AVAHI_PUBLISHER} printf "\n" sleep 2 log ">>>>>>>>>>>>>>>>>>>>> Marker [9] <<<<<<<<<<<<<<<<<<<<<" printf "\n" #******************************************************************************* # Remove one of the NVMe device's file=/tmp/getdev-XXX.py getdev=$(mktemp $file) cat > "${getdev}" <<'EOF' import sys from dasbus.connection import SystemMessageBus bus = SystemMessageBus() iface = bus.get_proxy(sys.argv[1], sys.argv[2]) controllers = iface.list_controllers(False) if len(controllers) > 0: controller = controllers[0] print(controller['device']) sys.exit(0) sys.exit(1) EOF # Find a Discovery Controller and issue a "nvme disconnect" if dev=$(python3 ${getdev} @STAFD_DBUS_NAME@ @STAFD_DBUS_PATH@); then log "Remove connection (disconnect) to Discovery Controller ${dev}" run_cmd sudo nvme disconnect -d ${dev} printf "\n" else msg="Failed to find a connection to a Discovery Controller" printf "%b%s%s%b[0m\n" "\0033" ${RED} "${msg}" "\0033" sudo logger -t COVERAGE -i "@@@@@ " -p warning -- "${msg}" fi # Find an I/O Controller and issue a "nvme disconnect" if dev=$(python3 ${getdev} @STACD_DBUS_NAME@ @STACD_DBUS_PATH@); then log "Remove connection (disconnect) to I/O Controller ${dev}" run_cmd sudo nvme disconnect -d ${dev} printf "\n" else msg="Failed to find a connection to an I/O Controller" printf "%b%s%s%b[0m\n" "\0033" ${RED} "${msg}" "\0033" sudo logger -t COVERAGE -i "@@@@@ " -p warning -- "${msg}" fi sleep 3 rm "${getdev}" #******************************************************************************* log ">>>>>>>>>>>>>>>>>>>>> Marker [10] <<<<<<<<<<<<<<<<<<<<<" printf "\n" sd_restart "stafd" sd_restart "stacd" sleep 4 log "Create invalid conditions for saving/loading stafd's last known config" rm -rf "/tmp/stafd" sd_restart "stafd" sleep 2 log "Remove invalid conditions for saving/loading stafd's last known config" mkdir -p "/tmp/stafd" sd_restart "stafd" sleep 2 #******************************************************************************* # Change ownership of files that were created as root sudo chown -R "${PRIMARY_USR}":"${PRIMARY_GRP}" coverage >/dev/null 2>&1 sudo chown -R "${PRIMARY_USR}":"${PRIMARY_GRP}" staslib/__pycache__ >/dev/null 2>&1 sudo chown -R "${PRIMARY_USR}":"${PRIMARY_GRP}" subprojects/libnvme/libnvme/__pycache__ >/dev/null 2>&1 #******************************************************************************* # Run unit tests run_unit_test test-avahi.py run_unit_test test-avahi.py run_unit_test test-config.py run_unit_test test-controller.py run_unit_test test-gtimer.py run_unit_test test-iputil.py run_unit_test test-log.py run_unit_test sudo test-nvme_options.py # Test both with super user... run_unit_test test-nvme_options.py # ... and with regular user run_unit_test test-service.py run_unit_test test-timeparse.py run_unit_test test-transport_id.py run_unit_test test-udev.py run_unit_test test-version.py #******************************************************************************* # Stop nvme target simulator log "Collect all coverage data" coverage combine --rcfile=.coveragerc printf "\n" log "Generating coverage report" coverage report -i --rcfile=.coveragerc printf "\n" log "Generating coverage report (HTML)" coverage html -i --rcfile=.coveragerc printf "\n" linux-nvme-nvme-stas-a8026bb/debian/000077500000000000000000000000001440613556600173715ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/debian/.gitignore000066400000000000000000000001521440613556600213570ustar00rootroot00000000000000.debhelper/ *.debhelper *.debhelper.log *.substvars tmp/ files */DEBIAN/* nvme-stas debhelper-build-stamp linux-nvme-nvme-stas-a8026bb/debian/changelog000066400000000000000000000002151440613556600212410ustar00rootroot00000000000000nvme-stas (0.1) stable; urgency=medium * Initial release. -- Martin Belanger Wed, 8 Sep 2021 13:40:00 -0500 linux-nvme-nvme-stas-a8026bb/debian/control000066400000000000000000000017651440613556600210050ustar00rootroot00000000000000Source: nvme-stas Section: misc Priority: optional Maintainer: Martin Belanger Build-Depends: debhelper-compat (= 12), python3-setuptools, dh-python, meson, ninja-build Standards-Version: 4.4.1 Homepage: https://nvme-stas.readthedocs.io/en/latest/ Vcs-Browser: https://github.com/linux-nvme/nvme-stas Vcs-Git: https://github.com/linux-nvme/nvme-stas.git #Testsuite: autopkgtest-pkg-python Package: nvme-stas Architecture: all Depends: ${python3:Depends}, ${misc:Depends}, python3-pyudev, python3-systemd, python3-gi, python3-dasbus, python3-libnvme Description: NVMe STorage Appliance Services This package provides two daemons, stafd and stacd. The STorage Appliance Finder Daemon (stafd) automatically discovers NVMe-oF Discovery Controllers (DC) and retrieves the list of NVMe Storage Appliances. The STorage Appliance Connector Daemon (stacd) establishes I/O connections to the NVMe Storage Appliances discovered by stafd. . This package installs the library for Python 3. linux-nvme-nvme-stas-a8026bb/debian/copyright000066400000000000000000000276061440613556600213370ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: nvme-stas Upstream-Contact: Martin Belanger Source: Files: * Copyright: 2022, Dell Inc. or its subsidiaries. All rights reserved. License: Apache-2.0 Files: debian/* Copyright: 2022, Dell Inc. or its subsidiaries. All rights reserved. License: Apache-2.0 License: Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 STFS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. # Please also look if there are files or directories which have a # different copyright/license attached and list them here. # Please avoid picking licenses with terms that are more restrictive than the # packaged work, as it may make Debian's contributions unacceptable upstream. # # If you need, there are some extra license texts available in two places: # /usr/share/debhelper/dh_make/licenses/ # /usr/share/common-licenses/ linux-nvme-nvme-stas-a8026bb/debian/nvme-stas.postinst000077500000000000000000000022621440613556600231200ustar00rootroot00000000000000#!/bin/sh set -e action="$1" #oldversion="$2" umask 022 #**************************************** dbus_reload() { if [ -f /usr/bin/dbus-send ]; then /usr/bin/dbus-send --system --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig fi } #**************************************** systemd_add() { service=$1 deb-systemd-helper unmask "${service}" >/dev/null || true if deb-systemd-helper --quiet was-enabled "${service}"; then deb-systemd-helper enable "${service}" >/dev/null || true if [ -d /run/systemd/system ]; then systemctl --system daemon-reload >/dev/null || true deb-systemd-invoke start "${service}" >/dev/null || true fi else deb-systemd-helper update-state "${service}" >/dev/null || true fi } #**************************************** # configure #**************************************** if [ "${action}" = configure ]; then if [ ! -s /etc/nvme/hostnqn ]; then stasadm hostnqn -f /etc/nvme/hostnqn fi if [ ! -s /etc/nvme/hostid ]; then stasadm hostid -f /etc/nvme/hostid fi dbus_reload /usr/sbin/modprobe nvme-tcp systemd_add stafd.service systemd_add stacd.service fi #DEBHELPER# exit 0 linux-nvme-nvme-stas-a8026bb/debian/nvme-stas.postrm000077500000000000000000000012371440613556600225620ustar00rootroot00000000000000#!/bin/sh # see: dh_installdeb(1) # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package set -e if [ "$1" = "remove" ]; then if [ -x "/usr/bin/deb-systemd-helper" ]; then deb-systemd-helper mask 'stacd.service' >/dev/null || true deb-systemd-helper mask 'stafd.service' >/dev/null || true fi fi if [ "$1" = "purge" ]; then if [ -x "/usr/bin/deb-systemd-helper" ]; then deb-systemd-helper purge 'stacd.service' >/dev/null || true deb-systemd-helper unmask 'stacd.service' >/dev/null || true deb-systemd-helper purge 'stafd.service' >/dev/null || true deb-systemd-helper unmask 'stafd.service' >/dev/null || true fi fi linux-nvme-nvme-stas-a8026bb/debian/rules000077500000000000000000000003761440613556600204570ustar00rootroot00000000000000#!/usr/bin/make -f # See debhelper(7) (uncomment to enable) # output every command that modifies files on the build system. #export DH_VERBOSE = 1 #export PYBUILD_NAME=nvme-stas %: dh $@ --with python3 --buildsystem=meson+ninja override_dh_install: linux-nvme-nvme-stas-a8026bb/doc/000077500000000000000000000000001440613556600167145ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/doc/.gitignore000066400000000000000000000000071440613556600207010ustar00rootroot00000000000000*.xml~ linux-nvme-nvme-stas-a8026bb/doc/dbus-idl-to-docbooks.py000077500000000000000000000063431440613556600232230ustar00rootroot00000000000000#!/usr/bin/python3 import os import sys import pathlib import tempfile import subprocess from argparse import ArgumentParser from lxml import etree def parse_args(): parser = ArgumentParser(description='Generate DocBook documentation from D-Bus IDL.') parser.add_argument( '--idl', action='store', help='IDL file', required=True, type=str, metavar='FILE', ) parser.add_argument( '--output-directory', action='store', help='Output directory where DocBook files will be saved', required=True, type=str, metavar='DIR', ) parser.add_argument( '--tmp', action='store', help='Temporary directory for intermediate files', required=True, type=str, metavar='DIR', ) return parser.parse_args() ARGS = parse_args() pathlib.Path(ARGS.output_directory).mkdir(parents=True, exist_ok=True) REF_ENTRY_INFO = '''\ stafctl nvme-stas Mr Martin Belanger Dell, Inc. ''' MANVOLNUM = '5' PURPOSE = 'DBus interface' PARSER = etree.XMLParser(remove_blank_text=True) def add_missing_info(fname, stem): xml = etree.parse(fname, PARSER) root = xml.getroot() if root.tag != 'refentry': return if xml.find('refentryinfo'): return root.insert(0, etree.fromstring(REF_ENTRY_INFO)) refmeta = xml.find('refmeta') if refmeta is not None: if refmeta.find('refentrytitle') is None: refmeta.append(etree.fromstring(f'{stem}')) refmeta.append(etree.fromstring(MANVOLNUM)) refnamediv = xml.find('refnamediv') if refnamediv is not None: refpurpose = refnamediv.find('refpurpose') if refpurpose is not None: refnamediv.remove(refpurpose) refnamediv.append(etree.fromstring(PURPOSE)) et = etree.ElementTree(root) et.write(fname, pretty_print=True) FILE_PREFIX = 'nvme-stas' FINAL_PREFIX = FILE_PREFIX + '-' pathlib.Path(ARGS.tmp).mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=ARGS.tmp) as tmpdirname: try: subprocess.run(['gdbus-codegen', '--output-directory', tmpdirname, '--generate-docbook', FILE_PREFIX, ARGS.idl]) except subprocess.CalledProcessError as ex: sys.exit(f'Failed to generate DocBook file. {ex}') stems = [] with os.scandir(tmpdirname) as it: for entry in it: if entry.is_file() and entry.name.endswith('.xml') and entry.name.startswith(FINAL_PREFIX): fname = entry.name[len(FINAL_PREFIX) :] # Strip prefix stem = fname[0:-4] # Strip '.xml' suffix stems.append(stem) tmp_file = os.path.join(tmpdirname, entry.name) add_missing_info(tmp_file, stem) os.replace(tmp_file, os.path.join(ARGS.output_directory, fname)) print(';'.join(stems)) linux-nvme-nvme-stas-a8026bb/doc/genlist-from-docbooks.py000077500000000000000000000016101440613556600234760ustar00rootroot00000000000000#!/usr/bin/python3 import glob from lxml import etree exclude_list = list(glob.glob('standard-*.xml')) PARSER = etree.XMLParser(remove_blank_text=True) def extract_data(fname): et = etree.parse(fname, PARSER) manvolnum = et.find('./refmeta/manvolnum') manvolnum = manvolnum.text if manvolnum is not None else 0 deps = set() for elem in et.iter(): keys = elem.keys() if 'href' in keys and 'xpointer' in keys: dep = elem.values()[0] if dep in exclude_list: deps.add(dep) return manvolnum, list(deps) output = list() file_list = glob.glob('*.xml') for fname in file_list: if fname not in exclude_list: stem = fname[0:-4] manvolnum, deps = extract_data(fname) deps = ':'.join(deps) if deps else 'None' output.append(','.join([stem, manvolnum, fname, deps])) print(';'.join(output)) linux-nvme-nvme-stas-a8026bb/doc/html.xsl000066400000000000000000000030131440613556600204050ustar00rootroot00000000000000 .html#   nvme-stas
" "
linux-nvme-nvme-stas-a8026bb/doc/images/000077500000000000000000000000001440613556600201615ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/doc/images/Coverage.png000066400000000000000000003706531440613556600224400ustar00rootroot00000000000000‰PNG  IHDR§%NêZcsBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝw|SõþÇñWvG: Ji¡´ÊFFÙ¢€W‘­?Ä+K”% ÷‚‚‚¢ A6A¹Ú‚Z†²§2Z Èjiº›ä÷GIìHÛ´tÂçùxœG’“äœO’6yçûýžo@!„B!„B!„B!„B!„B!„B!„B!„£(íl(‹5 !„B<¬Ì¥]@f¥K{ÿB!„"§R ¬%íÙŸV!„Bˆ’cO-±°ZA0¯}ö:!„Bñ`ò œ…½îgÌmÛÙ×Ûº]~uIpB!„È_~AÒÖõÙ×å¶b ©Åòì › ;¯ËkB!„(˜‚†Q{‚j‘†Ô¢}ö†Ð‚ž·uY!„BØÏÞZÐó¶.ZQ>{‚in§ù­Ëí²B!„ÈŸ=Á4¯uyÝ&·Ë…Ra¯°¡4¿Àj+¬Úº,„B!r—[ˆ´ç´ÄCꃽ܂c~aÔžó¶NsÛ¯B!„È© Á´ çmæ¶_»=Hȳ'˜f?¯(Àú¼Nó«E!„âQ”ß‘õù…ÓÌ‹=ëmm+¿Zò¤.ÌlÈ-˜fÉ~ÿÌÛÍ~>¯uB!„šüަ·:³_¶gɼ=E¶ÓRØpšÛ¸Òü©2ŸËyÕÜö+„B!rÊ+”f>ok1Ù¸lÏþ2ÔB…Õ„ӂÓÜÂhöÓ¼‚jöíg¯ÁÖe!„BˆGQ~GåçL3RE¶uÙï“9¬ZnEP¤[?·ÖÌÜB§2óyUlœÏ^ƒB!„ø‡½­¦&çMdd1Y»ì³_Ÿ9¤f£…îâ/h8Íë ¨Ü‚iö@š}É-¬æw°”­z„B!DþãLmµ˜šÈL-¡4û¶,ë”6®ÏL T Ns …y/ÍkQe:ïTÜ Ít}æýeïÞB!„y³ud½ 0©@ {IäŸj$g¾ËD-ç³g3[]úvÔsš×O¹…QU¶ËzÀ÷ƒ>h1tèÐ>J¥R«P(” …dP[Ž=JãÆ­—ÍfsŽSËy!DÑP(XÞ“²Ÿ !Dyu?3˜ŒFcê;wί\¹ró{ï½w¸$ðOµU[2·´æØ…èÞ·÷ÝÕVKiæóù…ÒìáT TŸ>}z»qãÆÖétÓÒÒHOOÇd2a2™$`åâäÉ“ÿQ³Ùl}Î,ç…EG©TZjæóP…å™å=M©T¢V«Ñh4¤¤¤Äý÷¿ÿ7cÆŒ½d„Ôtþ §ÆLçM6–ìcXÉvìª §¹µšf§Ù[H3/Z ÖÉ“'ß ì’’’BjjªeˆÌáÔäF#éé餧§c4³´žJÈ¢p2·* T*jµµZJ¥ÊR…âa¡ÕjÑétœ;wîdž .þ$£ëߘmÉÜ¢š[8µg¢þŠb*)[ãL-ç³S Pëúõë³ÝÜÜ‚ ƒ„§`2™HOO'55•ÔÔTRRRHKKÃh4Z[Oåù¢p,¡S©T¢R©Ðh4èt:´Z-Z­µZR©Ìr[!„(ïRSSIKK#00ðéèèhŸIÀ¹\nnæŸãƒ2-Í~¹È§’²õ®[˜®}5Pýرc¯»¸¸%$$¤NaƒÑh$55•ÄÄDt:èt:ù ¢ˆ™ÍfRRR¸~ý:‰‰‰Òµ/„xh™Ífpwwopâĉ74h0¸l¹Û­¢¶¦ž‚œÁ4ß°ª²£Æ¼ºôó:ßH-§®S§N}ê_ÿú×+IIIvìVØò÷ßS¹reRRRHIIA£Ñàçç‡F£‘J!ŠB¡@­VãîîŽÁ` ==]ºö…½ôôt*W® T*ÏïÞ½û–éêÜÆ”f¿ÞÂî7J{nhÏÑùÙzÊN-µnllì²ôôtOéj.¼ÌcNãââ0 àààPÊ• ñhHNNæÂ… èõz³tï !ÄÃæþ—ó›îîî/gÉcš~±5ÕHîcOíwú ï¨ù…ÖÌE9¿ÿþû- …Ó"d4IKKC§Ó•v)B<2t:ul·LÝ&„xØ™Íf E¥éÓ§?NƼôÙ~·çG“ $¿pšÛìíâ·´ V:tè3rT~Ñ2›ÍFéR¢) k0•iï„‚ÔÔT† Ö¨HÎyë³ÿêg^Õ®àZ˜–Ó¼ZLs ©n®®®…ؗȃÌi*DéP*„xÔÜÏqîäJ¸µ á4·ŸÍ­ ÌÝûf³Ù© Å‰¼É‡£¥Gþÿ„’û9NGÞÝùùeE»fžÓÌ;±§ÕT è …=3ˆ’H!Jžüß Q¶¤§§g XdÍ‹¶~Î4×ZØà˜½É–l—þ–ÓûaT•xµálF05&b6)P˜pJ\ƒ9Έ9 ôšåhÌPš-§ñ†x¶~»•>Ïõ¡mÛ¶h4jîÜü›_þÎÖ_#‰¾}‡*ÎZ:øxpïð^&Ž|…{ññrà‡%äâÅ‹¥]‚e^zzºÝ]×yIIIy @÷×ao(µÕi·¢ Ù ±gZrO™‹kÄËh£ÄéÌw8ÜG¡T J<ÒpEJ Šä¦$ôÉ Ñ¦…£ÀP*µ~ÿý÷ô¡?ÎÎΞƖЌÙÁ^âó£9ð'jubß=Î(q‰¹È’ÿÌ~H_¹Œ®ŒWÚ¥Àøñã9yòdi—!D™–˜˜XdÛJJJzhëÈkôÜ®³³Ž¼2^‘å½™Ó ·¢þVS…ç_ º}÷(ŒÄyçä>üQk?Ä9à4þO‡âàa@E:úô¤p•Íà-õ÷ß~# '''’ ñÌ]µˆ4-.>ÕÑÞ1fºË½Æwˆ­ÏCûoÓé\­‡ÂÿǾnÝ nÓ¦Dë- ×®]cÒ¤I|þù縹¹•v9¢˜}öÙgtëÖÀÀÀ,ëÏŸ?ÏÞ½{6lXé&D.¾ÿþ{žx≌…<$$$°k×.zôèQB••žôôtŒFc‘n/==½ÀÓ*åUÇË/¿œç}—-[Vìu˜L&æÏŸO«V­hÛ¶m–ëöîÝËþýû7nJeÖxV€:ì™tÿ‚jAÃinÍ´yT[·/÷T7.¡;N:”.·1+RÑÿý9Uîúr)ê_˜|‰‹õÀ­öi¼uÇA©C©1ã`ÞE²úIŒŠª%V뙳g4h˜M,Ýú»Ìz<ý|0ß¹ÇMóq.·8†Á7ƒ§úU8PUÃ@§Î èõ?þ°õ¡ §âÑÒ­[7¦L™ÂÌ™3­õüùóÖuB”5áááìÚµ‹™3gæP˜2e f³ù‘§iiiŲ͂†Âüê°@!ïàZ”u(•JZµjÅÊ•+IKK£S§N@F0]¹r%/¾øbŽ`Z€: š÷²;µËƒÎsš×ú"oæ-K´gC¼<“ P JWTÚxêJ¥vƒØµêRS*‘žæ Ú4¸i% 44i§1jK.œ*J4 /\`Û•ÛTóóÃóz¿Åæl×ãPÕÍ%3I7cqôu!É[Í¢C?òãÙ\»—ëvÿøã>ýôSbbbˆG¯×Ó³gO† ‚R©dРA´hÑ‚7ß|ÓzŸ3f˜˜ÈìÙ³Y¸p!´v­7jÔˆ6mÚ°}ûvþúë/œû¬õþqqq,Z´ˆC‡ТE ^ýu\r9xËRã… P«Õ<ù䓌=ÚzýÈ‘#­çCBB¨X±"ÿùψˆˆ %%ooo^~ùe‚ƒƒ¸{÷.Ÿ|ò GŽ!55•š5k²hÑ"›ÿèGeñâÅ\¾|½^Ï!CxöÙg1›Í|ûí·lܸ‘»wïÀèÑ£©S§ÿý7/¼ðÓ§O·ÎšÐ§OúöíË€øóÏ?Y²d üñ...<óÌ3Ö7›äädþóŸÿpöìYnß¾³³3Ÿ|ò U«VeĈÄÄÄ P(¨]»6£G¦zõêÖ}lß¾uëÖqëÖ-\]]ñööæ7Þ V­Zyî7»_~ù…M›6qíÚ5éÖ­cÇŽåÞ½{,[¶Œ3gΠÑhhÖ¬/¼ð‚u þ?üÀåË—IMME­VS£F Ú´iC… ¬Û6\¸p»wï¢ÑhðððÀÇÇ•*cLwjj*iiih4´Z-Ìœ93KÍV…(KfÍšÅäÉ“­§Ùjæ`:kÖ¬Rª²de«éƒl³<Ôai1]¹r%ÆL-aµuØDmݧDæ9;ÓÜŽähÇœ*/߆$Ä8ƒKŠÀ¢ïµåÂÅvٌܾÛNzÌ7µ¤¬ߺá(SR1Ç;¢¨dD•z´%W«›»;›#£ŠNÍÔàzÔv«Â“á{©êbÂí|§¼õèÓ0Ƨ¢¼Äi—~?sœQ×sÝnLL §OŸfúôé8;;sêÔ)V­Z…§§'=zô M›6ÉÉɼû¥¥±páB.\È{ï½g³ÆéÓ§óØcñÚk¯Ùì™2e 5kÖÌxžîwï?ñÄtéÒ¥RɶmÛ˜5këÖ­ÃÍÍ/¿ü’¨¨(¦L™‚££#7nܰÎN:Å[o½E×®]yõÕWIIIÁÓÓ€5kÖ°zõj^~ùeüüüرco¾ù&K—.Å××—ZµjqäÈk8½xñ"wïÞ¥mÛ¶ÄÅÅñöÛoÓºuk† Æ•+WX¸p!*T {÷¤°k×. @‹-¸wïUªTA­V3hÐ <==IJJâË/¿döìÙ,^¼€uëÖ±|ùr DPP·oßföìÙܸqƒZµjå»ßìŽ;ÆÝ»w™8q"Z­GGG>üðCŒF#ÿþ÷¿1¬_¿ž 60|øpþúë/*W®LË–-IMMåÌ™3|ÿý÷<÷ÜsèõzŽ9BZZh4n޼ɕ+WðóóC¡P––†ÙlÎñí?s@ÊT0MHÈøxñâE‚‚‚J¹Q8;;çP³ÓüºþÅq€na¶Y^êÈP‡š£›¿uäÖRZ¤ ’E1æÔÖzr¹î¡aŠU@‚SšEBQ‰Þ|>ƒ¶>ÎÎ>>4jÔ€èèhÖ¯_ÏsÏ=@“&Møé§Ÿ8tè:tˆ£G²nÝ:ªT©À½{÷˜;w.'N$---ËцNNNܽ{—ZµjÑ A›µ{{{ãçç—e]³fͬçkÔ¨Axx8çÏŸ§Y³fܺu‹Ê•+Ó¼ys E®Aâ믿¦aƼýöÛYÖ'''³víZ^|ñEžþyš7oÎ… X¹r%“'O¦mÛ¶üøãÖûìÛ·___|}}Y¶lz½ž·ß~¥RIPP§Nâ§Ÿ~Ê5jd}^-:vìh=Ÿ””Äûï¿oíZ³f ýúõcèС YÜ ±k¿™U¨P–-[Z/:tˆcÇŽ±råJ*W®l­cÍš5 :Ôò===©Q£þþþlÙ²…'Nкuknݺŭ[·hÕªî÷¿t9;;sñâE ƒµ]¡PäÚmUÚNž<ÉÔ©Sm^·bÅ V¬X‘eÝðáÃéÙ³gI”&Ê[x$ƒ©(“òÊ{%:æÔ^u—>@²K œï: p5£0éqˆ»æk$™Ì˜¬C«#¨Âit1'jQTƒË®¤W®ÿŠÐÍ[·iݺ5]Z6£ªW®þu™±ÇÔ(•'tjprã­sµØøÓIbœqNUÒ/1•)…šµ ´¯jÕªqüøqêׯ»»;{÷î¥OŸ>DDDðØcáááaó¾^^^$&&’’’‚N§C­VãááÁ½{÷€Œne€!C†Xïc6›1™Lܺu‹¯¿þš°°0ëuááá 4ˆåË—sâÄ ž}öYZµj•ïcؼy3?üðÿý·µ5ÕÖú÷ïÏôéÓyå•WèÕ«O=õ:.Ç6þüóOúöí›cýåË—INN¦I“&Öu …‚FY[™Û¶mËŠ+8þ”!47É ë“´Ê Ge ¦d=•qŒð[Ïâ?ûq;Á„t5ÆÛìÚµ‹^½zåz_ËoüfžæB«ÕZ·g 2K–,±Ž/´ðððàÅ_¤k×®YÖ4ˆvíÚÂÔ©Siݺ5Ó§Oϵ†;wòÅ_ðÆoаaCâââ7nœõúæÍ›³zõj¾ÿþ{–.]ÊúõëY´hQŽ#þs뱬Ïë  fÍšøúú²k×.ôz=gΜ±ŽÛÕh4ÔªU‹wß}7Ë}²?Ù}öÙgüöÛoŒ3†ªU«òûï¿óùçŸÿt)çµÂî73Ëë7oÞ<Ôj5 …ºä5c‚B¡(Ð~lÉL-a4óÔÒ¨Ù/[©të‹ì2Tà‘ ¦ñ~“ÛA@…9J޲͢¬£°ŠºŽÌÁ4sµtñçPð½¶È¢Mq´œ>!UÛÔ›;5šSíÚ”®ñ˜b*Ð*ñ þ—rÊà‹ÆÑDc¯ èSc1ÝÒ¢T7=IôlŽÂÝ»Dk­_¿_-ÿ’mjR¥+}ýž!ñ¤Š®é§¸vCC€_'¢®_Âh2QÁÍC’TµJ:´Îý–=ž~úiFEXX7oÞä©§ž*ô¶üüü0ܼy“Ç<Çõ–®ïìjÔ¨ÁøñãiÑ¢Ó¦M#::ÚúÆž½ûúÌ™3T¯^Ýz–­Ááîîî <Øz@О={r)[³fM>œ1KB¶ÇàààÀáÇ­!Äl6sìØ1{ì1ëíºuëÆÖ­[qtt¤fÍšÔ©SÇzÿC‡áææ†««k¾ÏYæÇÕ®];ëŠ;wîX¯óõõE©TrâÄ êׯoóþ…ÝoömFnß¾¥å8?7nܰ¶Ðº»»c2™ˆ‹‹³vë§¥¥‘ššj³Û"444GµŒA å7Þ(Ôc¢¤8;;óñÇ—v¥J£ÑœœlóºÜ§=Û,Ê: ÿ \Üu˜L&8cŒ©åüþýûéСƒÍ¡O…©ƒbÈ} §y…e«y÷¡žJJ¡Ó¸gˆEåô«(]“1ßÕPYLeí P™ Y9U2Å×Ü0P¤)=)ÔKÿš5kÆþC{ñó¯ŠV£ÃÍ9ÑN/°ïð¨4ž´jÕ•*•{ÇÜæöÍ;Ô©HüU#-[æß ž—ºuëâïïÏ‚ ÎrÔuA5mÚ”úõë3gÎŒwîÜ¡bÅŠYƉf¶qãFêÖ­‹F£áøñã¨T*\\\puu¥R¥J¬[·€›7oÒ¬Y3Ù¾};Û·o§víÚ9¾•†……ááá‹‹ /^Äh4Úlõ8Tó²þôiIüÂpÖ³’^:ãäè„ÊUGZj+.œ»ÊSm´Ü3&á¦wCè„WµJü¸nz -’}÷êÕ‹ùóçÓ»wïÞÖœ9sX²d +W®$!!hó¶ ìÞ½›+V`6›©V­“'O¶¶¸M˜0O?ý”÷Þ{J•*Q£F ºuëFtt4«V­âÎ;888P½zuëxÈcÇŽ±wï^’’’ðôôdðàÁ6~lÖ¬³gÏæË/¿$<<­VËÓO?MË–-8p ŽŽŽlܸ‘ØØXX¸pa–Vß *СC"##éÒ¥K–õ_|ñ .dÞ¼y˜Íf¼¼¼¨S§Nž!ñõ×_gÁ‚Ì™3‡„„ôz=µjÕ²¾9­V˪U«HNN¶¶ÔZ®/ì~³›5k+W®$$$„ôôtªT©’ã`NÇo¿ýFbb"•*U¢k×®YÆ£6mÚ”óçÏó×_Y§’ªV­Z±ŒB”-ŽŽŽÄÇÇÙ¶Ö:ò: 4·ë¤Ž¢–×»yö¬Ìtª$ã‡âUd\ˢɴhï/ éÍ›7Cý¥ë~ÈL8iîϨý…^‘Š£ÖˆHNUb0;b ®‹r\{êyÉpñÉ“'­ÝÁ7oÞäîÝ»´hÑ®ûÞ»w-ÿÛ„·¿+ÍZ7B£Ôrìä)tjú×àæ­[Dþú;ŠGú>û.®.(¾Foa‡K—.ñÒK/±bÅ ë‘ó%á³Ï>£^½z<ñÄ%¶ÏÂ8xð *T@¯×£Ñh Õê‘ÝúõëéÙ³ç#9–Pˆ‚°8û t:u~e©Ãþ:*Uª48 ¤©÷—´LKz¦Åx1Ý_Ì™N--¨6[R¥åôA(`2áÜÄãšA$ºÆí]ç!>“nN¨;ÔÄ¥Åý‰Î0˜>³ÙŒ««+ü̞=»Ùºj&uî•ôD]ŒfÓ×Û©Zٗǵ¶¶šÍæ‡p@†°å—_~2ºÍãââX»v-6ÇðŠâñ /”v B” ŽŽŽôS¦jµºHZ ¥ŽâSáôáŽ7÷æJ©Âåq_x<óRœw1s—g»víi×®}·Îyñp;{ö,»ví"66www6lÈÿýßÿ•Ù¹B….…B‹‹ IIIn1Ôét8::ÉçÛ#\G±‡i9B0bÄFŒQÚeÈÁIB»( œœœÐjµ$%%å{”J¥ÂÑѱ°G£K%L©B!Ê%µZ‹‹ éé餥¥a4³Ì'­R©Šl\¸ÔQrÊvu"_Òõ.DÉ“ÿ;!Ê–ÂNÇ$u”M2 ¬“H!Jüÿ !DñpZŽ) 9`EˆR T*%œ !D1‘dSŽYÆäö;îBˆ¢g6›Q©TÖ/‡R…¢hI8-Ç,›tò]!„ýRRRÐh4Ö€*áT!Š–„ÓrL­V£Óé¸~ýº´ž QÌf3ׯ_G§Ó¡V«%˜ !D1pZŽ©Õj´Z-)))\ºt‰ääd ©B³ÙLrr2—.]"%%­V‹Z­–n}!„(åwžJ¥B«ÕššÊ… ¬óš™L& «B’%t*•JëËïOkµZk·¾Bˆ¢%á´S*•Ö®Eˇ§eÂ]K(•p*DáX‚§e\©J¥²Î¨R©¬­¦P…¢hI8-Ç2-¬T*Ñh4˜Ífk«©¢hd¢™ÏK0Bˆ¢Wbá4,,¬¤võP»|ù2W¯^µ^ÎÞ2š¹ÕTòê> IDATQ4lQ ¦BQ<òzwUd:Uqð”åT ¨î/êL‹&Ӣʹ4IMM]P õ !„Bˆ ÕjGG€ÔLKZ¦%=Ób¼¿˜î/æL§–V4›­ir´¾B!„(3$œ !„Bˆ2C©B!„(3äh}Q&$%%qíÚ5bccK»!„x主»ãíí££ci—"„„SQú’’’8}ú4~~~<öØc¥]ŽBèõzΟ?ÏÊ•+™9sf©ÔXPcÆŒ!88˜?þ8Çu}ûöeÆ œ:uŠM›6áææV¬µlݺ•­[·òõ×_ÉööìÙCDD'N´ký7ß|ƒF£!88˜ 0vìX|||Ф–âb6›ùðÃéܹ3­Zµ*ðý£¢¢øì³Ïø÷¿ÿý@µ¨_»’ŲeË1bD‰M¿f2™èÛ·/ýû÷§ÿþùÞ>sË—/G­VóÎ;ï”@¥bÆ Œ?€¥K—ZÏ—7GeîܹܸqîÛW©R…wÞy‡ÆseÿˆŒŒdÞ¼yÖ}–×纬*×áôüùóŒ?žÁƒÓ·oßÒ.'‹¨¨(þüóOZ·n««k‰î;,,ŒÅ‹ãääDŸ>} 2ž¯Í›7³eˆ ªU«0 %ZÛƒˆŽŽ¦_¿~çz›“'OòüóÏ?P@]³f ~ø!çÏŸÇh4’˜˜ÈŒ3ذaGÅÙÙ™½{÷òÓO?ö¡ä0yòd*Uªd×ú„„FŽÉìÙ³9wîóçÏgøðáe>œþòË/Lž<™gŸ}¶P÷ÿã?˜;w.ƒ~ ÇZÔ¯]IZºt)K–,aÊ”)%¶O“ÉÄ·ß~ËO>…Ba] £{ÝÓÓ3ÏûFGGó /P¹re¼¼¼4hÿý7Ñ`³K{Íš5<ÿüó9ZIl­‰‰!<<œAƒe¹mÿþýqrrÂÅÅ…ž={rùòå,×=z”:àèèH•*Uxë­·HJJ²^êÔ)Ú·oŽŽŽT­Z•7Þxƒ иqcðööfÖ¬YY¶›””Äĉ©Y³&nnntêÔ‰#GŽäxŒß}÷ƒ_|È×´iSªT©‚N§# €O>ù„ñãÇãïïN§£nݺ|ÿý÷9¶õÖ[oYëìØ±#§OŸ¶ûu°åÏ?ÿ¤N:èõzéÔ©'OžÌõ¹Éí9>qâO>ù$NNNèt:s|8EFFÒ¡C\]]ñ÷÷gÚ´i¤¥¥ݸC† ±ÖâææÆ²eˬ÷]½z5uêÔáñÇçõ×_ÇÓÓ³Ùl½¾wïÞ<õÔSYöçííÍ[o½dŒƒŸ>}:ÕªUÃÁÁ–-[²wï^ëmóÛ¿E||<Íš5£M›6Yþ†²×hKÛ¶m³¼?½ûî»Ô­[´Z-|öÙgÖ룣£Q(Yþ§oݺ…B¡`õêÕLš4 WWW.]ºd}íÛ·§sçÎYžŸÌ2ÿýiµZ*W®Ìo¼Arr2õêÕ³v[¬X±µZÍÍ›79uê]»v¥qãÆÔ©S‡öíÛ3wî\BBB9r$ýúõcÔ¨Qüïÿ˲ôôtöíÛǦM›Ø²e ‘‘‘ÄÇÇç¨Ïò7‘›¯¾úŠ®]»Â¸vígÏžÍ2ä)88˜³gÏríÚ58@HH]»v-w_ÂÂÂ7n\–VBƒÁÀ¸qã²üý–ËkÞ¸qãá000Ðz%ÀŠÂ+W-§å-˜Z”T êÞ½{óý9a„rõÜ•wwwöìÙS û$$$Ю];X³f ‰‰‰Œ?ž²sçN ãƒ){kÈáÇ9}ú4_|ñ…]ë×®]‹££cŽÛ¯_?Ú¶mËÕ«W™0aÆ c×®]@F¸jÙ²%O=õ7n$&&†É“'süøqkWØõë×Ù³g‹-"((ˆ#GŽðöÛoãååÅŒ3ðññaóæÍL™2…víÚÑ¡CÌáÇ™={6^^^|ôÑGtéÒ…?ÿüwwwk}«W¯¦cÇŽÖ.Ý'Nð矂F£!$$„±cÇÒ®];æÍ›‡N§cΜ9¼øâ‹ÄÄÄdùåææÆÂ… IKKcþüù´jÕŠsçÎáååe×ë]åÊ•™1cÞÞÞÜ»wwß}—¾}ûræÌ …õ¹Y°` 4àâÅ‹¼ûî»YžãÓ§OóøãÓ¦MÖ¬Yƒ^¯gÑ¢EY‰å ÂÈ‘#™5k§OŸfìØ±h4&OžŒÉdâ›o¾aèС 6Œ¤¤$j×® d„•72zôhžzê)>ÿüsNŸ>Mýúõ1›ÍìÙ³‡ÔÔTŒF#*•ŠóçÏsýúuk`>|8ßÿ=3gÎ$ €µk×Ò¡C~ùåÚµk—çþ-RRRèÝ»7ééélß¾GGGëuÙk´ÇîÝ»³¼ž6làßÿþ7 4°þåçý÷ß'44”áÇÎÿû_Nœ8Á‰'¬_.³³üý­_¿Nǘ>}:•*UbÚ´iôéӇŋ[ŸKÈh­lß¾=•*UâøñãDFF²páBêׯÏüÁ¢E‹ðññáÕW_¥J•*:tˆ­[·R§Nkùᇈ‹‹£mÛ¶èõz.\¸ÀáÇiÓ¦ jõ?Ç)))yvéwëÖùóçÓºuë<Çà[øúúi zåÉÇÌØ±c7nóçÏ`ܸq˜Íæ=˜×ÙÙÙzГ%ƒ”fw¹Œ9-CÆGBB;vì°{|Iq¾(aaaù~ÃÍnß¾}Åö­ê¯¿þzè›úK‚J¥*ð¸ÈÏ>ûŒ;wîpâÄ ëVzz:}ûöåÊ•+T¯^=GKdtÑרQƒ¶mÛÚ½¾wïÞ9fèÓ§µæØØXFÍ­[·ðôôdÒ¤Iøúú²uëVë ¯¯/O?ý4;vì K—.Öí´oßž   Ú·oOxx8ׯ_gذatìØ‘5kÖF‡8pà›7of÷îÝ´k×€   <==ùî»ï2dÑÒš#hët:ëÿB‡ذa­Zµ¢wïÞxxxЪU+"""²Ô8eÊëcíÒ¥ ¾¾¾,X°€>úÈ®×!;77·,G¡§§§Ó«W/bbb¨ZµªuýO|˜5kÖðõ×_[[s»wïÎ… ˜0a‘‘‘¹î?==€ÔÔTž{î9¢¢¢Ø½{7*TȳF{ùøøXt—.]øñÇÙ´i“ÝáT«Õ²zõjš7oΨQ£X¶l+V¬ÀÇdzٜ¥ë[£ÑX¿èèt:ºwïÀ“O>É¡C‡Ø´i“5œÎš5‹Ý»wÓ©S'RSS åÃ?̲ïÖ­[óØcÑ¢E Ž9‚Á`°þ­Z¾ä9s†ÀÀ@®^½Ê¹sçxñÅñöö2þövïÞÍÝ»w­ÃtL&©©©yÎôbb4vìX¢££ó=R|Þ¼yÌ›7÷߿؆'%gggkðÒëõY*d {øøã­ÿã ÅÞ øå—_Zç2]¹r%sæÌ)Õ™ndÌi2jÔ(æÎ›eÜG~ŠóEiݺµÝáÔ2h½~ýúÅVOAYZ¢ËÛ7ª²èàÁƒÄÇÇgéú·t)^¼xÑf(2¬_¿žáÇgiáÉmý©S§8zô(sæÌɳ–ºuëpíÚ5<==‰ŒŒ¤OŸ>YZf:wîŒJ¥"222KðËÌßß?ËødµZMµjÕ¸yó¦õ19º’-ÙbÆ ¨Tª<[õU*Õ«WÏÒýîçç`ÝŸ-*T I“&?~ÜZSA_‡+W®ðÞ{ïñ믿gíÙˆ‹‹ËN3ËþGDDЫW¯<[»úˆæÍ›Ó§OFŒÁõë×y÷ÝwéÔ©O?ýt¡Ÿƒ–-[Ò½{wÆÏ­[·hÞ¼9III\½z•×_ÈÚ+V| ýd÷óÏ?sëÖ-._¾Ì|@… ¬¿@”ßë9_»Úµk³lÙ2¨R¥J¾AÈ–©S§Ò¥K† ÂË/¿ÌÍ›7ùî»ï²ÜfÚ´i´iÓ†§Ÿ~š‘#GR¡B._¾LÓ¦MiÒ¤I®Û^½z5]»vÍ1[„‡‡={ödúôé´oßÞÚ Ò²eK¦OŸÎóÏ?oýPüñÇéß¿?£GÆ`0Èš5kØ¿?áááv?κuë²mÛ6:vìÈĉ­-¥¹ÕèííÍþýûùí·ßr=‚?/ …‚þýû³xñbªV­Jƒ rüjÐÅ‹™û쳜•]õêÕéÙ³'£FÂÝÝ=Ï.}{T«V """0Ö×+==ÝúùpïÞ=ëÜÊö° ³uòäI¦OŸÀôéÓ "88˜±cÇU®æ:9s&cÇŽåÕW_eèС9>çÃÂÂX¹r%ÎÎÎ%2ö3!!!Ë/B•6sZFe¨z½Þ:6¥¬ˆ‰‰)Ñ` €–q¹ …"×àÂçŸÎ;ï¼S®þh]]]Ù´iS¾Ý]›6m*öç|ذa„……1bÄzõê…««+{öìaÒ¤IL˜0„„ªV­Êk¯½fóþ[¶l!))‰صþ›o¾¡N:4kÖ¬Àµ6lØ}ûö1zôhzõê…‹‹ Ì1—ka¬_¿ž©S§òÅ_ƒ‡‡mÚ´ÉNmM“UÞÞÞ´mÛ–Ù³gsçΜéÞ½;sçεΠ`Ïëýµ aܸqôëׄ„ÜÝÝiܸqþ7žzê)BBB˜5k=zô°N;•¹•»U«VìØ±ƒ÷ߟáÇÈÖÏñZ\¹r…Ý»wçúË7#FŒ°þâ›ÅСCÙ¿?#FŒÈrÛU«V1cÆ fΜÉ;whРááá¹¶zç¦U«V|ðÁ¼ýöÛôèÑÿ\kœ:u*/¿ü23f̰9-˜=>ýôSÔj5Ó§O'11‘Š+ÒªU+jÖ¬ À›o¾I‹-¬ÏA:u˜8q"S§N¥ÿþô3¯cÆŒaÛ¶mLš4©H~€£gÏž8p€³gÏòÇàææ†——W–pšy–‹üœ:uÊ:Á¾å 'Ëgc—.]?~<ãÇÇÇLJS§N•«pjgºyófëÇXÆ3_¸pƒÁÀsÏ=ÇСCËÕçXQyØÇœæõ)¯Ètª cNTË©PÝ_Ô™M¦E›ii’ššº ê'""ggç2ómÆâöíÛ¤¥¥•Øï_gÆ¢E‹Ðëõ6¾Ô`00jÔ¨2Óâ|èÐ!»~k=22’1cÆçí|||øä“Oìšb¥´tíڣјãg4m­ONNÆËË‹·Þz«\Mb¿oß>‚ƒƒÙ³gO¡»V˳¡C‡rìØ1Ž=Zèm|ôÑG|ôÑGܸq#Ë´MeIy¨±œ Ûôz=C‡-ÖŸO…³víZ4MŽa¹­_½z5ÁÁÁøûû—d™Ä2!ûk¯½ö@Gˆ——/_æã?æòåË( üýý™?þ4aù!†Âü oI)5–f³™ØØØBõ´åuP«››[¹˜×´<ÏÓ’Uî»õEùgo·¾BˆâU»õEÙQRÝúyÏò+„B!D ’p*„B!Ê §B!„¢Ìp*J»»»uŠ!„¥#&&¦@ó¬ Q\$œŠRçííÍ¥K—$  !D)‰‰‰áÒ¥Kx{{—v)BÈTR¢ô9::R¯^=®]»Æ¥K—J»!„x主»S¯^=™¯V” NE™àèèhýi:!„B<º¤[_!„B”N…B!D™!áT!„B”%6æôСC%µ+!„BQN•X8mÒ¤IIíJ!„B”SÒ­/„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê §B!„¢Ìp*„B!Ê ui „¢äÉ—Ø!ü›ñÁjº²š©Ÿ@×ö¦ôô ´?"MÉw¹v=–œíÅB”=¯½ö¯½öš]çãããs½(^N‹á÷¯xs`wÚ5©Oå ¸:;ãVÙŸæ=GóåÁÛX:þ÷ø89âÙoÙzë0Ç,åwG\Z~ÀI#˜oífÁèÖ¦!Þž¸»èqõ¬N£§ß`éïQœÞ6—‘=[RÛ»".®•ñoчɛϑh£¾äKaüç•®4®î›[%üš>ÍëŸîå†]]ðfîþ¾œq½Zà压Kªú7¦CÿOø-ÛØ½ôSÒRÿOׇk§\4˜¸²yÝ[Õ£ze7ôÎ.xVoL·Ÿ²÷ïlE¤\⇇ñD/®z\=}©Ûª;ã·D‘_glZLKÆ=GëZU©èV¯õi÷ütBcþ¹gêÕ_ùô4÷¯‚»›>A:#„3ñ–$“êãìàÊŸ^ʱOo‡º IDATÃÖ¡x9:SkìnR-Ïá ›g ¡c=ïŒý¶¤÷ø¯9œíEN>·ÞÀS-ç’;z½;UkµddH &Àxz/?ÕœÚÕ<­C?;އcstƒ‰»ÇÖ2e`êûxâªw¥¢—AÁÝxñßóÉZ¹½5æÜÍMö}õ/?ÛŽ†þU©èªÇų:AÝæò›5•¤³w1oþëqü+»áæéKƒ'†óQèeës”±­hBgb@·`‚ü¼¨èªÇÕÛ:Á½yóÓp¢Rl`äÖoËy»o0µ«VÀ­¢7uÚögâªCÜÉöâä÷üº›±Nÿtѹ÷e]vŠÅ½jQ¥z'fí3äòd¤°oÛÜ0»ÓcÌ(š8[Ö;Óèõ·éU nlßÈ®„û«ñÖ†MLyÆgK’Ôøð¯‘Ϩ6î,Ñ÷ÿüÍ)‰$™U¸Wp½ÿæ¬Æ½‚JSI)€ù.¡|DxâcŒ˜õþª¼_¶¢~óß~>›iïNdâô œNý›=_Ìdþ—1bæîÁu|¾õÖ³‘ôô2!DàââRÚ%<²¤[¿È˜I8ú-+7ïBQ¹ ã¦I'öò Žþ´„Ñ‘‡¹óóOLl¬Ã)øIÚ:¯á»}¿p yÝ2uËöíæHŠ’êž¤Ž L7"X·ò[Žë|iÔ¸ ô ¢OðÛ/Kùw‡(M&ÔµiÔ0˜€„ :üó†žà¶ów«`mUI9µˆ¾ÝÞáçÛNÔ ~’g«$rv÷/¬x§û£¶²kNGÜòh‚1þñýžy‡½I©×®;ªjH¸þÇ¢cQfû+RVhL5¦âýí©‚pµœ¿s™ói^4îÔwu—îæ×¯ßáÀ±8™L`ŽeǸnô_~­o :õ Ä5íOœ$&EŸgKQÚùU ê6Šï®šÐû4¢EOˆ‹æÜ‘(Ò\2>îÓþ\ÁÀ®ÿfûu›Ñ6HÇÍÙøáBwžbËö©´qÓÐèÉNT^°Œc¿Dû†Ÿõñ@*GvG¯p§û“ÍÐæø}Ìz¦ýž€ûcíéö¬wŽíbÇâ‘Dü~ƒ°° dôÜš‰Û½„Ù_îÂ\ÑŸ†M;âåj"6ꎮî(tc ¯ª¨ñø“´tQûç>vïX̨gH‰ØÎˆK"1qã£é6x)g’tT®Û”vMH½ÊÙ“¿²åøUjÍ^ÛþmýœeÓÜOX{ÕªõÜÄmR —œ©¨0reÝpž|5„(M5šwúmL‰Øµž}÷r,ï]%ãµ3^dDz¯ÙzÕ‘ªõêÓ¼žeâuÎ å«wv°eû ¶mO3kè3qíÛ‘tþ ÒôToÚš.÷8{ð{¼¶ƒÐßÖ°cá3TVØóüÞÌØ¤Ê‹–}ºQ×éþ.4Ђéï=l ".õ:ßî¼ÀÄÖr¾Iš¹s“ª:ÕªeëSwjDózjVí=Éñ é<Û0÷·Xsj )f*w*Üo&Ð<LËŠŸ²}ñl¾¯÷<–ô+ÿ]s êL¤…§‚”à ™¶î:•û}̸æ%0æÍ|ÃßoåŒkw^›Ô€ j%Z‰«[€GÅœ­æ8ö/›Ï¡À7Ù±’´~ˆ2gÉ’%Er^”ÅýE ¨  'Àp</À¨ ÔêM€ÿgï¾Ã£(þŽ¿÷îÒ)¤“zï”"]Ž€ JQ)*Š `Gì¢((E&½÷Þ!t!éýÊÎï$BQAãï;¯çÙ'Éeofvgoç³3³{M€VÀ¸ÜÜ\ñÿ{ÉÑß?-,0Mf\Ù…¯çÜ&7VŠF8 \!ssEnN¤ø¾¥P´þbÜžŒ"i¤ŠÍ#<„Fã.FlN¹¹¹"ëÔû¢®²ë/"ánš‰b×øjÂLѧg狈ŒÂ÷gˆK³ŸŽEXwž'âr ^Ï:+>kj%è¿ð²H/H';f¨ŠEc1ý\V)Û–%B?¬/Ì Ñêë+"«Èÿr²³ENáßI‹EOK„®Æq<³Œû-騘ÚÈJ(1rkzþk)+ÄEh+½$6ݾ¿YY¥¤•}I|×ÖN(š ¢ù»»D\‘us23òËuA|ÝÊV(GÑbÚ~_¸RÏ‹_úù¢cw‰”Ü\‘{{¹èï¨Ó@±êN‘|2Šwªé„b÷¬XŸ#rsÓÄ7« b!ªÞ b ó͸,æ÷tÅNt™S°Ÿrœ”¶äÄ‹m¯T:E'j¾{BdnÓßEWP̃Äß.‰Ô»ïIËûÙE,Þ<˜y÷µ²—±„%c§x¥’Vhœ†Šuéþ?;r¾èꨗŽâ›SÉw?·öNu-¡ ~SL/=­Œ¨mbrsG¡QÌDÕ‰DZávÆ,½œ5B±®-^Û}wû3®¯#«[Eã&®ˆ/Ûþ;*¾ni.0o-¾½–]¾Ž›§öÏôœ –]Î,y_䦉M/º b+ºÌ‹-¶Ï’Åâž–­—½#ã!ïϹ9qbÅ O¡Õ8Šnó#‹”3KD¬-êUÐ (»£˜~ð¶Èɾ"f·³û§ÄÌË%”ýO.!!!"$$D”ÊxM¬ýtªXzÎðTqgÿñÞD’*„PSġ߳÷& Sé)KÿSO¶­ýk „bäÈ‘w—¢—å÷¢¯ÿú6ýKûq\A\פ ΫQ÷ùÄîqa…‚8Ѻ n4/ˆ#µqeaŒY"yaû¤)4zu$m,iÇrÉ(ntìÒs5–ÛÏa,\W’Í;Á¹]›”Ò+¢ØÓlHªéT²2ó0»Û9c†ÿA´±‚¼‹g /HØxz¿†äbÙú->ëç‡yÁÚ×gxsD}tyaìÜ—XÂñ=BUA¨d¦¤Ý7·LÑhþÞœ7›: Ø35‰+áwò‡]…@¹©¤ä-•m)ØƳ‹ùùP&ººø~rK\Ь«èÌÐÆ3‹˜w4 ]±Ìz» Ž……· bÐ7ÒÕÁDäÒylOlÛе­="u/[Þ»%Ú¾m×LØ´ìÊÓÈÞÃüÅᨎ=™úAG\ ó5ó£ÿƒÐdrhÇ!²ø‹GZ}Ž*Z×/]-"W‰[õëoƒÇÀ¯øº¯?¥ö£=Ñ2šˆ\>©Zj¿ú£jvG*84}WŸ²Âµ‹WKŸ?bæÞ–©¿N¥¥•‘ð…?±33;£–ÿÌ–d¿á_óq·»=™fž]™>ãy<•DÖý¼šøÇ1’¬¸ÒîÝŬ_>ƒ^~ëõ´ iïxi²ØñÑ+ÌÜwT½‘œÔx.\Çþ AóÐFWæ¿ÄØå 8vü„Ïx9kñêñ‡¯^äðŽMlÙÂÕóx£‘-©[>æ³}¹Tó1Ãü4€Ìä$Òòžäº “IOØoï2iÒ$Þy—Œ™ÿe*ßíŠÈ±[¿bò¤ILšòGR '>d-ó¿ù„iï¾ÏŒVs2!ÿH©gYÿÓ7Lÿà]¦L™ÆŒÕ—îŸ"IÒÿ9¬ÿP¬¨aû[$ª¼{èRëœ:ÍÉp#Ýêë9sòzÅíQüÆtÛíif½€5¡'8k| Oó“}ìlŸþ˜_ß¹ÌÀÏ7ñN§¼S|3\KºöÏ#|Ñ0º½±ƒì:Xýë‹–p¬jì|©ßÚ·ÈÛNñÝ+¹åÞŸÙê‘q`ƒ^ý†m×2P-=iþâÌû¬7Odû-¨óüdúU× Zm'J]_‹÷3ãÕÚ ä\ÞÀ¢ñÔè7’þÞ®lûuËöá5¾#îYñDÄZÒxä[4s‚\“%¥}Ò%éï(>4/‡óË'œþK¬­D¢þn¯£Æ«;Ï5ÌÁÃXy5j¨„®ßH¤¨ÈàçZcSj‚ ˜›c®€ÈÓ£¿¯ËÓ ÅTÐ#«’‘–ŠJJÈrf…””š†œ¬l„H#2ì4§3ï%¨qªIšmåWXµÓ‘éSg°àO±roÕx–qŸ}Λ¼ËÖ˜èÃYüro^_y•¬ºiÍïõÜ*Žtøf'ëý§ñÁìÕüøæ~|Ï›V/LãˆPǾ¤ˆX%-%  NÎŽPIKIGEƒ£S ë(pvÔ ¨i¤¤©€Ž ž£}…µ¬Ý¼Ž3ZÓJÉÆõam;ð\g@ÍH']SüAά_j¥/º“â‡äg_k3Ö äÝk»ømæt¾X¸†û\ uËaf´°áÞFŠï*Y0ž×WF`Ñôuæ~1†öµ½°é\;€Æï½u3/ž~ãž~m:!k~âËÏf³qîh:G™8ºf8¾D–…Û©’’œ‚ %4ÒìKYG¤’”¢"{*ÀJ…Ž èâšå›X{ü3š{¬gí#º «sþ:{ì5`ÞôKÎí[BÙÊÎxî;ÆLÝN¼sÞÿþ^hU7k#)'gЭã\.\Q±ÄÊJƒ"2HKtÐ÷8ËøÅ{{3ÃÖ_åÇ=-5#?(Õ8PÁ^“Ÿ¶ƒŠH%)ÅÅ/ƒ ɤdkÊt…TøYÌŸ:ò÷hp¬Þ‰áÕ;ü-Hü½?c³Á¾û34¿¯›×DìÊQt»äjcX¾ñs:T,[%ˆ”Í|<ã¹5'ñÑP²Ö½Í¶O/ÍØ6–л)‡êñþºMD¼^ÊÿÀüŠšEZº¬¸ßø ¬È9BTÀ7[ÈVH’¤ÈÓ¿FÁíÙ!tž²ƒµ«Vr¼i&k"' ¡Åc}¨¶‚s:xk÷r}Ï^"§Õ'øo5^ vAxeöÓt­Ùúö³jÕ)>nÑ3­Væ "#Ì~#O…‘£¸ÐoÊômTÀ8âåfÿðÉÏæn40]Z1¡i7æî]Éö[ÃéQ<4¿·Ñ{wnlLŽn…Šõâ«ÛGä¾=\56¦z‘uDÒ>v‡êQìëѰJa¤cOû¡½ñ^þ3k—íf€÷JÎ+2ppç»óUµþu©e¯áìÙ=ì¿ýCÝþú,Ü̳¡„ëµý€·ºÖ.ø€šãäQëû’µ¤J´ë¯pôÀYô•Úiø8Ëø sjÖ­†nU(‡öœ!¯Cþjx*Rrè¼Ŷuƒu€†º kaþÇ í !§sËû†ö³ŽîæH6X¶nDí²§ŠÖV ˆLÒ_ÿ±LšÕÈóµÌ1œú†q_'ù¾ûRT²2ýý‡¦´dÒŒE_Ñ`co‡UUó{Jµ>új1ÝÜǶ3÷ž´jÈÊBg'‘ÎÅ3×)ñQ–…D6))¹÷÷¾Z8PÁZ¡¢ª%fõ1¸ŽưY¼úÉ>‹–W1˜@Wï^nbñÌ,&|}’Ô¤r¯³â­Ùœ¦ÅÐH:9Ü{«eó¡ ¬¬áÖÚ÷xeÑyTŸ¾ }ªÈ¤`ë¶ íï‹&sÿ…ó÷—Ϙ•Yúöaå섵F%álñÆÒÖÔQ«oêZ˜¸:o_L»·¿²£¹^üͱŒÒØg­íL\›÷&슿ÿì"‡¬†ôE^g/¦Þ»©&/ŠÕ“>bcª‚dzƒhg ÁÀH:9 ¢¾Å»)<| Ûyï¥Üyvd<Ër6SìðówFc¼ÌŽm÷n¼ÉË"ÛˆÛì1Œ^ße}L¿ƒWÍ$bûW j7Œ¥Ñ:ªŽšÍÔÖEF?D ›¦Ïà`¦#æÎ¢O¥²Gjä¦Î½„M»)¼Û± œ<=±2EqñJÁíkÆ«\ºj@ëî‰Û¿˜š™›!R㹑nD(68;[’tå,‘Éé¤&ÜàN®%Uê×Àº…­gbIÊÈ$#%˜[™%Ï)„­_ÄŠ£71b"áø*­ %I}ô~~_¼¥Èóˆ%©ìEyl‹ôdÉžÓ• mG ¦òâ/¸pQÁ¦Ý‡ ©úZ]m&ÌyÝÝßgïûOüS-ê×ðÂNÉæväy®ø~Äù /<'²$FÎ~×öóUê5¬†—ƒŽ¼ÛW8vø,iº=—ÿ¬OtÕéÕ§._pŠo:×e_£*ØçÄr.ª?^ü…ŽýSox¿µ6´ a€•Ì+ÇïfÒ72¦êXBƒQÇ¿"Ö"˜Ðÿ®RáéÞ<ãñ(DW‹ sÞg÷wÙÿyª-®IýšÞØš’‰º˜ÄS‹NóMë Fýð9{;M`ëOS}i#ú›“xîa7s±k0‘Ÿ¦¶ÄºhºfuyaxcfN:Âù43êNN³ûn·¦ÕÔ{¨/3×¥éÞ¯¨S· n6* ׸Ý„¯-äÙ2ôˆ[´Ì  ÌÙöêüFóz>8˜)“Îr©X¼©­ö*ß¾½‰nŸáãvÕYѰU*dpåØ Â“ ˆû>ݯŒ%Ñøç»Ï·Óqܾí^¥ÕêQÛ× ]^1/a=fû'Ý?RdàýAÌ«Ýꮂ„³'8Ÿ‹yà¾ý ãÝ›¼·þ|={'ç^\Ƭ5ÙP¿•íÓ¹|â41™f›ÃŒgÊ8ÍÅœÆýúøËlŽ¾×šúëào™NÄÙTº¯>Íg>«™1}9ûõ:b«õ£ë´žs `ºÌÒ·?gsD áa¡\LÈF˜yÐúíŸX0­Ý½§@dígí¶;=W—¼ÁË¿KK±¡Åد^ü™¨"™}ÎA}m&<˜J‡½m»¡ôõ^Ãâñ©páilÎþΜ‹v´ÝóñN×(»êÍi²–ÕkB Öˆ ¶]©óû~µl+ÑvðHÚÕìÉðÜ­lÞµoWf#ÌìqoÚÑmìèYÜŽŽ"Ê”ŽQ¸‘Id”=™B`‘zƒ¨¨ *å°“‚Tv¹¹¹^I*7dpú/3«7ŒMfñöQ[º¿Ü¯'Ô¸X7x‹M‡kñÃß³|g'wŸÃdQ·€štjáûÈ÷ÛT}šö[9zhG²L˜Û»Ü|(¯½þ.ãÚ~‹†Žo,ãwýD>\¸—³ö¡sô¡VëÊØå Ìë¼ÍÚÍö|4}[Nîgý)#:+{œÜ<©ß¶9í«Û£ÂèI“î ‰ØwšgÒÈS¬qö©N·ñ/óÎ;ƒñ/eYÕ{uGjñÓ³ø}{§ö^BØTįzs¬óç,šWy‰å™ûÙ×,Þr‚ý{ØVªMß)¯ñÎ뽩þ@£§ÁÐË<óÙQ6èÛðÒÐo:QŸâ³Ýi4ó æ®ÞKØÑ]œÁGÏ`êunŒG©½ Eصá³Í«qûðK–î:ÃîµÇQͬ±sv£RãvTiXdÈ܆FS6±+às>™óNï&ÊÜ•ªÍ‡Ò/i9+CÍŠ¦ÉÝòÔàÙ×jðláßÎõè3¶}îOÏF½x¹Q¯óðz†7>)ò·Æ›öãÞ§}ÁŸÁϾÍ…ÿ«3ˆ)uþúæH’ôßPÚ¥§RägáÃø >˜_K~€[¸˜YÌ‹,õrssg>òK’TœHbq¯@FíiÌ·—¶1úI]ñüU†ƒ¼Q½s³žgMÔÏtú§î°—îsþüy4hð/—DúÿâÔ©SÔ¬Yó_.‰ô¤XZZŽB}‘ÅPd1YL‹Z°ˆ"?ï~áqIù”³VK’¤²3m(öš åèL~ÚŸ‡®jkZ¸É¸$I’ôß"‡õ%é¿*w7¯WÊ.—jWòÀÅ2bÎrüT$iÚ F|4ššò.I’$ýÇȦK’þ£D®ÕÛ7æü±sœÞwŠ,“v}©Ñóu†Ž“!œÿÞWËJ’$IÒ¿@Î9•$IúT8çT’79çôÿ/9çT’$I’$IúŸ#‡õ%I’þÉÞ-I’Ê+Ùs*I’$I’$•28•$I’$I’Ê œJ’$I’$Iå† N%I’$I’¤rC§’$I’$IR¹!ƒSI’$I’$©ÜÁ©$I’$I’TnÈàT’þ?3Ý"tËzŽÆšþí’H’$IR™ÈàT’Ê+5‹„ð‹D§•øíneK"þÛv_'ÇêO~ÔCÞ’$I’ôWÈoˆ’þy‰8zø$a#IÈð¡×´4¶Vú5í"æ/f¿¾5'uÁ«xlfJ$tÓv'>]`ëU›ŽúÑÒÛâÉnÊŸ ÞØÇ‚ŸÂi2¹¾)AfLIö¾ø9>|_ýÕ¼ÕŒëÙ¾ƒÃa×IÌÖâÔœ^ƒºQÃá^^ú„6¯ÝΉkɨA´8”NÁ6(²ãN²kûN‡Ç“޾ º3¨w*jóËžu}?ë7â|TyæÎ5ëÅ î5px覘H>¿5›pùV6>Íè=¬µ+(ðÈüþdz¦pV~0‡CŃwß~‹Îž%_ ˜’ϳyñ2NºáýUóOÀ"¸cظ?Œë·²ÑTð£a·<× "%M¤^bûºm¿GªÑ·jmé; =6ù;FM»Âîõ›9|áéª-•êweP߯¸•ñlÿð:¤™Ë‡+.a(Üt'Þz›.^šGÔÁ£êH’$)Ÿ N¥òMä³o ‹ö$ãY¿Íz´ b<¬Þ˜neÙª3¤eçaS9·’âEÅd@»AíqÖ&qjýrÖ­÷§Ö+-K ~þI=°¼ÇHldZŸfx=4û«y RÏìâhºmú·ÇUǾëù}s 搜9`J<̯³×p;¨+ƒÆ¸“°s [Ö¤Î[ðT²¹¼w?7›Òë%/¬’ŽñÇʬó«ÂKmQÅŽ z2¢‡ y×÷°rÃJvVŸFŸ C6ÒB—2kÉUÜÚ÷aT@&‡—­fõ®ZÔèŒV<:¿?•žâI›—^§qa€fˆçÈÊ5\«Ô&îp"/‘ ‡w²m×Ib³tÔh^é^à©h0 jt~.*7¯fÕÊÍ×F]Ë’6ՀγýÚ¸a‘q‘­Ë·ðÇÁš¼ÕÉM^~˜G¨c{z¿ú<sÃX³`«¼ƒyµ•c Ûy¿RëLKÄ•*4΋mÜó‡Þ *¸j½ÏùI’¤|28}ì Ü9·‹M;OpùFFKÚ¼8î:rogÓú=„F&‘géAý®ƒéÛÔô½3™~$ñ“»ã£D6§|Äj‹ÁL{¾&æÙÑÙ´‘}aQ$é­ñiÒ‡ž«£Fºš•».w;•,“#mFO¦W°à ìÛ±—K±ÜÉ4aáÚ€¾¯ ž½"›Ø;Ø~(Œˆø4ôfŽøµĨΘ#È‹?ÅÖ »¹–H®¹u¬ÀL IDAT»e@S·51„Õkör1ú©y:œšÐc`ê8 ™?…U–ÃùtpMt€H9ÀìOvã1z}ƒ ›¡\Bæ½ËÛ—øx`µüÆÉÎÊOæ“Úe*/7±&ñàB~¿Z•¡“Ûâ[JOi!câ1–.»FÍ͹4ó:V>%äwvr/øÃM•í¾ªÇP†YÑÙ²™a‘ÜÉÑ`W£ã†7Aœ)¡.³¹ºo›'6U`Ô†/t¦rA/×ÃëÉHLd†¤+|÷ú>ÐV¢Û¤7éè®’t~'ë·ãJ|&§jt2„§|ò{|Ev'wíàЙkħæ`PÁ¿§/æ%mGn<¡{vrðôâ’³ÁÚ“–CÆólå!y ºœZ¾ÄÄVš‚€Çœàm\HOÇ ÀœޝÞH”WO&¾Ð  TNdÛÒ›Ü2‚§¹ õO¤¾RP§ªïy\üc¿„ÙÑ¢Ë`º»$±wÉz6iÀ\É‹ØúðzRˆŠÖS©ãk j`‡¢XRÁM!%ä7f-‹Âã©. ëeGôÖÅlÞ|š†£›a›~–åß/æ¬E}Úu{^æ—Xµà¾~_éçùcÎ"NéjÓ®Ó`ºƲß#1³Õz£„¼Kè~V4wÓ¹\¼nÀ£©V ¨qGØÕŽæã›ãRðVN‡FQ)Œå^© ñ—Ou 8À©ØDx•ܤëœÜ¼·–Œ|H¯iÊÉ}„QŸŸòƬàUNŠr·Œe˯ìéå3qëÀo¬¼êK߉­p/ùJø°‹ÙGç²ÃÖ—ö§ª'-ö Û7†aÙx5¬@%;-“…?–Ú¢9 LÙ‰„^Ͼ[~´íï•ĸ˜¼:áoYd=£@1×<²×T½ñè:˹Êe­ý]þì>û3ûT’¤ÿu28}ŒDÆ)ÖoˆÀýÙ7ÙÚõ^%R8²~;7{3i@c*( ÜZplÿ:âM­,Ñäå#Tn?ÈǦ¼¬#a÷jv§Ö`ð¤Ô³SÙ)¸j/¢7ÆX¢â´ø÷mL°oAk”Ϊ%{Ð7ÍëÏa­¹t.­O*ii!«Xæ@·ñ£hëaúsœH7§’Ÿ‘ÊÑu[ˆ©Ô“·_h‰³ O,çÜ›_vW±Ñ XWîFãÊ~hñÅ%õ§7G¯6ÄÖÎ’2È`AÎD`YóEÍŠ¦¡`ie‰š”Mži„ C[5mTbvà†KužiÛg3RBÖ°tÅZ|+¿L»bÍ™á»mÁÐþžòÒ‘v0Š§Ú£rcë—|½í&&3†L``ýG {šˆÝµ’ƒ†FŒ|³/UŠöä.>P†ëkù阠ÙèQôªl‰"ní´ãzä]eÓCë Dz4QÉöÖij ‡Jäœãu°ë0¼1CÅÚÇ–QŒ"ƒÓ«WpÆ®ãF·ÇËôg“læC€g±€Ndºz!Ö7¦^æ‚쓇H·òÃßUƒÈx0ïÒˆÜXö/\BˆÕSŒj톕›çÎsÇ­ +¾_‘œ‚Z!ÇbIO°lÁô^ ½‘SRn( ?XDh¶À*¨ c^눯9 ÈÍ3\Ðh0³Èá¹ljw¦òÝ)ÃzR’31wr¦ø¡R,?aÈ#רæÿS£ÃÂ<«Ìé™nígù–$jN½RæƒhÌÌ1×cÐø6y`š…)ü>úñ ©ªžmF0¾gµ‚|̨Ò÷>¿o‡§pð‡OYn»šô;Š–îZ@ÅÒÚ 5á:×ÒQÛÎHzÜ)ÎŪ85/~l›Ðçè)|†ƒÖÜœ;¬3ñ ™T0gÉ´)lªÚŠ^ýºPÃQ"­ô}FGÿDI’ô¿M§ ëÂ).ëê2¢©ë}={"õ<§"¬h0ªþ½¡+“æXX€ÆÜcÙz¦kì?|‡*[àI<[CnâÔ¤µíT²¯qbóv®ØÔ`„õV$Ñy4õ¹wOÎ…C„èkóü3AX+€z‹Èè\<ùa)îpìÐel[ާ•G~”¨ÞŠ&ÖèI‹JfˆÔ³œ¼jFÝq&—¤ë¡ìØŠZµÅÇ…M7‰Šøtõ¾Ûó$Tf昡¡‚SK!MÇ”S°¥Þ˜àbÃË ¶v(qÙd–7°?ÂÖ½‚0W¸t)ŸÖÝiS7ØÕǾ1‡íæF¢ vE÷p›ã¨s7ި뀂ž˜È›X´Çã¡7©+¸4ÆëAqœÛµŽ½»OЦN'*•‹™b8šL¥Ví¨\lŠÁƒua$2ä4©•ž¦m°ÆŒ›\>¼ž½qÎ4îåNޅů' 1‘Äé|hQ$°Ì½t’sú*ômå…NŸF̹=¬9šF¥ŽU°O åðE šŒi‹—9€J|d4¯6Tºï‚Dj(‡/XÐdÌSw׋ŒAøtÃGWrÞ“ŒU ×^¡=£^iG€eþ´‘˜èÛXûÜ›ï+rˆº™ÏÓÜ›`$)l —ŸBÛìEÆv«Æ}×fUè>n< ¯eó¦Cì½ÜœêÙ’²€÷—žG/@±¨ÍÐiM‰ŽSðmQd ‡!†ë1P©³w‘ÏbIù¸¸êC~:–‰´A½™:Ú¹lé‰$Ž®ÞARÍA¼TÝæÑ½j<×£ôx6÷¥x¾R©£'Ô$úÔVÖŸØÃ™VUhUñ!¯bKÝ~¯ãyãû7ngïá(õ©Œ<šu¡é™%ü:í8 ÃÖΜ¬Ì ´ v½¯‡XÜÞÏœéëˆ4Š=ÍǼ…ß#ëÌŒ€N¯1©“@Ÿt- ³p‰=ï¼Ögc\éûÌSÆ:’$I’Áéc¤r;!áQ ¯bÁ€šG<Þ4÷¹·» ±QÜÔxÒÐUƒ’S{"IËP¹}i!-xµ¾=JÞnÜ1qçÀÞÙ—‡Acw•†øÚ+¨Wâˆ7f³è=Bòô`ëNp^ŒéÚð›„DJ4Ñ™nÔô±Ìo”E.ׯŢõjŒ»FÁÜÅ‹ô;ÜÑÑ=B¬[3úû?Üì!=• S:Q[’Û`Í]5`L"1Y‹­å½F_oÀ€VÅn†wްñ@z‹ßóLXºP«ã(ºµqC\ØÁ ­Oc"•¨èTýý¨;S\4qZ?Ú®«Þ&*:÷º~X)*7È»$*©ç×2oɬZgb§*Øß jRIN8Þ›N 2ÎrECµ("‹kÛ~eÁ=õ¼Á³uœ<i­qöôÇÙÓ CÄ», ‹ÀX¯.fÁ]xe|ûüuÌð4^äˆÁo‡{)䆟â\®?ÏT·+8>–Ÿï§^f|Ó‚~X[w*dŸ"åQé!È:·…íÑ><3©V™zþDZQÉðó+~ü€ÆÊ_G<¼µÄž™ËÙ+™´¬hÿ€× »Š^ØUôÄ%ã2í:Kl¯ÊiAq¨E¿IÓ99 ƒ™†¨µßñûf4ó+þÙÖ¦÷X?ŒŠΞ™^÷ˆ:»KÁܹ&ÚsðLà”•Rê>ãÿ—§’$%ƒÓÇHFCþ ¿UŸ‡^Ñ¡-l¼ÕdNì ÃT¥Õl4æqÖ¦p'ê$w%SwÐËTÒÈ1`PœhöÂX:ùYcmc‰îîYÜPâØú<=B£¹Û¡ÞºA^4öÐ"r2ÉRuT¼<›ˆ‹¾¶Rs<´ ŒŒVµé÷ÖsT³¶ÆÆÊì¡ÂÍ‹&ÁÆ—n.ùwéæFîdû9u†ÕÀJܽpûˆ»Êùc™TëÙ”obV°pqÁ63‰ç¶²72€.oWÍïU*ªÉˆ>Ç@þa*H¾pž¶þt-Ö£¤Ø7äùwª£ÎÙM=ÎâŸ/<âEžò¯øˆ‡ù roß&[ìuÕŒÅç8”pW¼Ðc0jðn7†—Z¹amc…öÞEDiõzî$¦báæ^ä&Á`DÜ7×ÇÞÊkó{[–§ÏÃH‘Ç`™ˆ‹×á]xGu¦¼<ŒŠåÝy„¸‘h‰—· Œ%äýà>˾²Ž¹¿]»ÿÖw.Ö󥢪`4 ‹·mçŠ}3^«a•ß³½ógæ³¥Û¸Q´ô(5 5™ÛI&lülÑZGo‹”&Y-¨—‚ 1ìÙ|¥Þp8(Pj~Zì=ü±çϤ¨‰Ù†yÓWhâT¶ÐJ}83_Ú”òè‘q›Û9:ll-ʰ¸s;l‚ïŽsìœl‰Þõ œwàé×Ú<ðÙS¬\ð (2qT}D=°5܈¹…p¨›Q¢>bŸ¥”aŸJ’$Áéc£Å#(«ƒÙ°ß…Ö¾d&ÜBW¥UݽpW÷rtï%œª@ô¡õlˆõ¥û„úùØ:Üœ²Ù»væUúñVµ‚ÆÀÆ?çtŽ !Ø"GM6·oš¨Ô¢6î$‡G3Ÿ"½\=Ý ô({.¸SKËÑ!¤ëjaiŠÆ'ódÎ:IM­;¹QÇÙ–ƒg§‚4¼üñ1mçøápjUÄ,7™9.4©_‰¼3KùfÅ-š½ö:½7£b0:6FĆsúÚ ví:ƒ©ñ0z6döÞxÛ§pjÕrl[0¾^ɽ@7\Ôãl^®Á»ë›÷*'¾Þ°íÈ^.yÖÅâæQÖn‹' Û@‚´qìøf‡+`Òà:X›ÛQÑÕînš†Ä4R,}©ìF3AÆ™%÷ÊîÇÑíWÑùûâd¦'5îûw†¢­;ŒÚö /òû´_Hê<•×ZÝßÃ¥Xä™Ëîm›ÒÔÁÉ”DL–+-ë+Ö…Æ__[vœ9D˜O ülU2nÝFW¹)ÕœJ¯' @^Ü%Î^Õ¢KÊÄ®N]Üý}0?q’\ièe…1%žT»Z4 ´ÃÌÝ‹Šúìß}ûj ±G6–cNC«!3wO\òŽpøÀìý \Û¿‰óKZY=,ïzøí­6\gÛÊCèë>O=‰ññùUfëBE;3Pœñt×±çìaÎÖ ïü6œ0§Õ¨ø™¿•;’îÙ‹@’È»kçŠ8˜CúùÝNu!Àò¹v|{oyÑq ‰Ã¿Š½;îÖ™\:~œ+ª9W÷®goN=^èV=ÿæ¬Ró{ð¨|Tzúkû9ïMÛ¡þ˜=BIL܈ŒÆèõ4>…o9\=¸‡›¶x;hÉIŠâôž]DThÆèj Æ±ã»{ǹåíSì8cÀËß+c:ñ—±ó`6Á}⮑“D\B) ‘\8q˜S·iýâH:û>"ø‡GÖ™H¿ÀÞc¸úWÄÂÆ‹‡Øu8“ ÞMðÒ”aŸ•aŸJ’$’ÁécdUóY†v\Îê ˜£ÃÞ»6]ƒš£qkM¿qü¾}>ßn7ÇÙ¿>}Çö ±[AS«qÁËÓ‚¼¬ê ì]û“µÆ›vC{“ºb;¿ÿ°Õ¢•ÛÒ¿yþ㌢“íñ½oˆPÁ©I/z\ûm fr°B MZ¶¤fH&†v5èÒ·‹×þÁœ0K*zÚ‘•çDcÿ‚4œ›3``"Ë7­æ§=zÌìÝhþ è33ÈÕÚ`mˆT¢£S1Emç‡Y8xR·×xži\‰»Z/‚ü-ØZC“þOSé!Gšb뉗½ž8×¾ hY¤—S©H‹~Ï·t+¿|µ3'?êv…Í+¢1†“‘)°ð³Bû@æ’{ƒGK¼ ò,Zv‘™Æ­èœÚ¿™ ƒgo*·z‘¡íka§€z3‚H½7ͪ–LkÜhû|’–oaåûQ­]hÕŸ¦™ %Ô…•»½@ç«Øµè{²°ÂÉ»&Ï4-C=™Sµu;üÛÍâŽbëÕˆ>ÕëáW·7Cn.cíÆ_8š«Á¦¢/ ºÕ  ñlMŸg¢Y¶w>³ö9ؤ­j†¢×ëÜßó¥ñjCŸŽÑ,Ûþ35.TmÑœ•à Ï”œ7V÷Þo¼v‚SwŒdÜ^Ä—G ëËŒê?dTS3ÅšÚ]{QoáF–Î=g-:ŒéÅSV(¨Ä†œ$NŸNÌÊo+LTëKɯÓÞÕDZr,÷îfWj.˜Ûãê_‹^¯u¦ùÃzuÁtèÝœ«×07Ì·ª-xqì3Ô°Ï¿Û=®ÔüJˆŒJM@ÏÕ“adùu¢®K#+‘LTT:ÎÁ~÷>ß"ƒ”<¿—¤,3ÛŠøTïĨ®m¶ ÙEŽsAnÚbÏaïö4ôŠ<iøüXžiàŒ‚Jâ‘ÅÌÚq[—JT®Ù±/6À×®Œß Vj>)ëaØ^·£geš OÇúŸÙGí³GîSI’¤{J;3(E~*äÕiáO  -XtE³"‹y‘¥^nnîÌ'P~é/¤Ÿ˜Çô-N¼4µ%>¡çaòΰðýµØŒ(úÌÒbÔdŽÍÿš­Vý˜8¸Åo®/Ÿ \[ý R»0éņÈvS’$I’î±´´„ú"‹¡Èb,²˜ µ`E~Î+ñ;²eÏéÿ ‘Æù}'ÉvõÃÍNCzÔq¶m‰À½cþä­²¦›QÄ=èXü+E*±WSÚT.ïÝÄž´&¼<¤ö$0‘s™ã—éðR=˜J’$IÒ¿D§ÿ+ ©Ü¾~†;¶‘’«`[Ñ—š]FÓ­Õƒ7Ì”NMºs^Å&‹©IçØ¸`žJµÛ2r@kþCÊ«Z šR󾇵K’$I’ôÏ’Ãú’$I’$IÒ#ýSÃú®ÓL’$I’$I’ž œJ’$I’$Iå† N%I’$I’¤rC§’$I’$IR¹!ƒSI’$I’$©ÜÁ©$I’$I’TnÈàT’$I’$I*7dp*I’$I’$•28•$I’$I’Ê œJ’$I’$Iå†îŸÊèüùóÿTV’$I’$IÒÔ?œ6hÐàŸÊJ’$I’$Iú’Ãú’$I’$IR¹!ƒSI’$I’$©ÜÁ©$I’$I’TnÈàT’$I’$I*7dp*I’$I’$•28•$I’$I’Ê œJ’$I’$Iå† N%I’$I’¤rC§’$I’$IR¹!ƒSI’$I’$©ÜÁ©$I’$I’TnÈàT’$I’$I*7dp*I’$I’$•28•$I’$I’Ê œJ’$I’$Iå†îß.€$I’$Iå_JJÊ¿] pttü·‹ðÄÉžSI’$I’$©ÜÁ©$I’$I’TnÈàT’$I’$I*7dp*I’$I’$•28ýé[x£uO¾;k,Û2¶2±U#6lHÃFÍymm2âÉQ’¤ÿ4=G>îÈÓÓö“÷sÉØò&­{|Ù2žÊþßyBçæ?ÝF”@ä#:¶{ŸOòŠ0pçz$I†»å“ Nf=G“†Mòk$*‚ôÑ¥q#š]Gò_< ™"/A •ýÊøp»Î|uð$'·O¡‰…'U«8 üµ¬%éOQoþÎK-‡² RýWÓøG˜.2§O+Æ®ûpñ'rÐz·¥_ûj˜=±LT2-éÔ§5Ú'–Iùö„Îͺx€ÊÍðkdûU!ðÉÒ]yDoú„q¯Œaì§[‰ÕÿÛå)dpú¸ˆ4®„'âäíFRì ŒúK,ý% 3<«TÆá/…)W¯‘äLÍŸ{§áê%®é*SÍïµ%þi¹—Îa[•êÞýÔò8Òø'ˆäK\Hð zÕÿŠM†Oatk×'Ø(hðxú¦ mˆÝ~‡ý=÷Üü×Ûˆ{r‰¸ƒ]P®åûc÷ÿ@Q›>eòÏadi´dù‘w¦Ëµ8ùœÓÇÅxË×4TëÒšÄ 1ÜQUØð#G«·£úUýШ)œY1›9+öq1ш}`k†MžL¿ê6 \.QÛàËŸ7vK‹Ç1tS#0 èC%- ?ÂG]ßÃðî>nk @ÆÖ‰tãÆwëÞ¢þÝU¹yù Yþí ¶,¡¼9×ÙþÓll !&ËŠJŸcüä‘4¯(ÏLRI Ü<ð 3çoæäµ;äY:á[³7oÍx‘zÖFNÕ“ÑË(ìë|µÙ@ƒKïïÙ8¹1fÒC—ñÝ‚-„\ŽáN¦§ÊmxaÒÛô©fƒBYÒD:×Îáû¥» ‹×cØ’ç'Nâù:(ú#|Ôí#n6¨Jâñ³hŸz‘úM,ÚŸJðË3™5¤J~8ö ǧÓý;ô~Å•e»¸x[‹ç‰|þN<4³ç]:OÚNfAwé‰ÁMøÐ½ÄoKGSY ¨wY2‹¹kp)!3/ªuǧc[áôOf"Íovc¾±%®‘G‰pìΈ¶q¬ú-c£‰ü0£ÞP¯ÿÊ ƒ~à’0oÌäõsè]±hK«÷¬.²æûïY¾ïº/_¥mÒfÖŸˆ"Û±!Ã?ú”!µ ΋úXöÌý’Ö‡’hY…&ñó?pŸ¹†7ë=¦f¬´c Á-o3p¶S–ÈS ¢Y3i4ß§õaöìÔ°d†oeþÜ%ì8EºÖÿz5i4-ïF}¥œ›Ët~D>¿8ôË—|¿úq&Z @ÅkFüþý Àx›Ó«²dëqÂo°q ¢ásc×- ‘ÆùuóùuÝaÂo±õkÆ€ñãy¶Š5Š1”™C>E?ao5)ìÂÕsáçÑL¹Ø…ï¿éC% ÍÞEóY¶ç qY–xÕëÆÈ Chä\¸;¦ñüRWÆ ·fÿï;‹Í@ëÑ÷~|…yDlý‰E[Ns%ö6éª-¾ {òÊøÔ.ìEÒßäà¢ïY°íIAtÞŒÛßoÀõ“…¼R+/ޱlÞol ‰$MëJöÙðreáºõ¬˜; <°µÐQâ&NœÜÅ´æVTò+ÇBB 9Á¶Â •[73ñï2Ž/ç¯bíŠY¼èÊ·ÓW?z_†4D6gçŽåÍÕÚ¾9‡eËæíF ü:y&G²AçZ¦®­^哹±yqí>å‹.œÛŠÛ*e8öUn]½FZî.é»ðÙŠlþº™›~fÍeVOʾ“'X=¶–5DZæD!!!_^˜¢»r ÿÈ¥ý”y¬]ÿ¿LAS_lþ©–ÇIx„óJ]˜4µ.ákØ–;¯>ì„rt?çròWÓ¼ÈoÇN²çƒVX¹èxK¯w@$³ãÓ ü|£ãç¬dÚE|õZ;*;Zßkduõxsà N¬O- _‚ŠEdÆH¯ë¹q>– ×æ±qÇ2Æx†2oÑ2 òØ÷Ù«|tÌaß-eÉôŽÜž÷‡LTxL£B8¶@Á¥ãËôsÞǼáÔ$|þ³âžâ“/_,L!ûì<^õç¼òÉ‚?Xõë' 4Cß9øáçæGŸßÝF<òYœš5ŽÉ›µôødË~MÅmß±*Ö…ÊAs!Òõ' IDAT@MdÏç¯óá¶\šŒšÎÏ¿Îeúè8¨ÆüÎò¸¼x2ï®J¦Ñ+_1þ ö¼ÀÏ_,çª Ðzâã‘ÇØ;w§ÊˆÛ»ù}›vCºä¦†(Ö~0™ßkò‡?0ö$ž6má³™»‹L3…!ë89ÐsÚϬZ·„/F·ÂÛ )ħV¤åÉ|õÓB~ýz Õâ–òÅÒóù£ž"…Ã3'ñuˆ+>ù‘9ï>EÒ’Å7ù䛿%ê­Ý|þÖ.UêÔYóùñƒç°?ú5_¬£´ IŠG>øõ[Æ4vBhœšðÚ·¿0µ« L‹’=§IÞµËDY3"ȇ,óõl›s‘›]>¢yÒ|f[…*Ô¸µÌZšÆ3_üÈð¦¶(@¾møuìy.& ª;^fù»°ê?Ÿwž­Ž9à=¨=K¶¯!(Ø s=œë@‚Ý Îj"kWq«tÿP™ñ:—®ª *VÉ*±«g°À8„ùÓâ£ðcpߦ,ý4ŒcWœåœ#é>‚” çˆujÂÛÏÔÆÇpuÅ»r±ÕL×¹r]! “ '-Á]G|÷o7º<]ƒï¾K"UåÞer)iè/-ä³õnŒ[2•Î.ù{¥a½X·j gcMÔ¿NœëÓLì„ç+t•›3 µ?æ7,ÐÚWÀNS–cßÀõð(ÌcòÀÚ8i@â®9Y¬4yD\‰Á:0÷.ñ\ »„®ö‡tkä-àêêI`ý²ïñ¿K¤Fp59˜.ýšãGæõè5 !¾ Ç1·ú?öî;®ªòàøçÞ —%.PP”=TÌ™#WŽJ-÷Ö©©e¹S+Í–£ÌUš¹Wæž?·åȽ÷b â÷Þïï. Š¢†Jù¼_¯óâ÷ÜsžsîsŸç{ž…|úŒ{ ¯z<à>Áçn¼ÈÉÓ ø|Ø‚ê>h€BΔÉ"M©¡A„[{áã’ù†™®œXŒfŸõ§®«(‚—[ˆ zv!“·9ÒmþPy耢´«ñ;»úâ'gªóìòVu?XøÐ®×[¬ùv2?ßºÃÆýÞ ž1jéMá†@þ0Ÿ¸úX8  iI+J‘Þe3Ÿì‘e3$gW¾²«#²Ïã"×2muõüš•íÒ^o\‚•“íÿÁ˜U!n÷L~;\„~ý’æ.æÌäÔ¯ô=®ndæªÛÔñí*¦µŠ7hZÅ_\ Vðu(ˆKQ+®]¾Š‘"X̹UË9çÛ–¾lW×Of‘¡5“¾lNÚ)ŠÓªIEVLý+å¹ÿ„$&LX`iÆ ì¸ìAƒ†%¸Wwˆ žøyYF.cðh…Wú'g %(T‡g72Ö+r+€€è"”ö}àIØÄúÕ'‰ =F‹*ã3]…έ yT[ºò +Ô¢ì´I ìp‘ºõjP£n}Þ,Q Sž3Ý &8¶(å½³(fQ^<¹ëöq.â& ©&DÀ²Âg™Z}ŒN¬YGÈh¾jP‰¯2¾dY†ÊyLD†BÉzx[ ¾D^ÿNÓ8“—;¶Æ f—÷M§PªY5Ò{—B¸¤u¥Qñ WkŒ 04YuƒZà_«Vß}M‡¨Ô«Y:õëR¦°þ¡=ŸChaùüéî¢%u_W‹½F©|°?”h××É4ÔQb Ž¡Hi¯ZvŸàs×ùPãMgëȇêñfµZ¼óv%ŠÙ<4šˆ &Å£1žܰ¤ "òW¦O)ý½ô„…Åáäï† Îþ¹ÈÒix/Ñ‚ÑhÄÊË·i8Í.o¥_‹ûêi]´S7—¡÷oßðžËý‹1œ]φ0OšŽªÄãbæG–ÍOP¾g[Gd[¾›¸¾kgó׿ãJ÷¨ŠIÐû'cV‰eß–HÕÏ©ï’Õcâêží8Ô¡W9»û×n2"X`a £hñ"$¹Â-©ˆãí,Ú ¿'MÚýغñ,±a§ø ÁÔÌ×W¼íýük '$Ü’ =âšE”#qØðûïlØŽK7H5 ‚·Nè0páï]D•hÃ[¾óF£ +wo\t ‘;Ùxð!{>á½E¬!_ƒ<*°Êêæ¹MPÀ5 úùâ ÍO•ŽŸñmžz”³Šgu`…ý|ɯ1p* <Úg¨LÄ„„W¸$žö°7«ÖÞø»?n&!4„ÈÞx;hAâ ºNo šw1E_ŒªÞ6™’”tŽ`½­p/ A^u¢Ã¬U ,§>~åÉè<Û1eUïÞÅ®?Wð킼9ú¾­[à^%c ࢥ'm‹?X1%rdÒÇ :X–!ßüÎx?l´·Ù8¨Sœ½É8Ìù‘Ç0E|›Ÿ,g^W,Æ#ÝfcÐuŠ—ñÆZnt ú^X˜®lÀ³š;Ú„íÙçý„‚®Ä×· ù&nëò:™âå„`‚®æÇÛÛ!‹´hqj8šåe޳çïÝl_?–ó6òùï“h^ôE<ý™ˆæŽgSí3Ýs ¡^ÔáÙ<ó.<Áç®ÉKµ¡‹XÝh/;ÿÞɦ©ý˜·²³æt3qHw—Р+äõÊüyß È<ÛßÈÌéñnîŽNâ¾A~î80EOñj^izšÛ•]Þº·#Q;ç±6TƒNk]^« ¯ q!!DÛ—Ä?›‰|*›‘„lÊw!!äñuDöå{ {‚BÀ³ñýû‹!—°ôlÏC_ݧaŒ 4Ü„[kl²ÜÁ@Xð%pm‘á<­ðKÄ;øàn§4*V‹ÿEp͘Ìõ•Ë,Ó‘A¥ÒîµÜ %äZ!ZLœËÇ¥]IT(\yÃ/ÏÃ]å† ʦB]ù|â0J²Å"õã;Žæ®W1tÏÅ‹1äss½?‰YnOÑÊîXư`ÂôuùvíçTS=Ï…j'Ë Æ.kð.á‰-E«¶ Aé|h Á\ÖáSÒF ­öþ—%5”uëÏR´ÞÛ”´’î&#V6XßûBÜb϶c<ÌãL׿l¢¸{qs%bâæÁ}è½è*3q=8”Äâ~xg,? 4’@l¼Z\My:Ú¼Ty¯ ƒ'L »_çÎ_Ë0¶ÊDdH(‰Å}ð| Ï‘z‚M›#©Ð¹?ï•rÄF§Á½ÍGx—ðÊ=æhÐ`">6>ëñ\†‹]´ÆÇ·ÚÔÂññ·&)„€ËÅðó²~¢¼o¼@°xâ㑞*¡aè½}Í]¤æ¿†sÑ䉯ǣjs ¶.x§C?ÆüÖŸªIç9f|äys–Àœü¼É«I 8𮾞èM1ÞÂÃ/sk¯)*ˆàÛÅñõÊ:Ô{üç`Eaÿº´éýÓ¾|˳„Ü}à ÆpCŒxøzfn‘B‚¯SÈ×›¼šŒéqÅ×Û$™¤dA£Éx¨­l9g…·¯KU`Ùä­´„d ¿?Îk#¦1¨ÜÌ=@B†=´Z-˜ ;nÿÑesöå{öuDöy\Hº›‚ÆÊ«ôc$aû¾Û÷Ǭ>3-:- ZkÕ„Áh]†:ÐÆÖ-pªõ&>æŒaY´8Nñ—‰ý“EYÓ¸Ó›÷&jЉÄß~üz®)—Bˆ°qÇãá17Cv²-̦=šPº°-áΑºS o+ •”dA“!Ó#v²3Ð Ï´áuh4`ˆ'.AMÒx^TpšLQƹPÂÛ.ÓSZÚ$)7JúXV”(ãG×°äè%®…cå÷_°àvúu,‰Z x{ãwŠC‰ ‰\\÷Sw'RÄ7}<©ÎÄõ‹a$ˆ‘øsËùqöaR=^›ÎjÀ}ޣ׏q3†øds±›§ ¯—JfÇ´Il<AÔõpÎî^Τ¯çpT-e¡dÁpvælæhТ£Â9¶vëÃ\¨UÓ;C`)$ÜNÀ”t‹èØ$RRR0¤×ôkl¬MD]ºÌ] 5ê3¾þCIEðõΛá;ó˜ch©P±‘ë¦ðÛö \ºJèÉíü1vKŒH|A7<ðó²Àt=€ $OüÜuÃ.bá…¯‹ö ò¾L´S†±Œ¦ë'âîëEÆNyI¸ÍÓnFß&)%…”{ 5qqýd¦ýïQD_>ËÖ™+9]°&µÓÒ“£L— 1àåçŽÎp‘€P¼}Ñ‚¹V(-hϰ»!40+/¼ šíçnŠfûŒ_X¾û,á‘Ñ\ ø›yK ­\›ŠŒÊ;ÁE9>tnŒ¡ƒ§û½ 55$0oü\´ uÄ×· Ñ;—ñ¿3W¹za+¿ÿ@ñÄ÷ÁñÏ*›¼°!Ÿ¯§àÇþNYšôh†Í–é,MàЯ\%üîîdÞ¬]]½JØÙ}¬úe;˜eúȲ9Ûòý êˆló¸%ž¾È…ƒH¹ÊŽI“ØxÃîÞ¼†gfáI¹²ö­›ÏƳWˆ¼ÊéK˜¶6À<¼ROIo8¹‰Õ'¯qšõãG²ìN]z¶ò½÷ùkŠQTÎæ)K ½#Í3~ζþ”óMfÏüéüuþ*ÑQ\8ð?¦]ÌÉ{ñ¸‰k!a$»z‘ÕZ• ÖË•+ &âέbÜÔ¿¹e牗³4ñôÊÏͽkØ|á:‘A;™>~9Á&7¼ÝÓ*ZOy^ÓŸ`ÉÔõœŠˆ&új0G7Ïcìœ}ÜVñjŽPýº9 %è¡z:¹fnEI¾@˜­/=Í]yÅ[cèÅ‘LЖ™:¼«4aÔ¯]©‘?­¸¶,ו/;†0¦o#6æ/Š_Ͳxæ³ÆÚ×Üݦó¢É‡ØýãÞýË™µZRÞÛš³N®M§Åý½n4Û7I4a´1 ~Ú”¶4‰¶8­¿ùŽ?NcRïµÜÑä¥[Iª¼ûžª{Byˆ”t›Ë;óżHâ¶)Q•Æc~¡s™Œáš¯í¨½ký,&…ü4ž¸Õõ`QŽÎCÚüS¯ÏƒcñJ4«þ:Ï)‘©kó1ÇÀ‚RÝFóEü8æîΉ–äwöäµjMé^\‡á\ a}ø° –ä]\*ê‹—­H”{µ´.Ìl󾑋!è¼ß˘B`Dž´à.CJ-Ë4¦C¥oXðÁ[ü–¢ÁµË–õñG‡‘ä„«Zö K~ˆ%źÞ¯7`Ø”îÔÌû˜ˆ9))”À+Åðó¶Fbƒ¼áN]O‹´Õ ’½èஃ¤ o0„Íwîפ«o­-&¯çË*Oð¹ßånÔYV¬]ä›IXt£lÝO˜òQ£´òHbXÕ§c¤Ü›y½·[U¢Ã³ûì‡öVÁ1.”óJfM\ !Ù£‘ylªž Ý¿¤CÈh&õh‹÷›´®ìutqürlAÎlòÖåõŒ8—»-'2®µgÚCJéŽt«ºŽ 3·Ñdô;Ô€Ö½=ß}ÃOÓGÒuÁ, §tn É´ÂcÊæ'(ß³­#²ÍãZ<Ú æ£³ßñuóäqò¢re [0Ïkø'l©Òó+ºOÎâa=˜b´¥{9õld~ˆÕR´Éú^šÀ¼²P[÷ ørl{ªd\ܪ(ÅcØZ‚ƒßÈ<ÑW[”¦ƒ?'fÊ|f ÙÌm]|©øv» há¡WÉëá‘å²mZ·Æ|üþy&þØ…vÖpö‹Ú•\8rÙ KÊvèG˰Ÿ™1  =ªÑ´‚Ö7]ðrH; ¦@]ú»Æ”Ù‹øªçTLvõ*K­–ï>v¼±òäw5~jHkeMÿ©tæÍ"Ãf™aÓgØÊ‹ÈÏÏ!ýŠ¢(Ê+#‘=ßµä;ã¬ù¶–šáü‚ݺuëe'á%Häà¸nŒ7õcÞ7rEž+P ÀK;·F£éR2l©6C†ÍhÞLæM2ü¼·ZXVçQÝúŠ¢(JîdºÄ®UÛ8}%–ÄÄ7ç—…hݾj®”ÿ ÓlØÅùkqܽKè¶iÌÜë@“毫<÷©n}EQ%wJ‰âìÆ)¬štÛ ûU¥Ù˜qt)ñâ–åR^1©Ñ\øk6ë§G‘`´ÆÑ«ïÿ–6>jÜÛ‹¤ºõEQEÉÖ«Ù­Ÿû¨n}EQEQEyTpª(Š¢(Š¢ä*8UEQEQr œ*Š¢(Š¢(¹†š­ÿÔ`pEQEQ”çK§OáeÎSEQEy¨n}EQEQ%×PÁ©¢(Š¢(Š’k¨àTQEQEÉ5Tpª(Š¢(Š¢ä*8ý1žI½FƒFÿã‚Þ9n!Mí¼´/5ÓŸ G‡áo÷Ó®fù_År§ºVEQEQþTpúÂ¥²g€vMûŽ®+=œc)‚áü*Yf³³Þ“º[P©ð?φ3#©h_™1çž.@”¨Y4°ócè!ÃSŸó©®UQž„áSÚÕ¦ŒkAl,õØ)Mý¾ó8u;óƒZRÐ >oZžbùl°ÉW”ÒõG³÷îý×%þsûÔÇ¿ˆ=Ö¶p-ߊ©§ž>ÿç[UŸö€™iÓQôãm¤ÊþÁ>X<ôºË2#8¡néSK ZÃ×ëRÚÕ[ë<8û¿Ã§sNŸ»Û"rló,€‰è]ãx¿ª;l¬°s*EýþKH2#á³»WÇÓ¹>5{0ïìÝGŸï¥–’z•ÙT£ßôj9p x¶LœÎå†ãèQR—ÇS”—DLØx7fPûáxÉKjØfÆìÉ{‰…9;ë]òrãô~ûv•ÂØ•ïàms›ð³ñäјa g^Çwè±>#¦¬£F1-1!˜¬5;ó«IçN×ßóNÂýÈH¢WÒ»Ù\^kV=–”ð?޾ŸÌ½=Œ!Ìêø>;7Å_Õ`OIH8²‘]šš|:n(¥]­¸¼a4{5àFžS,nS•K³‘mž¹¾ˆ^-¾&¸å ¶þQ“ü‘2êÃ4×»rtlU"g÷gœi »ÂaZÛ“Ÿ-àí ½pQÍ…ODcÞ´€°ô€5` ØùGÀ(x¾€?P¨ÔúÊ« %DVn(þNv¢·Î+N>oHÛ_Kªˆˆ$Ëæ ‰„L›NÜûï–Ó-Ù=¡‹Ô+ã*¶Vbëà%5ºL”½7LN’ §ç~$µ}ÄFo#ù‹–”Ú½J!sR çÇH%ÛªòÓƒ/ˆˆ$-—ÖÖæóë¼dàÞ”L/§ùRJÙÖ’A>’ê®öbm_Lªv›)'ïd}ÙÆÐÉR'o ²?é¢eÏÏÝ¥¶o!±µ²“B>5¥ËÔÃrGDŒ¡¥†þÁ{`U_fFÞ¿ÞäMݤ}[™¾yŒ´®PTòXÙHAïö2ÿ’ñÉ®5;ɤ«Cy»ï0iRÆMœ ‹ÏÛƒemxÚ§f¼ø‹¼iç%vg¼¶xù_')Ð|¡D›²>¬ò_‘*G¾,%V~CåPªˆˆA~¬"¶e¾’£IY¿#eßgâc_[~ y†üøÊ3JȤZbç3Hö&g½GêÑaâo[Y~¸ îoŽ0œ•Qõ’¯ÓZyD–Vëá<›¸²Ø[7”Ù÷*“ÄüÞTò8(›rñç:R¢ë*¹œœ$áË:K‰w¦J„¹J3ÆÆH¬1Ëå @_s\WÅçù›ã>sèlŽ ó›ãD[sܨ7Ç‘:s\™cfIÅé9FˆZ<€n  ËÒã„gëôÞT°OïæÖSfFIawwlÏç–".N¬%€$}»0M¿Yȶ“œÙ6‰:á£húÑ¢ÍiÆ ßèõÉŠ ÙÀ™‹Z7‘N~V©œñ`)œü± ¿9KÙ¯Öp,ð,;gvÅ)òw´ýÙ,˜"gRßÖ—!S$i3Μ_%é/F¹D³9'ˆŠ‰`ïÔvøXåäó}";6^£ëÆ ®]=ʘ¢+éÔi AF굦ó›7X¹(õÇmeñïvj„£jfø3°ŽyÂ(P±ž:@bÙ÷÷Y\kcïGÕðr*„K©ºôœ~”80rq÷n®¾V ûÅí¨èZ˜B®eywàb.¨ž»ìN1öa|:v¡Š>«ù{Öï„Wû€>ª—&GH"w58-‚º£Ï ‹¢åT_D>Ü”`þ‹Q"~­+6Î=ï¥#éúïÒÜÑUzn¾ý@Â7Jw'©91D—RSäL©oë+C¦fùzò¦nâhá!}ÿ¾ûÈcüó–S½Ts^Òßm8ý”·©(£ÎDÄ$7·‡"ÝdÃIû}QK)èÒC6=¢%Yù·K‘ýCüD¯ÓˆFc--¦Èñxs ˆá¬Œ¬¨—¼=¤öç+äàéãò×ä¶âeå"]ÖÆˆIRdW?7±Èç ¥‘ GÏÈ‘õ£¤A+ñ´G_î…åz‰;>wÛš21$ëRóLÚ:æ £EuZ䃄Ík&ÎŽõešjé&YåYcè/RÛ.¿Ôu@n¤Š¤Dî’oê8ˆ¥…·|¶ÿ!5JÎ(-} cÖK‚än¨–Ó>-ºR+ê'ê•®M›¿à§»¸””ý;ïK%|ý×´®â‰ƒ­%:­Û‹‰»s›ôá-–ÛÑ­Ü)†T*GýŽ}øjÒRG¦>þ°Ït9Þ”.amþE‹S©’8Ä…|#ã33&ò§çÇô+O¦·/ât¼5j¸þóLfYŽª¬³ßïÙO€‡·Û½V»7î„j4|¯3d=‹¶ÆD³nñ6ò·ìH»ç˜$å%²¤ì€u;v˜í‹¾ ôñoùxÂn`BLÂÝâðËÈ–T.]ŽzŸüÌÐÚ1¬Y¼DÀd$¹,Ÿþ6„w+øSñ½¡Lø´4aË—rè9|Uÿ3$–ͳ–SûÚzdUj˜¸º|uù ‰£ù 1£õg4;žªÝô©="Ïj=zòۯ͈P';kì}{Pï}*[jÑjÌ9Wî°v$›ôb‰´ã·Õsùü=/l_Ò¥ä6*8ÍA–%?acÐ9Öé@y«@þø[Ï!Üôdï7ÿJ§ö3Il5‡£×1˜„¤uÉÜŸ `[•»ƒ9¾d8M}Ö“ê•{²öÆ‹éÈT!Ämfâô˼;ðCJ<ÇrMcaõs/ >$££÷nºý[tjnͦE›¸qu-‹ÿ.BÛŽoð¨Ê¿Ÿ³þe*R»ÝŽ­GÀ¸Y# -ˆCA-:O<Ò󼦮ÅíIŽŠ"N´t,ˆ.Ÿ^…Ò‹WÅÜ‹£‰Ž$Z­vöHr}%3×iص9ÎYEžÆ@ÎÞC뮼cÿ“÷#Äîý–&mVàûËF&7rVÁÀ3xtžµ¢D—¹¿Õà`®\;ÃüZ®ˆE´¿‹ï›µbô¹ò|½b_—>@ÿjî8»”¤þgkS±*?æ4­½'Õ[ôä‹I+Ù3»l_ÇÞ„öÑé 5Ãñ¤áôAŽ[7äÓ¾µqÏk‰#—Î’ð`ÜiQuÚÐ{ÄT6ìž@½èMl8–97kôz,IâîSµÜf` æL@²ù‘çÎs³€Þ÷Yš¸¸`<+óve`³‡[1t®e(7œ½{#xll®Ó¡Å@jê³×Or­Æ;‘„‡_âFbV¯¦ráô…{ãvS/œážøz¦O¶¡f§68þµßf,bŸW{:TPkW½*DLHêmâï h Q¦Œ Jxz )q\¹r}¡BäÕèð,[ûø0Ân¦çi#W#®"Ž…qTS`$há,vÚ· kÃY¶Š¦™ËÜ“tø :ϳå¿OˆÛ?’&-Pì§ÍÌiïŽ*ÍžEöy‹<8/†ƒM»–¬ãšg-jÓ‚}yú,ØÀü/ÞÃËê“?…èCD®¡yà`†®ŠåUuª‚ÓcäÜ‚a|;o§Â®qýâ!–¯<Àwül2î§ÃÕÓ Nnf]à-î&%“bŽRuž~x&aûá[BÂùy ûõX¦ÉN©§2èÇ%ì>ÁõkAìZ¸–S:?ü½2¯©¢u)OÇ@VÏÝBÀ•ëDÞLà©mLѬøö+Öœ#dÿL߇kûŽTOŸ¤´É“OPµß'Tʪ Ѧ}ú–áø÷¼èA—8°œ1ãוá[§±wÇ£à5v¯ßǵ;I$%§>>˜ÍBö×*DÿÑ?ÒôÛšœÅŒÍþ‚1[ÏváO~6‡«¯¿Oë ÍÁúJi纑?À¿};ÔŠYÿM©»|¥”^ IDAT~ Ëg“X¼q'à¯?¾¥Ó5«4æ-g-`IåN)u~2FoätHûçæ§ùiÒ¶v€]½®´/º‹1}&³ã|(çwLæó_OR¼ekª¨( k©Ç™7÷8ÅÚ}ðˆá2wØ>ëÂËv¦S9uÿ‰Äc?ѬÉxîvø‘eîpîÄ Nœ8Á©Ð˜§›Xûª{lž5pdîH¦¯Ýɱpx+ºÌJà½/>¢œ ±'>s%"&Œ&-–V–è,¬°¶RSUË㨠QOÅ(—÷—†½¤°½^,l I‰zŸÈüÓÏš1Åî•›•–Â6:Ñh2.%'‡'w .âäæ-þo~(Sl+Žöíe•yÙÙÒµ^Yqs´½>)ÓH†® ‘‡W]1ʵMŸK]w{±ÐhIJâ(9gI•CŸû‰ÅCKZ!V gK´)ÃRRã{JU;±²/.U»Ï–S÷Fj›äú‚fâàÖK¶Ü~èÄ’%{&u“7} ‰­•­8zW—N¿¦-%u_ª„,é-5Üó‰^«yÄRR÷¯ÿQ÷>ëk½ŸÞkÓÞ+½tXýÀ’7HW{iòÕOÒ¦t!±±Ê/^o ’ÕaNÐ2È…*‹¥u-™š‹×ùPþÃÙyòÑ{•ÄÛ9¿ØXZŠ]a?y³ëxùûzÆÏ‡£ëJçXŠ`8?†J–Ùì¬÷¤nÇT*üϳáÌH*ÚWf̹ÇÃ/S^«òß%wN²phj”p!¿5ùŠ•§éç+¼›q/†Ñ¸´v6ö¸ThË»¢3ýƒè¤€Åô«ç‹£­ ùÜß Û´cÄçòg½—ÃÄå?ÇÒ³AyÜ åÁÚÖŸZ]™´'Sú.ϱ=©íãˆÞïZô˜¡îç3“h¶ýÐ…÷ªúâh­Ã²Ô—1¼ìDý˯ñר¶Tr͵ކ‚žÕè8nѦ û˜n°ïçî¼é[;k;=«ÒcÑÅ´|pŠÙÝ«ãé\Ÿš=˜wöî#NôêR5õ«Ì¦ý¦¥·î(ž-§s¹á z”ü§ÇzNrìZ•ÿ2‰ÜËÆSywðdVlÛÁŠïk5£ oãŽyÃ…ŸißnqMg³ÿäßLªÌȲàйfJ:À7-»±Â¦'KgËö hÎÀ±¨xêAÎnÙB\¹®Œž·‘[fÑ£Ð.†6jÏo!i÷óöÖÁ4ë³ ç«9|–­£*prH3úo¸¥îç³dâ“òS±ý— oí*Ÿ–‰+ó?¤Õè ªNø›ÀðólýÆ£Ã[ò銛æ<™ÂéqÍx÷û³ø÷ŸÃæ¿·±âçÞÔp¶L„ÍîÏ8Ó@v…‡±­O c?[ÀÓãϪܧ1oZ@XzÀ°ìü€#à <_À(Tj}åU"+7';Ñ[ç'Ÿ7¤í¯Ç%UDD’eó‡…D B¦M'îýwKŠˆˆé–ìžÐEê•q[+±uð’]&ÊÞ¦ 'IÓs?’Ú>b£·‘üEKJí^ %È9)†óc¤’mUùéÁDD’–Kkkóùu^2poJ¦—S|)¥lkÉ  IuW{±¶/&U»Í”“w²¾lcèd©“·„ ÙŸ”á'dVφRѣ䱲–ü®•¤õ·›%"Õœ¾s£åu›Š2òl†ô™®ÊÌúöâöéICÐRجªø8çk+{)Z¦‘ ^(÷Î’¼M>.j+ÍÆ-“Ïßñ•6¶R¸|g™s.c:­Ù»%óÛJñŸIß·JJq'qöo*ßo’´O%Qþü¨¸Ø7˜)W2|L†€¥ŠM)v$õ)ϧä.ɲ«Ÿ»è=JZÖI•CŸû‰•ßP9˜ž•îl”îE¬¥Ú¸`1ŠHâÆîâdõ†üdL{Ý- šæû&ó%Ú”õY”ûL×gÈ;VVÒpv´˜$Eþîë*V• ÷ŠŠDÙÔÝIò6û]nªûù¤ÊÉoÊŠuÉ/ä°*¦žB²lëí"VUº_撚/KYI©aGÓêûÛ뤋saiµ(R΢F¹øs)Ñu•\NN’ðe¥Ä;S%Â\\cc$Öø¢®åé}Íq]sœçoŽû<Ìq ³9.ÌoŽmÍq£ÞGêÌqezŒ™%Õršc„¨Å趺,=Nhðq¶NïMûônn=õgFa”v÷wǶñ|n‰ bàâÄXH"Ñ· Óô›…l;À™m“¨>Ц-!ÚÜD` ú^Ÿl¡È œ¹È¡uéägÅSõÊXµbÙ]Ab§‰Õ#öIÙǬ¥Ö øßINmŽÛŽ>´±‡‡;’Ø?e2Ǫôã“Êf¸AŒu]ÏÞÌÑ€ ìÓŽä™­éô[(&@çÛ‚V¯gõšÒïDndå>š¶z+À©âÇLY{€ó‡YÖ·k»´`䱌W›Ì¶©`×w)û÷-¦³å* [ÁÍô&•'¹Öl™¸¼qVÃwqñz;û ¿¶ûˆE×°¡V§68ï^ĪˆôG_#ç—-áT©t(§þÏÅ¿›‘„„$´Î.8é¹Áñ£aX¿^•×Ò‡ÍØVâ²pæÈI’0zô8±E«PÕÍ\¼j P©ª)'ŽpVuŸfKïH~бCƒ`H5¢±Ô£¿WéÐ[Yp÷ô .¨û©¼p–ø×xƒ|›Yu2#nXͶ«^¼[¿@êé¿Ù{§iôš … {ðzˬO´¸w›ÀÓXjw¥ö$ ë‚KR(›Çõ¤E¯)Q}£ZNŸŠAÎŽ¬(6¯ —cmœK‘ÝýÝŶñ|¹õGM\Õ^òé%[“ÍïÞÕOܬߔ_ÂÿhõØ–Ót±¿KÛG´œê‹È‡›Ì1JįuÅÆ¹ç½t¤3]ÿ]š;ºJÏÍ·³¹’ÙÕÏMò´X,i{$àÇ*b[q”¤5žšäÚ¬†bïÑOv%?↳2²¢­¼ùK¸EÌ-§z)÷ÝiI»J“DÏj ¶ýe÷ƒŸÁ#®5{·d~ck±n0K"Ó“wJ7;ygúµ´§âÔã2â5[©6.(-]©Çä«ÒvRcBˆäâ`å $Ÿ'µò“÷W˜[@ gäûòVâÞ—DoþD| UâeMǼbõÖorÕ”";û¸ŠUÅQræÒÒ¶¸³Ôûùœ\›Õ@¬ìÚÊŠ»/ûŠr9S´¬ïî)y+”ãÉ""&‰^ÔR Ú”—[¯J²$æØ/ÒÄE/º]d]RvTMµœ>3S¬šÐD\­t¢·²Ÿtü=XÒk—¤ÕÄ^Ÿ_ —{_~ý븜>°\W+ vU~3VÉ©QrpÎ@iÙè³>X)m“å^Z[GŠ+„ñ4KWœ¡`õZ”T÷SyÑ$–›·Œi½#÷þ¨A§CBÉ€EÉò”¶ŒäbØý5¸W#¸*Ž8Ò‚}yú,ØÀü/ÞÃËê“?…èCD®¡yà`†®R+{¨à4Ç9·`ßÎÛÆ©°k\¿xˆå+pÇÝ¿Le­WO78¹™u·¸›”LŠ9JÕyúá™p„í‡o! çç1ì×c™&;¥œÊ —°û\ׯ±káZNéüð÷Ê\Jk]ÊSÁ1Õs·på:‘7xªHMѬøö+Öœ#dÿL߇kûŽT7ÇŽ$ícòäTí÷ •šh¤ÃÝÏÝéì¸f’ _=ŒŸ¶=)kqoÖš ççÒgüß85kÅëé“L´Eñõ±!x×Â’C$Û¿ɪç5P<õßT÷§ñ¯dõ Œ‡~泩{ ;Íšáß²Âø;Ý/œ4N4ëü6Ñ3¾aAò{¼_?¿êÂý’˜|Þ° ‹ ôᇋuú'NœàäépâÀ‚ò]>äõË3üÝNgå—ÃXšü=Ûy lêtç¯cL0‰ç98#7ç£u¯&8¨LñAsÚÓ`p oúŠÚqâÄ Nœ8I`”ùáØxec§°bÛ>î\Θö­úƒÔS] ÏDˆ ?ÍÉ' ¸~SR$'3æqå±t>Ô«çÎÍeß0|ùa‚ÂØ¿`c6 ¯7¬Ca h 7¡[cKÖŒȼý„œÞȨ¯píµV4+¡=ùó™»QĄѤÅÒÊ…Ö–Bjj.]/<—P¢žŠQ..ï/ +zIa{½XØ’õ>‘ù§^É»W~lVZ ÛèD£É¸”TœžÜA*¸8ˆ“›·ø¿ù¡Lý±­8Ú·—Uæÿ†3³¥k½²âæh'z})R¦‘ ]"Ï!2ʵMŸK]w{±ÐhIJâ(9gI_ Çâ¡%­«†³%Ú”a)©ñ=¥ª‹XÙ—ªÝgË©{#µMr}A3qpë%[5êîyYЫº¸9–â^%¥jûeRO_±}gº\ϸ¶†ñ¢ü\ËJ4~2ôà³Â×ÈgoyK!Gñô«(M‡O•AUm¥Âȳi ÌKIµ\ryÒŠ6bçÔC¶$?ÙµÞ“²Sú¸ê2ܧtiKI¹uü^†¿ë#ù­¬Å±Tùn[äÓâ×J§BâÒk«$>â¶(¹[êÁ!âkñp~Ñä¹ÿI•Kë¿”÷J[+;q.ÛJÆìŒÊ´dÌÝ HŸ:ÞRÀÚJòºV•¦‘8µìQÒ&>x¿Áòþ÷Ü(ó:¿.®lDoë(¾µ?”Éûod±Dòd’de;;Ñ<6+•xA– zOÊÉ#–V’ßµ’´ùîþR‰""¦˜ýòsç7Ä=¿•XÚ–R ˪¬fûšäÆß£¥©¿³tt“j=Ê…\ü9ð‚&D=î9^“ágzšþ3=`Õ6Ë ›>ÃV^D~~̹”Ã)¾«\ƒm±wh‰ÿø"α,hâÂ×^[L_òëLW¦Ó°äXJ®?ǤZϼn•¢(Š¢äJ¦pHɰ¥fØ 6£y3™7Éð3½>Ëöz5bGyj¦7ql9‰‰=ýþãé2$ÆŸ_O`·ggÆVS©¢(Š¢<+œ*OMë\‡ÞÃê¼ìdä©û¾¤B½ßˆ÷zQ QV}«EQ噩n}EQEQ%[/ª[_ÍÖWEQEQr œ*Š¢(Š¢(¹† NEQEQ”\C§Š¢(Š¢(J®¡æ?…[·n½ì$(Š¢(Šò +P ÀËNÂs§‚Ó§ð*dEQEQ”—Iuë+Š¢(Š¢(¹† NEQEQ”\C§Š¢(Š¢(J®¡‚SEQEQ%×PÁéŒñÌH*è5h44ú7l|ôÎq ijçÍ }©™þl8: »·˜v5Ëÿ*öâ="/üŠ¢(Š¢îb"lvÆ™²+<Œm}RûÙ®˜²=ñ+E§9)5”UCÞ¥´s¬lòáì[vSOöŸÂ–…ÑiôÔœFâº.ÐhÐh,ð°‡T‰eÏÄx«¬ŽvÖØ9zSóƒIì»™±ÈHä̼©ã눭•-\JQç£?x\ïýC’WÐÆÆÜõoñ¨®îT‚—ö¦†[^lòçî³8•õáL0~E>Ø GÍS¤ãŸ¦3e;½]ìhôÍ4zÕòÃ9Ÿ=Nº0÷|òãöͰÑh°n¾ˆø›syÏ*m_ýë£9ÿ³f"d|5ì^ëÌÝkàëRÇâ¯ÓqÊsk†psIkœ:±&>ÃÛR÷3Ø×ŽZ“.¢ÊµÜ"•ã7U®'ßö¬‰¯«'ÛŽá»V)¬œ½[ªözÚb4ÿæg¾ëבºÞyÈÉ"æUtwÇ,æ—gÀ¤Ôó/AÕ®øª~,Ëfü›*>GO’T¦ýš—ŵˆ;¯wø”^·9s*#´o%¼IÿQ¨äåŠOµnLü²¿ÏeWz#©F‹Fh@L¦{¦¸[Ä©]§9GˆZ<€n  ËÒã„gëôÞT°OpôÔŸ…QRØÝßÛÆó¹%‚ˆ‹k` ‰Dß.LÓo²ídg¶M¢Nø(š~´„hsÎ5ýF¯O¶PdÈÎ\ äк‰tò³â©z¹¬Z±ì® ±¿ÓÄêû¤ìcÖRküï$§6ÇmGZØÃÃIìŸ2™cUúñIåGì=I:IfÛò“4\xŠk‘Çù¾Ø&[q¿Íæöï¯á®I«;ס+’!åÈ—ð^«é=)Q§ ½GLeÃî Ô‹ÞĆc™s³F¯Ç’$î>UËmÆ`ΤO,2yî<7 øà}¯™ÎÄÅãY™·+›9þûŸ¸u:´Æ‡?“œ–}‰ððHîdñÀb ?ÍÙ8säçÎ^ÁÆË—ô â4:úsäüºp3v-:QÇîù¦WGæh· IDATyvZ[GŠ+„ñ4KWœ¡`õZ”|â1>Šò<èð¬XžüWqè’¹’XŽ @_öuüUþÌžÄró–1­õùÞ5èt`HH 9ã¾yp*^ ›v-YÇ5ÏZÔ,¦ûòôY°ù_¼‡—Õ9&: ‡Q‡ˆ\CóÀÁ ]¥VNPÁiŽ1rnÁ0¾·Sa׸~ñËWàŽ»?~6÷Óáêé'7³.ðw“’I1GD:O?<ްýð-!áü<†ýz,Ód§ÔƒSôãvŸ‹àúµ v-\Ë)þ^™K­Ky*8²zî®\'òfO5œÈÍŠo¿bÍé0BöÏdðø}¸¶ïHu½ùõ¤}Lž|‚ªý>¡Rσztn^¸%dÃæ0âï&‘œb|…C2›û–Âï K²jé¾»•‡,äHh(G~ÊÛ Ò²S]îÇŸ:|Úv¤ÒÉñ|·¡ ­:TÃúá£(/›ñËÆNaŶ}ܹœ1í[36ô ¨§ºìž‰~š“'Npý.¦¤HOžàäépâ^õüØÔéÎ^ǘ0`;þÏÞ}‡Eq¼qÿîW8AP”*‚b,;*б+6lÁ®ØöØ£F£Ø£‰164–¨Ä.ø 6Ä‚  vA¥_yxࡈQQßÏóÌÃÃíÜîìÜÞì»3»s×¢pfÃÌ,¯maúÙ÷2|bGxxØ"ᯘ²ý,nÆÜÀ)ÿñ˜{ðm‹F0@…sëfcÕî£8}úlšÒ ½þHA«IƒPU€`ãâÚ^Ò@­A"“@¬'ƒ\BP*ùæß¼h'9€€€€€€€!cf,”` <ÕÔPÀúâ©)zû(jQÝÌ ¥¤§_’œ=†Ò†ËÉoäÔ$…ÒÏí*‘¹¾˜AL¶£B(“ˆHóŒÎ.ïN®V¦daSŽ\ö§•?w!3Ãnþò½ªÈ5ÔÇ£ Ù˜TZŒJUnMnSF.åyxp"5¶5$=A Iõ9tUED¤¤°‰N¤‡—Ó±é&Y‹5§!Ržû**И…>TÛÊ€d†e©v¿5‘’½ôÈ¿™Ú ¤ ¦6óSNʦÁ¥ÔqkvÁ(}Gg2°@Aù\Gvu=¤ƒ“¾#Çr稯”N;» kF«çØ8Ýò«CŠjƒiÞˆ†dc(#KWê¶4Œ’4¯­Bóþha@—Ét^YØåc…BEë{~KÖ&ú$U˜Qy÷þ´üT<½þQ²üÒ~o^û Å^µ‹¬`Ò®ÿIÕ#¹ŒŒ¬kSï•çè ù—z¶ŽiE•K#‰žŒŒ­kPçénv›¬¤ó‹[Ó7¥H.+F–.Íi¤½ÈµŽ5ì'òt±¤f6T×g]/ÂÇ5€Ú¸®–6ÎsÑÆ}vÚ8ÐRkãD…6n”jãH±6®ÌŠ1߀¾k™nšõ7+`ÐÓI$ÕIÕˆhiÛbŸU~¬é†]Â:Á™'Å~oÜ^è†Ê[ZãÜ™wLa¥¾‚Ù5kâïöa›âÂuÏcì£a$€‹2u’R'©t’Z›4ÚD:³Æ=rÿà;LXiâ`Öq û8qpôÑdâEüD혆ßnÖÁÞ¸îcŒ}‘88e&²l„!“}êb|]2öc ]'èWF—¥[ÐÏ–ogŒ1öeâa}VÈ4x¸ ï¿õ,‘aX¤ÝÃ÷N”¯ÇNS1Ä­ø§@u ë'­Ëþ„7‰a^Ö wÞ¾¯ÅÝ0xZ'8r7'cŒ±ÏÜÇÖçà”1ÆcŒ½ÓÇ Nyl1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1VdðÓú˜˜ø©‹ÀcŒ±¯˜‰‰É§.ÂÇÁi| cŒ1ÆØ§ÄÃúŒ1Æc¬Èàà”1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1Vdppú…QGΆ«T€ ¤uàwëm?¬ àÙ&x”Ø“9ŸSu~2\ šà·¹þpÃçé-ûÊcŒ±¢…ƒÓN‰£í`ÐÖI`íâJSp!“ º65$ïÈ,µGcï¨aþþ‡*r6ªÖÄÜ«yßR!î+û‚©.à—®î¨l]ú) KUB³ëñBçB-í¶ú6‡‹E1È䯰©Ù BⲃOµƒšºÂ¾¤¤R˜;¹£ß²“Hø‚®õ> “ݽa«'B±nÈоÊõY¸2oîÂôžQÉÚ y1Xº4۵áxÎõ™ê‡øgNÔ°6†\ªöuáíwqš¬ Ä÷Ã÷µma¢/ƒEE4µ7Òµ‹S"°¦_=Ø[–‚cýX%ííHÑÅgꯙ~]Œ\5]ˉßsEÏ´xÞw]H¡í+û¢‘úåÚ`Ìò¿ðOè ì]Úâ´}Ï™8;»#úlÐcc¢®‡`E‹ÇðkßëïiÏLj Êx …ߟ Âê–™Ðý6>€&Mí4þ¨©·a]áµ¹> !åÜêc˜ßf:ºK:›Ãgû“Ü䜽Fƒûú£ÓO7Q{Ñ1DÅ^áN8?¥#†íHxù£ñ6c`‡é¸\eEÞDøßcPêдŸviÐ fÍ(øi|q<6ÁÃ31¬?îóÁœo‚6‰ˆHHÈ(0`À@vÊpP @-õŒ ¯AæmÚ9®¹XTnDŽu¨ËŠ‹¤$"¢ ì_’Dxyì¾Jb²B™DDšD YÔ‹<*[“©BF Sr뵘Bã5:I¡Ëë‘»£)éKõɸtr¸‰nªrEum.ÕPÔ¦¯/ "JßN^ríöÅäš™c±òÜTQÑ€Æ,Dõ¬ InX†j÷]M—’sßmõåÔÈșƟJ×yUCOO-!ïÚ6d,“’¢DYªÜlí}¨ÖÉO¡Kz“›C Ò—E¥64yo,é–&ã`_*iØ…VÎ%/×ÒTL¦O%Êu£ ÿªèÁêædh7’Žgè¼áùßä]Ò”ºlOÌ×¾j÷˜îΦnuì©„¾œ -+R“A›èZVÕ壜yK¤ mT¶ÃXѤ•µ0%KOšuø iˆˆ”iê7úôíOWéÕ§¥¡Gk[‘Q JÉ÷†X¡SÒ¹*’Ìi…)‰HI³\eäàúêóW†ÑDg}r[|‡Ô¹­BMKêËȬïÊÈm9#RÇÒ†ö¨õïgé÷22躓Òßš—ë³P©®ÐœêR*Þc÷ÛëœéÈ à!V$«½àÕyWyŽ~¨(£Š“Ï“’ˆRwv%Cy Z—uîÖÐÓžT̲?¦ª)zi#rî@÷2Ò)ö¯žäÜt%ÝÕ6ꤧ””kCR4¡ëjiã<mÜg§-µq¡±6NThãF©6Žkãʬ3WÜsZhO¶ŒF_ ×¶‹¸së"­Wìan)š­~5e"d”-m6 ‘D*D/vƒ(q/Ìá9c‚/Ý@dð4ŠÏA[§½¤Ußü‡¡ÔøýˆŒŽBØÞÅèá$ƒª E•uÂ_iJÚˆ¶²·äÉ<‰?¶É1zÏ%DNÍ‘áðšvo>¤ãÔ/Ëq¡ÖH ­©³2ÕYüÜû\ª¶GnDãZèvÌò, qvA•¸²°Ú,MDëÅÿàrÔyì^Û{tÆÂÈœ{Céÿà§¹ÿ¢ÝÚpè0ðœÊ}ØyážÓQÑ•ÙÕIÿ›)t!Ï.µL eKŠ6(1kM èFÆ¥Ò!mAæñ‘d#oHËbó¾´Ê³ç4KÒFj«xKÏ©´õ?˜Õe§¦»+“¾¥Ov9²hm¤öfÖäø"ç‚´êfXœ:lN å"ý4ÈÊ”:oK|µ\óV}g@•§‡k{›_öœšéÙшcio®CóˆÖ¶2"ûQǵ=(/hws*Ù=€žås_IGÚ’Iûô8·‚泜yK¤ mä$oþÇ«md¥á6ÔtÕCÒ‘:f9¹s¦ g^–O½ŒT¤Îæ¿––L:5Þ‰¤bANv~¡‹Ï³>¸úß ²$-ׇþºBÍ º±©•—둼Õ:JÈÊ¦Š¢uå$$–ÔdvÈ«e,åõ¥äaÛœ~½­"ÒÄÑšÜzN¹>?ŬoG–fÍè·Ûyœ+XNš$ [Ô–¬eb’Ê$$6p"ï·²GSÔw–‘»1¹Ï9MñJ¢ÌÇÇiF#S’è•£±§^kÓ•OèÌZ_êØº7ÍÝw‹Šú@¸çôs#†c‡>hðd<*¹£óàIXàÿ¦¿û¯(»o:¼jÙÃT!X$@Ña ž%¿@ŠöBJR½+úVÀøUÑÌ{8¦.Ù†³?Àèâr¨ä,×þ#‚EÅ 0}v·âu¯èTˆü}1þg?£šËù~YôêS‡úWFÖ½1zæJ쾜˜}˜æÞ%DÄ%à¯.& ÚÙD¥0ðixüðIÎ{É$UQÛUŽ7æhåÕ {p&@òì<4ój£üî§* —®¨àR¿Ìrùš¨œyÁÌÁ&YÛг…ƒµwnFC@dÝ =<Äö-§‘5îìØ‚sNÝнڻžjc…O‚*£÷âÂ…³8¼y*]œ‰Á‹Nã@×c’ýaôv,¹¾9ÜW—FÏ’DYŸ¯Ø½7žÃÅsÇ0¯1.Œéq|Oßë”W°d€Äã–c€}ýv\Ÿáé±Éð{žëÖç]ÿL!ñè\ Yôž«àÜÅ0ùÕÂ…í0!8 @dçƒ_W´CÒ¢ú°0ðü@Üðø5%"ˆm#Aɸ±{6z¶ˆ­Ô¿þ½[9€Ê^âà´I* Å›W±wnwT“EáOß&¨îµ±ùŒ`Ô·V G·ÕHí´ç¥B¥!¤ïí cЫFXQÓBnáâÖ)ð,¯Æùß|P¯¦vÇœf:Güö,‹WÝCKßþp~½]LÑléÜ:¹£¾+‹çGç£{Íz˜’š…Ä0é¬D¤“Ôx´ê;HuW¥'‡<×M@É–^høtÎd ùÈN¡9¼< m RÎw¬j•Zçdª†ZM ¬ ´ëÙÏ6#$ù¶o»ŒªßwCQ}¾ìK§oé—ÊÕáÞu6Í÷À ¿Ÿ±[;Ô&”lŒAÑHˆ‹Á­˜Gˆ=<e“`TÊRçÄ"…™½ ¾quC;ßµXÑõþ˜³ÑüÐCôâ,Žœ}€#¾•` —C®_ƒe"u{W7R²rr}.BRèL´í¼å—ÀòÖ– ä—æ6ÏZ†û-~ÄüõñM…ªð´ “Ýcðû²½H$Á¹×:\|ô÷oÝÂý‡‘ØÐ@À}²@i ðü8fµë„Ÿ®VÃô;0½ÒiŒªk K« h6vbxÆC> ›ÈÐõ:ø`Ò’8±¦+px/BS^Ë#J%T¯Å“ªËgpQÞÃF¸ÃÖHjü{-*»×4›^ 87êŒ!ÓVbÈ"xÄÄþ 9fA*…éH+PÏ­õ-DÞÈšÌEƒÇW¯!ÁÄå²»5ˆö_ˆF}àÛÎì-}óRXTi°æŸ@Lý&¯½ì)´ªŒoLbz÷½ž¸ÌZ«áSì 8‚ A@óNh\ØT¯<ª¸èáʉS¹NMS r*Ÿáal,î?ÍÈe!!áJä«'2Ó¯!ò¶öåmµ÷Í (ѲZgî†ÿ²µØUÞíø Zi@Êxž¦{€—°BYK#ˆbw`ë)ê6tEîý܆@/ž#™ƒ©ãXu)ááGøÅ£˜QOy³… [ˆ¹v#q}¾³S³Ñ¶£?Ê,ÄÚn¶o9nY®( ‰j@tÎ{Äb@•’‚­¿^1X”-Sýߺí ~`X Ãý÷cäVp]ÅòaÀtNîFíBû¨q˜ôÕ ðýä…F«þÓ°]ÓíÝ+œîâàÎÓH¶í '}Ý|bXÛÛ›±7ªºÚ( Ö“Bª'@lïû”¿qøl"š»#õÚzL^q*Øe¿[yf%&-vmêÁÑ$7¶îF„Ø Ír~”"«jp5›Š¿×¡³¯+Œå†035Èÿ ïš8ì˜9­JÃ7ÉA˜²ð$¬»ýˆzY]…é'±|y8jñGÜ4zþü~Œ„ƒWKÔ´1Àóðͼ­çQÖ/Ë wǰ‘Uáöã÷cê‡Aõ-‘y/¡»·áI³u˜æ‘Ï>IÁ-¼aÄ„±˜–´ÜÐÅÞý.÷›¡ýèþ˜×rzÌR`Žwu˜¥GãØæK°™6 PNõåhå:‰C"*ë!7ê°¥»ò[Ìk]W–ÍÄuS¬hcñª3l‚í%h=s9$MW¡½‡¦›òø<ôß#GóÆUaWRŽä¨ ,›² êZ?£‰åËÏCso–m{Ž µ`pk¦NÃi‡áno*„¯†µ)uФº#Jf"öøjL_ÿŽ£ZÁ™[ÜœDF(S^ç&Ї¥‘‘œÊ[AÆõYèR/,@»¶ ‘Öc5|+'ãjx8@ddŠö%8(x±#<Ò@­A"“@¬'ƒ\BP*‹è|áE?U jŠÞ>ŠZTw sC)éé—$g¡´áò›ó/i’Béçv•È\_L‚ ;•Ô3:»¼;¹Z™’…M9riØŸVþÜ…Ì »Q€öéUäêãQ…lÌ H*-F¥*·¦ ·s™REMN¤Æ¶†¤'$©>‡®ªˆˆ”6щôÞ˜Ò $k±†â4:SI-ô¡ÚV$3,Kµû­¡ˆì;µ5ôÈ¿™Ú ¤ olø¥”Ó´¨K}r)S‚/û-yÍþ‡tg’"u….ëGîåK’B*'c«ŠÔ ëDÚ©ó×Ë©¤^ín4 ›©cqD–}iŽê~÷¾¾”IÿΦnµíÈD.£bÈÃgã«©¤òQN""ÕåYTM¢óyf{9•”÷,šÒÒ‘Œer2«Ø–~ ~üÆÔC¡¾ä gLþŒÏýA2öA©®¬§A­jP9KcÒ—HÈÀ܉öYHǽú¤ÔwÑÈzdª’ÜÄŽjwÿ‰þ¹Ÿ}°PLÀêP×™J›(H"Ñ'Sû:Ôeæ>ŠáyÞí¢¸> —šnùÕ!inmbÓUôˆüI½N[Ç´¢Ê¥Š‘DOFÆÖ5¨ót7û Y%_Üš¾)mDrY1²tiN#ý#èE®õ«¡øc?‘§‹%•0³¡º>›èzžÓ 騷.ÐY¦¤fýÍ XÅxÙûš•$:Iª“ªÑÒ<¶Å>'ªüXÓ º„!t‚3O?òNIðok…éA¹ö¨¾BH ð†Ó à×›¢Cñ·fdŒ1Æ>:AF¸ S')u’J'©µI£M¤ó7ëÎ…\ï`à|V`šø˜u\‚Å>N˜uZâïÆÂŸö@Ñyšs`Êcì+ÅÁ)+0‘e# ™ÜèSã ¢ÆE¨:# Vî£ñÇ wžN„1ÆØW‹‡õcŒ1ÆØ;}¬a}~˜1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1VdppÊcŒ1ÆŠ ~Z¿?ucŒ1ö311ùÔEøà88-€¯á€`Œ1Æû”xXŸ1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1VdppÊcŒ1ÆŠ N¿0êÈÙp• ‚´ün©ßžùÙ&x”Ø“Ê/«ÎO†‹Aüö ×_+Í,r3@­Ÿo ’|xoÙ×ÕcŒ1Æ>8N?:%NŒ¶ƒA[$}€µ‹+MÁ…L‚êÚ\Ô¼#³Ô½; †ùûªÈÙ¨nXs¯¾Ü †piáV߇ðÞ[É]Æ®ïQܬ/dæ‘é-ûZ úb Òn`«os¸XƒLn ›šÝ± $.û¢ÕQ1¨©+ìK@*5€¹“;ú-;‰„B¸Öûú(qjœ#ôí¤N’Tž†pÕ§.ßç)ýÆŒô(3…>ŠÛÖAßß.à9Ÿù÷Ž6€’/aÓ„Îps¶‚±¾ÅËTƒçĈJÓfH‰Àš~õ`oY Žõ`ý•´·mé«ÅÁé×L¿.F®š®åÄï¹¢çZ¼ ÷ZŒÁ€ ¯­K(‰f“WaZKËO{°Ú¾²¯[&ÎÎîˆ>[ô؆¨ë!XÑâ1üÚ÷Åú{š—YÔ”ñ ¿?„Õ-2¡ úm|ͧ-ügH‚*£÷àüÅ‹¸˜•ÎíÀPgÊ·ñ„ O†Xpé§1£c_ìÐ÷Á¶³4Í'F·‡ï$p|šïnèq(D”@Ëq˱#øvÌj€'¿wG‹qÁH†1kFÁOã‹ã±1ž‰ùcýqŸ‡|´I@ @@ @@À€13–ʰP€ €jj¨`} 2oÓÎq-ÈÅ€¤r#²p¬C]V\$%eP`ÿ’$9’˜lG…P&‘&‘Bõ"ÊÖdª‘ÂÔÜz-¦ÐxÎFRèòºAäîhJúR}2.]Ün¢›ªœEQ]›K5µiÁë ˆˆÒ·“—\»}±ù†fæX¬<÷UT4 1‹Q=kC’–¡Ú}WÓ¥äÜw[}g952r¦ñ§Òu^¼G+Kµû(¡šó®SÎ’$Ò†6 *Ûa,hRÊZ˜’¥‹'Í:ü„^î­šîþÒˆ•¦ÒEeÖNEÐ̪ ª¿$šÔDô|“'ÉߨO¤úºšµ±wìk¾êë”gÆSyý:4pJúÖ¾™™Ù’Û@ºš¦­ŠèeÔÐÀF‡èÔ=§=½,Ȥý&ŠÓäºZV©"i–«Œ|C)ûHR†ÑDg}r[|‡Ô¹½GMKêËȬïÊøx%ýb)ÏO&EMšw½àßUF”z YÈêЂ›Ú£UGþžFdØv·Eùñ_ÚÊ ã#mIjïK¡™jŠ^ÚˆœûнŒtŠý«'97]IwµoT'=¥¤ÜWR$¡ëjiã<mÜg§-µq¡±6NThãF©6Žkãʬ3WÜsZhO¶ŒF_ ×¶‹¸së"­Wìan)š­~5e"d”-m6 ‘D*D/vƒ(q/Ìá9c‚/Ý@dð4ŠÏA[§½¤Ußü‡¡ÔøýˆŒŽBØÞÅèá$CF·dðW’6¢­ì-y2OâmrŒÞs S`sd8¼¦À›ƒé8õËr\¨5Ckê¬Ld…!Á õm,¬÷¶ñr î†lÊqD?º‹£#+ºÂæGù»~7ü~Òˆþww™öÁþ !óÜÈîÀÍϾåì¹ò6_y€G×Ö¢æé!è<ë,2ˆl¼Ð³aRA¥ ‘JtZUdR"/Dâ;K4Ɉ ^‹—åp­Qõä½¥âØ[·7º;ò(HÁ©qçüE$•®…Ú6ÚÓ¿`‚µ~Wø6‰w+hP#%%"K+XˆE°í»£5óáVÖîKô0ί¬Òï ÐÏþ‚óO¹›ƒÓB£AüÝûÈ4¯Ž&uaeeʼ1¾GõüŸD¥Ñ~Ú| o_UÊÙÀ®JkLñT¡Ç®}ŽGóè.îk¬Q§i Ø—.G×fè;ºœ »•„׌9èXÅŽuÀol]Üݼ'^ûæÑãX蟎.c¼aýŽ&™ûPŒm`1ôáÔÛ^úAðßóøó^٢똞p”b3wŒ\·7ÿ‰3J‚Úõø)»6ãH žØ‚ Y;x7-þ‰ Î DÏnuKâÎŽ_ñ÷T%#j›¶DÒŸ& 5ûÆÓ›ð«§==#Ø·Z ÙØ}Ø2ЖÜ÷D‰û±zû |×Û e¸2ÿ âŸÄ%JÂäÑftµ.…&Ë¢`\ÒHx‚x~ôÝòÛèȼ²swè¡£¯7ìEŠU…ÏúSˆ~üwŽÍC¥sSáÕeÂ+LÀæ­Sј{,¸­,»[ñºß<"_ŒÿÙƨ&ÅþÃFD0s°‡IÖ÷PÏCð3@ IDATÖܹ]°žà¢@ÏŽvY—!Líì`ôø&n¿ J´ê‰Ö´›=({·ø£7|ÊB³‚SÀãÇõ˜d½‹C®o÷Õ¥Ñų$AQÖ±,¶AïçpñÜqÌkŒ‡Kcz`ÜçwÑU¤hð`û8 nƒÞmÍ>Ø–_A€ 1‚EÙ²°*!õò',Òç#Ÿm€–úÁn í4iC¶`EóWuLɸ±{6z¶ˆ­Ô¿þ½[9@ñ±w§ˆâà´I* Å›W±wnwT“EáOß&¨îµ±ù¼ÑY}kzt[ÔNkqþQ*TBúÞž0½:©)jcZÈ-\Ü:žåÕ8ÿ›êÕôÁîøsÚËñ½{ˆÅ«î¡¥oÿÿØsKP«Ô:'l5Ôj½mW(eŸ©¡RëN­†šðª¼†MУ½7DüƒÝØr¬ºx×Á‡¼Ó€}BÉÆ˜„¸ÜŠy„ØÃ£Q61 F¥,uN,R˜Ù»àW7´ó]‹]_à9Í=üwê(lZS¯>hjø© ó¹ÁÌÜ Hx‚§¦­±44 ¼­‘—˜šÃ”ï”È—üµ€æÑAŒjÑg¿Ûˆ½³ÜP<ëúü8fµë„Ÿ®VÃô;0½ÒiŒªk K« h6vb>@Ó熃ÓB&2´G½>˜´d'N¬é ދД×òˆÅ€R Õk–êò\”·À°î°5’@€ÿ^‹Êî5ͦWÎ:cÈ´•زq±ÿBΣYJ!A:Ò Ôs«C} ‘72´ÿhðøê5$˜8¢\öpƒÑþ ±Ó¨|Ûý×^ B•ÈWO)¦_CämìËÛBôú@z*Ò²‡JãÁ“\Îîb1Dê7ë³ òS_©qÿ"6ö1’s 0TQˆ¸šõf þ½r Éåà`”U3ú¨ß£3ÌþÙ„_ߌ“ÝÐÝ•ç®ú| —°BYK#ˆbw`ë)ê6tEîŸ(A£!Ћç¹;,_2ϭúKvèÞ»äïÎÎr%†}õj0~†°µ#%áÜ™Vù–g?(¼ÛÍ“ ø¶è‰£u×âÀâæ°Ð¶ «a¸ÿ~l˜Ô ²«X>ì˜Î Ãݨ]h5xæN Wý'cæú`DÄ<Ä£è0lßyɶ.pÒ×Í'†µ½ p){£‘–žLmT%¶w‚}Ê9>›!åÚzL^q!Ç·òÌJŒùy+B®ÞÅ£‡7q|ÓnDˆàâ³UYUƒ«Yþ^„÷áqBJÁ&Á×ÄaÇÌ©Øu9·O­Æ¸…'aÝÍõ¤Úåé'±|y8jŠïÑý§[б+OàfÌeìš2;ÔMÑ£(^Õö÷!àÌ3Rqmí/ØÿæÙ]lã›ô3؃çiéÈÈTø‹ýîúÊ@àˆŠ°sê…­¹õRSvÌ˜Š¿#bpóÄJLXvÝ¿G-hEZÃ]­ƒ1{Þi¸tëŠ×gÝbŸͽ}X²p3‚BÏàÄž¥èßîœvŠñíÍ @…ðƒ0bþì >‰³aG±Ã¯†­ Ç6­àÌ'ÿÿ(‡ÿø±Uz¢GU¾¨{úú¡·Ã,½G®Eá̆1˜X^Û”Çõó%ï6 §G0±Egl6ŽyýËâÉåp„‡‡ãÒåX<#‚!Œ‹kO¤Z#‚D&XO¹„ TòÍ¿yᩤ DMÑÛGQ‹êdn(%=ý’äì1”6\~sþ%MR(ýÜ®™ë‹It§’zFg—w'W+S²°)G. ûÓÊŸ»™a7 ÐÎB¤Š\C}<ª™I¥Å¨TåÖ4!àv.SÔ¨éáÁ‰ÔØÖôAgz%%…Mt"½\¦`’µXCq©¤úPm+’–¥ÚýÖPDJöÐ#ÿvdj3‚^ä^û’™èÍmÅ{Оt¢¬©¤l¼gÑ”–Žd,““YŶôcðãWSqhâ(xj²3.N–åªS»)‹iHåWSI½ÚÕ‡tpÒwäXBNâîë»ë+K:íìj@‚¬­~œs¾å™ñT^áNcô¡¥ HfhMõ|6ЕÔ×kEE×çÕ$‰¼-¹S„ç ayR?ÜE#ë9©BJr;ªÝý'úç~ÖÁ¢¦˜€ Ô¡®3•6QD¢O¦öu¨ËÌ}ÃóHýgš¸?©ƒ‰y¬ü÷-Sõ°‚H»þ' oTŽLä22²®M½Wž£g›èúyŠ|¤©¤òºNtþf©Y³V1=$ÑIRTˆ–æ±-ö9QEàÇšn8Ð% ¡œñß:“àßÖ Ó‚•5•ÖgJ6..a\ôAô7Ïë+•Žã#]Ðúú\;è+·`Œ1öa$€‹2u’R'©t’Z›4ÚD:³P+·íð +0M|Ì:.Áb§ÿ˜~mÏãð0b fnz–¿vDiLcŒ±\qpÊ LdÙC&7úÔÅø|Ðlð²Á¦¨Ùï7ÌïhÊS¶0ÆcoÁÃúŒ1Æcì>Ö°>.2ÆcŒ±"ƒƒSÆcŒ1VdppÊcŒ1ÆŠ NcŒ1ÆX‘ÁOë@bbâ§.cŒ1ƾb&&&Ÿº§ð5Œ1ÆcŸë3ÆcŒ±"ƒƒSÆcŒ1VdppÊcŒ1ÆŠ NcŒ1ÆX‘ÁÁéF9®R‚ @Öß-õÛ3?ÛOƒrsR™ãeÕùÉp1h‚ßäú«bŒúú<Ô4xG™cŒ1öEãàô£SâÄh;´õGÒX»¸Ò\È$¨®ÍE É;2KíÑØ»j˜¿ÿa Šœê†51÷*–ìs¦AÌ®нiuØ™H!’ÕÇ’hMÎ,ê‡øgNÔ°6†\ªöuáíwqÚlê¨ÔÔö% •ÀÜÉý–DÂǽÖû|¤œÁŠž¨_Ñb1Ì!SgqæÍ]˜Þ³1*Y›B!/K—¦¶6Ϲ>ÿ®ÏBv[}›ÃÅ¢drcØÔìŽ!q¯~$^u¿tuGeëЗHaXªšXˆÚ)XÓ¯ì-KÁ±þ¬¿’ö©ö¤Èâàôk¦_#WÍG×râ÷\Ñs-^…{-Æ`@…÷]cŸ!í¹ ¥ê÷Åc[ÀLx}¹÷7ôG§Ÿn¢ö¢cˆŠ½†C3œp~JG Û‘ðò䤖 ŒÇPøýˆÐ ¬h‰ mÐoãhÞÜàWTÉH‘¹ÀsÌlô©òú솄”sp\¨a~›qèè>,é lŸíOÀñTAq}¾¿LœÝ}¶è±1 Q×C°¢Åcøµï‹õ÷´ßpÒ@¿\ŒYþþ =½K;CàƒV£â94ˆY3 ~_AððLÌëûÜ8ä› M"bRr †Œ˜°P€€ò\TP @}#èky›vŽkA.$•‘…cê²â")‰ˆ(ƒû—$@È‘Äd;*„2‰ˆ4‰²¨yT¶&S…Œ¦äÖk1…Ækt6’B—× "wGSÒ—ê“qé ä>pÝTå,ŠêÚ\ª¡¨M ^_@D”¾¼äÚí‹È743Çb幨¢¢Y4ˆêY’ܰ Õ.%ç¾Ûê;Ë©‘‘3?•þjáÓè…;-ÿWU9tdXYR´û“^h_Ñ<;KË¿¯N¥ ädd׈FÏB®9ʬ¤{A³©K k*.“‘a™o©ËÏG鱚ˆHCV7'C»‘t¥œ2öSÓbô݈ÉÔ¶² Yš™“ãwãhwìËOM½Œ8Ðèt7=§=½,Ȥý&ŠÓä¾ZöùË8<„¬än´øŽZ÷U bE²Ú ^}ç”ç臊2ª8ù¼ö»þu4-©/#³¾(#·åì%õ=ZÑXN%ûæ]Oª+4§º”Š÷ØMéyåcùÃõY0ªHšå*#ßPÊ>s*Ãh¢³>¹-¾Cê\ߤ¤s?T$™Ó Sª)zi#rî@÷2Ò)ö¯žäÜt%ÝÕ¾Qô”’r_I‘`„6®«¥ó\´qŸ6´ÔÆ…ÆÚ8Q¡¥Ú8R¬+³bÌ\qÏi¡!<Ù2}ý^Û.âέ‹8´j\ ³†¹¥h¶ú Ô”‰Q¶P´Ù€D"©½Ø  TĽ0‡çŒM¾t‘ÁKÐ(v<mEœö’V}óW „Rã÷#2: a{£‡“ ª‚UÖ ¥(i#ÚÊÞ’'ó$þØ&Çè=—86G†ÃkÚ ¼9øŽS¿,Ç…Z#1´æÛV–›ç84®ÆuÁœÃ—qa[o<ó_I99-ºn´÷œ¸v§ÖuAê/ÐgÝ¿Ð@€ek/¸%ìAÀ™Wƒ€/à5…×wÆ(K*Žxˆ>nâáƒó˜[z'zôø7Õ€ÈÆ =Æcçf}v[ökвGë\zÖØ—M·:(~#—’ † ñ§ÿFð´læü毚h’¼;/ËáZ£ÿêIa T$§ °,] †TP©H¤ÈJ™T…È ‘9nIyI…g7öbýþ˜T¯{±¶}a´f>ÜÊZÃ}‰Æùõ‚Uúúù ÃÀ_pþ)÷aç…{N DEWfW'ýo¦Ð…̼òeRÈ([R´Ù@ùéÛK èFÆ¥Ò!mWBæñ‘d#oHËbó¾´Ê³ç4KÒFj«xKÏ©´õ?˜¢}EMwW4&}KŸìrdÑ<ÚHíͬÉ'ðEÎu¼«ç4ùoênb@­Ö>¦—JØÔŽ ¥Ú2kžÒ–Ž&d3$˜Ò^íEÎr%ƒF¿¼¼ÊÔ<¢µ­ŒÈ~ÔqmOË ÚÝÜJv gyÖŽŽŒýÔÇTJ5æ^£¬šR]þ‘ªéW§9WT/˵¥™–êKû“µåÜÜ‘JX  ƒoéIf_†Ü{N‰H“Da‹Ú’µLLR™„ÄNä½ñåø©¢hA]9‰‰%5™B ÜËž·|õœª(f};²4kF¿ÝΣmcùÄõYp)ô¿AeIZ®ýu;…4štcS*/×#y«u:ßóL:5Þ‰¤bANv~¡‹Ïsi”OèÌZ_êØº7ÍÝw‹RÞÌQ¤€{N?7b8vèƒOÀ£’;:ž„þÇñozAÖ¡Dì¾éðªeS…b‘E‡-x–ü)Ú )Iõ®è[5ãkTE3ïᘺdÎ>Væ½Úÿ´;åPÉY®ýG‹Š`úì6nÅë^Ñ©ùûbüÏ~0F5)V Õ«ÜÄ4+¸T,¡=:¹T†mÖ¥»ú&Â#_ v¥ôíì‚*M½€ŒÇñD@0G+¯HØ€3™’`ç! ™W¨4Ø•³Éî5Û–ƒ-îàÆ%ZõDkڇ͇ž‡½[‚aÜÑ ´öE $‹!‹Ãsõœ»† ¿Z¸0¢&'½ºgOlƒÞÏáâ¹ã˜×—ÆôÀ8¾§ï½ž› ¯±7à¹n=Øs?ßûáúüoðøq=&ÙFoÇâë›Ã}uitñ, AA”nIPeô^\¸p‡7OB¥‹31xÑi¼ÈZLɸ±{6z¶ˆ­Ô¿þ½[9@ñivªÈáà´I* Å›W±wnwT“EáOß&¨îµ±ù¼ÑY}kzt[ÔNkqþQ*TBúÞž0½:©)jcZÈ-\Ü:žåÕ8ÿ›êÕôÁîøsÚËq™ó,‹WÝCKßþp~£]^»$"浊ô §§û¯Ä:o"’Àmñ¨‰@:Iye6\õ^n£dK/4|ºg2|d'‚Ð^†Ü+‚J¥S6µ j"dWºaôh/ÇÁÍÿ`7¶+….ÞuP›ØBs›g-Ãý?b~úø¦BUx Z†Éî1ø}Ù^$f ¥0³wÁ7®nhç»+º¾Às6âõÿY~’Bg¢mç(¿ì–·¶ä“×{áú|BÉÆ˜„¸ÜŠy„ØÃ£Q61 F¥,s—ú–Žp©\î]§aÓ|Üðû»Ÿðü8fµë„Ÿ®VÃô;0½ÒiŒªk K« h6vb>@Óç†ÇB&2´G½>˜´d'N¬é ދД×òˆÅ€R Õkñ¤êò\”·À°î°5’@€ÿ^‹Êî5ͦWÎ:cÈ´•زq±ÿBΣYJ!A:Ò Ôs«C} ‘72´ÿhðøê5$˜8¢\öM–Dû/ÄN£>ðmgöFß¼ ¯€>Òš–Uø4+rÖGýaöÏ&üúûfœtè†î®ïš§‹}‘( ‰j@нø ª”däþ&h4zñÉœþ„g§f£mG”YˆµÝlÁß¾÷ÁõY8ÈKX¡¬¥D±;°õ” uº¾µ.‰4 å ½Æ.Á–GqúìiüóçLô¿ êZmÐÄR†0.®=‰j™b=ä‚RÉó…ç…ˆ*5EoE-ª;¹¡”ôôK’³ÇPÚpùͧf4I¡ôs»Jd®/&AÐJê]Þ\­Lɦ¹4ìO+îBf†Ý(@;LJ*r õñ¨B6f$•£R•[Ó„€Û¹<@ ¦‡'Rc[CÒ’TŸCWUDDJ ›èDzoLi’µXCq©¤úPm+’–¥ÚýÖPDöÚzäߎLmRЋ76œ½ý†Q+#2±r¢ÚÝçÓ™Úס.3÷Q Ï#•»Ì£4ÜZüf{Ôt=Ò¨é–_’æÖ^5]Eø!³âú, ꇻhd=2UHInbGµ»ÿDÿÜõ@™êÊzÔª•³4&}‰„ ̨aŸ…tìQnç Åû‰<],©„™ ÕõÙD׋ðœ^øHDå5Ž ó7+HÍú›°Šèé$‰N’ê¤jD´4m±Ï‰*?ÖtÃ.aàüyO?’y}KwCÆOðg»¼î"MÇñ‘.h}}®ô90ÆûÊ‚0ÀE™:I©“T:I­Mm"¿Yw.äz2±ÓÄ'À¬ã,öqú¼Ó|!d<ÃÈ5˜¹éZþÚ¥90eŒ1Æ>NY‰,aÈäFŸº=Á/ 9aŠšý~Ãüަy70Æcìýð°>cŒ1Æ{§5¬Ï”Œ1Æc¬Èàà”1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1VdðÓú˜˜ø©‹ÀcŒ±¯˜‰‰É§.ÂÇÁi| cŒ1ÆØ§ÄÃúŒ1Æc¬Èàà”1ÆcŒœ2ÆcŒ±"ƒƒSÆcŒ1Vdppú…QGΆ«T€ ¤uàwKýöÌÏ6ÁÓ ÆœTæxYu~2\ šà·¹þªX¤ÂÙIÎ0h¹ñ9Šœ€nÅ ‚¦}ö#ãS‘1ÆcùÂÁéG§Ä‰Ñv0h뤰vq¥)¸IP]›‹’wd–Ú£±wÔ0ÿÃ@9Õ kbîÕ<‚áÿB} ?}k€º oCóÖL"˜¹¶‡wGÈÝ×eè°%JÇno£Â-û"Qò%lšÐnÎV0Ö—£x™jðœ¸Qiº¹T¸»2ÚT²€¾!¬\»àçãq¹þ@´:z Z—CÏfŽ)sÉÀ€”3X1Àõ+ZÂ@,†ù€ dê,VGmÄ ¦®°/i©ÔæNîè·ì$>—kçíõ™ys¦÷lŒJÖ¦PÈ‹ÁÒ¥)†­ Çs®ÏüK»­¾ÍábQ 2¹1ljvÇ‚ÜÛdDàçz†I]1;R{~L‰Àš~õ`oY Žõ`ý•´ÜÞùUãàôk¦_#WÍG×râ÷\Ñs-^…{-Æ`@…÷]×!‚×\¬ò­bŸ`ëìËACq ¢ZŽ[ŽÁG°cV<ù½;ZŒ F²6êúRtëúžy®Á©KǰÄífwèÿû¯]>©nâ·Á‹ñÌÙŸâ[ñ¹ U2Rd.ð3}ªä2»¡Z‚2Cá÷g BBƒ°z %B&´A¿ò¸`ýzå]Ÿ„”sp\¨a~›qèè>,é lŸíOr®Øk2qvvGôÙ" ÇÆ0D]ÁŠá×¾/Öß{ýˆLÃùyƒ±ET ÙÑ–1kFÁOã‹ã±1ž‰ùcýñzóÁÞNÐ&1 )9CÆÌX(À@y.ª¨ >€ô5ȼM;ǵ  ’ÊȱuYq‘”DD”AýK’ äHb²B™DDšD YÔ‹<*[“©BF Sr뵘Bã5:I¡Ëë‘»£)éKõɸtr¸‰nªrEum.ÕPÔ¦¯/ "JßN^ríöÅäš™c±òÜTQÑ€Æ,Dõ¬ InX†j÷]M—’sßmõåÔÈșƟJ×-ÝÝ7‘š:š\aAU»/¡ÛQ‰Þûèe®TÚÖIA¥ý2´ïÐÜÿ•<é‡sJ"åYšTAﵺA0¤îgmGI—fT!=í2Y‹5§[U¯v˜v{×ÙvA$Ò†6 *Ûa,hRÊZ˜’¥‹'Í:ü„4Úýøß ²dØ|5Ý×Ù¶êÆÏTK¿"M>§,ðYQ’AÇGÚ’ÔÞ—^~M”6щdNèLÖ×&ùõ+%§º~·H­ó¾+~îT±× ù©&ɬ‡ÓÑÌÜÖϲ©ïÑŠÆr*Ù?0»MÈ=_4-©/#³¾òÎ÷µËo}ª®ÐœêR*Þc÷h¿BªHšå*#ßPÊþJ+Ãh¢³>¹-¾£Ó%‡N¤êÕÆPp /9ȪѬË*"RSôÒFäÜ'€îe¤Sì_=ɹéJº«}£:é)%©©È0B×ÕÒÆy.Ú¸ÏNZjãBcmœ¨ÐÆRm)ÖÆ•Y1f®¸ç´Ðžl¾þ@¯mqçÖEZ5®†YÃÜR4[ýjÊDÈ([(Úl@"ˆTˆ^ì P*â^˜ÃsÆ&_ºÈà%h;žƒ¶"N{I«¾ù+ B©ñû…°½‹ÑÃIUAŠ*ë„¿Ò”´meoÉ“yl“côžKˆœ›#Ãá5íÞ|HÇ©_–ãB­‘ZóÕÊ4·E¯ïGª—?ÎFÅÂʇ°ê@Jþ˨÷-~ºª©®bNuêøÝ‚š¤yŽ?ÛemG•§‡CIJ„Mtú€½SÜ; Ù”ãˆ~tGGVt„Í€>ôè Ë͸›u髯µ¿¶"¢bwt¯Ê¿sñyS#%%"K+XˆP<.žüÛÚø&ë¶E Ô©Dž»„tíKá~°º,fÍ÷DIne &1Ák±ó²®5*ð¯ÈJErªËÒ¥¸‡??H•J€D*щ¬dIUˆ¼™} =?Š)ƒ Áâép3Ô]¶}a´f>ÜÊZÃ}‰Æùõ‚Uúúù ÃÀ_pþ)÷as³Yh4ˆ¿{™æÕѤ®#¬¬ìQ¹‘7Æ÷¨žÿTTí§ÍÇðöõQ¥œ 쪴ƤßAz áÚûÕ4îâ¾ÆušÖ€}é2ptm†¾£;Á¹°[QIx͘ƒŽUìàXwüÆÖÅÝÍq"3g6z¼ ýÓÑeŒ7¬³&5¢¶ûã¤y_ÌŸÞ•ìœÑxTýq™ IDATÌOèWþó=ÜdîC1¶ÄЇSo_xéÁÏciMot³?ƒ-Ûï¼fTEà¯mQ¨þ}·Âÿ\ØG•ye%æîÐCG_oØ‹hð$0)iŠ” ap2¯Žéa2˜™I‘ÿÏ@êY̰v³ ½ù[;XA¨o¯ž>ôôŒ`ßj5dc÷aË@[>½75bÿœƒuqîíãÊÁ~~è9­nIÜÙñ+þ¾“ ¢dDmóÖ(BúÓ¤JÄÿ&ÆÿÜ—`FCÃ7»‹U…ÏúSˆ~üwŽÍC¥sSáÕeÂ+LÀæ­SÑØŒÛ þn1;ôAƒ' àQÉOÂÿãø7ýÝï|E‰Ø}ÓáU˦ Ä"Š[ð,ùR´R’ê]Ñ·jÆ×¨ŠfÞÃ1uÉ6œ}üž´—C%g¹ö,*V€é³Û¸•ãqx"_ŒÿÙƨ&År¼~ûú-*£¢Tû’^yTr–âó$‚™ƒ=L²Ú =[8XkpçfôËk½oÐýûopiË6\WÊ [ñWl]xwæ“ççLý`7†vš‡´![°¢ƒyŽŒ@ÏÀ eÊ–©B{³)8ùcltœ ¿Žo³bóöî;:Šêmàøww³›MB !…@i„)R”¦ˆ ½KA¥ƒ€ˆJUP@E¤# Êi¾‚Ò›@èZ" %tHHÙÝçýƒ .4JÀçsÎ={²swæÎììÌ3·ÅJÛY¿²ã×5,Y•Sã^gðò» @Q÷I¸°zß™¤FSÙv: ›CH^Üä‹°g9ÞY{˜óR·€mŸw¤B™Ž,:÷ï\¦o¹Ù^^ΘI'x±W‡¿PCh¸ãÆ-âÈ¢7Án³»”ÍŽÝ.ÈÍ7LD5kIéƒ_3ow"›ç-àlÕV4È£?¯G•ãô2zÔìÀÖçf±xHEr¤Ÿ¬F?ýáBÂÜ+¾ËÊm‹èVô:çΧbñ÷'‡ã$ë~9Àñù­ó°bµzQl௤ŸÈó9Âè²2õžÛUwcÁ?"š'JV¤^¯©Lhv•/‡Íâ˜"ù‹„KëߣN“ùød)ãki0ð UywÅ1Î'Äq8î4ñ«zrñÙsá‰ØÕkˆ;4‰=°Z­xWÇÑÔ¼W*%ß‹Áve Cê5bø¾ ž?ŸÁE7Ñ£|AÁ…©ñæ÷ÄéÌz>f6£wt¤ÿج›Ò V-fým]-&¤¥a»-³íÞÌkMºt«LXv3ìü¶?öf­éMn9)T¥ o¼3‘%k?¦ZÂ2–l¿õl6X,˜IæúÕܺ°fÏÁôYAœÙ·Ÿó¾Qä¿ÙÜààØÌX½½êùßhº‘¿P~äÀnö§ËËÞƒ®7fžî¤$%ß úg~çÌ7&ØÒÒþFàz£PÚõä{ŒîMá‰xâO]æÎë‚p~ïž?FS&ïgÏ#Ân6ƒó5¢Õ3Çùfê'Ì\x…­^B[fM޳+èU³5¿”ŸÊÒ1/¸Œ² þ”(Fò¶MìN?Q®ocÓ.xâ©'±šÂéðõ.öìŠ!&&†˜˜í,ê^sPs¾Üòƒž~T[²Ááäê®ipú—7¥NÙ䵜©ÍÃø³YUF Xs”cü|æmt§ü³%1c¦DßìÙ³Óy ˆá×é­Ék.Äó·° saܼKÐuæfô¯E¤û>Æwù¿a[8û=õcûÐoá¥,ZQóïÑà4ÓØÙ7sïM_É®¸Sœ>¶…olâZX4=\ó™È ;—³8ö"דSHuF©¦ˆ‚D$þʪ­„ÄýÓ0aû-ƒÒ6O¤÷óX»ï8§ObÍìEì2$:òÖÞBÆà”ôå»i+8xò4gÎ'ò@3:˜ÿÞ ¾ßÇ‘“éóÑò5oI…ô{kòÆ¡\÷Δ¾cP•‰¨Æ­)z*o ]ÁGY;n0Ó¹ÞIÌ}ª)k°üŒRãY8~‡nÙeÊEx¨…ØŸ~`gB"ÉÉ)¤=ð É¢OGÖÌfÚÖxN>ËåÛgãOÛÄ»"‰ziýßû–q¼9q‡âvóýÀ÷˜ožV/¹4ÛrQ¯õs$|ñ.3Sjñr mÒ}É…Ÿy«fæøved‡Îî¾qsÙ¹;þFRÜ(ѦOø‚>ï/a÷¡,x{_§oQL¬Æ[ב.õéšÏärœÒݘJ*´åøb”ø¸[Å¿Hyå¹c¶+‹¤U€›wúQ’îvXT––¶¹¯p»óœ2dûã7(’&¿ýð¶Ô* žî^T¼‘Œø%}j±ÛÙäÀHJêžœ¿½;®GÏO’Ó»Ä-ì' Ê’<¾žb6{ˆ_ÄÓÒô½$Nç‘ÊØŸÏãŸKF×ÿç'ÉéŒObuû©ï¥{…Hñó´ˆÕ7\ʵ.?Ì`ÚF§Ôõ®SIÝÎ!çV—ºÑA’Ó?TÊwœ-²ðœ^üKSIÝ«rÇàòš¤¦¿¦¬&ÀÍ%™]’Å%•‘q÷Ø–z”Øvñ~™Š,mº…õý Ýßô#r†/jD2¾Ü:v¼ÿä#4*ô3ë38r±éS~Ý…ãä$jþÂ?ìcì3w›£K)¥”z4 †îÀ Õ%¥¹$›K²;“ÙÄå5½çB†=´Y_=0ǹóø7˘Ž÷˜[Tβfö,VíýËI‰]4Œ‰[¢hX?ú Lï“-‘ §öòíàYÑšvå50UJ)¥þªÇ.NPÿëe,q3^­K¥"Ax™L¾º‚T—Å©‡¾gpëªÍ燧5AÑÏÓejŒÏ 9øýý¨]¦AÞî˜=üˆxº9Ã~<É+lûDáfpV`¸$s±wˆÑËðý¹~y½^ :W6Ü­>„–iÁ¨µ ü“xûnÞ/a¹õ=iº åÆòÄ]Ly¥A¹‰ªô*Ó÷^¿Û–þ³t*©ÿ2òtŸT>Vt…c&q¢æh^-lÊ„õ)õpÈ™õ,Ý•“ûŒçݹ‘ƒóx§w j^óeç§ÕÈȹÿñÆsmYód_>\ð<ù=®¿÷ ٠Ε$oâ݆í™6„¯·ÖÆkópZw®O¯L®åƒá^øÛ5Ý£©Ûû%ŠOèÌ7·.%ñ×¥¬1T¢Ëè~ÍçΉ%ÃéÕéÎeÛÅÜ&z¤²uhCÚÍ að¬-4/`g÷´¼R¿=þ1‹h—7½²ÇÒo-fRÓ\7ÎQƒß0 à nJF;z±&¾6ŽEyáÍ™<·¤ÁZ]x_ ÎdL€°VÀð| È „€h P¨t“ÿ‚Ô#² OM‰Îå%kvÉõ´4°CÒDD$E–w#·$“„õX+©""Ž‹²öã6R­X>ñótO¿H©ØfŒ¬?çpÙH¢ìžöšTŽò‹‡øä),•;Í–C¶[‹bÛ?BJ{–“Q·/IþV[Û7EJ¯õ©·,Nûõm)âùŒôþø5©Ï[¬Þy¥\ûɲóZÆ»m?:^ªd/$}7&»¼ë ÇJËr¡âãnÏœ!R¬F?Y|ÊîüÌ©h¹ýX ¸×Égnì¯íÐ×Ò«^9‰ Ê.VwoÉS¬¶ôù6V\·bÿýGy¿A Îî.îÙ$ü©:2|õ•?û¦þ²DÚùe“çº :ÅB%È?P¢žë#‹âo|köcŸÈ³^‘Òs­ëV¯ÈÿÚäßú³%Á‘ñjÕã"EÖtKD/¹ñ3±ÉÁÊŠg±A²-9ãO$-}Er¹?-£Ý8×Å‘ 3ëfï:3ô|¹û ™PÕ*–KʽòÙöʰRÉÑj‘Üå+P®®|%õ¬V©3óR†‹Ó¶ hÏ22ò@÷ u'ÛRÒ]"{­—›wδ-òV!©8æ¨ØEDl»ä½'=¥Ê§ÇÅ~Ç ìrl\)Ôn¡œHI–øoZK¡ç'ÊqgFû¥ réÎe@7g\WÖçE;ã¾pgäŒ }œq¢§3n´8ãH“3®L13¤qz¦ÎÎíIû™Ðæë=¼ƒ'½AIïôfn 5&ŸÅ.©¬í†çK3¸(‚ˆcc*b$®R÷ÝÙ¬Üy=+ÇR%~u_›G‚ó1×~è3:u^Aî¾KØs,–-‹ÇЪ ;ÔãÞˆo® riuÜï’'u_~m¥çÿv²kù@BîJãwÖqgãC2?Ïö²Ýé\Æee¶­|Ðömv–Åϱý· ©‚ÉYPcxÖ¦Ž3“©áY€¾›Ó$y9oœ¯Ž 8J½Î§‹6±?v+ßtóeQ› Ýž¾·I,ØšÎÔdòÆÃÝý33ûU!;Öâ—ÄÏKOÑné!Ný¾yЪէ²ƒ1´1­Ÿ=Ç‚9.û~ùGæ.qðb«ÚøkµÍcÎNbb2Æ `r™¹Ä†Õ{É÷L^Ö¿VžÈ\©JÇIÛ¸,7òݶƒKyÊR.Ôyy5øRº\ARc~e¯6›þ}’ĵ$Ayr£í4÷&É§Øøå\6‹Pº¸W9’Xýå,âË·¥E”Íû"6l6f‹Ù%²rÇÝbcÏö=.]Rll}¯ ~^Ùð /KÓ!Ë9ž`$¬ýÇôt|HÅ|TëFŸÑmN>ÊòÑiÐéS¶]Ð>+÷¢5§Ä&{‡–'ÊöÔ{åK•µ=ÂÄó¥rñ>Öš´°¹øäî$?:«R×t—Pë³òIü½­îYsšîÒ,©ãy—šSKné°,ÑùŽ]ŽO¨*Ao–#ãô,©ïŸO:.¿zë‚ë ¥¹wi0ç¼Ü«²Èqf²Ôð, }7§ÝsnìÔ^ZÊSžý$þÆÓ¨ã´|ñ¼‡„túQîR©ûçR–H;?‹”±_Ò”m÷ûR£” Ûk‡œŸÛHür·—%×äÆßsJÎàWeÙ_Þ¨zT¤ì-Ïøä•—çŸ¹qÛöÊÐRÉž3\*¿5_6ïÞ!?o*‘îÁÒfÑqHªüÒ5Ÿ¸—&{~ûJš†IµqûäÔ—/ˆ»WS™ýaïQv_5§6‰›^O‚ükÈçG´¦ïnç§ËKž&1 bÌYVz/9!]a¾‘¦þ¾RvÂ=¯ÓÊU¢üßk!bÉßN¾9’(ÇU98»•°º‰µÖ49ïûqYòÉX™»b½üºuµ|;²±ð°J±›åŽK@ÚYÙ<µ—4¬ÝVFüpX3ØbV‚Öœ>jLD5hÇ3gGQ­heš¼ÞŸQ3×ð[òƒ¬#øÓ¸l~žfLFž ærùÚURæRÍhÿä.ú–~’-»2hì×l=“vïÕþ¥ÝÉOÑBVçFr)Œßå#>çúDgcÏcø¿ˆ×éQ=Û­Ÿw†6íòòc‡b”®Ý–žïMdÑî‹8 réW&½þѹ³ãn2bp‹fà¶4®]M¼Q3jà…¶õ1ÍlHÑ xµÏp¾üñ×ø¡ÓLxþЛµ0¦°ü„q”ƒGm€œµZS[~`ΗAX [R%£Šõذÿ¾ˆÎFrý¹LhÞ·Ñ8„ë!mùdhCÊ}’jÇѯò¾Ÿ»Š¤ô ÌÙÉBpNëÍujEûß!\X=€Æo¤î´é¼¡5}wcÈQ—ñ[w°mýLh"Ìîú&ßÄß~õuðû·_²Ôômëøë¹yß<©öþtúG¬¢mT¬Tžœ‡¦u0Œ €1//víN³çËSê©ghÔo6³»Gp`ÊÖ¦W­Ê5.Jë:˜'Íøì»i¼U+χ¹kYˆ§™È\¸3Kícñˆ”på«^Õ)Õx*w\îÂ~x­šO&©ÑT¶NÂæ’·Æù£™Ú³ï¬=ÌŽy©[ÀζÏ;R¡LGûwšn¹€]^ΘI'x±W Ý~Ÿ0øQcÜvo˜BçB¸òˇ´(S~k“¸?‰üد>½7FñΊÃ\Js ¶ƒ|PÖ̇7#!Í¿b_ìJ>éPžœ¿ÿw^*Åóî~°n6›Ë—d·aáæA÷®N«úV–ÍYƹß1wunš¶|š»õˆP>Çéeô¨Ù­ÏÍbñŠäH?ñ9ñËiÄExú9oð%_ˆ7)gÏrYŒøúÃù³\ð«Í¸õ[˜Ñ2—΃_ ~OýEÂ¥õïQ§É| |²”ñµƒôæu/&B‹<Á“O¿ÈkŸN¡söïòù¶[¯‹öXfOY‹_ãv<ïý° úh2TåÝÇ8ŸÇá¸ÓįêIÈÅKdÏt—àÒB‘â…0^üß®¬aH½F ßW‚Áóç3¸è&z”#(¸05Þüž¸ ¾éQ£¿ïLfôŽ BƒŽô»€uSšÁªÅ¬O¼-ÉiiØn‹'m»7³ÃZ“.Ý*–ÝŒ;¿í½Ykz“[N UiÂïLdÉÚ©–°Œ%Ûo=›  f’¹þ@5·.ì‡ÙsÐ9íÎìÛÏyß(òßìdéàØÌX½½êÝí©ÛB®â5hÙ}S~ZΠ'âXº|ÿ­H“ #6ÒÒnÛI{[·^ xë7i\,#|ˆýÇî ;=ò–æ¥voòÁWkø¶K.b–¬âÄmökgˆÿsÆÆiØ}àf¹ÒìáˆHºêA¥VMðÿi6Ÿ}1‡ ‘ÍiQòÏæéR*ÇÙôªÙš_ÊOeé˜Èåz•4P¬X0”øôîär™“'¯b  »ÁDD©øü¾…-¿9OB¹Ä¯›b)þ”ކþK„ˇR§áLòŽZÎÔæaè¯ïA"®^¹zKËUê¯Ó˜¶3œm+`½ëgÕݰæ &$(;ÆøùÌÛèNùgKÞåÜLãÀžƒ8|óÇËÞ%è:s 3ú×"Ò}ã»|‰ß°-ýžú±}è·ðÒŽ›xüè¥2ÓØÙ7ó¾uT¥~å"Êq–-ØÄµ°&ôpÍg"_D(ÌYÎâØ4 õÄäfÁâfÀQˆÄïXµõ"/Tô!iÿtLØŽð›ŸNÛ<‘·~ÉI½—*å›ÌÁy‹Øe*È ‘·~•Æà”ôÄwÓVФWI|¬ÞøûyÝÿGóßD­<]xâÚ ~´|Íß§‚ŹjɵÕ#õ³uæTÁ 0Ty…¶‘Uù¸çXJ|PÏÍú<ç×ÁOÛNï$‰œÜ˜„”3œ¸æÀvþ;cbpÏž"9IÝ>Šzu>âz«Éô*v}11Ëõ&æÂqŒÙoŽäX‘(W(9Í–¯†2f_nš|𖛯±ê˯ˆ/Þ•VOj¨ÿ '~à“¯¯P¸\$^ç71eÐ;lŠìÊÊú7*jÒ¶Œ¥Ë·*Wy’'×~Á;cR¸çT*Y¼ñÉá\™8°;Œ˜Ý͘ÜÜ±š…´´›/ü¿FD=»û¶‡Ô,)ÞqóBÕ:ËŒÝwŽšq\Z/Ô+*&1\§’º,[Ç·’Á~’+4¿D?ÛA&~ÐTü½›ËBçœ)¶=S¤]µâêï%K6É]¬¶ô[x$ƒv9µì-©æ-nƒ˜K “}6‘4ÙòVAq»cJ+ĽæIp¸L%õQG)ì%îÞ!Rî•)²ëfOm‡œžYOüB;ÉŠ«wlø†ÄMòqÓJ7§xšÝ%GÈSÒxèOrêŽq\irdÞR1,‡XŒ†[¦’J‹ÿ^Þ¬ž_üƒ%¢`)©;p¢ô.ç)%‡î½1xÉ‘ ?¾[OÊÈ-9¬f±úFHŶŸÈ¦ ·wíwȩϫ‹»Á[Z|wÛä3)K¤Ÿ·Ô4Jš w‰¬Þ[¾‹»}ø€MŒ,#fë32öhžçCý-i›ûJ·;†lüERäÈ‚>òbt.ñ4[${¾rÒzÜFq=í®øJºVÉ/¾VwÉž¯œ´ø«\Ö'KýEºæ3Ýy=z~’œvØåðè§Å’ÑõêùIrZé­gåÿ†5—ÊECÄ/›EÌî>R²®ô·ÿ–6Ž„¯¤¯—T›ø[S©?c?õ½t¯)~ž±ú†K¹Ãå§“ гí›,/—Ë/Ù,b2Y%gDyi1üG9‘á¸_‡œ[=\êFINÿP)ßq¶ÈÂs¤ñ/ ˆº×s¼Áå5=HMMXMܨ}MOf—dqI%DdÜ=¶¥%¶]¼_¦"K›na}¿Böt.©KiŸ§9)_žÍ FÕU2kºGSû@?ö/먓%+¥”úÏ1 Ý@ªKJsI6—dw&‡3‰ËkzÏ… {0h‹ˆz`Žsçño8–1 >Úé}R®$pj×Þ›}•?kH L•RJ©Œ§êƒªðÆ€*»ÿ9ˌơ¼±Î2¯|· ýtÊ¥”Rê¤ÍúJ)¥”RêOý[ÍúÚ@©”RJ)¥² N•RJ)¥T–¡Á©RJ)¥”Ê248UJ)¥”RY†ŽÖ/^|ØEPJ)¥Ô˜¯¯ïÃ.Â?NƒÓð_8!”RJ)¥&mÖWJ)¥”RY†§J)¥”R*ËÐàT)¥”RJeœ*¥”RJ©,CƒÓÇž0¥&^ú²)ía—E)¥”RêÞ48ý×¥±®g8^ufréaE)õÀR4ÅÓhÀ`ø#YJ¼Ïn»3ƒ$°rdj•+€¿Õ„¹ÈÛüj{¨E~´éñÌ\z<ÿ¾ë™×ë¢seÃÝêCh™ŒZ›pË?‰OÜ3ƒ.Ï&ÐË‚Å;7O¼Ø›y“ w1å• Då&ªÒ«Lß{ý¡ìFV¦Á©RJ= £c&mÝÁŽ7ÒÖ¯;er.”®$ûPªùÛ lŽéžkRJgæÒãù7¥²uhCÚÍ5ÐjÖb¬eBÍ3Œ®ßžé'Î,Ü #ßy¼ÊüqÛ:ƒ&)3hÛd$16qSz0ÚÑ‹5ñq¬ìšÊ‡oÎä¤ãáîUV£ÁifJ;ʾ/R4(î9*Pžfc¸ñPšÊŠW1,TGÒâ6ø  n„÷\G€\bݘ¶T/Š¿—/ÿüTj;– çå–ÍÈ•¦u«Éy²ãáåGxÙ&ŒüåÜͧ¶äý³x½R9<²‘·\GfǦþ«‡A©ÇžÙŸüÅžäÉ'o¤âraM_fÌKýwÇñ~÷–TÍŸ ÃÃ,çã@gæÒãù÷رbùaÉö† IDAT‚[ ¢÷óE {‚ÚƒFÒ>`%SçÇã 1lÿ-ÏwêÌ3ùó\èyz¾R CìNö¥W’Œ €Äá¸yÿv\¾Èe T58Í<ÂÙ¹=i?Ú|½ƒ£‡wðã¤7(éÞÖg¡Æä³Ø%•µ=Âð|iE±qlLEÌ’DÂÕ@ê¾;›•;²gåXªÄ£îkóH¸yæer³çèösgodÿþÌíÍ¥¸ó8R·2¤IG~ðëÉâ»XÒ/;ß|± O•Ê<’0!ÞxåÈCôó]˜²ã òçSJ=êĆÍfÀl1»öî¸[lìÙ¾‡TÀX–Š’Ù°h9ÇS@’³pñv²W‰ ^FÂÚLOLJT ÉGå±nô݆àä£,Ý‘>eÛ½šèˆÊ4Î?Ij`Mª—"Ø ÁÁ{UóPÿ]Þ¥·çßy51iÍyζ_¿àãU¹è¶n¯=e ,ß`Ê9?‘¶ù+æ.N·…]x&ʆòÞ²yÔþ%söR©ÿ:SX-LlAùb!x\ÜÁ¼¡ýyí…SX·ËËÁú¼¯ÔcÍ-ŠŠå1ÿ3¾{5šFá}=š¹±BrÈy’¬æR úá+.5hK¸÷uÜÄNŽò™¿¨¡F Û“tœ¾‘޶¶ÌDã·/ðÔk™óf$žy³½’fQ ÚñÌÙQT+Z™&¯÷gÔÌ5ü–ü ëH#þ‡Á4.Ÿ§“Ñ€gƒ¹\¾v•D.íÙÅoÙËQ©˜%ƒÏ Wæ´W¢CÓ{¹S¸X} Q*“¸•jÍÛêR¹LIÊÖx…|D­”%LšmSêqçIµ÷§Ó?bm£r`õ¤òä<4­€Á`Ähl‡˜Ù£ËBð¿ÛÙ¶vÝ>§ÉË“9tsàä5.Jë:˜'Íøì»i¼UKÓtœf"sáÎ,=´Å#ZPÂ=–¯zU§Tã©ÄßçË~x­šO&©ÑT¶NÂæ’·Æ¹­ÉÐp×~BLn.Á¨77“ö+RêbÈY”'òÂé“§°ÿyv¥Ô#ÎP•wWã|B‡ãN¿ª'!/‘=wž@êš± ^–—.czñb©"D—kÈàQ¯såÇLÝnƒ+kR¯Ã÷•`ðüù .º‰åà .L7¿'N§}Ôà4³½#¨Ð #ýÇ.`Ý”f°j1ëoËc2AZ¶Ûº•Øvof‡µ&]ºU&,»v~Ûë¬50àS´ù®lb힌z‘ÈY€ kq»ÙgÅFÜ‘xt¦¥þri{O@î¼¹uä³Rÿ¬9ƒ ÊŽ1~>ó6ºSþÙ’˜ûÅs\r0¸FXF#&IâZ¢€w ºÎ\ÂŒþµˆtßÇø._â7l Çc¿§~lú-¼ôŸïîÁi¦±³oæÞ›¾’]q§8}l ß.ØÄµ°h z¸æ3‘/"v.gqìE®'§êŒRM‰Hü•U[/"‰û§3`Âö[K·§:Ò«êi>i÷“Wïç·ãGضx,Ãç˜˼ÌËù73yÜz.: åè\ÆÌ=¦5:Je9Í‚·;1dÊ÷ü´a+WL§_“7Yl­M§F!Î ªp9~7;cvrðôuÉgˆÝÃÎÝñ\þ¯ßqþ=ž™Kçßå8ñc?šÃŠõ›Y÷¿qt¨÷6›";Ó·¾?À½l *{nb\Ÿ ¬ÚwŒ£»–1êí)|žš%Ì`ðÆ'‡óQVØFÌîfLnîXÍBZšÞ±ïÅ9ÉFÀ˜ `<oÀð‚€¼@8PˆJeJ@7yìÙ娷=¤f©H ô¶ˆ›G€ªÖYfì¾vGNÇ¥õòA½¢èaƒÁ$a=ÖJªˆˆã²lßBJûI®Ðüýl™øASñ÷n. “]>y‡LíRC¢sgw_ -ÝH†ÿœ çòëûgÉkÃ%0WˆD–j$ý_}Z<¢úÈÆÔã8(õsœ—oW—¢!¾âáfs¶ÜRô…n2}ç•›¿?‘dYÐÌK ¸$C¶[Çê~éñÌ\z<ÿ.û©ï¥{…Hñó´ˆÕ7\ʵ.?´¹æ³«GK« ’Óê&nžR¸ú2%Æõ:‘Î!çV—ºÑA’Ó?TÊwœ-²ð÷tsÆueq^´3î wÆAθÐÇ'z:ãF‹3Ž49ãÊôó®èŸ-s RÓ_ÓV7Fü§'³K²¸¤"2îÛRJ)¥”RY˜Á`èìR]RšK²¹$»39œI\^Óëé3¬¯×f}¥”RJ)•ehpª”RJ)¥² N•RJ)¥T–¡Á©RJ)¥”Ê248UJ)¥”RY†§J)¥”R*ËÐàT)¥”RJenžE¥»xñâÃ.‚RJ)¥þÃ|}}vþqœ>€ÿ ¡”RJ)õ0i³¾RJ)¥”Ê248UJ)¥”RY†§J)¥”R*ËÐàT)¥”RJeœ>ªìq|Û¥"¡9,˜ŒfŠÚŽ-ÃŒB”šxè˦´¹ŒJ)¥”RHƒÓ]ëz†ãUg&—þÆZR׎¦÷ #ÿ/ŽsαqÀ“:õ‚Rÿ’äCóy«n òæðÀ#GŠÖÎúëwæ³›Bín¡ÝX­‡$°rdj•+€¿Õ„¹ÈÛüšñ“¸ºz<ÿ¾ë™×ë¢seÃÝêCh™ŒZ›€¸dIÜ3ƒ.Ï&ÐË‚Å;7O¼Ø›y“ w1å• Då&ªÒ«Lß›ÁÅã?NƒÓGTÒñß8ï_ŠgKäÁ×'ÞVý*•ú7ȹÿñÆsm™o¨Ï‡ ~fõÒ nXl†Û2Úñùëc¸\(ÓC)écBR¸’ìC©æo3°±Ë¿Mçß”ÊÖ¡ i7×@«Y[ˆ=°– 5Ï0º~{¦Ÿp8³l`pƒŽ|çñ*ówÆqlë š¤Ì m“‘ÄØÄMéÁhG/ÖÄDZ²k*¾9““އ»WYF4™)í( û¾HÑ l¸{ä ¨@yšMŒq6·§²âÕ@L •ÆÆ‘´¸ ¾ƒá=ב —X7¦-Õ‹‡âïeÅË??•ÚŽeÃùôç1'&TÅÝ`À·õb’âÆRÉbÀ`¸µY?yÿ,^¯Flä-בٱ©á`(õ8²shêp¾ÎÑ‹o¾~‡ÕËQ¦Âs4îØâV×|©ìב‰Aï1âE?n[Õ0æ¥þ»ãx¿{KªæÏ¦ÇòïÒãù÷رbùa‚[ ¢÷óE {‚ÚƒFÒ>`%SçÇã 1lÿ-ÏwêÌ3ùó\èyz¾R CìNö¥W’Œ €Äá¸Yëê¸|‘˨jpšy„³s{Ò~&´ùzGïàÇIoPÒÛî\n¡Æä³Ø%•µ=Âð|iE±qlLEÌ’DÂÕ@ê¾;›•;²gåXªÄ£îkóH#y;¯"E\˜^Ïðž¬KDÒØ3¤äfýÔ­ iÒ‘üz²xç.–ôËÎ7_¬AÃS¥2\bÃê½ä{&/ë_+Od®‚‹T¥ã¤m\viÓK‰Í«“Còa]ô*«ÔãClØl̳K`ïŽ»ÅÆží{HŒe©X ™ ‹–s<$é0 o'{õ—¨àe$¬ýÇôt|HÅ|TëFŸÑmN>ÊòÑiÐéS¶]»oÿ?B»)f玟$5°&ÕËGl†ààŠ=È*Œy¨ÿ·.o„Ò¿ÛsŒï¼š˜´æƒ‹n¢Gù0‚‚ SãÍï‰Ó™=48ÍlFï*4èHÿ± X7¥¬ZÌúÄÛò˜L–†í¶ˆÓ¶{3;¬5éÒ­2aÙͰóÛþØû¬50-²A×â8v³ÏЏ#ñw™U)õ@ +ŒÄ%þf ÈeNž¼Š% €ìnátøz{vÅCLÌvu/Š9¨9_nù‰AOßGߥÔ#À€5g0!AÙ1ÆÏgÞFwÊ?[3`¿xŽK×ËhÄ$I\Kð.A×™K˜Ñ¿‘îûßåKü†máxì÷ÔíC¿…—î»Bêq¥­½™ÆÎ¾™ïð­£*õ+!P޳lÁ&®…5¡ ‡k>ù"BaÎrÇ6 Y¨'&7 7¦ˆ‚D$~Ǫ­y¡¢Iû§3`Âvl„ßw)Ìe^æåüU™CìÎÌ_І’C›…Ï¿Ëqâ>ùú …ËEâu~S½Ã¦È®¬¬ïp/[ƒÊž¯3®ÏŠ®M˜í_¿=…5Y 3>9œ+v‡³»“›;V³–f¿×æÿóœ“`L€°VÀð| È „€h P¨t“Çž]Ž}ÛCj–Š”@o‹¸yH¡jeÆîkwät\Z/Ô+*&1LÖc­¤Šˆ8.ËÖñ-¤d°Ÿä Í/ÑÏv‰4ïæ²0ù–5È…éµÅ3¼§¬K½³$×÷Ï’×*†K`®‰,ÕHú¿ú´xDõ‘äUJ=¨9² ¼K<ÍÉž¯œ´·Q.82Êk“#ˈ{¾®ò‹þþþ¢dYÐÌK ¸$C¶Û¯‹êþèñü»ì§¾—î"ÅÏÓ"Vßp)×b¸ütÒæšCή-­*DHN«›¸yJáêoÈ”˜+rçeÂ!çV—ºÑA’Ó?TÊwœ-²ð÷tsÆueq^´3î wÆAθÐÇ'z:ãF‹3Ž49ãÊôó®èŸ-s RÓ_ÓV7j_Ó“Ù%Y\R wm)¥”RJ©,Ì`0tv©.)Í%Ù\’Ý™Î$.¯é=2ìÁ }N•RJ)¥T–¡Á©RJ)¥”Ê248UJ)¥”RY†§J)¥”R*ËÐàT)¥”RJeœ*¥”RJ©,CƒS¥”RJ)•eèˆz/^|ØEPJ)¥Ô˜¯¯ïÃ.Â?NƒÓð_8!”RJ)¥&mÖWJ)¥”RY†§J)¥”R*ËÐàT)¥”RJeœ*¥”RJ©,CƒÓÇŒ}ÏPJZ   –§}Ø~÷Ì—gS×+?½7¤Ýò¶mÛ¢½ªóùïò—6³W6¦~Ñ<ÜŒ<2/ña—I)¥”R…§ÿº4Öõ Ç«ÎL.ýk7ÈöTÁ¶¥Í’ÙAÕ– (ø÷OÛž¡”ò.È}÷†ÿ)Ž“|5ð]bžú”ݧ/pñô,zýûÅP>¹¶“ÙýšP±P0>Vrä-AÝ·æ{Ý%“ý? kJé|>X-äŒ(OËÑkHp¤¯$•#ÛP«\ü­&ÌEÞæWÛÃØ›Ç„ÏL'Wb˜Öµѹ½±zú’¯D#&îÒƒzß®d^¯ˆÎ• w«¡eZ0jmTç8HX3š—Ë…áëáŽW®"Ôè1ƒÉÎʼn»˜òJ"‚rUéU¦ï½žñvþÃ48ý/ó(O÷IÒ,¿éo®è +ÆLâDÍÞ¼Zøï®ë/püNü ¡`Å*Dúûà“Ó?‹Ë•ʈœYÏÒ]9y±Ïxæ¯ü™ùCžáì-¨Ùg%×pprF ?D¹W¿Ÿß-ȶ é2ÿü›“¤p%Ù‡RÍßf`ãpÂ/âñ¢Ç3s9â™ÞòyzüHûO³úçELêSƒP«áa—ì‘ÊÖ¡ i7×@«Y[ˆ=°– 5Ï0º~{¦Ÿ¸ñ„*§çЩÁ`vÆ{ó]orÿø*õßÙÀuÄMéÁhG/ÖÄDZ²k*¾9““Ž?Ù¬ºÉàLFÀ˜ `<oÀð‚€¼@8PˆJeJ@7ù/H=" úÔ”è\^b±f—\QOKÓ ;$MDDRdy‡1‚pK2IXµ’*"â¸(k?n#ÕŠå?Owñô‹”ŠmÆÈús—$Êîi¯Iå(?ñ°xˆOžÂR¹Ól9d»µ(¶ý#¤´g9uû‘äo¥±Õ¹}S¤ôZŸzËâ´_ß–"žÏHï_“ ù¼ÅêWʵŸ,;¯e¼Ûö£ã¥JöBÒwcòm dݸW¤rñt÷’€¨JÒfâV¹¹š”#²°o­ÇË#§ä¯üšLÛ•¾Ô.Ç?­"žE:ÉGïÔ•â!¾’-G¸<7è'IH?)+åõ<ÆÛŽ'‚µÌ½KYï²DÚùe“çº :ÅB%È?P¢žë#‹âo|köcŸÈ³^‘Òs­ë¾]‘ÿµÉ%¾õgÿQõ˜J‘5ÝÃÄÑKnüLRdåÁâ^nÔ¿¹´_åí"îRdÀ6ço=]šì|·¸X ÷—­i·¯W=8=ž™!uÛå]Y>9’Á½Aý9ÛRÒ]"{­—›wδ-òV!©8æ¨ØE$iA3ñ¶Ö”)7o¹0«®d ê Ë“ìrl\)Ôn¡œHI–øoZK¡ç'ÊqûœöKä’ýßß­ûtsÆueq^´3î wÆAθÐÇ'z:ãF‹3Ž49ãÊô3CZsši„³s{Ò~&´ùzGïàÇIoPÒ;½™ÛBÉg±K*k{„áùÒ .Š bãØ˜Š7jú$‰„«Ô}w6+wdÏʱT‰FÝ׿‘àl/°úŒNW»ïö‹eËâ1´*èÎ5ȸ7â›ë‚\šE÷»äIÝÀ—_[éù¿ìZ>П»ÒøuÜÙøÌÆOdz½lw:—q]Y*;?¨MÍw÷R|Ð÷lÝË/“Û‘ëÌo\€4vŒhDËYZÍÚÊÁ‹èô¼^÷MþïÊk±ZÈŠlƒø%îñ‹pzt7Ælwî­¥*OÚ‘Ôô‰òä…/ÏâA®/ Ù5ë'ñóÒS´[zˆS¿ocDž´jõ)‡ì` mLëgϱ`ŽË¾_þ‘¹K¼Øª6þZÙ𘳓˜˜Œ1(˜\&3ÑŸ&ÇÁå,Üy ;6ÎmúŽ•¿GòbBú_MTgçØÚµüþDy¼ç6£T¾@òçÅ^s9 -Ë÷GlØl̳KdåŽ»ÅÆží{H6vƒ‹9=‡³».ì"æ¸Öþcz:>¤bH>*u£Ïè6'eùèŽ4èô)Û.<*ã=­9} 6Ù;´”x<1P¶§Þ+_ª¬í&ž/Í‹÷±Ö¤…ÍÅ'w'ù1Åùé5Ý%Ôú¬|ïG«{Öœ¦»4KêxÞ¥æÔ’[:,Kt¾c—㪊GPÇ›åHç8=Kêû瓎˯ÞVð¥òJ.©4æˆdXÒÔõÒ+Ò*¥Gô:N)5½|¤ù‚«’^sj h'K’Ò ¶Yúô”“ÏÈ-–©¥O”§¼ðåYyàŠÌ”%ÒÎÏ"¥Gì¿YÛî÷¥„G)¶×&"9?·‘øån/K®É¿ç4”œÁ¯Ê²û­U¬”=£åŸ¼òò|—sÎqI¶|\Gò¹›Äân“WAi9ë°Üù³×š¾Ì¥ÇóïK•5ÝCÅ-‡Ÿyi„,Ù¶G~ýa˜¼Û] ö^'I¾%‰ò¯…ˆ%;ùæH¢8WåàìVRÀê&ÖZÓä¼CÄ~ô©ìå#•‡m’si"©gÖÈ»UüÄì–_ÞÜxÛ•"í¬lžÚKÖn+#~8,‰o4Ë@kN5&¢´ã™³£¨V´2M^ïϨ™kø-ùÏ?ù‡4âLã²øyš1 x6˜ËåkWIt>H™K5£ý“»è[úIj´ìÊ ±_³õLÚ½Wû—v'?E YÉU¤0~—pøœë=_Œáÿ"^§Gõl·|ÜþÛ.v_ ¥bÅ|ŸdIG8ü»Ež»Ù‡ÌàW”'‚“9rè$éõÍÆÀ`r§w 5xàéI‰×ÉÜçJ3áùCo–ÖŸ0Žrð¨ 0³VkjËÌùñ2H‹ç®Ä§aKªè «Çšý÷Etn4’ëoÌeBƒ@çUT¸øËÞøø u'ÿ̯;¶°btY¶w«G¿•—2ù¼T*ó9‚¤§Ëg}y±d4¥jõãã.E‰ûök¶ü·’Ç'ÕÞŸNÿˆU´ÊÕ#Ê“óдnƒ£ŒáùlB=.}\‰\^V¼ tâ`µ—)c6b48ã1¹ÆÁECi]§ó¤Ÿ}7·jEâùpw.ËÐà4™ wfé¡},Ñ‚î±|Õ«:¥O%þ>;:ÛO UóÉ$5šÊ¶ÓIØBòâÖø Üô<ËñÎÚÃì˜7ºìlû¼#Êtdѹç¶xËcÎå匙t‚{u Ð_¥ îþØtsƒåÉì}l6—/ÉnÃ.òÇf¼«Óª¾•es–qî÷EÌ]›¦-Ÿæn="Ô£Ïqz=jv`ës³X<¤"9ÒOBÇ æ ù„“5ßçÃV•x¢ð“T{íTŽã‹OsQ£S•¥ÉéŸSŽH"Òoÿ&ò†…`H8CÂC˜låQd¨Ê»+Žq>!ŽÃq§‰_Õ“‹—Èž;È\ºS¨Í4vœ¾ÀÉÇ9yj3ž1pRr‘'—®¬aH½F ßW‚Áóç3¸è&z”#(¸05Þüž8}HÐà4³½#¨Ð #ýÇ.`Ý”f°j1ëo›sÓh2AZ¶Ûnd¶Ý›Ùa­I—n• ËnÆ€ßöÇÞ¬5½É-'…ª4áw&²díÇTKXÆ’í·žÍ‹3É\ š[öÃì9˜âüÃÁ™}û9ïEþ›,›ù ²·£W=ÿ;HS¾bÍÏúõÇÉ06÷Œ *w{wÇݬ%•ó{Ù}ÒˆüÁ™>"×~í ññ¿q.)£¥iØ}àf¿Ý´{8@"Ò{zP©UüšÍg_ÌaCdsZ”ÔùW޳+èU³5¿”ŸÊÒ1/Ëõ*)—8Ñ~ÛC““ l‰‰¤Ü¹:¥²Å‹â}%ޏóé7;¿ÿñÄ_§Bx¬9ƒ ÊŽ1~>ó6ºSþÙ’·Îã–\!yñóHdͼŜŠx†Jyà]‚®3—0£-"Ý÷1¾Ë—ø ÛÂñØï©Û‡~ µFƒÓLcgß̼7}%»âNqúؾ]°‰kaÑôpÍg"_D(ì\ÎâØ‹\ON!Õ¥š" ‘ø+«¶^D÷OgÀ„í· vJÛ<‘ÞÌcí¾ãœ>uˆ5³±ËTèÈ[‡bƒKPÒ?–玲ààÉÓœ9ŸÈ=;˜ÿÞ ¾ßÇ‘“éóÑò5oI‹syòÆ¡\÷ΔΨ Ñ£]»cÇ–ô™³‰CÇãà¦oñÑœÀ\š/`Ï'½øè§ÄÞÀ=‡³Ê¯1­«gv{¹ðUK †¥û…vMéψ÷wàÿ9`*¿?õ2]ªƒ-¥[Ò,ßJ†ŽÜDtóf<Œ³Ô?O.üÌ[5›0Ç·+#;„pvw 111ìÜÏeLQT«ÆùoÞeà·[9w3û2b™ðTÍ*„Ëñ»Ù³“ƒ§¯ãH>CìN—u¨¤Ç33yUkGó¨WT=Lb0¸N%uY¶Žo!%ƒý$Wh~‰~¶ƒLü ©ø{7—…ΙŒl{¦H»jÅ%ÔßK,–l’»Xmé·ðˆ¤Ü±»œZö–T ó7ƒAÌ¥†É>›ˆHšly« ¸Ý1¥â^sŠ$8\¦’ú¨£” öwï)÷ÊÙu³§¶CNϬ'~¡dÅÕ;6ìR„³²nl{y¶@€xº{Šþ Òj‚ËTRɇäÛÞ/Há1»ûJD¥2iûçÀçTREÉŽôÁ¶]òÞ“žRiì±[Yýé€(‡œú¼º¸¼¥Åw·Mw•²DÚùyKA£¤IÑñp÷‘Èê½å»¸ÛG\ØäÀÈ2b¶>#c2² q IDATfáy>Ôß’¶¹¯p»ó·aÈöÇoP’ȼ޵¤XîlbvsŸ|¥¥ÉûËåøÍS&Y4óý֡€ÏÌvm÷4y½J”øy˜Åš3¿T~}ºdp«Rwa?õ½t¯)~ž±ú†K¹Ãå§“®ƒÓdÛ˜ÚòDžìbuÏ&AÑ/H÷™»äj†7(‡œ[=\êFINÿP)ßq¶ÈÂç5ÿÒ€¨{uù3¸¼¦©é¯é« psIf—dqI%DdÜ=¶¥%¶]¼_¦"K›na}¿Bö¤Ø©KiŸ§ùÿ³wçñ1]ÿÇ_“É2IKd!¶(b«µ_J©oíK-_j¶vªjiëÛj•Ri•RôKj©Zº(¥ýik+%‘Ä.öµd‘!ËÌ|~˜¤JZŸçãqydæÎsOîÌ}ßsî=!cN< [ßî*Òt6 ¡ùþ‘ìû!Œâ:æ ”Rê1c0Ñ@fŽ’•£Xr«½ØìErüÌ÷ÈuüC§åSyfKL»]8SÃÊþ½ƒé]2.$ðÇιüwÁEþýi;Ši0UJ)¥þ2NUž9ø6ä•1 u5‰g~^Ùd¦fï™Ljg¾ó J)¥”ºg:¬¯”RJ)¥îèa ëë¥RJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê74œ*¥”RJ©|CïÖσäääG]¥”RJ=Ƽ¼¼uþrNóàqØ!”RJ)¥%ÖWJ)¥”Rù††S¥”RJ)•oh8UJ)¥”Rù††S¥”RJ)•oh8U7ÎÎz“Á€Á`À±ÌklÍzÔuRJ)¥ÔãBÃéC—Ŧ¡¸·Œ åQVú÷ª»SgÊal7oÙÿ;Í$µÕ\¶Ä®'¼Þ!Æ·íCÄiû'\RX=¬5C61fõ"—¿‚ÇŠÞ´·•tl›;„ɶal8~Œu3™ôj§õËá®ìÅ0N€3`ÜÀð|@ PB§€úÀ yd–å#šIHwq6"Áµ¥ãôhÉ‘ YÓ§°8€pC1JÀ’)"bK–v—F•JŠÙÍEÜÌ¥¤^÷©²9ÑvÃÛØR£åóÏIÅ¢br+$5;È„_äúRYrrÍxé\;H ¹šÄ÷‚4~i쳈HÖvUÞñ¦: <¤Ë×é9ßEâç<'nÁ#dKæ_ÞrJým¤/{AÜ‹½,ë2îòÖ£^ßE¼{­–»}‰ºîòêÞRÄ¥¶|pÐzõ[‚D´* -çK‚íö¯U·“%±ã*‹©ü(Ùžõ¨ë’O¥/•&“´Zp!ǃY²íõ²âRv¤üž}l¼´Zz5IɇÄ*"¶„iéá!-æÅÛËV9üa]1é%«Ó¬rô£†R®ç 9•‘.Ç¿zQÊ5™!'í»·5弤XæFæ 0Èžëž²ç¼{î ´ç@_{.ô´çD7{nt¶çH£=WfgÌ\iÏé#Ä/J¯è¾$š#‡¢ùqÖ+Tõ°õáLÓÙñX%“Cpk1ŸdD,Z'¹LÂEZ[ÀºØì^NÃãïÒê¥/IÈî"°av§gô‹/ýlaß¾-,Bʱ¤k½2׿F“6ÓI|öÖî<@ìÚ©´2žä”p¬Î{{³Ë^Þ­æFíɇ°Š ¶ ,l­ƒøJÝ Iø’.~¸,FH“̾{/žíÇÖ}Îò]&ªÖ(¯ÿõ$Ϭ‰Š&¥ØSÔò·® ^Ô¨U–̘Höèp´zØ$‘è¨c˜ª×âÉì^U·Ô® »#cI,{#‰É,Kíš…ìéË¿š5)–MÔ! ×‡ µM¢ž_I„;2brwЧaÍä0Úöû„¨ó:& ß•ŒÄ“§ÉôiFã:Áw‚âѨ”—U8£Í›“r<àϨAÏ2­ÿzb²:ó¬3X"?ãß‹0hÓ,^ªî @@É·¨•ýIäëçpö¹ü:®->€’ ø¤ÉƒØH¥{Æ€ç3£ u*ùášÍ—ãGñÒs`Ú±”ÿ·(ëA&?]‰×·d`s,B£·¾gq¿í È3‰ñ‰P¨0^gÑ©îp_ý™…ÍO¢•«}1J=,¶$âÁ+ÄLÚÚTé¶…N+¥ª·3gãI(””@f {Æñqãg˜â=…Moú`&‘sI6¨X…°y[°$°í‹7è0ú<Õ_Ë¢WKáö¨·1ÐpúÀ nÛ“§§ §QÅ4~¦65j7£ã OSÒt·ëÈâø÷ãyõ/øy×IRÒ-Ø ]H!e÷NN¨EýJι¯ÂGì !ýëâý§æJ©{åXíEFWËþ­*5C9Z¦/³–¤ó`ÿ«ÔèO/"iš’Ìá_?eìû/óVÕŸù¸Yá?ÇRÎ`ÀàT€"~~¸2A†ýáG[+õ3ŽîÞ”ð+ÙÍ~Á^ö)öžOƒ ¯b~ø*ˆ‚`¸¾ÏÊ%|λ³vàÝ~Ÿ~]ƒÂšÈ®ÑùÈ©|VÜËÊ ]u‰cá°ÆTëð9ÇïòBgë¡étë<›Ëí?'êìe,6!}å‹x"7 ôKY©|ÂP¨"O–€³§ÿÀzíQg¼ƒBx²j=Zûœé.2çÝ/8ª7=ä‘Þ>ÞÏyss>Ú¼ù]K’’fÌz7zØÌøxÃù„ó¸ÔǺ¨oTñ ‰I™8{{SÐÞ>˜I"!¹$Ý"¶±)üyÌÉ œÇŒÙ.làÖíyoo(o-[Æ[·2¤N¾ÅËÓôÕo8¦Ó7j8}Ð<‚¨Û6ŒQáËÙ4·ü¼’Íi7-c4BV–›.+±ìúhS3 j@@' X9±/ÎÞk `À³b%J^ØÊÆÝ™¹WÀ± •CÙ³i˦®1b4‚%+ëOîx5àììéWHÏõy¥€¤ìeÏ)(Z¢èŸÜù,Øl‚\¼À% §yd$¨Z(žg¶±íÄõ;¡#?€såê„hO“zØ Þ„V =j+»²Cä•(¶î„'«WÁ8V¨NçlÝžl?¾Ú8µ};g0VöFŒá¿óÖ±óØœ=º¥Ë·r) „²®9—3R2Èb×°2.™+édÚSª1¨,Ai‘ü¼=AHÛ71ÓwóšÇêa {æ,÷|…Ùë÷qâäa¢V†óÞ—¯öÚ¼i3´¾?Œ Û;ßuä$Ç÷n bì4Öç̳Æ"ú;÷ß›FzzY78Ý+W#8a Ëvqòì9/êéœzÌÉY–îÇ;s¿áÿ~ÛΖµóù«¬45§_{?°3ý%MšÏwë~cû¶_Y6¹7æ#¸Åó”Ó0•g® {Ó£Ô>Î/ûâø}þpƯ)H‡~-1ëÒ=Rï"6&–g¯`K?G\l ±»Ž“ú¸'¢lö6‰‰=FŠØH=¾“˜˜Xââ3GB»÷¡ú©Ïñö*vŒfùè1,ÉhBX§À«·¡›[ÖÁ“Çåó-Ø·~æDÔ£ ݃ží§²bÃjsÀÉÅ ££ &'!+Ëz›Ê)J*O¬rtéiV­”øx8‹£ka)ר¿Ìßué–%m)›ebëŠâãjƒ!çTR©²}Z©ZÜ,EüKKÈ¿úÈŒ‰ÅÛ£³¬È1Ë“-5Z>ÐTBŠ>!.®^â_£½¼wÃTR™rbÍxé\+P¼L.òD‘òÒ(ì‹«SIåpyç,éZ­˜<áhÃ-SI‰ˆ-Y6¿ßBÊz9ÝXO¥W¶$Y;º±TôóWG£8=QT*>7HæÅ^¸6e̱#¥mrRÌËMœœ\ÅT[:þ÷{9¦óHݳ+ûÊÀ†¥ÅËä"JÖ’3"%U§‘ºGé²¼“»nšNÐðÄǙǙe×;êtó´NRuü¹zÍ’ß–ç+7wñ­Ü^&ü/9wI[j”|Ú³–”,à"&¯RÒ`ÀÙw%·w³Iâú÷¤Uˆ¯òö—:a d>þ;𦒺Ýy§!ÇÏìšý3;°¹zSUvqÊQœs”Pùè6凜RJ)¥ò1ƒÁ0ˆ2s”¬Å’£XíÅf/’ãgv?}®ýõ:¬¯”RJ)¥ò §J)¥”R*ßÐpª”RJ)¥ò §J)¥”R*ßÐpª”RJ)¥ò §J)¥”R*ßÐpª”RJ)¥ò ý%yœœü¨« ”RJ©Ç˜——×£®Â_NÃi<;„RJ)¥Ô£¤ÃúJ)¥”R*ßÐpª”RJ)¥ò §J)¥”R*ßÐpª”RJ)¥ò §WÖc,Pÿ‚Μ¨øÆ,dÅÂÙYÏb20 8–y­YdÅJ)¥”Rw¤áô¡ËbÓÐ@Ü[FrkÉÜ8™áóûé‰çÙ2¦JÞ¦^°îã½êîÔ™rÛ Oðí÷éb#~Îs¸ÜG•úDZÆ1©–3ûÉÛõb¤ØËëÈäÌL›nzÞÔ˜™gäQ×þo+ýÀb7*ƒ·›+jÓkæ.hsÞI`ÝûÝy¾V¼MFœ*Œ&òÁôlücX÷D0°}C*ûÀÉÁ•Ö /Þ´„…“«ÆÐ¢bÜ]=(^µ#7$s—” ;˜Ù«žn¸™ƒyfÐ"¤ÛŸLÛÉÜÞu ò-Jpý¾ÌÛså!mÙ߇†Ó¿©Ë'Oä]…Ã˳ &ýS*õ—3Ðó‹íDGG_+;~K-·¢4o] çkËÓoIäõå¶Í¢ƒáQÖüï+}+ãÚõb™kK¶G³öÍ`6 mðÕ)h>½’Á…tOªuÍØu}ò![Ú%jJÿ‰ý©ëtëó–ýѹÓLR[ÍeKìzÂëb|Û>Dœ¶wõH «‡µfÈÆ ƬÞAäòWðXÑ›vã¶’Žcs‡0Ù6Œ DZn`&“^à´íÖ÷Q¹3Ø‹`œgÀ¸€'à ø%€@  „OõAò8È<,ËG4“"îâl* E‚kKÇéÑ’%""²¦Oaqá†b”€!%SDÄ–,?ì.*•³›‹¸™KI½îSes¢ÍþV9ùICq¾eŽ26Êþ>""YrrÍxé\;H ¹šÄ÷‚4~i쳈HÖvUÞñ¦×#<¤Ë×é96Æ&ñsž·à²%ó!µŸR;V9þ´¸—ÍW±þT¹=)oÆdÝþ¥ê®\^Ý[Š¸Ô–Z¯>`KˆVÄ£å|I°Ýþµêv²$v\e1•%ÛuWÍ]úRé`2I«r<˜%Û^/+.eGÊïÙÇÆK«¥wQ“Ô™|H¬"bKˆ–Òb^¼\ÝE­røÃºb*ÒKV§YåèG ¥\Ïr*#]Žõ¢”k2CNÚwokÊyI±>ÌÌ`=×=eÏy!öÜhϾö\èiωnöÜèlÏ‘F{®ÌΘ¹Òî¶Fˆ_<”^Ð}I4GEóã¬W¨êaµ?ïLÓÙñX%“Cpk1ŸdD,Z'¹LÂEZ[ÀºØì^NÃãïÒê¥/IJôÿ™ ±q~^s܇²)SÉb÷;U¯ ë_\ÿMÚL'ñÙX»ó±k§ÒÊx’SVÀ±:ïíÍB,{y·šµ'Â*‚Ø.°°µâ+•'–ÌŸ»à®ÝyÊ9ÇãÖCLΗ'ܽð«Ò’‘Kãл{aåHT4)Åž¢–¿ýpeð¢F­²dÆD²G‡£ÕÃ&‰DGÃT½Of÷ªºÕ veØK:`ÙILfYj×,dO_øÕ¬I±”h¢Ž½>d¨mõüJÒ Ü‘“»S<ýk&‡Ñ¶ß'D×1ýQŒÄ“§ÉôiFã:Áw‚âѨ”—U8£Í›“r<àϨAÏ2­ÿzb²:ó¬óŸ¾ò:Iäëçpö¹ü:®-WGK2à“&y©‰Rê.\Ù4—ˆÃÕÜ5äúð¨[y^?“aO•§ˆñ,¿Í}‹±ÿiJŠG43Ÿóüó®• ‰ñ‰P¨0^gÑ©îp_ý™…ÍO¢•«}1J=,¶$âÁ+ÄLÚÚTé¶…N+¥ª·3gãI(””@f {Æñqãg˜â=…Moú`&‘sI6¨X…°y[°$°í‹7è0ú<Õ_Ë¢WKáö¨·1ÐpúÀ nÛ“§§ §QÅ4~¦65j7£ã OSÒt·ëÈâø÷ãyõ/øy×IRÒ-Ø ]H»Û)K±{,„ô¯‹·•úëH kæ|Åùèx}Êàù/Â^½¾Xµ§*u „·fÿÀÄç:ãùªú·g0`p*@??\ ™ Ãþ𣭕zŒGwoJø•Àìf¿`/{ûÛ`«˜~… â„ ®ï³r‰ß…óî¬x·ŧ_× °&²ktXÿr*ߟÕ÷²rBB]âX8¬1Õ:|Îñ»¼ÐÙzh:Ý:Ïærûω:{‹MH_ù"žˆ^ø¯T>#g—3{¥•f=Ûà{»”dôãÉŠ^d9}µ§OåÞ>ÞÏyss>Ú¼ù]K’’fÌz7zØÌøxÃù„ó¸ÔǺ¨oTñ ‰I™8{{SÐÞ>˜I"!¹$Ý"¶±)üyÌÉ œÇŒÙ.làÖíyoo(o-[Æ[·2¤N¾ÅËÓôÕo8¦Ó7j8}Ð<‚¨Û6ŒQáËÙ4·ü¼’Íi7-c4BV–›§e×ïD›š1`P 8aÀʉ}qwßk àX†Ê!ŽìÙ´…¤Û¾ÎˆÑ–¬¬? ¾œ ý é¹>¯ÔãÌÊÁsøÕ£-=›yݾÏvš½{Sp*Vo Syd$¨Z(žg¶±íÄõ;¡#?€såê„hO“zØ Þ„V =j+»²Cä•(¶î„'«WÁ8V¨NçlÝžl?¾Ú8µ};g0VöFŒá¿óÖ±óØœ=º¥Ë·r) „²®9—3R2Èb×°2.™+édÚSª1¨,Ai‘ü¼=AHÛ71Óó8¹¾Á›6Cûàûú½ó QGNr|ï"ÆNc}fÎj!Ðß™¸ÿûžØ„4ÒÓ3Ⱥ©‡×½r5‚Ö±l'Ïž#ñ¢žÎ)@V4óþM‰N=hèžó +û?ÌÀ‰¬üe+Û7ϧý;ñßß‹Ñ5ìß|TõýsmØ›¥vðáÐp~ÙÇïó‡3~MA:ôk‰YÇõïz|±1±8{[ú9âbcˆÝuœÔÇ=e³·ILì1RÄFêñÄÄÄŸ8Ú½ÕO}ƈ·W±ë`4ËGaIFÂ:^½ ÝÜ‚°žü8~(Ÿo9À¾õÓ6%’ }hè<ð,h?SV›N.N]09 YY:Är;:•TžXåèÒ!Ò¬Z)ññpG×ÂR®Q™¿ëÒ-KÚR6ËÄÖÅÇÕ(CΩ¤Reû´.Rµ¸YŠø—–õ‘;Š·GgY‘~Ãäü¼æâ8T6å:ÍS¦œX3^:× /“‹Tb•_õ—§Ë•.F1:¿ªmdôŠƒrå‘ÔôŸáÊþ…2°aiñ2¹H’µ¤ÇŒHIÕi¤îQº,ïä.†›¦4íYKJp“W)i0`ìËõKÀ&‰ëß“V!¾RÈÛ_ê„-ýùøïÀCšJêvç†?³CjöÏìÀjäêMUÙÅ)GqÎQBEä£Û¼—RJ)¥”ÊÇ Ã` ÈÌQ²rKŽbµ›½HŽŸÙýô¹ö×ë°¾RJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê7ôŸ¿åArrò£®‚RJ)¥c^^^º 9 §yð8ìJ)¥”R’ë+¥”RJ©|CéRJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê74œþÃXw§ª³ƒÁ€Á¹6“Yÿ|áÔ´r/Íðß²nxØ5†÷ÆÌ<#°f|×ÍsÏUdäõ¥Öc,Pÿ‚Μ¨øÆ,°fJ)¥”Ê?4œ>tYlˆ{ËRþ‚µ+ŽeG¦`Ù7NwXØ9ˆgº¶¥†Ïýï–Ýã©æQ“ {o†ïQæÆÉ Ÿï@ØOÇH<ŸÈ–1Utš õè¤ýÎô¾­¨_Áw£Ÿ¾kɼöd[Fãh°Ÿ æ(N•Þ$FϪîIúÅ nTo7W Ô¦×Ì\xçÎmÏÛ³î‰``û†Tö+€“ƒ+­^¼i 'W¡EÅ"¸»zP¼jG&nH gÊ…ÌìU‡@O7ÜÌÁ<3hÒíO¦ídnïºù%¸~_æí¹ò¶ìïCÃéã̵ƒgM¢Siã}®èk§ÎâT³áô-¿ëºÕå“'Hò®Æ¿B‹áåY“î¶êÑË%Ò\Bh5|<=+ß|šäDå¡ßMtv‰\Fÿr.”iÑŠ=«Ê»ô­Œk׋e®a,ÙÍÚ7ƒÙ4´ ÃV§ yêh{Þ‘-í5¥ÿÄþÔÍ¥“Dzÿ#:wšIj«¹l‰]Ox½CŒoÛ‡ˆÓ¶« H «‡µfÈÆ ƬÞAäòWðXÑ›vã¶’Žcs‡0Ù6Œ DZn`&“^ û¥êÎ öâ'À0n€à x¾@ („¡ÀS@}`<2ËòÍ$¤ˆ»8› H‘àÚÒqz´d‰ˆH†¬éSX@¸¡%`ÈFɱ%ËÆ»K£J%Åìæ"næRR¯ûTÙœhËñ&i²ë/Iƒ`³¸:»Šg±òÒ ß9h¹±*–}¤†[-ùàæ'DDÒ—J“ýý¥dØæÌžÎŠ-Üž–á¾$uKzˆÉ£„Ôê5[b/å¾ÙÖ#Ó¤aròÚ–ôœk‘ãߎ&Á…ÄÕÍG*uš"oµ. …z|/×–²%ÊæðR¯T!quv—"[Ș•ǯ¶…Xåä' Åù–ör”±Qö6±žùQÞn*Å ¸ˆË…%°zKyoý…;ÿ­²e¬’žæ'äÙAc¤e%ñõö‘àgGȷǯ¾ƒõèÇò/÷R2tcÎm» ßu/"^mH‚-÷ժǀõ”LÆ$…û¬‘ŒÛ,–5FBÜjÊûûsù,ª;º¼º·q©-´^}À– ­ ˆGËùúù»Úžy¾T:˜LÒjAÎcJ–l{½¬¸”)¿g:/­–ÞEMRgò!±Šˆ-!BZzxH‹yñrµI­røÃºb*ÒKV§YåèG ¥\Ïr*#]Žõ¢”k2CNÚÿÖ”ó’b}˜™7À {®{ÊžóBì¹/О}í¹ÐÓžÝì¹ÑÙž#ö\™1s¥]PŒ¿x(½" û’hŽŠæÇY¯PÕ#{˜Û™¦³ã±J&‡àÖb>É"ˆX8:µNr™„‹>´·€u±ؽ.œ†ÇߥÕK_’`?¥µü”~ý×RôµUì>Ƕ•SéVÖ%o×`º´ç«+‚¤|AK—?Y&ó7æ,11ô»Xv®‹ÿ/éðæ&n|HgË'ÓØñÔ`ú×¼¾2kÜ'tíú9™]¹ûW>YÃÌUi9^—Åž)­iñQ2ͧþ»â¢X6° K»½À”ÝÀý&ClœŸ×·À¡lÊD²ØýNUû°þeÖŒ}‘)çš1{Ë!Žìú…ˆ‘ )Œ5=—ùeõô\}?ÎD1¡Ørºuû„ƒVpðïÀ‹ÿJdù¢Ûžú#‹WÙøw·æxÿéGK)€Ë¬ŸóÇëô KðƒUøç³r$*š”bOQËß~¸2xQ£VY2c"Ù£—Iä‘¶ç}“D¢£Žaª^‹'³{UÝjP»2쎌%°ì$&³,µk²§/üjÖ¤XJ4QG„€^2Ô6‰z~%iîȈÉÝ)ž~„5“ÃhÛï¢Îk¶†ÓÆFâÉÓdúT£q`Š¢Rî¼Ö­ÚÝ_éPŒ6oNb`›úT.íO`åæŒô,–Í뉱߳d;{’Ó¶’ÔnRƒ b%®Ú”^CÛSîA÷ ÓaÜ»´«Hp¾L~µ'}Á¦Ì“s˘‘NÇá])ymo²r`Ù¶ûöä½1ÏQ!° ßGã÷çñZ‹PJù—¥^ß^c7‹—7‰ù  IDATí¹»°-9}*•ðt… ¡^û!ôyÚóÏOÇråHhï´(îN%hýjJm_ÀÒV0¡u·gIûf¿¤çW/f­Kkº6)˜§wQI^Åì¥y¶GJè·í=°‘Ÿ… ãuvJ¥ñÇqx6CR<‰þ÷8mÏûfK">¼ ›I[;€²>Õxk› ÞÞÎd$Æ“*`KJ 3…=ãø¸qQü;-⬗f9—dƒ'ª6o GÏãÈú÷©ù:¾CLù‘,úò žÑ^ §Ž‘à¶=y:þUlÀ /⃈ œH¿ó+¯Ëâø÷oÑá© ÌnN ¸µ]Lꥋ¤ÙO¤œªu¢W•¼V£ M»äð%l?—uûÕÞÓæ”¦b9“ýŠT(9õ0‡sžÑYØýÙT~ z™!Ÿ¸áñ£qG ì“”Ï>³t,O¥ò×/Þ±ŠegB_uôÂ!û¦‡¢ôûé çþˆç®.¿1æ¹m0F´£bݶôñs~<È¥<Ÿt:XÚŸì|o (MG8pÄ(ôü‹4—ïYôc*H+¯Ã³]Wºçõ}ÔãÅÆ™¥sXmlA–Þyœa€¾¼R×0uÖ)þ=¬O®=·GG®?쀣ã_}b,ϨíYˆHŽbåì¬gq¾«Ú8à×y!{ãÖñqŸ::óo¶¨F“I»ò8Õ”`±äø#Y-XE¸ÖèéÖÆÄ‹~ ñÌ·,^_”Ž]kógWD(€5Žs7bîГ&º2WxûxCR<çÍÍùhó6æw-IJB˜}0ë•y¤íyßÌøxÃù„ó¸ÔǺ¨oTñ ‰I™8{{SÐÞ>˜I"!¹$Ý"¶±)üyÌÉ œÇŒÙ.làÖíyoo(o-[Æ[·2¤N¾ÅËÓôÕo8öô7ýÝh8}À<‚¨Û6ŒQáËÙ4·ü¼’Íi7-c4BV–›ò¤e×ïD›š1`P 8aÀʉ}q×zM¯q,D¹†/ðÊ›3XµñC%üÀª7îÍggœHçJžzns°b÷ìImœÛ»$¯`J_n°q4b Ë ôdXë›{…)U¶?‰ìa"ë çøÄ9¯Ä“^ÇÙ¼ùäÝõ’Þ†k‰´èù*n`é€"Ĭú™S7­ÔzéÇŸ ñrnkÈbÿ®ý×mÖþÝì'ˆ2AÙd¸R¿Û xÿß>ýl¿•êL—ªwš§K=î2#ÿÇÿbéÒ£.¦;/®re$¨Z(žg¶±íÄõ;¡#?€såê:ûAži{Þ7ƒ7¡UHÚÊ®ìCÚ•(¶î„'«WÁ8V¨NçlÝžlïã°qjûvÎx†R-È¡ ŒXÅüQÏSÊe/ÓÌÁüî6NÆ}C›¸Œ\¡3'h8}`¬ìÃç­cç±?8{tK—oåR@e]s.g¤d?Ä®ae\2WÒ3È´§TcPY‚Ò"ùy{2‚¶oc¦ß8á|Öï3>ñK6î=ÉÙ?²aÁ·ì4–%¤Ôß*ÅC©êÇ×ÿ[ËÓg9—”Fž.'²%°ì¿oðÍ®cÞ2›S~£dç®ÔÍîÒLÿiÓb¨5¸?5néB4Ü¡O |õX°pv͇Ìʱ%¦ \…˜·ÿÃð¿qàøvmþŽ™¯ý‡·×eÞ¼Â?©ã)¾~g 3Wïà𙳜ˆ]É’_þÀ\¡<7NÝ*$,ìJÙÀŠ þ1·`åàÜQLøqÇöÿÄûc>çLõÿÐ!Gw°s®t*¹Žñïo%¤s'þ‚³Ôß…¤qzo,1±{8uɆ%é0±11ìÇgõ?ÏYÈñÊ/Ò­ŠžÈÜ׆½éQj ç—}qü>8ãפC¿–˜u:Ï´=ïBú9âbcˆ‰=FŠØH=¾“˜˜Xââ3GB»÷¡ú©Ïñö*vŒfùè1,ÉhBX§À«·¡›[ÖÁ“Çåó-Ø·~æDÔ£ ݃ží±aµ9àäâ„ÑÑ““•¥ÿÞŽN%•'V9ºtˆ4«VJ|<œÅѵ°”kÔ_æïºuþ%[Êf™Øº¢ø¸Å`È9•TªlŸÖEª7KÿÒò¯>2cbGñöè,+ì3YvÏ•ž*‹¿·»8;?!E+5—‘+ç2•Uþøáuy&ÀC qªö®ìµˆdOƒáxËMˆK³¹’`Ë1•Ô”0©UÜ]\<ü¤Vï¹²3íÚÈÙˆÖböï'k/þY{dÉñï^“&e|ŧD <ùï×dÀ³7N%eMÍ÷–e ‹›³I<‹W§;½.Ëo˜þÊ&çç5·À¡²)ó¦·°%ÈãZK­2E¥ ÉIL^AR¯ÇDzõüÍó¡Øä™ÅÅà!]¾N¿ñ©ŒUÒÓì!-ßø@^¨XX\]<¥Tãáòõ±¬›Öa‘ýï×'ÓÓ~$Ïó¡þz™¿ÊÀ’Æ[??MfÉYû®gKX(m½Ü¥ÑŒ¢{Ëý»²¡ lXZ¼L.R d-é1#RRuÚ£{¦íy{–]ïH¨ÓÍÇH'©:~\=:eɉïGËó ‹›‹»øVn/~Íž6ê*[j”|Ú³–”,à"&¯RÒ`ÀÙw%·w³Iâú÷¤Uˆ¯òö—:a dznËå<¤©¤nwždÈñ3;¤fÿ̬FÀ1GqÊQœs”Pùè6ï¥þN,;y»f=VwÜÆæ‘åø[w"f®¦W±Îd̉gaëÛ]EšÎ†Á!4ß?’}?„Q\Ç”RJ=f Ã` ÈÌQ²rKŽbµ›½HŽŸÙW.äzƒ^a¢òÌ–˜„w»p¦†•ý{Ó»"d\Hàsùï‚‹üûÓvÓ`ª”RJýe4œªêj<Ïüþ¼²ÉLÍÞ3™Ôάӭ(¥”R!ÖWJ)¥”Rwô°†õu€R)¥”RJåN•RJ)¥T¾¡áT)¥”RJåN•RJ)¥T¾¡wëçArrò£®‚RJ)¥c^^^º 9 §yð8ìJ)¥”R’ë+¥”RJ©|CéRJ)¥”Ê74œ*¥”RJ©|CéRJ)¥”Ê74œþÃXw§ª³ƒÁ€Á¹6“Yÿ|áÔ´r/Íðß²nxØ5†÷ÆÌ<“ë» ÂÙYÏb2\­‡c™×ØšuçW)¥”RJi8}è²Ø44÷–¤ük7VËŽLÁ²o5œî°°sÏtmK Ÿûß ,»ÇSÍ£&öZ¾ý~"]lÄÏy—û^»Rù…S?M"ì¹P ?ÉÍ›à§{¾)[.K[Î¥ya#ŽþƒX¯'h÷,ýÀb7*ƒ·›+jÓkæ.Üë¹³Òö¼ëž¶oHe¿89¸ÒzáÅ›–°prÕZT,‚»«Å«vdↄþI¼\ØÁÌ^uôtÃÍÌ3ƒq ÝþdÚNæö®KoQ‚ë÷eÞž+iËþ>4œ>Î\ë0xÖ$:•6ÞçŠ.°vê,N5Nßò÷».¥ò3 {Ö®%µJOÞ›·š_×Ρoá ŒlÞ™OßO-™ùòTRË¢ŸŠû¾•qíz±Ì5Œ%Û£Yûf0›†¶aØê4OÝmÏ;²¥]‚ ¦ôŸØŸº¹tòXöDçN3Im5—-±ë ¯wˆñmûqÚþ )¬Öš!ƒ³z‘Ë_ÁcEoÚÛJ:6ŽÍÂdÛ06?ƺ™Lz5‚Ó¹Ý>Æ4œ>HYGXñÚ¿©èû.®ñ-S‡N3b°ÉÚ¾> ÎÔ?Æå•Ýñ20 º‰,IaÓÔ4®ì·» wïÒÔïÎoI9¿2.³{ÞË4,㛋^Å+Ðð¥…ÜnôþËxÁÕ>ôïxë°¾}c8´äêùÀµ€µ{ÏagZ`ʲ'è1¬5Þ†<Ô#ó_l~µ½ÜÌ7|™y»r¼‰$±áý¶<é㆛¹<­Þ™H@wZÌO¾Ë/Q‡§ÔÁýÉÕ»eŠûàíW®ŸDÚ{ „¤/;`.Òo.äÜô-Œ(ãÎÓáGsí S3gšN^Ç’÷Ñåù§©U¿5#f¼NƒôM¬Z>Ç~™ÉÞ˜áû_&üÛL^>êFW~™Ã¼C¡ F£rÔêù!o4Má«Ï¾#IÓTži{Þ™SÍW˜6éuÂÚUãÖE ÑógY¼/“Ç5§R™ªt˜0žŽÎ?òÙ—W’´’ϾJ¡ÉØ©ô®SŽ 3ux5Ï›Í/—í«18`0›íÚw‡-5™T=ðh8}p„øÅCéÝ—DsäP4?Îz…ªÙ©Ñ™¦³ã±J&‡àÖb>É"ˆX8:µNr™„‹>´·€u±ؽ.œ†ÇߥÕK_’`ßs­?¥_ÿµ}m»Æ±måTº•u±à»äÒž¯®’ò-ÿlÌ=ó7æ,11ô»Xv®‹ÿ/éðæ&n|HgË'ÓØñÔ`ú×ÌË~ÑÚÓõ ݾØÎØoîû/·z•Ÿ.ñ_½Â ïåé7±û÷Ïù÷ž9,9•÷O­eïwl-3¨Sç8òM[N¾Ñž?^ ˜Ÿ‘æ†Õ,úáz°ÈØò%ËÏÖ£ký€¨;’Ë—¸Œ'E‹º_ ¡1“é;Ûw&µ¢°îD÷ÁÊ‘¨hRŠ=E-{C¼¨Q«,™1‘ìÉÓŸÒö|$‘è¨c˜ª×âÉì^U·Ô® »#cI,{#‰É,Kíš…ìß øÕ¬I±”h¢Ž½>d¨mõüJÒ Ü‘“»S<ýk&‡Ñ¶ß'D׳ýÚ|`l$ž[ž€àzô EÃø/ùßÿ]‰gå¼UH»w˜Ô©*A¥kÓ÷ƒÁÔº‡í4xµdè€P<  TÈ ç.°4biéÖæ ~Zô½ýà MJ£n´-¦u’Èï~ÂÎÊØÈõêc—·ónßÏ ÿm|´ÏôþØHŒO„B…ñ:»ˆN%‹Òøã8< ›!)žÄ¼Œ)´=[ñ‰àUØLÚÚ”õ©Æ[Û\ðövžT[RI˜)ìÇÇ‹âßig½|0“ȹ$ˆØÀ‰ô;¿òº,Žÿž Âìæ„ÑÁ€[ÛŤ^ºHšýDÊ©Z'zUÙÉk5ªÐ´ë@Þ_ÂösÁÆÒT,g²ÿâ@‘ å1§æPbÎ3: »?›ÊOA/3¤ñy[ÿåÃ:ãJ…'®]g0WäÉâé>x«å(Û(]©®Ùµ(R ÷ð¡5–(E@ö¦àJ`©¢¤>ÈiëÕߟîÖ¿.âÛ?lö ‹¿Í Y·y»DA=†ÒˆžÒ‰ÞÿWO¤ŠóÕÇ~{»_O`r»":œÿ  œ PÄÏâ…L×~„Uú[Óö¼oÀÑÝ›~%0»9puèÍÞ‚b?NLxóÃÏ· N‚ázË%|;ž[öãKéħ_ÿן/…ÛCß’üIÃéäT¾?«îeå„.„ºÄ±pXcªuøœãw9m=4ngs¹ýçD½ŒÅ&¤¯|Oäúµlnµxsã!¢¿K«2V¢f†Q·fß&>œa€¾¼R×0uÖ)þ=¬Ï=õÜÞðAý ‰ÕŠ5GóX-ÖkßÎ5»ÑÙ3‹Vœ å§Å|ïЂ®M >„š©¿¯ËìúäZ„»ðÆ÷óéd±žfÓ¯û9¹¬®&L&w*$óä š `ÀºÌÛ¯VÝÄooHŠç¼¹9mÞÆü®%IIH³f½Ó,´=o8Ÿp—zãXõ-ƒ*^!1)goo ÀÁÛ3I$$—¤[Ä66…?99ó˜ñ1;À… ¼Óº=ïí å­eËx«âV†Ô À·xyš¾ú Çtf §šƒGuÛ†1*|9›æv‚ŸW²ù¦‰ŒFÈÊÂrSž´ìúhS3 j@@' X9±/îZ¯é5Ž…(×ð^ys«6~H£„XµãƽÙàìŒé\ÉSÏmÖCì>aÿÅÆ¹½ûHò ¦ôµîDG#¦°¼@O†µöþ“iÀÙÙ Ò¯pK5Ü‚.z™=»Ž‘=’$I{ØuÚ… ÒÅ1:R¶´‡ví½v«-~ûr áœ?uœã¤’ÛgÚz|{Rí¯“döî9k©`Šg;V¤K×"~ÆôkpoÛ†îwn"õ¸ºÂžO;ñï÷¬¼úýWô¯èzý)c }–ìd÷Îbbbˆ‰ÙÁ·ƒ+âäÛ™9Ûþ7j;?ºjÿ- ªŠç™ml;qýNèÈßà\¹:!w}Í”ºJÛó¾¼ ­@zÔVvep®D±u'ú?›;ðbcw0øÐ¢ûó–½Å¨å;9y<’y£¦³-·OlÖVÆÕ-Ep‹é¹ÏZpåG&¾¶€È#Gˆüb$ï¯+D»nÏp= îØ•±Sx{U!Úw©ƒ)—Õ(~Þ™çFÄñì»oЀƒöK\|àD!ÿ²”+WÎ^ÊXØ„ÁÉ‹’eKSDÇìò̵aoz”ÚÁ‡CÃùe_¿ÏÎø5éЯ%f‡Î3mÏ»~ޏØbb‘"6RïÌñw$´{ªŸúŒo¯b×Áh–Ã’Œ&„u Ä0˜[ÖÁ“Çåó-Ø·~æDÔ£ ݃ží½#bÃjsÀÉÅ ££ &'!+K/þ½û$8FÀ pL€àxÞ€/PÊ!@(ðP$ÿxV9ºtˆ4«VJ|<œÅѵ°”kÔ_æïºtË’¶”Í2±uEñq5ŠÁ`”€!%SDÄ–*Û§u‘ªÅÍRÄ¿´„ü«Ì˜ØQ¼=:ËŠô«¯µìž+=Uowqv~BŠVj.#W–Œ\êóǯË3âh0ˆSµwe¯ED$K¶½^V¯^!sCqi6Wl"Y‘£¥‚ÛÓ2|J˜Ô*î..~R«÷\Ù™vm älDk1û÷“µïÐ,¶dÙü~ )ëåtã¶Šˆ¤”¥ÃŸ“ò…]ÅÉÅK‚ê÷‘Y;.ˆíÚkdý»­%ÄÛ$&¯2Òâ‰ÒÃßMZ~‘zã{dþ*Kslãõ684¹¶¸…¾,ïú—ø{¸ˆ»oUéüÑ6I±Ý¸ ±ý!sš¹‹SȉʺÃ6©ÇX²Ìoaºå³NRuü±Ü²¼Eö¿_S\J”_3sYº+Wö/” K‹—ÉE ”¬%=fDJêÍŸau×´=oϲë uºÝgïër|²Ó¦÷¥ŠNl¦þ¬û¦Òå?Ÿq¡Õ\¶>ÆÎ¯ൺ?mGÿ‚‚îAúVƵëÅ2×0–lfí›ÁlÚ†a«SÐ6ÍhÊÙ©íé2ýÐ A1áûoHìû=qñgØÐßùÞa]žºÒ.óËê?è¹ú œ‰bB±åtëö ­€ã“üç?O²÷Ë/ÙÝÆ¶Ó|³xÞí»ò/·¼¼ºΡõyÊ%š•_Ç‘öÿìÝw|Í×ÿÀñ×½7;BâFŒÈˆ 1"F¬~)¥­=bk­†ÖÞUÕjiÑšÕªÖ(©Y«(E«ýÙ+ b$fƽ÷ýûà AŒ”užÇçîýŒsÎýÜÏyÎ9Ÿs2ÏýÁ¯Û3¨Ôèu\Õ7ŒÄ‡GZ¬AæÂÓ8Q%È—ÌEIQ IDATaV£lòH•ç“d"ÂObS9ˆrÙ­ªvU¨^…E’¢Ã8éKõª¹UKhq«Z•b©„Ç žÝ&3ÐôµÜÜ©3Õ‚¡ßÅ5=ž ChÙó[Â/©»u©|jL$Ÿ9G¦K õkøàêêEùºÖ9ðñ†K[ŒŸ|Eßµ©PÒƒ3¢ßvl္%Дp†s&wª7¨‚W±âøTjH·­)ý´Çkj <ú ZU(O÷˜8¤gýÌö{"?¹¸œI¡é´Ü ÷œg“á§Ï[âS»<Šá^ºMßïÍ[ÅóvÊiÓ¿O ŽZ-k¶ám÷“„G\B€¬½?óó‘òôŸ<×K{â×x4Ÿ¶qÉÓImˆœÅ¤ÍeøxþçW+…‡wÚÉ[‰KY¼ëqÂ\!é·yüflÉç;RÅ»å›~ÆW]]Ø= Gr4Z×ù€¡ÿsÁB“òmšS.5‚ð“yiò¶  ûPš¸Z‚eqšé‚÷¾,‹5:|Úu¦jü/,Þëd1^Åân´ëăîA”§OW²KWv#å£r8ÚÚÏ£#»ßø™CË©ŸäË3ɉÉP°N ‹hç^”úßÄáXH)‰$ç}ˆû+N•ç3¥˜ N…ô\ߨ_—@>Ýk³³ɉ¤ ˜R’HAO!Ç8¾©_v‹HprAO2SL¯"!óvqââEâ·ŒÇ?lÁmÇp Ìp-Åëwº _Y*8}jtø´ìÊk‰_SÏ¿mÞÁס[9þè-ïÈâÔoŸ\Í ½%:­»–‹I»v•ëæ)ËÀvt«xaU*Ò°S_FM]ʾ‹yèÃ~ìì”Ä¿´ù?Z —-ƒ>í8Çrö©càÐSøÃë}ÔÏw÷öÖ¯ñn×âlêQž*»0𳬎ºL^{+´.®;;ÕØbg 7®ßD®‹ãB>?ÊyfGæV”ö/E^âôkQ‘»þ7½Ýt·ºë54­X|)™ '85r*.“w9ÊÚf¿fEÙ ¥áxÇo·DhÈ_¬(ù4ÙY±Ç–\¿™—;dKJ”ô¸?gI<‰'6þÖA´î­éüÚ–-ÞMFâ—/&Ì·=õdœò4™Î¯fdߟ±Y̶ðýìZ3÷5]h5>œ<]”;44–ù)ìæ†kA›;/?Ç$½ÔTy>1 `aïLq·âèí´Üê—7— ˜¯ëœŠ¹áV¤–‚æNË5bWå¦=Y"íø~ÕO|ØÈÕÉu‹ NŸ"Ë2½Y4šµã:`ÇÂAõ žË©ÇŒÈŒÇ¾£sûYÜh=—ð„LBúÚwpDî T· â“mLjXò1ÍJ ŸBͪ!¬N~6Ýw]¼Ò60凳¼=¨Çý-·= §íçØÎ9 xÃ+ÿ÷ªÖdø¶<~Ôhr¹`>ݼj\z°!Cnu×ß^n²²}¾GoüˆÔäL»F“KNò”Á`Èq2 Eî$@S˜æï4 må"¶]‹eÙÒ(*vlÏ?xFMùÇ Dý8št`ü'­ò+KàÛƒ™6ÐýS¿çÿÔ\fy¤ÅÙÅR¹¤oÌ´{™ßÉÔ¤л Wçv©ò|bZ=.Îp)éÖµF³9|5ýüo’œ’‰•³34 uvAO I—Ý麗íS¡¿œÄ%ô¸èµpe+cš·æËè>]¾œOýw3 †'E\ËÐpȯœüÚ›^6*8}Ê´^Ôlˆ©+Ø>§üµ–÷ö4ä+éKÑkÑ:•Ý•Iì᣹xJøP"ý¸Æk9uê4ɹÆðY‰:r»ü²Žâ^”òÊ>ˆ†‚ow¦qæjB¿™Ë’¸ :µ)¡¾äÏ”p9å2&ö®ï„V«…›×¸®tÈ#^8žßËÞÓwž„Û‹U…Êø©qy¤Êó‰iœ ¨äIzøn¢²«Ý›áì>å*Wİ([™ŠV±ìÞwÙÜv`âì¾}œw ÐKô ]Çüð¶ŽfzŸÙè¿ØË™¸_i7”á+ÕÌ ªÞzjŒD‡Žä³y›9xò 'ö²lÅn®yúák›s=î^¹µq—¹™žA¦9JÕyùâu=Œ¿ö]F®ÇÌcäwûï æ²öÌ`ð„%l‹>CÂ…£l]°šƒ:_ü¼ï¾ªh]¨äǪŸ6{.‹)×ÿÉpSË?ůQ'9¾kC'íĽ}'jZ™ßOßÉôéêß›*¹ h¼ò'‡LeÕž8Î%œ#æEl8nKi?÷û#C?_GÔÑV|4’¥ iw«1@£oBH°#›Ædî®Xb¶LgФ0¼ºô ® qÀ±€ùº-&Œ&-–Ö–è,¬±±²²Ôà߇1Or€Ð–€`Ø€#à Š%€R€Tjýä?Ï('– ·½ÅÅÁJ,l Iéz½e~ÔµûÖ4¥î ÍýÅÅV'N<l“LSšì›ÞA*¹ê¥°GIñû_™1¡­8;´—•é·¶5š#]ëUg{±²Ê'EË7–á+KF.é¹ðû‡òº§ƒXh4bø…DDD²dbq«3ø®Åú­9’dÉ ûHÊÚ½&ƒ'…H«½X;¸IP÷9rðúíHBhsÑ{ô”WP×wË䶵ůxA±³´–n•%xìŸrÁ˜¼ã2©¦Õ}iK©:þˆÄ(g¾­+vþ£$"˼á |VÑNjO=!Ù»I](ï×rk{)V-D¾QOìJ({³îIσ2³Cy)d£ÆA:¬J¿óy¤Ÿú¾)þEó‰•u~)â$-úÍ“7rî KŽ/ù@jy+­F°n(³.šn—GÊŽ)Ò©š»°¶û"å¥Ù¨urævnÈÒÖvR´×·?'ӹ丹]Yù(,gBMraf}±¾'}""’±Nºê¤é¨¯¥!±µvïúƒeÕÉ{3*’±cx[8JË…Ébºï]å_gJ•ð{I½Ò.bga!6zo©õîÙž¤>êæ‘…Ò·nIq²±–üîAÒeF˜¤©âüÇTy>œ!jŒXÞ_7U{X ""’%§ûH•-$vÖöR¤Bk÷‰w]oMiáò}× qÏo-6NÞR§Ï‰¹™ÛÑL’¼åKiæWD :{Hr$=·õ^ @?s\WÍçù™ã¾æ8°ˆ9.t4ljvæ¸ÑÊGêÌqevŒùÀôQïå R³ÿf¬:À"Çb™c±Ê±ˆÈ´‡Ky™òyÕZ¬o»—ÃK?µ–Ð'gäðU©úGŽüÕ·ÿR£aæzºkOÆìD6X󛺲¾½àû£ iYà™¥PQEùÓh4ý 3Ç’•c1äXŒæÅd^$Çß;…çB0QòÌ”œ‚s«©L ñ}Ω‘“CÙk_ƒÿU(ŽîÄrÆÿt‚ ^Éù$7SI>»›I_®Á®ÍÞT©¢(Šòz«påIi‹Ô僑ݨîôü(eÿ‹¯ß©…·³žoNæjÇ…Ìïéõ žØFb'×íl0Ëœ2{t5%‰¢(ŠòRRÝúŠ¢(Š¢(Ê#=«nýW¯IQEQEya©àTQEQEya¨àTQEQEya¨àTQEQEya¨©¤òàòåËÏ; Š¢(Š¢¼Âœœœžwþu*8̓Wá„PEQEyžT·¾¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁéŒñÐX*YiÐh4h¬ª3ñ˜ñÁ+§- ™}Iï̺ëeCøHüìë3ó|®¿*öÔenèŽKÁwX›ñL§(Š¢(Ê L§Ï\Û–À¾i(©ÿÂÞuþ³?S0ÄŒ£Šå#V¶òâõN-©âòä§áÐXª2.:g0üxyÕºÖ¤mûÚ¸éž8Šò/3q~ÍpW-Ek,mõxUoÏ›Îa ‹]C}°Ð˜os,–å?á€á9'ÿ%•»˜þõJálgKÏêt›¹Ÿ+ÏæÞù?I•çÇҷu]*¸åÇRkKó…WïYÃÀ™u#iâ_{[\+µeÂÖ¤»~$^®ìgf·”p´ÃNïÃëý›n~óúAæt¯‰W‘¢øÔ~y‡o>£œ½ÀaewZÞM:&NÎÀDÓ ¶ž:Éæ¾™|5$”ìM•GÓ˜- ,+À°GÀ(J¥? ¨ÔúÉ« ó¸¬ú–ø¶+›üRاº´ý.B²DD$C6ô($ZîZtâ9`›dŠˆ˜.˶ÉïJ½ò³;½·ÔzwŠìH6å8Èu‰ú©—ÔñÑ‹­•­8+#uz.£†»“bˆ'Uì‚äë{ßI_&Á6æãë¼eÐŽÌ»ÞÎ ûHÊÚ½&ƒ'÷’šîbãP\‚ºÍ’Èk¹gÛ?]êæ/-Ãv¥›_yŒ¼ŠHÆ_ˆ«öÖ{šeMzަ/“à|%¥~‹êâZ °Tú­|Ù¤„8äó&3¢Íe*bºr@æö®/¥]ìÄÊV/¥ê÷“E17²S&Ç&V;ÿÎòa·šâS¬è‹JÇéû$-»H«Ì³dbWÿkùí»®RÝ£€ØØ†S$Ê "b’K»¦J§ q´¶»‚nR¾ápY{Áxg¦dÙ1µ‹Ôò.(¶VöRØ¿‰Œ\{Jî.ù‡ÈX']õùä~#¥iy)âì">o •Õ§Ì%‘!£ÊÙJå/£åÎ'n’„¹$ñÙxýq¤äÙ•…ÒÜÆFš†¦æúvVøHñ³«*ãäò]TéÆúîRغº|}Ôü}2%Ih³üâÐt¾$™¾­r?Užy¾L‚ml¤Ù‚+9^¼UXû—=Ùðkë¥{Q©1ñ˜EÄ”*M¤É¼D¹U¤F9>¹¦Øî&ë¯åÄ´ºRºëJ9›‘.§~yGJ7˜!ġ1õ’¤å…ô3ÇuÕÌqžŸ9î+aŽ‹˜ãBGsœhgŽ­Ìq¤ÎWfǘ¹R-§O¸x ÝBáݥċ`ÓPÉ!»›ÛІ³1J&Ûxb×d>—E1pbJ-,äIW]h6z›#c9´y*uO}A³^KH2ßÒ~OÏÞ):l‡Nıwí:ûZ“§ÞBëÖürSÔŸijý€u2w2{© ×DrpÃÇxüÝ—àO¶sçC:»¾Îþjýé]5{g‘WÀªîwœ5 ¿wÃ9·3ÑxjLcÝäÚÄOùœýÍ×°õ˲ìüv‘ÀtšùÞddL5>_A\ÔïŒöÝÎ-F±=GB ÑkØ]j:ág/ÿkKÎŒjÍÐMWn½ùe~{?Sø|g5&ï<Ç¥ ûù¹WEòö1¡ËGD|Íß±'ˆÙ±Œ1ÍÜÐÝþP²8<©9M¦]¦ñ”?‰Š gyß,ë܆I‡òòÉÝàïõèºþ(·3®Ø :wþ–£FÀ¢;–#zÉnïÒtŽ_oŹu'þg—‡Ã(MÒ/°köböhËR¥‚}.kÜ`ËìŸ9U£ |Ô¸•¼3Aj±jy˜/'ªù’y ŒÃj˜D©ò|b’LDøIl*Q.»2³«Bõ p(,’tÀÆL_ªW-h޾´¸U­J±ÔÂãÏn“húŠZnîÔ™jÁЉïâšφ‰!´ìù-á—T¶ NŸÉgΑéHý>¸ºzQ¾n'†u|üŸáÒ£Å'_Ñ·Em*”ô D…ÆŒè÷†[8`~fÉ”p†s&wª7¨‚W±âøTjH·­)ý´ë=m!‚GA« %ð©ñ‡ÔàÌ¢ŸÙžy÷jrq9“BÓi;¸îOûlÒyR£~åêÖÄǺ ¯Õ/‹ÿkUq½p’3F0DÎbÒæ2|<ÿs‚«•Âû íÇä­Ä¥,Þu'¡§¦ 쀃FCþÀ¾ô{ó ËB7s«ÌoçUS‡áßö$¨˜=¶Ž^Ôn^O`8Çéó–øÔ®Gb¸—®FÓ÷{óVqsdláÛ©1Ô?aMðöð¥Ö{_3¸Ê!/?œ‡ º¥‰«%X§ù.xï[À²X# Ã§]gªÆÿÂâý·n:½ŠÅ;Ýh×1ˆ݃(ÿŒ\šOS{ ,ì\©56‰ËÖðaùû¿éry³–]å.ÁWWÛÀDrb2,„SÂ"Ú¹¥þ7q8ÒCJ"ÉyÞSÉ*Ï'fJ!1œ é¹¾±¾.|º×gg+2’I0¥$‘‚žBŽq|S¿(í‘à䂞d.¦˜ _EBæíâÄÅ‹ÄoØ(‚ÛŽá@™á,Z2Š×Ø øÊP—˧F‡OË®¼–ø5õüëÐæý|º•ÓéÞòŽ,Nýö)ÁÕ¼ÐÛY¢Ój°k¹˜´kW¹n¾‘² lG·ŠV¥" ;õeÔԥ컘õðÝþ£ì”Ä¿´ù?Z —-ƒ>í8Ç’sÞÑ8ôãþðzŸõó=ý4h¬±±Ñ ±¶ÆZ{çßV†t2Œp-*’c×ÿ¦·›î΃'­X|)™ 狀âÞxfg[Jx%ýøQÎáqÊ<›E™ªæÏ%Ö¯ñn×âlêQž*»0𳬎ºLö"ÓÙH&¥ðK['´ÙéÔ¥ç7¹x!‘ÇjdI‰’d߇è3„šUCXülºîºx¥m`Êgy{P§ßrk>šæöQïü[CŽï¾K6d"9—›¬l'X£cŽâ1Œ··¬2ÏN­-6¹]½5zNÛϱsð†Wþï+:T­Éðm7î¤AW†û²îI§‘„ÞÀê±ËC0rœLFFn'TS˜æï4 må"¶]‹eÙÒ(*vlÏ?xFMy#eËQ±úÛôúv½óÿʘ™áw·‚ãX0gúà®4px^ }Ùiqvq†”D.é3mÇ^æwr'5)ô.èÕ¹Gª<Ÿ˜V‹3\Jº„u­Ñl_M?ÿ›$§dbåìL h]ГBÒew:‡îeûÔFè/'q =.z-\Ùʘæ­ù2:€O—/çSÿÝ ¨áI×24ò+'ÿ…ö¦— NŸ2­ƒ5[†0bê ¶Ïi­eÇõ{ÖÑé + ýc£öaó}úÕÁ3¿%ŒœŽ‰»¯‹‚”®Û†>™Áºm“©—ô;ëöß}6k¬¬°$›yj¹ÍÁxŒC±Ùš¸CŠ“%ow7˜8:‰ù»2¨ùƒ[…”×§!Ÿy¼®ìfۡ̇®g<Åá4sä2чÏaë탫.eþHV®ÐNýÇ0çÏ Œ*w’õb0Z×ò”s:ÅŽgÙJj¼v‘S§N“|#·w³8uäv”uäGð¢”Wvw²†‚ow¦qæjB¿™Ë’¸ :µ)¡¾äÿ:AÄÄÕ+Wïú|3Ã~â§ÈtèR›n«<œ¯ÀÏïeïé;OB‡í‰ÅªBe5ûAž©ò|bg*y’¾›¨ìj÷f8»B¹Ê±,ÊV¦¢U,»÷]6·˜8»oçôÒC}C×1D#¼­£™Þg6ú/ör&îWZÄ eøJ5s‚ª·ž#Ñ¡#ùlÞfž¼@‰½,[±›kž~øÚæ\O‡»—Dn`mÜen¦giŽÜt^¾x]ã¯}—„ë1óùÝþ»Zc²öÌ`ð„%l‹>CÂ…£l]°šƒ:_ü¼ï¾ªh]¨äǪŸ6{.‹)×ÉÓp"SË?ůQ'9¾kC'íĽ}'jf7ó¥ïdúôõïM•h|p^Ÿ‹Š=Tÿ"Óºôbæ_‡9y"†=~âÓw‡³2%Çqnnb°„ÅÇöópÆo.H«Î¯cÏã•ù#]ù“‰C¦²jOçÎóÇ"6·¥´Ÿû­.x›:ôé_‘Ÿwdð‚ÄžŠ'jÇfëÈç›sÖBÒÂNø–ð§ÿ¦Ü~‘ÀÈÑ9#·)†“Gþ`üȹœ¯Ü‘àœÍÖõéÜÂ’eŸMçÄki᪾âO•é õdÌìUü±}/û¶­á»ÞÝ™]”FM+çh¿Æ_³rªÂ;t®¨†U< ÛºÝé⽟ɧòwL{æfì†÷lŠ^õCç™*ÏÇ~‘¸Èˆ¼T˜Wãq™TÓê¾4€¥TD éË$Ø¡ŠŒ‹1ˆéÜ÷RÏ¡¡Ìºhº5E–CKYl.VSÚù©ï›â_4ŸXYç—">AÒ¢ß<9pãV›X]ìÞ—ñýþ'Öb_¤’´Ÿ¶WRoO%õè2¿=•Tƒ$!·©V®ï–Émk‹_ñ‚bgi-Ü*KðØ?%çLRbL’ßt—:¥ ‰•8º–•×Ú}(+îšêË$fÖkƒtX•~÷12ÖIW½ƒ4õµ´ñ/$¶ÖŽâ]°¬:™%÷ÊØ1H¼-¥åÂdQ3Ã’Fe ‰µ½©ÐZÆý_â]×[SZ¸|ß5HÜó[‹“·Ôé³@bnæv4“$oùRšù‘‚ÎR#dIÏm½Ïh*©‡Ý'irüÍR³ÿf¬:À"Çb™c±Ê±ˆÈ´‡Ky™òyÕZ¬o»—ÃKóâS2q|R-Ê/nLØž^î±—™ëéV¬=³YØüaÏÞ ©+;áÛ ¾?º–žY EQ”ÿ8FÓˆ2s,Y9CŽÅh^LæErüÍîÞ̵;U0QòÌ”œ‚s«©L ñ}ÓWñf*Égw3éË5صYÛ*0UEQ^B*8UòL[¤.Œ¬û¼“¡ÜÅHìäzT‡kÌ]GMI¢(Š¢¼”T·¾¢(Š¢(ŠòHϪ[_=Ê«(Š¢(Š¢¼0Tpª(Š¢(Š¢¼0Tpª(Š¢(Š¢¼0Tpª(Š¢(Š¢¼0ÔÓúypùòåçEQEQ^aNNNÏ; ÿ:œæÁ«pB(Š¢(Š¢×_{öÎû IsÞ¾Ô0v?jUEQEQ^H*8}æ²Ø>°öMCIýö®óÿ˜ý™‚!fU,±²•¯wjI—'? ‡ÆhûÄì IDATèP•qÑ9ƒá§”×§˜NEyR+Úb§5ßš«€Ï‰º}êg¿êCšT(†ƒµö…|©Ûsû¯¼ 7{/¡ôØÅô¯W g;[ xV§ÛÌý¨âüçTy>œñp(}[×¥‚[~,µ¶4_xõž5 œY7’&þ…±·uÀµR[&lMºëGâåÊ~fv«A G;ìô>¼Þo±éæ7¯dN÷šx)ŠOí÷˜wøæ3ÊÙËCÕö¯2Ûôÿá+Ú•Ô=Ꭾ°qÊœ}k0ï•yÒ}åâ©¥SQž­s0?ì‹ "âÖ²oiO|̧§1f :þÈ•fsØ{ü$WõÁi}oZ~ô7ª úÒw3ºU7–Û†°t_?ñaûÀ ZŸŠŠ§þUždº~ ¼Ò{BojæÒÈc82öíf’Öl»"·0µÖ1ƶìAè9Ó­$•õƒš3`›#×ï'lÅ8¬ìN«Ñ»IÇÄÉ9˜hÄÖS'ÙÜ7“¯†„’½©r‹ NŸ¦¬xV{ÿ"ù°¶-@‘R5h7ã2Ùøž :µ§žäÆÚwqÒhÐh,(1p;Y’Êö)]¨_Ág{ìKR»ËTv¦ä¼dÜàм÷©[Ê;k;œ\ËR·×BÖ{ŸŒå´±5·úX<¨»<‹cK? –G~ló»Q½ûl^Ï}w¦¡LZž.ƒšã¬y̼``ßˆÒØ¿1‘u3ºQÃÓ[[G<ßœÊ!ãã¥3=ægÞ¯íIÛ| aA\f È\O7gôÿ˜f<)Z¨0¥ cÍé[Ÿšéätêä+É í96ºÊÚ.E(Ør!Éêjþj²t¦dùŠT¬xk©Pª06æ·2…qPjÒmà›”)^ ïZ=éõVAF‘¨* <»ù÷læ `àÔAÔó+MP×ÉŒj˜Ê/?®!E}ÿòL•ç£YVý€é_}HH«@îï°31a®ï1qtcÊ—ªD𸱴µÚÄKN`$e-?þ’Jƒ§Ð½FiÊÖéÏ”ÁŸ7‹¿o˜w£Ñ¢Ñ“éö)í2iê:¡‚Ó§GH\ßYÉ;ÏqéÂ~~îU‘|“ÎÌ}ŒiÂoú¬<ȺáùùåÇ­ä1<nð÷ú t]” çÃWl;ËQ#h=‚yçɬX”#ïi›X¼ÎÄÛ›ƒqåU#IKèàæ€}bø5è܈+·+«€ÚT³Ž`íª8® džûƒ_·gP©Ñ븪+n‰ µX5‚<Ì…§q¢J/™Â8œ§ Ÿ¢Êó)d"ÂObS9ˆrÙ•™]ªW€Ca‘¤†è0dúR½jAnUZܪV¥Xjáñ‚g·É 4}E-7wêLµ`èÄwqMgÃÄZöü–ðKê.A]*ŸÉgΑéHý>¸ºzQ¾n'†u|üŸáÒ£Å'_Ñ·Em*”ô D…ÆŒè÷†[8`n44%œáœÉê ªàU¬8>•Òm`kJ?íom!‚GA« %ð©ñ‡ÔàÌ¢ŸÙ~Oä'—3)4¶ƒ;áþÏ&ÑÔaø·= *f­£µ›×Áó1ò“µg!‹ŽU ß×}x­”ZŒå³`ò/ZÐ}(M\-Á²8͇tÁ{ß–ÅAS˜æßàú¯‹øû:€piýb6Z7§Sƒy>’òòÓy6b䌟X²v ý2†7¡×›]Ydî—Ó•ìÃÒ•ÝHù¨޶6äóèÈî7~fÅÐrê'ùòÌDrb2,„SÂ"Ú¹¥þ7q8ÒCJ"Éyé1RPåù˜RHL§Bz®o샯K ŸîµÆÙÙŠŒäDÒL)I¤ §cßÔ/ŠG»E$8¹ '™‹)&ÈW‘y»8qñ"ñ[Æã6Šà¶c8Pf8‹–ŒâuÕê¡‚Ó§G‡OË®¼–ø5õüëÐæý|º•ÓéÞòŽ,Nýö)ÁÕ¼ÐÛY¢Ój°k¹˜´kW¹n¾‘² lG·ŠV¥" ;õeÔﴓȿ/<š®+‰éìŽJ-…Ë–AŸvœcwõc8ôãþðzŸõóýãCY”©J`þ¼n%\=~Œû²øydG²Ö”)_ê–”(éAö^tž%ñ$žØx ¡`£wh,¿±hSHkoƱU'êÚçù@Ê€Eà;|Ô£uªV¢ZÃîL^1‰FëøaùL€éüjFöý‹Ål ßÏ®5#q_Ó…VãÃÉÓå@¹C£Ac™ŸÂnn¸´¹óòsLÒKM•çÓöÎw+ŽÞNË­®s йžÔØàTÌ ·"°D4wÊX®»z,ï4íÉiÇ÷«~âÃFÞØ=󜼘TpúY–éÍú£Ñ¬×ë8ªO`ð\N=æøã±ïèÜ~7ZÏ%<ᓾö‘;Õí‚ødÛ1"–|L³RFÂg†P³j«ŸÑàÇ».^i˜òÃYÞÔã‰Zn5¶¶Øüƒ«¢FEŽ`Tƒ……î\`ƒ!LJd4`áv¡;Ô§s ~_ô;ÉçW³xKQÚvªÎƒFD(¯MAʇ„s0b êÇÑ, ã?iE_YßÌ´~ìŸú=ÿ—ñèý)9iqvq†”D.é3mÇ^æwr'5)ô.èÕ3’y¤Êó‰iõ¸8Ã¥¤KX×ÍæðÕôó¿IrJ&VÎÎЀÖÙ=)$]v§sè^¶Om„þr—Ðã¢×•­ŒiÞš/£øtùr>õßÍ€žq-CÃ!¿rRM…¨‚Ó§MëàEÍ–!Œ˜º‚ísÚÁ_kÙqσDZ²²0ÜO¢öaó}úÕÁ3¿%ŒœŽ‰»Ýjz›EAJ×mßÌ`ݶÉÔKúuûï>›5VVX’ÎÍÚTc<Æ¡ØìšÔÄÅèRœ|(y»»ÁĉÐI¬Èß•AÍ>(¯ONC>ïR¹v’·Çç8yüT®ão×.rêÔi’oäò&Y‰:r{»¬#‡8‚¥¼²Ã^[jwnƒóŸ øþÇEìônO‡Jš§KyUHj4‡ÏBÑâEÑ!\N¹ŒI£½ë;¡Õjáæ5®«òH‡W`Žç÷²÷ô'¡ÃöÄbU¡2~jœD©ò|bg*y’¾›¨ìj÷f8»B¹Ê±,ÊV¦¢U,»÷]6·q˜8»oçôÒC}C×1D#¼­£™Þg6ú/ör&îWZÄ eøJ5s‚ NŸ#Ñ¡#ùlÞfž¼@‰½,[±›kž~øÚæ\O‡»—Dn`mÜen¦giŽÜt^¾x]ã¯}—„ë1óùÝþ»‚­¬=3 $æfnG3Iò–/¥™_)èì!5BÈ‘ôÜÖ{1ýÌq]5sœçgŽûJ˜ãÀ"æ¸ÐÑ'Ú™ãF+s©3Ç•Ù1æÐG½—3HÍþ›°ê‹‹eŽÅ*Ç "Ór,åeb8ÈçUk±¾í^v /ÍK݈˜¹žnÅÚ“1;‘…ÍÖ¬•ÎÖþ~4>2œ˜ßCÔ”@Š¢(Ê+G£Ñô"€ÌKVŽÅc1š“y‘³»QsíNU#L”<3%§àÜj*SB|_îÀô±W’¸ppŸ-¸ÊÛß·¢˜ LEQå_£‚S%Ï´EêòÁȺÏ;φ$2?؃¶ë©Ú}&_µÒ«éVEQå_¤ºõEQEQ”GzVÝúªƒRQEQEya¨àTQEQEya¨àTQEQEya¨àTQEQEya¨§õóàòåËÏ; Š¢(Š¢¼Âœœœžwþu*8̓Wá„PEQEyžT·¾¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁ©¢(Š¢(ŠòÂPÁéŒñÐX*YiÐh4h¬ª3ñ˜ñÁ+§- ™}Iï̺ëeCøHüìë3ó|®¿*örz@^EQEy±¨àô™ËbûÀØ7 %õ_Ø»Îÿcög †˜qT±|ÄÊV^¼Þ©%U\žü40K CUÆE?$~žžb^•W™‰ók†Ó¸j)Š8Xci«Ç«z{¾Øtƒy cÜÏôjP ¯BöXYÙãâ[‡îßì$å?t¯÷¬¥Ç.¦½R8ÛÙRÀ³:ÝfîçŠ*ÏF’Ø<þ]•ÂÙF‡eÙ3¼Þo±éæ7¯dN÷šx)ŠOí÷˜wøæ3ÊÙËCÕÔ¯2Ûôÿá+Ú•Ô=Ꭾ°qÊœ}k0ï•yÒ}ýKžZ^•WQ[ˆêÝÆòÓo[عyÃü0¡e[¦ÅšoÌŒ–¯×›‰ 7°mÇFfõ,¶áMèþóyLÏ7é/§ôÝŒnÕå¶!,ÝÁÆO|Ø>°ƒÖ§¢âÓ@2¸’îH`ûø8¸êŠx?ÓõkàÕÞzS3—FÑi´o7“´fsع…©µŽ1¶eBÏ™¿á’ÊúAͰ͋‘ë÷¶âVv§ÕèݤcâäœL4 bë©“lî›ÉWCB9§.Mc^´€°¬Àpg P(”ü€ Pè'¯‚Ìã²bè[âWØ^¬lòKaŸêÒö»É‘ ÙУhA¸kщç€m’)"bº,Û&¿+õÊ»‹ÞÎZìôÞRëÝ)²#Ù”ã ×%ê§^RÇG/¶V¶âX¬ŒÔé¹@ŽîNŠ!fœT± ’¯ï}CD$}™Û˜¯ó–A;2ïz;+ì#)k÷š žÜKjº;ˆCq ê6K"¯åžmcüt©›¿´ Û•n~劬êä"ÎVJZŽõ2¶ ¯|õdÆã­Lɲcj©å]Pl­ì¥°¹ö”äLMÆïݤC[ùaÃ8 ®TLòYÛJÁ’íeþé[û0žß$Ÿ· ×üÖb¯”¨ÜT¾Ürå±ó*"b¼ð§Œi]I\¬Åº€‡už*».e—ù YÚÚNŠ´';T‘ÎâàZCú¬<-¹”ì\–ùMìÄ­åéW¿Œ¸ÖK¿f2æ¯D1‰ˆdEȨr¶RùËèû4IÂÜF’¿xˆl¼þØRž‡+ ¥¹4 MÍý}ã ™ZÛZœ»­—Œg›²ÿ„ë»KaëêòõÑìëF’„6Ë/MçK’éáÛ*“%‘£+ˆM™²/ëy§å•¾L‚ml¤Ù‚uŠdÉÞ}ÅÚw¸ìÉ®N®­—îEm¤ÆÄcbSR¨4up&óÌ×x1ÊñÉ5Ŧp7YÝ('¦Õ•Ò]WÊÙŒt9õË;RºÁ É®©—$Õø,3™7@?s\WÍçù™ã¾æ8°ˆ9.t4ljvæ¸ÑÊGêÌqevŒ™+ÕrúÔ‰‹Ò-Þ]Aü±6ý𕲻¹­h8+£d²m€'vMæsY'¦ÔÂ@ntÕ…f£°92–C›§R÷Ô4뵄$sñè÷ô콑¢ÃÖqèD{×N¡³¯5yꕱnÍ/7Iý™¦ÖX's'³—Ú0pM$7|ŒÇß} þd;÷w>¤³ëÛéì¯ÖŸÞU³wæ@½à7Ñý±œÍWnï}+×’X=˜¦Å´@‡'5§É´Ë4žò'Qqá,ï[€eÛ0éÐݹ‘ô?ùrÜišÏ=@â¥3ì˜Ñk pƒ ¿Ã¤‹o1k×1â£þ&tx] a¼Ó¢ò¨¼šâù¾S+&_xƒï¶Çýç—TŒI³%$ÜÞ‰ôÛ¯$¿÷q‰çÙÚ[Ëü>cØœ§žg×oÆúã­œH8Ãÿõ¾k׋E åèØ±ÑK–p;ë¦süºx+έ;ñ?»¼Gy–$ý»f/f¶,U*Øß¿‚é'7ÏeE” •ª”Q¿z’gFâÃ#H-V su¥q¢J/™Â8¬º£•gM’‰?‰Må Êe·ªÚU¡z8I:`ˆã@¦/Õ«4G_ZܪV¥Xjáñ‚g·É 4}E-7wêLµ`èÄwqMgÃÄZöü–ðKªO@§O‰ä3çÈt ¤~ \]½(_·Ã:>~…¤-F‹O¾¢o‹ÚT(éA‰ Ñï ;¶pÀü)á çLîToP¯bÅñ©Ôn[Súi÷Íh <ú ZU(O÷˜8¤gýÌöÌ»W“‹Ë™šNÛÁpÏq69¼Þš7µ°";:Í cåÚDj´nB1-±…o§ÆPü<†5 ÀÛ×Zï}Íà*‡X¼üðÝÁ¶ä§Ùç“éP¡¶vzJ¿Ñ”ê.«œ;›F~ÿ:¼V¶8Å<ý¨Õz=^s|ðíØ=ŒG—ºÃ™®>§YùxUîÀ×cZ"ë~bÍèë:0ô.XhòQ¾MsÊ¥F~2oãk­ëôfÈkÎè°Å·Ë ‚m7ºæ"‚Ÿv©ÿ ‹÷ßú M§W±x§í:ñ ûåù‘Kóijo…+µÆ&ÑaÙ>,Ÿã›n<ÊÄš¶XXäÇ«Ñ,¬‡üÆâžžê‚›g&’“¡`!œÑν(õ¿‰Ã±RI~A‡¸+ÿa¦“Á©žëûàëȧ{­qv¶"#9‘4SJ)è)äÇ7õ‹âÑn N.èIæbŠ òU$dÞ.N\¼Hü–ñø‡"¸í”΢%£xÝùqk°ÿ.u­|jtø´ìÊk‰_SÏ¿mÞÁס[9þè-ïÈâÔoŸ\Í ½%:­»–‹I»v•ëæ8É2°Ý*dX•Š4ìÔ—QS—²ïâ¿ðº®$þ¥mÌÿÑR¸lôiÇ9–œóŽÎÀ¡§ð‡×û ¨Ÿïîíó½NðÛ:6­ØÌU +|%k.Ö$¸IQ4€él$“Rø¥­ZyvmQzþq“‹ï›gY‘ J6ÜGSˆ7»´@Ú ÿš-yoè—ÌÞt”ky¸é4ÆÇ¯)Mù²wÙ•-OI9Nì‰ìYCþbEÉg¾^hlí±å×oæåîV‹³·NÙ× O¼ÝMÄ=к·¦ókX¶x7‰_¾˜0ßötxÔSmÊó )ÐŒéû"ßñßµôÂ/§rœµ:ºüFDØVVŽ ÓÞçÓ IjŒä?¥Ñ ±ÌOa77\ Þ¹¨*\y^4€…½3ÅÝŠ£·3ØË>#ÅüMרàTÌ ·"°D4wÎY¹Fìê±¼Ó´'K¤߯ú‰y£:ÊnQÁéSdY¦7ëF³v\¬ãX8¨>Ás9õ˜Ǿ£sûYÜh=—ð„LBúÚwpDîTjvA|²íK>¦Y)#á3C¨Y5„ÕÉϦڻ«2HÛÀ”Îòö ¹´ÜÚS'øm,6­àÏ+™„­\Cb­`¹³Ñ•aľ,D$Çb$á‡7°ÊyL lrÑ´¸µ_HtÜf¾éQƒ‚ç×ðI“@|•·aÎ)ÍýÕ ä©È£!ÇpŒrgšÂ4§i+±íZ,Ë–FQ±c{^ÔçË^y:G<Ê–£bõ·éõízçÿ•13ÃsœwV8{ùQ®R-ššËwí®2û‹Ÿ9¡zÈ#-Î.Î’È%}c¦íØËüNî¤&¥€Þ½ú~(ÏšV‹3\Jº„u­Ñl_M?ÿ›$§dbåìL h]ГBÒew:‡îeûÔFè/'q =.z-\Ùʘæ­ù2:€O—/çSÿÝ ¨áI×24ò+'ÕŒ‡*8}Ú´^Ôlˆ©+Ø>§üµ–×ïYG§ƒ¬, ÷7†¨=DؼEŸ~uðÌo‰#§cân·šÞfQÒuÛðÁ'3X·m2õ’~gÝþ»Ïf•–¤s3O-·9q(6Ãü£cHqò¡äíî'B'±"W5wεÃþÁ4²üƒþfÅêDj7¢°yE­kyÊ9bÇŽ3Oü³mñ*4é:„ ·²¬Oa¬û‹³¹S—%äcî”ßè(ŽiJàã™Ç‚Yi\8uŠs—2rySH9|èΙé1:®Å«”§y؇†‚ow¦qæjB¿™Ë’¸ :µ)¡¾ /AÄÄÕ+Wp. &“ W¯pM§y¤Ã+0Çó{Ù{úΓÐa{b±ªP?5ˆWyÖ4ÎTò$=|7QÙÕÆÍpv„r•+bX”­LE«Xvï»ln0qvß>Î;襇ú†®cþˆFx[G3½Ïlô_ìåLܯ´ˆÊð•j& U÷=5F¢CGòÙ¼Íý+ràóŽ ^°“ØSñDíXÃÌaù|sævxoϲjÌHf®ßÏñó œŽ\ËÒ¿/ /[†ÇÎTW²5ƒ’øiا¬9x’øðÅ ýdÒðšÉ[§¡1v:|¼©5f¹Ýø÷NcÈŒí=ůÆrc:7)|§LêÓ¹…%Ë>›Î‰×:ÒÂU}=_8¦,Ô“1³WñÇö½ìÛ¶†ïzwgJtQ5­Œ|׋~_ÍgÍæìÛû,ŸØ>ó.âÓ¤¥U0•g¶u»ÓÅ{?“Nåï˜8öÌÌØ îÙ½ê×ÿ„´SQDˆ$6á&¦ô‹ÄE 2êi¯zD”Í\&"O’*&ÒNäÀHâ3 ÞíAå³?2ôóuD`ÅG#YšÑ€v·4ú&„;²iì@æîŠ%fËtM ëKêÚ ˜›ýńѤÅÒÚ…56–BV–Lý0j*©<1ʉeä­@oqq° ÛBRº^o™uÿüK¦Ô2¡¹¿¸ØêD£É9•Tšì›ÞA*¹ê¥°GIñû_ùÿöî;<ª2oãø÷LKo$ô „Þ¥ƒ¤ƒ¢ElØaÅÞÝU÷Õµ¯uU¬ˆ ‚Št‘Þ{ !¡‡4H›rž÷†É¤b”ßçºÎ5“ÌÌé9çž§åÃ×nV!~#ÕŒ’QšìÛ'©Û{·T±!>ÊbñUá-¨'fp3DC›û¤êç§Lš¦Ìm_Q;íJ•ƒa*7¤ÊãúIê”î4”Ô›ãTÇHåá­:Þ9Im-ÒHWÇ¿¢‚cïQóÏT¾WòÜ£" šòô•:é:ì‹ã”Zõß;U†¡ÊÛâ©#›¨î#žTÓ†¿*JêÜöŸ¿#O©/Q†«O³ò JP]ÇþWý^6 TÕÛª”Rö#óÕ‹C[©_‹²øG««F¾®–Ÿ:(©ð{–ícýÈGª·wõôúóÇ_±o{Iµ6;Ï2ÅCIÅÞò’z¶_èá©Bš RÿJ>¡\G )Z5Q%šÕ°o3”Œ’S é'ÕÂWFªÍ¢U°¯E™=Ut›Áêï£x IDATñ©»TñŸ‡Cšñ„Ö¹‘ŠòVf³— Nè¤nþç,uHÆ‘º`»¿Uõ¬¯‚<=”LG5öÃõ*Gþ@.P¡š>ÂGi.×DÍ·‚ëì¨øZîzß0«6/ï(îϦÏzZõoª¼=|TXËÕÿ-=yÞ5[ÏÙ >º½£Šñ÷PžA‰ªÇƒ“Õ®wKÓUƲ«ÁMÃTXÕyÜdµ».ÓPR•}ïÔœKCjéci`5&§Éì4Yœ¦ÖJ©w+Y–ø+±oå_í»2çæµ¬z¢‘ â\¥l¾É ‰óÙ[:l˜[Šì·t/|´ï[†\ÆUB!ª iÚ``ušlN“Ýir”LzɤœKËéÝ–×K%“¨1=ã4!7¼ÃÛã’$˜^"Ž‚l2ÒçÍÏÄû¦™ô•`*„â %áTÔ˜!¬'÷?ÓóÏ^¿{ÞêM«÷Ùãa>{±‡ '"„âŠ%ÕúB!„¢J—«Z_º !„BˆZC©B!„¨5$œ !„BˆZC©B!„¨5¤·~ deeýÙ« „Bˆ+XPPП½ 8 §5p%œB!„&©ÖB!„µ†„S!„BQkH8B!„µ†„S!„BQkH8ý[)bæ­4 MÓÐ4O®ýßq÷ÿL!„¥ër®€„ÓKBqjÒõx–…Âó'K»³ËQÝyÙY÷T#Ìš…¯í¡ÚÀHBŸû™0á!†4óF«ñvñwv–uÿ»Áä…‡w0ñ†óü̃¹y·~d27„Ñ4z}Ž^öJû¾ŸHŸÆuññô%¼Å`ž›“~îoU?Ų×GÓ)./oBêwe컫ɼïBê$ _Å5­cð7ÐŒQ<°Äzþ[r6ðɸ4ñÆÃ+Ø«n⥅GKö§¥Å`rs]55|œßmÆFÕ~…ûáù›;’⃧wâÚÞÀû›íÈþ¼4*?gAOÉ󣯡U|]ü<=ñoʵ÷}ÄÚÒ‹@Þf>ÑŒ:žž·ͧÛòÿ´m©­d(©KÄÒ€6mOa¥ˆ»wžo¡^RS¢|4LÃñ¼,IÑD³[þ·o±2ÿî©ÌÜž{9*Ä_ƒžÃºï¿dñ±–\Ý·=þ™ë™»h:/߉mù&þ¯ƒÅé½G˜2þQ~>¡—›MášqØwØ®êÒ¤eÇ7ýÊ+7&`ÝRmlàèä»öäLò"»pØxŽ/žÎ×q&d+?Ž¿²¾4ê§Y÷ó¬H ÔWãL¹KRsÂýŸ'¸ínm–ÏêÓyaØqü6,á 5|#›Ð¢e²/zv*;R³1øâ/Å+娓3×ûf&§{Ó¾ýê98±w[8 •QöçE«êœ5b?8Ÿ/fl% sw†tÕÙ·dÉ?ÈÆc>lŸq ¶/'òØŠxžûþ_Øÿw?û–þsï&⊺8\8­d2FÀ XOÀð ˆâ†@S 5ÐèŒWW ûõZ³Â˜¨&®²º¾¨Ž-~Uê«==”_D 5ðÉjañk;^n£Ì pJ3ƨ*¥”²®yB%™4åÙûC•î¸ÜÛügËWÇS©<{–új §Â©î_\tîå¢EêÞƒÒoRßç(¥”Cí£³²`V-ÿ¹Õ阔ÊS‹î‹QFÍCu~c¯›×¯tvµãå¶Ê¬ùªn¯mUùe¿×•îöF û³ÆªqÎ:NîVÛŽ”}Ķí%ÕÆŒÒnU¿ÚÕžWÛ+¯Fªyi‡Õ¯÷ÔW^]ÞRË® µûŽ Œ/ÉuJr^Ó’Ü_’ÃJra`INô.É–’i,É•¥Ó-ùžtY7ÿƒ<ÅwòˆêÒ$ö0ëµ›éûÈ"r”Fv7qß÷1¼M Ôë<šûx€îL3ß’yì˜Åw4ìu㼋kB°èƒ»óÖ¶6âJãElÓKVØm”æEDTHÙÅPOŸÂøGgáw燼|MàùWOG*›6g #éÖ# 3^´ëÕ™ƒC7‘¡ìX­PEäæ kn.…Jaݱ™ö˹½µõbÂð6Vôº«°!7OvÎäæ¡cgßæíä¹¼[eÎæÓïÓÑ}¯á®Qõ©p¶W*•ÁªåÛ±›b Ù÷­ƒ½ð ˆ¡ÓŸ²Åug"ûóÂT}ÎB“hîyî#vvƈh"Fêy†ÛÍ_Ó/:–ÁÓ¸ï¹[ˆÕ3Ø8åyFõ}€ï_‰m€ªOJN/D…%§jî]aÊ YT›7©¥”=åCu­Ÿ¦4Ÿþêó¥ß–ljí“IÊT®4§˜#ó¸:QhWù™GÔ¡ûÕ®éãT‚åÑûCu¤ì —”œ Q9‡:þëݪYSž-ŸV«Kkiê›aõ”¥þýjA–Umy±¥29—œZW©G sõÌT£Ðöê…&¨úF”¹ÍKj»Ý®ö¼ÑYyi(Í;JµêÔVÅú”JóºAMÍ«d•þÖ*(9Õ©¯* MyÔkª:µ¯¯‚Œš”¥Ç{êðy%Í•öñµÊG3¨º·ü¤²/÷&üØ·¨[š 4K„j×çZÕ²®IišY5z|µ*:ïͲ?/HÎY¥Tþõf¯ e0ÔSþ)­QJY³Ô¡;Uê‰TµjÒ£jH·kÔíÿ™¥öž©Ýwl¤äôoÆ‘Îöí§Ñ ±ôîÓOÀs}š™Q…;Ùº·:E*:'×Ä-CðŽ$.±>oø„ƒг2É*ß-ŒÑLæ©þñÏÝ˦ƒt¿c0I&@3œƒrìæëÏ–‘oˆåæ»úð'­r­¦º`¦Ý Y=okf=F£ýsç²Ëù6#ûóÂÔäœ-ÜÅ'£úóÄRèôÜt&Ž:÷º=‡K>àžAc™”Ù“×ç.àóGûÓÀW. áô2R?dDÁBžû³ö‘8ìy>úfSÿsÑF@wp~6-9Á•TqŽÎ©ä'¸~ØÛì ¿•¯}ɨsÉkvR6l"úŸ¯  ™iùâìXYò`I­Æf&4Øz&Y!·ðáwÿa w&Ù Á¡†Œøïbö.Äšw”uÿíŽ9[ÇP7œzÒµcT^™µƒ“ùV NmgÊà@2u ¯°p‚œîPEk>çËM6LMÇpgÏŠgx%3RÇš ›&b<š¶ É*'›§ÛìÏ W­s¶p7ŸêÃ3 éò¯ÙÌz¡ ¥ßwUöLîëqßöçýäd>èŸÁ¿{„áåDã!ÿaU–Ü·%œ^.Æhš7 Æ §²xþ ÇáÌßnCólB‹†çîZFcñs«õüq=ôÓûÙŸ¡ƒ¥?x‘{o¹‰ë-ä•;5|ý|ÑppâØIi‹*PL‡¾Åî¸{˜šü#âÍN¯ðkK—.]ʦÖ1>hLlGÛ8 ÆXÚ´Åà8Š¥»±QÀúÅ¿‘£›ˆmÝš È?¡“¥»ùlùø,)Ðêr5-ÍnVë g?qˆ´‚’løæƒ™œV^tèÑžs‘é ‹>›ÂÝ“ÎwÞFs ùîÂhÑ" £*`Øë®m쵃!<’°²;¾ìÏ‹Qå9[¸‹OG]Ç}3m\óæ|f=Ó çŠ-°/ï¬XʤG®'Ñ;‹éO?Äì |ÿËk´Ûñã?Ø)÷íJH›Ó QIoý¢/©vÞšÒL!ªYïëT»H¥ifUÿþ*»¬™‰®N|ÞOùhš2†uR£ïŸ &<òZ•£+U¸LMˆ7* !ª÷ 3TòìÕmM<•ÊÜêŸj«ýÜ/Xñ’ê•TWùX,Ê'´¡ê6æUµ0Íùh8Ôwº+OÍ ÂoŸ¥rÿ¬mùË(Rû§?®4W¾‹ò o©†¼0W¥—íRÙŸ«òsÖ®v¿ÚÞý°žÃÔwgËϯp÷d5®s”ò÷ UÍo|K­Îª½_.W8­,µjN¥!µô±4°)È¿t2;M§©µRêÝJ–%„B!j1MÓ&›«ÓdsšìN“£dÒK&åôXÚ Ñm[is*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*„B!j §B!„¢Öp*Äß™ã~ÎÊÃö?{M„Bˆj‘p*Dm¥ŸåØîíÊV>‹£ë˜5?^ÆË¾l!„âB˜þì¢jŠÂÛXµ| ›vàhn,7½t/}´ ?¡goç§'±¸¨'O>7ˆhׯaöløeó×íæÈð‹jE¿[FÑ=ÚãÝ”ÐÓ“ùäý=t~¡)q4ÅÙC)dÄ_§â}u¡ËÖs°bΖoÚω<#uvcø˜!4<·¬¢ck™ùÃl~ß{=°!½n½“I>h(òÓÖ0oöÖï9BŽò'¾ýPÆÜtuM%ë¾1Óg.cÛÁ -!4ì:œ1C›Xá¦Ø9½ußÿ²‚]Ç­xÇuaÄ]Ãi¤A•Ë«áüì»™òÌ;,s ï†(>ó ¢Üï·ŸÞÊÌÏ¿aMøí¼rK“â °*$í·Ÿø)yûOäa Œ§ý1 ¿ª®Û ´ÊÚÁìé³X½#l‡aMz3âÖ>4(ù{гw1úLVlO#G÷%¦ÝnÙ‘°j^í+>fŠÜïñô·;°•nº!’Ï<ËÀ(CÇ ªc$„Å$œŠÚMp(ù &-Ì$²]{º íFÝ P"½+¾™ÙŽ­äë©ÉγâÓ¨áîr‚AÇîQŸkoëCˆñ4ë¦Oæ‡é‰´˜Ð½’ðs9)ÎJ!# ®ÆÁò;©Ó0Æu%ªF§ÕY¶"kã;&s$¤ Ã{Fá•ñS§LæÇ„FÜ×Ñ 0Ø ÎU7rÏ0_Šö/`ÊŒo™×ìF4t·1Šì _óæ{ë;’ϰâ›iLߊf#bRU/¯Fó3DÒë¾Ç騗¼ÝvŒS¾g_l_:E”?áTá ¶-ŸÇì¿“zÖDó®±”m…fÄ®ûÒ|à] ppdÅ4¾›2“¤æwÑÆÓݦÚ0G^ͨžõðÈÝάoeÚÒ<Ý?CÑ>~z÷#ÖÕéÈ c¨[¸™ï?ÂwÑ ùG:n¶ó|•3@{w"¨ë8îé^\õ¦yXÏPõ>«òu!„(&׃KÎÆ©-óùyÞjv¥ecóŒ¤÷=1¤‰Â´ÕüåLxz±Z­e¿)Ú³–-Ö&ÜØÒ2–ñÝÌ#"røß³3)ôŒ¢ý°±ÜTQugö&¾ýl-±·=@Û¢Y̶FÓ=Ñ«Ò#¦lg8²-™WÒüƶ„TqƒTù{˜þÖ¬ÒÚÒï¦{‰ó·sÖEΰÒõXIJ}ê›|¼ÑŸîƒÆ244ƒä/§óÓÊ«x¢o= ÷ýZñqÒr0ÕJìõ3¦½?šæEP˜FæÚ/xãëƒD^3»‡û“2k3gn ýC]ðËÙÌä·?g“G[ú ¹“-;ùî“ÍÄÇ–_¹[™úög¬3µæÚþcÄ&¾ùúf_3èin–í¦øY3”ÍWîcû~]âñÒ@O[Áâ=þt{¬+¡%5˜L Cɇ4íÜZÙŽî`WV I‰Á. áu 3öóûÌÙì »š¸=òd®If#í¸çÚhÌÅsÇd2ƒA+[Çê-¯úó+æàø’/øvO<#ŸêA¸ûoBDö}œ4“¿ò=æúÅ“âfêV²odÎÏñêpͽ5@'/;»g^&ç%+ìy'سb:ÉÇè=::'§ãˆ@¢ç¹÷9:š¥êîzzÕÇ,çvZÓ}V“}*„¸ÒI8½„TîZ¦ÿ´ŸðžäžõÎUÛ©,VLŸÍÑ7óì­Ò@…w£Áâé¤Óéâí…¡0Ÿ srõRv×éÌcI&ŽÍŸÆ‚ìæŒ}öVÚùk¨¼Lê¶cµ*°æ`š‘„H*)9£h7ß}¹k—yↆxkvvlÉÄÛ‹X£"{íT¾ÝÈàG¤w„¬[Xk!&>ƒÊâ·g’}ÏÜu5!°Ù"ñXQ€UwÝÚ"RSŽâ“4˜0OhÖfÖÍLሣ¾~¾‘ËYä²uÃ~¼ZŒ£Ùy^^^è)0‘Íú¥›0^u-}tRç/!-´)ý{·'!ØBÖÚi|9ùâ’î§³¿Ëí̖΂I3±õ™ÀµQ&²—¦RŸúÇè¤Íú7¯ÎJÇa ¡Ãíqk»ªª=í¤.øŽ¥ÖŽ<ðÔH;7-°m/w,lûàýUŠ®=ÈðFžhê$)uf°ºÈE{ø¥Âã*ç)ÔoހȒ*•¿…©?lÅ¿ïãÜ; 3:Þ±~ÌK±bW¹¬Ÿ6™~ýyd|¢,P´y5™æX"]ªÁÕ6Lû†µ>ý˜8¾/ÑEþšeäzÇ“f@å–_veTa*‹?ý’5Þ×ð`Ï0 è¤oÙÊ©°«èSz‰QäffáL$Èe–¶«ùú“EX;ÞIßD§KRá>}ê36ä+¼bü?®'ÞÐmÙР±xä³mË!|[  QYq™§ÏâQ'_×SÅeyÊVH¡MGšÁŒ‡ÇÙjÏÏql1“Í ÕwÓ®’ö ³6R¤¢Åu&ÚåÊkß=•çþ»”,ÝBd¯{xôƦW˜i<òß¼sÞÏbé»/0m·ü[0bâƒ\nt¼¼½ÐícoN{Zù;ÈI[ËæTànu\B¸kAv aô°pªÊcfãØÑ3ZŽðÅ“ðsãž 5æu  ²+ßgd³²ÇHqe“pzÉ(În[Ç.SîéâL•½…uû¼¹ê¡«ÎU]9ì8°àá¡a°øãkÏ ÏªÀ±—ÅËOÒhÀ}Dq„_צÜi$­ýäØÇï¿Ìa—osî3¢?À¡ÂºÄ«´Íß¶ŒµE­Ó¿!Å/Ç9˜R@DÇx<Õ)V.ÝßÕÑ#¢8%êÇqØI÷X *ë7Öì±ÐúÞŽ„PHÆþõÌùy#ªñH¸Ö ;ÒII…ØAÑeÛªtÌ,š à Ø”I¶u2×±zŸ/mÇ'q^á+¾~h‡Ï’x¦¯`ɾpz oˆE?ÆöYÄõJ¯6ÅÕ®qX¶lé'tðwÞÃ…ì›ù+C†ðDÛ@4ŠH=˜ŽgBÜ4ÿ+[vÝ®wòxƒt¶Ìÿ‘ä«éÝj1•ýE8RY¿>ƒ˜ÒÈ¥ÍkùcaçÀšõdÅ^Kï$ìgްsÙt’ÓBè8<œÂm“*>NØRfŠ£»S°,Øñ;›­Ù# SQ6©[òÃÊbû5& kË·{Òi|o¢,:G¤`êEÜù;•µžåÛ<èôÐ5D[>ŠŠL¬Éý²+Rtô7¾ûôvõå¡ ×‘è©ÊãpÊI|ëS¯tÿ«î?‚%ö§cb'cÓ >›¼C—qLÒ?çÝjjÄÐG£ÃÞüòó2’wvåÎv~ä¯ù„'¿ÚŠUæÑš;_îLJš¸nNM8l©ìOUÄ ˆvú[t·<Û§<Ëû¿A¦7ó¯ñ!Õ›ŸÊ`å´9d´¸{›ùV]ú§å@J‘]ãpmJjˆ½Ž‡oAÊÚYÌø}!{4¦GÝ N^Í—¶£#2}'‹šÍ¢åé0¢žˆè:ˆ.›¾àO®Løú[È;D¯¤ºç…Su2™·ÿ9ƒv@ó§ÛøgH¨ò˜™Ið0Ï PX3¶1óÓI|ú¥?Ï?Ü“{ZåûÌžZÍc$„N/!“ÇN G´$Ú|þ+Ž£‡9ªÅÐÍ)ùXS’nŒ¤}=ZA ;ÇÁɳXëy5ÿ¸*­pé§d,y—‰É…X D7nÏmãûÓÌ rKzbÇK¼K9Œ=æzê—ÜýÔÙž >!CÑzñ!ihdÉÍ@q6õ™ñÄh8v¥qÔ~–CŸ=ÎÚ"+ø†“ÔæFÆîP®“Ê<Ä¡3a´ˆ+i;§ Ø¿7 cT' ¡!xæœ"£ÈŽqåJRû0:ÑõtÓ €Ülr¹¤Ì^FAûÛé^Ïö Nfñósj›gµbÅ o¯óWFZÉO‹¡>ት%¿s8еy-èqží¦êVÃ#0œØÀp¢}N°åÕÍl;Ù˜ŠÓ,ªðDzýˆ‰ r "çzÅ— •Mú‘³ØSgòÊ„Ÿ(²™©ߌnãFÓ7A#uC%¦_øæ‰IDATÇ c)‡±Ft%¶ìKÎÉ´#X­™|÷ìx¾)ÒñM¤U¿Ò+ µmi¦®‰+ÙÇ*‹””,ê$Æàrìi)¤è]ú^ýåÖ:oM'­Ü²ÝÑÉÚú}¹¯žãxªcÊBM6§³ Ëö»:³™µ»4¹µä ŠÊcïìùt‰•v·<É ­CÊ_ŒL>„D&‰mßc|µi?övm0% âö).é4aßÎr«/1Açþð w¯ekA"ý›ù—œŸ-O#úÚûx´‹4 ~aå­#³ªù¡ÈÛò+sRcé÷lËj•ü©œƒÈ"!ÁõüƒW"âêmäðÆ÷ؼû W× ¨ ðšñ«_Ý(Bswòì¼ÍÞˆ†FÐ[2êÙÿ0ðt6V‹ƒß¿Î×§ºÒ5Þeïúµææ‰ Å%§š'!QgYñcǬŒ†%¤z%±tj*Ç|6³Ò}F¯KÁ©™„ÓKI)°ÛÊuªQÖ"¬1–è§Y“¼Gã4õÕ0x„lÌâTÊZ~šŸI›1×c•oêÓ安ô÷ÁÇדsÍÎl¤LÇëÜ[QTdE e7ýX:Ç´(:FQùgÉÓÍNCç8H;”Ž1¦‘FÀfÅîÕšÑÏ §©·>Þæ Â-JMá˜OƒCŠ{éœÇœ-&ÚÜݼ¸$0<šp•Ìá}غ*—¦7vuÓk^Ã34¿3¤où•EøL“âR%¥£ë6Šò­Ÿ¦ŠÓÛ¶rÄ7‘ÁõΟ‘О±Ï7+®æTÖoLú`I÷ŽãšÄzU æ«(»gÍaW@&6÷FÃÊ¡¹ðñJ_†<òÝ#,n–á<»LNf8ðIðŘëD“XÇimNëÅÇ¥´Ñ¶-•¿¬Gk;®¤ª½²å™ˆH$€šÌгbîF,'Ð9¸zÑÊšr€ts½*)‘V¹'9™oÆ×׳ÍÆ©“™à›t~8Ö,øû’2ÿLÛÀu÷*÷·§y…—èÔpT?\Å1+·5¤¥G¶-þR¢W±Ï2«±O…¢„„ÓKÆHDÃúx-]ʌšôŒ÷äìÑã˜w£Ix4áú"V.ÚAÆphÙt~:ËÇÚ᯦º„ç³è‡ðh2Ч›–Ü |ãHÉaõŠ5$y4"ÈÇÉ#vb»·&œcL)$²«sOl#õ"Ã`ý*n‹ •é0+ZCޱ^fÐ|C¨cÉdËò5´0†Q˜²šY›ò‰ì[k¢‰qÌfÕò=´ Å\xšôüºtº*†¢_òÚäãtø×G)ޤ¤b î„:¼›õûV3oÞ&ìîbhé, šè€LÖ}7ƒ|¿î<ÚÎ}éˆ!4ŒPý7~™l$fГ´/½Q™"‰‹‚Y+±#²-GWòì£$ CCcs_{‡euoá¹±­ñ±øS·žÙ܈wp]= gë–g‡R?ÜΞ`ïêù,:M¿[ÜVÿjá„ûœaÇo«Ù¥[Ø»h ÚqçÐf÷-«lyåÏʪæ`Ý»„eG£é}g"ærspÇAúlÑמkf¡ سtGüê`¤àt ëÎc_PWÆ7ñ=¹¯Ÿ;ϽN®cÎ+щõð²çrtç2æ-Ë#iD{  ò3H;žA汃l]½œuÇëÐsÜ pm×áNÇLålcѪ\ê%ÖÅÓ–MÚöeÌ[~–†#:e¨Æ>«Æ>BˆRN/!¯7rg¿o˜6÷Þ*0ÕšÁ »bëÁ¨ai|5ç#þ3ÛƒÄvŒ˜8ŒNa%·ZC(Ñ‘Šòš1æævçªa Ñô¹ãf²¦ÌæëwÅáDdRoFu•wˆ”ÓÄÅ;(àÎ72lïÌúäu–5 ÓÕWÓbí¬ù ü›3hTg&ý0•w6yR7Ò¼¢`:–V3†tãÖ1'˜üóTÞ_hÅFý®7Ñ^õì ¾øX(®2>”ãàlÞ}Ó“Àˆú´þ(ý;Åœ»Ñ£iàÁâõ:¾ŽØ Î4Í/’¨+iõFrËÕNíâ´ºtu‡¿ú•_]€¹N-ñÓ@OßÇ¢hº4qS¥j§÷Ø[Èøf&SÞKF÷ªKý£è|減ca¦Ñà;øíTæMz›<åMpLsú%v©Æq²Ð¤çu$~±€Ïß]‰_TGF4mKBÛ›¹ãè7üøóǬ*4àOû!Í‹W-²7÷;Ä7‹>âä`tîN°Z­(Lçm‹!ª7#®?Ä7sÞç c(Mºw¥{Ò&(²î—í̾÷wÖ²“{ò3^YUz¼Ì4»õÿx°‹Mó¦õ ál›ô_½÷;¾Q-èûÐM\ÛÀ Ãk×fÍ%uÊkl*©1Ža/ÅáËIž“C‘æEPDCÚy„~íCÐÐ9±òsÞœs ߺ1$µÂ#÷´'ίšÿ°ÒcE§±ÓæÌɦHó¦NT]nEßv%³Uí³*÷©BœSÙ•AszÔ(þW§¥ÀX2™œ&³ÓdqšZ+¥ÞýÖ_\0EÎêùç¯ÁÜû¯4¬É×”Â|úÔ÷øÞ÷ #݈è§YõÑ«üê5Šgno}~G—ZËÆÞiÿ䓬<Oä¾)„Bœ£iÚ``ušlN“Ýir”LzɤœKÛǹ¼ZJN¯*›­ÉkȯO=?¹)«™=sáý†Q¿†gãÈAR‘ôsí<¤²HÝ“…2f±+ùgfwâ¾Û[ýE‚)¨ü¬ÞÂõ÷¶“`*„BüI$œ^)lYœÜ¿%sg‘Y á[7ž–ƒbpòf*§È>tˆœàD¹§”±…Ÿ?ý‘}z±­zóÀøž$þ…”iÞ-óB‹ókB!Äå%ÕúB!„¢J—«Z¿f…fÆý?ÃB!„5x®»áT!„Bˆjù£Ãi¥Å¶B!„â/ã²äºKN%| !„B\Ù.I¼áT¹=° eyÚÆÒz`Ú‚;¾DO¼›Þ§…¦³¦…QDgBè p}eZ˜1ÓÂÌtÖ´DO~aZx+é x€†èÉ/¬<5-|Üè x€&ѓߖ¦… Ñ“_Ø8tÖ´tï'Ñ“ßÒ º÷ÇTÆD=ùí¬iË_†Ñ½?êDO~áñéÞóCôäWÓŠ²C¸D™¶³v‚-M+ÊE‘v ­îŒ@iìȤ=­À [ݵ4m;wdÒžVàÎHt>Jư-x¹eM°ÖìçËæÓ%`M‚Q­úçô<À'‡è&Ðë—ÓðОiáݽ{=í™¶øD7ÆÔ¥'à¡=ÓÂè&ÐD£ô<´gZX‰nM¶0¤'à¡=Óf{\$º 4ó¢=홊D7ÂSÐðОia!Ý ‹”ž€xãˆnmÏ´° Ñð+*ÑM ÃS{¦…&ÑM P{¦… ÑM ÃD{¦… /ÑM A›§…ôCôiIþU{¦… ѧ%áYèi>Оi۳Ĩ@%¾qGê0=¸¨ÂìE‡Nư Hµ1‡‡ƒL -€¯•ðmX¸·¼@gMKt¨À¯bÑM 25-¼D7 ü*&ÓÂNŽè&Pþ=+ ÓÂB º )=5-¬¨D7Ò½?F§… Ñ_Ççyœ‚Ñ“_QÇiÛ³3’Çi‰n¸3ˆn•ø´"îPÚ3­Äwdí™¶³vF5m{vFðõ/rÇÆ–>Ú¼ÐÉ8Çë2ÄâØ£û[ZMAAD¡ŽÍC8˜›ya'cø²ø|Ùü—[Ö´ÚAAAäê’¼›Þt»ØÉ˜:`,ðœ9AA‘wÞ7¿ ýóg·óÂNƉF'ž/›ŒAAEE”ÄËNÆ/·¬øOA•´¬Ø‹ÞÆ·º¾{ÿ–237Qkã0~_¼z(×}`˜K Ó*^Ÿe:c*ª˜Ü>·º@yì³~ÏÔÆ¦zn/C¢¨} Ïï :€z'~çvP¤>Päí“@yïo.¼ÅDV…ì^nøPÃG›è¶¡Ï“uGM×Ýyä^XœMïÀÚu–DK?)¦èÿ ÇÝOµQl^Æ<Þrp–žÁïWýΕd5¿mDQEQÙ”USûÄÞý8ÄqˆÈÙù)ôάMPɘÕÂÅ…(0#HW¡¾¡ö”ÕfÈÄaQšß¾FQEQy±á5Ë/Àò±×m[z¯&>¢&ãÈ^õA…U]¾ãè<3ÇÝõ¯YoÞ¾FQEQ¹³°$Ó/àâßÿý›Þ·‰‰¨É¸±´Dà¤Q˜eùŸ]{ÓüEQEåÚæ·MÿmS85#¢Øüý÷ßÍðm»¹EQEÀ·ï ‹ &cAؼ}÷†Ø¦ (Š¢hçê|ÞáòÅËDQtó 3š›ÅËÇEw!·t;t LÆ¢à¼y󺩹EQEeÁÒÊ’©“gÿ¿Oþï«o¾ëùeßOþÑý·9¿}DñœËÁ;Üé]È4––ÐC-€ÉA™ú†Úõ{¦[EQí33Óúüç‡ysJ’bš*ŠвrŸÞ™6nB¿¿@b&: 7&ñùÁ³+è½äÀdŒ ŠLHÄý³çÿ"¶&(Š¢(Ú)þüó¨«×¿m¨}×Ôÿ¿©.‡p\1Êô‰f[õÏãëjéž„ÀdŒ ŠŒ×m[_gbS‚¢(Š¢¯•±Y¥oë«ß77ýýþÝû·ÍŽ›*Šk³âŠžÝøâK¥{·ï“÷¤å¦„ÔHz‡'!ØÉ8ÙÂ0Ýцj#¢H8zœxv³éMŠ¢(жÇêšò5kþìÞý³_”Ç^¿}+ŠS'Θ7}.¤á¿ß½…=üÿ®±žJÆ‘÷‡ü¬¼m£1‰pϹ‰¸Oíï$;ã=ðDQ9i¹9&ñ±)AQEQ±,)-œ5{¦££µ™ÛÝÿŒš9fÊoDQüöûA§ôö5×V½kjxÿ¶ùý›Fn,+¨IÉxºÚ¤™Sç“’ñ“P?z‡'!0#ˆ"sÄdÝ«ÄgoêPEQùòÙó§—=\=Ô’0öaàb*i˜šž4eÊä{÷ýöÙø®0º¹òÌ¥½û}DQmì¤Å¿iA†@ü¶¾þoª(®ËMb%†–†ÜPUVaå›JˆV˜Œ N2n|S‹¢(ŠÊ²¬š²Ûw}÷íß3uÚ”tëÖM]}üŠË ZÆN›6 z*)õ…©vîÜ~ý†gIiñÈí4"2TEM%0(PûôÕ 6÷­ƒÒL“ÿß'ÿWRÁ$z¶êÁÍ:þR›×P”õº$þ‡X\Q~«èÝWß|çpú 1‰pKËóĺ¤qeLÔóeó[¿ž1&cQT‡Ü¬d›EQTF„Ü ixüxµîÝ»ÏÑœcdd”““CoÄEƒÉdÂTVVV‹´õêÕkÔ(eHÉïÏÕ¯]¿:NU%œóÇ¡K9?v}™Õôö}DnåWý†Ø\t':·jb|ì¿z|åã i¸&ýü_±¸ø¡óm£]0ª¤$Ÿ˜D¸MÍõô" Qï‡ÉA•÷ïß6¾©AQEeÊÜüt3s£!C†Œ= Òpxx8½Õ– Rò´iÓ °ÿžØø(âÙEÔÄÌhö¼¹! )³t/ì»z'¾ ¿¶Ñ/·"ŸÕ0xâ¼›výEÑù¤Á×½¿K¿{ qiÈ’à«Eìò¯Ÿê£4ÀìÈQ¢s«65×ѯY4:'ÃS‚t£5DïLõä—-2Ää\éÑCôQzb‘ ¿~SÓN—ü¾rÐ å>JÇV·9gMŒEQÙ·[·nD…:´Ú‡«(=Åz@~[¶¶ÿ„K=>@ÔÑ—‘¡ .PRê«§§—’’Bo¬¥CNNÄî¡C‡L6åöݛĜwÛŽ¿ÖêèĦÎÜåxòVtHVYJUƒUBØôöý¢§ S!&Åà—á_õ°HmR¶Ñ¶¡¸:¢´Ö,¾2¿®ÑòFP÷/¾"&nFvÆÄY‹þõ¥ÒÖ݇o=|~ë°þ« ‹bÖν®«;gÉö¥äsç1I«¾d<*,Φ_¤(B2æí#\zA=…KOÃ%¢ôÄ"TV1‰íˆèæçgý»Ç7çì­ËMâþ”øMeéÍ?~øq(ÑEeVþˆFå¶–Ø·ÈæV¸M Ý„¹ÃÜŠvk©H}€Û_º‡–*Dà-rû Œ¸¨ÙsfM›6­ã31/ c‘Ö"ÈÇÁÏsÈ5'/}¬ªŠ‰«‡Å­ õÝ.†¤ç± Y‡ů²}°ß?’1¤ä°¬²O>ýÂ.1yK[;tÿâëe¶ç3óªkJK«J/‡§YÄûE݈¸šbê}¿×~û/³e»Ï.8|"òzsŸ¸ü ¥ÁãŽY ÐGÏCzôé÷"-/«¦1õ:UŸWUŬ.Ï«,K¯(O(«xUZñ‚YUüíqvöÄäBìZɘw¬À­v>–‚·n¢÷DN’qtB±)EV]ìÕÊžûÖ¤¿j,+à¹eÑ»wõçŸúºÏ÷Ä$"ú4ø‰ÍyKÎ_ôÈQí7%->àѽS§ vìükê´)JJ}©Ý³@ÆW[¸p>ì´`f‚Ÿ…*†ðFóW¸Eþn¼ªÈ_¡Xç¯pá­Ýx›T…‚¿È[á¹p+Dþa~ùÇòVZætqas4dÈƒÆÆFj›,;888À¼¹_¾È;·*“&_y»ÉÜgçù{‰…Åuîé¥KN{X8LJšLZorø~¤†ŽÅŽã§O»Ü9sùœÅk¸“ qÖï«g-Y±8£úu:«!¥²>±¬.¶¤&¯²‚7?+®¼›ýuÿ!>wnÐ’](óŽâK ¼³Q¼ˆØ ½'‚t:T2nhª×[>>ÿêñMyø-:S·¹Gßæ>=*â“t'&Å{?ùôóù¿/Y°à7È¯ÄØ¶™œÁÙ~ÍÚ?L›6M__ßÊÊ*((ˆÉdÒ Bááá~~~°ÓZ¤µh¼úøîݻϞ3ëðQ}Ø=OÊ¯Ïø+¢ù+T‘¨PR¢"°É;ÌmÜ&w¯T€·ÉKT¸ÃÜ¢Î?Š,o¥Õaî@WÖæ¼Õœ9sĽþZG[Èå+–9ªOÍpž=ï0’–÷0¸–UZT×ä”Ê\i~Ýê‚)3Å:è©ò “¹»­£Ÿ™Þ¼vfõÑ‹ŽÛ|;H™ûz…8büôu»O|ˆÅ Iåuq¥µ¯Š«_T%–Vð&ã'EUÆ7ü¿èý}U]ñ •V2ŽÞ½-þÔaªÝNÄJB: %!ý…Œ"·[«=¤Óá$ã§ M,qõ°³íñ¥RiÈ Vbh}AÚ›ªRÄïšàÿ7ÕåÌô„ÿ÷Éÿ“ˆ¢öŽýýÇL_xÈÝã~Èž=»þ\³ª²º„è#¢Ùyi&f†C† †4¬­½ÎÕÕµ{ ÆÆFÊ­!%CÔxt—xRT¾üÏþ¢¸^‰±Bš¼ÃD…; PÞ±¼Ãüî0w€¨ Åß$*­sº¬›·lܪ³Uóc`p|ÁÂù°)ž2u²‡ÏØua%¬¦·6IÅûýcèš3¬³/+|iáqÛá~ ý’½f'¬LÿØïà’øÉ§Ÿ/\ }=åà‘VEÇâøÒZ³&²–WœUöªø£dü¨¨êõ™ëwA^'‡_i%ãÆÒ’7¬*ªÝNÄJB: %ÞþÄ$BFµ É>‚H•6'ã‚Ô˜OþÑ=ç®}EäýšôW EYMÅá6?fЧïbQ®öë¤U{ŽÝýÃàªÅ•‡¶öÖ3gÍ(.É%º ÷‚³Ýì9³¤zÍ#ØŸAÔ¦®ºoÿnHáÄ< raKáŒÊm\ˆ¢ÀnT“;Š+5–¢¥QD“‚¨pûP¼R(Öù+üÅ–úpëÜ&·BÑR…[$†¹]PØ M6ÅÞÁžÞšÈ~~¾jãÕ¶ïܶm÷Ùº®…¤¼y÷Þ,¾p¦®Ý“ÇçJÂŒS´&V¥:ÔÖ\›ºÉtÙ)•5ÆËޏ$Wÿ³Wß{ݵ>oI,^¿ùþ§³.×îÇæ$~ˆÅQE¬ðüÊçÙåÓK$GÓÉøfÓ:,sÅQëÞ‡"Pi%c "Vjl©sKuá´4oWz´øHäA¤c`'ãø§ ,q­¯-ûâK%·c•_…p\Q›W—›DÝÉsƤé3&Í &Žûm5¿’[Õ`”ºÊØwïÍ»~*ª*É©qDO^p²ƒ¨ª­½®Ã~˹¨!<éš5Š8“¨ŒH‡3¾zÛ”ì£ ´ž•¶ŒØÈQ£”ƒ‚‚è-ˆüÀ`0~úé§qãÕ·˜ùp{RÓÔl™5e£iašÃí°{u5×|«ªoNÝl>s×ùMÇÍ7ÚÜ®nlî?jê·ß÷› òóŸŒÿyЀ[·}ˆBùy¯ÝÂ~YfWRS\MÅ✊ Ì²‡©Ì{IEw’ !»Æäø'‚Gn¿úôß_ùÝñ%‡_[·#ò”ŒÅ’žžCKuᙊw”@é~¢Ñæ ¤ãá$ã' UmðäN=%¥þ~¦ÌÇnåá·*cQ^·6üê›ïRc#‰þ­šžöɧŸßŠ+(ªo*ªm¼š¹ÉæÁºÓW††°ï>ñŒèÏ«‰™a_¥¾‰;å¤=êò€W%§Æó†¢( z_¿2~¼š,ŸX,œ’’’Ï?ÿü¨kàFÃ륵‡®25ñð¿5ñd ;¿ösó½’S\5·›z_ßünÙûßW¯Î ;žfðØk»ÆÄa“'©òoÌÿï³Ïo†'A2Ž.bET½È£cñ£´’ûÉžñ×bò|˜d”J9aÍ1åa§OVaÄFÆkEUassýD (à.„·†Â|º-ˆ.‘ŒÞ±-Iwm6L‚ E`ÈͲªüúƪ¶©6f¢ISò¯Ÿ*z`WüÐ"räùcÿêñM€ûE¢§(šÙ9öþaDt~åÓâj§Tfq]Óµè\½‹Oþ8téñ+ÆÏkÄ$àÓg”G)ëéé ÿ9]ÇùxÈÐ!‡¬`1‰ùDQ´+{ê´Á¢E‹X,½±O&M™rÀÒmÒ¦sEUõ'.Ú]{´Þ1ª¨ÔÍ?õt]íõš*¯[í³3‹/̨~}éYʬ¿ÌrÃO䆟Ì}ÉÖÛ~ýð¡ý7¬_••› ‹%øeÄÃU®W\5nå“ÔâÐÜŠgÙåO2J©X|+¾ð#ÿJTÎùgég“Ͽȴȶ{™õÛn‡'Ï|ÜõzüûŸ¼Ë™ðuS5=ë"ÓXZBµ@WIƼ}Z’îÚ2buFÎåÝûæúÆÊ6{ÔÈâëÿü˜¼xrÊ_Zéû—'¯°~ÉÀ¡c¿þîGkgW¢s«ÎþcͨYËËê›ÜÓKa«j“TœSÓø(…yØëż½Îq©3æÎ52=Ëí_T’½ióúñêã ýzd€ÆÆFƒQ£”ýn_çÎ*Š¢]Y¯ë‹ém„<·[õ&l°ŠJgZy??ku¦(Á¢,ÛÑàÉŸîÎe•yäe]œ¿ÓìèClÀ‹ëš\óƬ1Íxq*/âT^äiŽgÀ³ú ¿ÿ®Ï˜‰3>ïñõI3;xä×Íï&n9ïû*'(³,0½„:‰‚ŠÅW£sÝ"²C2Î&;Fæx$ºÅå/9~ÙjóŠè-ãºuëF,m^_7Iþ«H×JƼ=J÷kÑ{"H§Óždüûš-=z÷Û|ÆYç| ©‘³óË3WûÄdV×”:_¾ÚwàPÐëÖ-b*!¦²Xïl燔V E‰•õy•gü¢¼ä¶r‹Žö¦õÐùEDð!ƒdõW,)))sæÌÙ´yï DQ´ «ñãÇËÅe(Z%((hÌø‰+Ny;ß|¹|a~ôYÇŽÁ›ÝB·g'Ùÿ±ÛìèÍgÓJÊÞxDd¯1½¡sÒ,?ê tãý 3# Íì56›nÒ;‹åï¿ÿN(ª8v?~üFkïˆLÈÄ÷“‹ï$Q'Q@,¶{š¼Óñ¡}H:$c—Wy~Y¥×Ó˜‹_¶Ô^¬hɘ.µLK[ª §ýSñJ„ˆÝDhs2ž©µb”ÆÂÀ̂蒊癅OSsÃ2ó^å–W—–ÖTåW×ÔÖ;^îѧ_Px(1mK~òéç3ÂÒ‹cò6»>þe©Ñ„M–z~‘á%5ieµ6ñ ^º’¸×Ä|ÔØ±***²pú„pìíí§NRT’M¼RE»ˆY¹)r}n1Ù/zôÜ`që€í½šÆæÅ{¬-lN¦‡Ì1~öÈôw]ˆÅÞYå%uMΡéK޹±0Ïe˜ÿʨ 4.`-Ô;gq%°¶¾ñï÷o³‹ÒLãŽÜ‹U7Z_K»P™Ø'6ßûUÞ•¨×—Yǯ‡ÏÚãbœÉøbLÞœrŸô’µwެ]Ój2~PXœMϽ„`'ãØ£ûSmÌ©v;+5¶Ô¹¥ºpÚ6ÞÉ…?‚ˆÝD¨¨*&¶#¢¸dݦQ3—>+®|Á¬„dœPV‘VQž[YVĪH)¯M©¬Og5dV7f×6ê²øQy1¹@‚ž|Þ«ïÃøü ëͦêXí½¹p—ÙýÛ&+šoóxâ_PUPýÚ#4ý÷Ãî&·ÃϺ{«Ož";›   Q£”cb_¯E[rëömì‹P´•þû‹ÁÄc¢b«xê´)áááôæ@!˜4eêjûù{]ê›Ã *V<¯¼Üè—¥FÓ7™»PPUXýÚ%8å·½ÜL9iؤ Ö¤0Öô¢»Éšý†óÿ2NI‰…Ç©¨Ìw|Keb:o°v~–rƒÁÎÄW£sÝ#²/…g9…eö ›¹ÛÙòiªÉ“n2Ö>w÷ÀZí;+GöSêM,v^­œ÷‰um Q`'c¿]Šþ-ÕùáöÞYÄn" 1Y›ò¬®±Bt¯z}õÝOyåD2N.+U\_Z›TQŸZÕÎz á8,-ï“O?'A ;ôõ5õâƒèíÇ ƒýfëÙ.8âjébYši¿ý¬„cï¬òÒú¦;ŒÜ¥G.ó ¶øL}ÊT¹Ç0“ãÇ«y]÷ ^2Š T}òäNw\c*Û¦™Ïó_ƨ…E‹v¼«×¬ru½Do…ãÇ æjï·Î²¤ªŽõæ­Y|¡EX‚eHœ9#7”Y“[Uïü$ñ×Su¬ìÌ ãL ã̲cÌvŸ±Ø}Üú–“UþµcÙnÛ"ž^àÍÄ{oD-8ä2O×òþ«hÈÈÄ®/³œ_d:†fØ>OßéòxænÓÇÉ&Oÿ—Œÿr~¼uõV*‹WKLÆ-!¤Ku~¸=…w±‚ÈŒ#ã×½®Ñòª¢žJ?œñº÷¤¨Š7G–¿È«Œ*bÅ–Ô$”Õ&ÇÕ¯»uëF<ˆ@Õg.RÕÚäz7â±Ñ“ÇVn>¶3¶[Ù]µ/ynTžå°`·Å~ÿØ‹i%•¯›Ÿ§1tõyž4zôh™úíX,Ö¢E‹LÌίEùUŸ4ù Ó÷˜ÊËŒ¶hëÏ€õŽ_Œv¤ðy×ÕÓ¥7 DPPÐH• Z‡Ýï…%¿}ÿ·MR1õ›TVCNE½Ýƒ˜Y».\}–|=*gÉQ×ËÞæE‰ûM,Ì®±BÊ#î?rÉ»v,Ý[—ÊÄ|_-=ã=y“Ù•kfг(Á"…á|÷2±Ýótãñ‹yþ~Ð51‹’UvþyšStît¦grÑGɘoásí¢É˜,ðÎBFˆØSÄn" ˆ›ŒÏ]pùzÀðëiLÞd–Wö$£4$§âeAUt+®¤&±¬.©¢>¥²òñÿ÷ÉÿåeÃoÞýöÛ]·º2‡‰ÚZ“9ÛÍ‹‹¯ÖÕÞHÑšXxyOx¸ý¯;ÏÃö÷EimVY­¦žÓ³¨ä^½zÑ/CN€ÝäÒe‹áÛñÚQ”W*Cƽ[å'¶vþì?USáøipñàhÇè{ûúœ9s¨¾"Á`0¦NV_ðûo+Ìo\ðoh~ç—[ḨþMNy­…Ï‹¥GÜsË¹å ºö÷6°|ðÀrÊ–sOŸE6gÕ¤¿*¸›îkå|Ñfï¨õv¦éXš;Yä%žc¦œ+“­Š“,!"G‡;o?ç3UÇö~Xùôóû±9—ž%NÐ6ÖØi;{·½W€kC£_ñå½¥Ï+‹\—ì3ßsçUQ}STF©Š¶¥×u---úeÈ^^ž³fÏ$–ŠòÊMÆ^qU7X>bjÏIƄ㡣U½ozJÛÌœ¤Q£”eÿ'Âb/gíê#†ö÷<¿66Äò/·ç¿ïwzóö]fmcUÓÛ¢ª†ÃvZøUÂë¯mŒÍ.ÝsþÖä-Zý¦gsÈ#xƒáµŠ’bFd´¹íÕIÚ&‹öSÕ6ßzÊ*9æ|I†mIºmIÚyfš 3Õ"²«§Õo»¬/ø†65¿…§Î(5xpä^¼éã$« ‹çi›Œ½þÐ>½eÜ…ùƒ'ªŽâ]ø„â&cQï'›Éàí °O«¢Hà¡Õ¼ˆÞA:Ñ“qRJÌïófü»wýg“Á‰ÙÌÛ‰· $¦—<Ë.‡pü² *ªˆS\½øˆ»WhÊ¿¿Rºùàñh°çXµêáCû{Ù®¼³{Ú”Ñ}«¤—×y$¨¯3ñó·ÛoíPÉòdÜÐpÓúÁ3Çû<,–_k~÷Þùþ« g®íعËÊÊŠ~r…¥¥åŽÛˆ‚¢\y“±o"ëNrµX:ÐÉhllT›:Ãq»ióz™½ dh¨­Ù¯·£ÿ÷}M-È=ʾ¡Ý‹÷¢Æ­3¯®o‚mraeíº“^š»ìW½´òÈÅß÷]³ÚlâÓ;÷¬lÝ,·ÛÞ ,bò §mùÛç}vwgn;W˜n_–í–fÙ³Íí "?}b½ÞÀîí­rV=±ùŽýS˜Žƒ³ÊBr*^äUFTýqØýòó¤o)/Û¸ûPeU…ú‡ôú}ßÇü¨Vnø ØÂæ¼0°²;µø¤guc³S*sÑ~{ßû¶¬ÒËu5×ëë|àÿCž/´,î›ÏÛk_ûæíq—À³nG%/'ó³N{Ã…óÜe‚¢¼ò&c¿DÖƒÔêGé5¢ëøðÉ€p<~Ú,ooâYP)ú2¶NôÒW¯_2{Ð×1ç—gæÄbƒÜ—']Îßwá#+¯´Zk¿ÓŒm¦ówÏße²@×d¡ž‰±ƒEò+HºG­­xA2(¬z˜_a=i‹µ myî0áà{ÿ|YŽ#xïžÍ¤õÆl7ô³?Z•âW]Stå%}57Êm®ÏÝé0Gïâ:“{›-îëjq“1ñðjëv¸Ë%c€·[KÒ][†èß’to¡ˆÛA:N2~TûºL¸°éq]?>Í[Wy…±stî%Fžcd¶ñÓ˧)îÙŽoÆýïÈñ“ŒÒgÙåµ¹=ˆÊHLÍüq¸ZŸC½®[3é÷}ßÝ›g¦‡gß)âT.øò䯣¦{Ü‚¨{|~»`§iqŽ3„ãkŒƒ†ÁÚ± ž^7Ã~Õ±ª¨oZvÔÝóa¤ÜdÌ „•iÓ¦> ö'–3Š‚D2H­ Ϊ Í©Ñs÷¾áéàÍðtŸi7^¤] Ф¬Šë[Ǩ϶¨¦^Ýœ~6Ý÷žy®6¾¹ßæö‚ݶþ¦E ÅI–Å)瘩Ö%éì$J3íJ³ì³“í'm¶œ¼Íî—eFÃW˜¨i[Þ¼k[Qà’žìd`gû›®­Ž±ç’Ö&N6Úú¶©n?r.ð5̹´íþn&Þ{#j¾þźæLCŸ_Ô³e'ã í3!h \µlñðZV•×ÜÜD¿è„d ˆEïLõä—-2Ää\éѢцI¤S|îSR‘]ÛP&ÜßfN3Ÿý“ÿ¦©Ö™x$^Oe^ŒÉƒdlñ4Õ1$ý"Ï‘ãûÉÅÔÁcÈÇ6ÛX?N¬kbo•\<|¾Rêÿ‹Ê¤€úôÍ¢ÎäQFž^~Àl£}ÀÃfNM£MRñnç[뛕fÚ»…íP¿<6µ…±¦~§èÚVÕ©h[Ú]òX$‡'óÂd2ÇWKL~E,jå&c§ðR› ¢ Ï™n/J<#J¯E– ôABÕ«‚^£ ÂsëgÔÞM®¾Ϻ[õ«Ö*‡ 6Ä¡×ÓÛ]1îMÐP[srëÊo¿ølÏ„~º“2oëeFš:ݼ°WwÃ?Äž‘x¨0ÎŒs´ØŠ ±8‹3íèLœ{¡<Ϲ¢À¥¢ðbeÑ%¶Å®QÑN£þ4ÝméWYûºéÝû†·ïjÞ¼­ljžþ—mNVÎë²üÚÌØœg·Žž0ÿã˜Û’³>7Y9Zå'±™Wœlá¸0ÞŒÚ)ä†O´Yþ!“o×ú×ôÜ‹&céꈕŒ=6hÎÙis-y'·Ü=¾’±é㔵f·Ö[Üvy‘å‘}%*ÇûUäã›q·â ¹É<åÇÈL|ÿŽ}üØíÞË…»m<«‰Y󦤶Y Ìšæ¢ê7Ž&cû ÖÄ¡’U±„ûßRØ{b¿7WŒH9¿²8ž}¿RˆÅÜCÅ•œCÅìL\ÂÎĬÒˬ2Ð#ŠqqÑ^ó‘+M&lö‹INŒ½œn¾ù„ùŒ6;.‡pÃñ=Ÿs9®; |JC®W2ž6¿ll[:óÂx³µG,·ž»}Úóù¼ýoGe—Ô6W›ÅR¦V¿n|óVEÛò¨ÅE:0ÓØØ8dÈ„ähb™£ÒP¾’±g|ÕõdÖí´š€¬Úà¼ú—Å ±eÉUi¬¦–L¬h|Uú:´ þqNݽŒLÆæý¿iÓ¦ÒŸê.ƒ£Å_ýkñ°Þ´Çd¸o.Ë´§c1u¨˜sœ˜“‰=ªË¯TW€W)ïÑ.?úl^äéìÇM+ÿðõ¿ˆ7´pÞ+•kS¼aU½­gŸ#ˆ ˆ‚qÄdmLÒÓš†áò&c·Øü«‰EÎѹFOR‚3ÊàA ˪w˜ûÎÞí|Ð3ìJDR ã:û§I–ìßd$[=~d©¹Ó|É ·½7¢ ?¹eýDÉSˆÅõù©ÕÌü1«Íb^X¹y™-9êæû*÷EFÉíisôœ®‡¤q¶•T2nxû>)›9k§ýêÍÛV,_’z €ŸŸß‚s‰eŽJCždLŽ’Õ'Mjù˜ñ›òú·µïêšÞÕ,TX¯ß1k›…3&Ÿ •”®YáêêJ¤»&º¿íñì#žmVɾ©Ç=TÌ*uçÍÄ5•ž5U”^`Y™§¹»ýÈ?UÖY.8à˜R_Yÿª¼.¢¬6´¤&˜Yý¤˜µÍöÞˆU¦K÷›¦s.xœáøÃ Çqf “¼§’,—o·jd_Õ½‰7´pÞ#•dŒ ˆ¢É8"î!±)á—JÆÑ[Æin8}ôz¨cD¶]x–á“dHºv¡¹•ìK²Ç¦ýyÜuó‰s¯Â,Øwøä\¾¾â³Ï?K;ïtÅò×m¿Ÿôœ®ë°ëqÂÃu9 MU%%ÌRõ V wÛ„[NÓ±´¼žPP™_Q—’WþûÁKWÇU7Ö¿qO/mz÷þâ—댮? ¹ó×:Á?ÿ’z À¢E‹®z»‹•¸ ’Œ›Þ56¿ozû‘ oÞc2î,K+óú*õe±Xôç¹ëÑP[³{™æÀ/?ß3¡cïÔ‚ ã<‡Š¹™Ø«–åͶúƒ,«»-l~Yf4nÅ.Û»Á¹å/Jkž—T_z‘6å/»ÑkÌ•W[¹Z—å8²o ’aÇ>á˜sNE!Ã4ÕmCÌŽ {'öûö‹Ïüù[II&ñ¾€˜Œ“ñ‰SGÖý’±ÿ¦©“µM–ž¾vêAÜ©G‰Üs$ħ7¿©…|™6W÷¼¥‹5çwÊœÛ~R¶Ì´ÏJ´³»lõð±Í{-&m´Zyø¢…Ç#M½ £3®D´´¾tÕ|ÊËMg=½Ç>Ë~¾ô°»_xiíëØñ7¿Ûgí«ëu5'ìøó;4=uòxÅ8±/''gÈÁ°%–¼”ä ˆÜan…èÆ[áJŒ¢šT…;Ì­´Tä—è„HL  òv±%Ñ$:´Gn2öL¨º‘\}'­öaVݳ¼†ˆâ×qeM©Uo²jšóêÞÔ¿-l …áÜÚ·¬æ¤Š¦˜Òư‚†'9õ÷1w”ðµ¶+üö®UòÓ“7Ì™0°WwóÙ?%h–ĘÕT^ý_&f§ákµ5×êØ^‡ÿa˜]©öŽŽw_´×bÄ “që, ¼‚Wœò»Îâ²¾•uFŠKEKyž}Nç°q–Ÿ^ŒÞ$‹9ƒ kÏRÍÍŠ!Þ®â&㢀»Œ óé¶ 0#ˆ"Éøel@u=S¸öŽçü’1¸ÇÄKëÐÕ‰[ί±¸¥›Á ǧüãâsRß¿kjj~k|ÁsþN{Ë”XÎ|8¿VæzÙÇfâÖó¶'.?Mf²Þ¾K/(×Üa›¼;÷¬T×MÞd9m«8u«Õ”-çÚÝ«®SßÔ¬¡cmþ 63Â2ç…ANر죞֫‡ý tbó2úõÈ3zzºF&§ˆ%/%‰TGÈß[i©Î­ˆ%wráBt(1 ¯Â;´:9HõáJŒå•èÙ6E:füöýÛ÷¿ÿ›¶ùÝß­3v›ÛG­ÏS¿±€||Î?ÀêÁ‹÷ÍÜ3;fi±ÃÄ+>‹ )Ù#4–}Û¼ð9aé7·G욨=ZiÆðþôë‘g˜Læ€ýK*r‰…/ †9þ"ÑäÊ_çVÚVä­·ZäW`7Ñ‹-Õ¹EÞ:¥¥b›'ÿ÷ïÿþþÇdܹöìÕ³+ŸJ!ñ?7ò»^{ÝýsdºãV‘+; ×ݨ§õáÈ®Ðòqõµòr/·[NA//UW\a•y°JÝ«˜n•E—* \Jâ-Ï.¸»f´Æ_*÷åý«vÄ» Ð6$ãVÁdŒ Š 'ûW× ×ÞÑŠ7»ß¸œ{®0ÎìU¨ùºcæ³wÛr¯>áqÛ›{é VBHzLä¤ ó÷Ø?‹Ë IÈ]càºõŒíËGØÌ= ¶ßh`1]Çt‹åúVÜ«óÐù˜ºze2çgÈ ÞæÓu¬—x<}é—y:óþÞ¸#3 güðíŸm[8­¢¸€~=rÎ‡ÃÆäò—¸?µ©ªdå×DZٵÍïÞ¾kz÷þaDªÖ>ÇÆç“œØWö)ºÄþKY ûeœ3Éè|œaW’Î Çô,ÙwÌ0Ê >špfî…ùƒýÏycå§'Ó¯D!ÈÉÉ0 ?±ð¥¡ðxÇ[¥"VQ`¿Â•ww˜Wþ:wZ~EéCÉíIôo©.AE?fü3Æd,LNÂwZú3Œ|ÀÕÕõÏÕ+`ù0KÒnúãÛŸíÿ]ÄŽ‰y÷ö×U_oh¸ ÖƒTD¦ŽÓáØ»–åUSéY]qµªØ-Ë믗ÛÔØ?³ëÑwá·jIeVs3ûS„Œcê&[RmA‰Àç7ŠÊ3XõŽâuImð·Ywôò_Ò±8Þœ}]¶äÿÝ(ÿÂUkµõf#WšÌÞfºQß&èÑãÆò¢è„LM½ Eµ¯«ß¼­o~×øî}yMƒ¡[ ÊZ‹ÃÖÖì«]»V‚ò1û\ îÁcv8æ\À’sä¸0Ê0ùÜ2Ï%Ã&öë¡:°w¸ÿ-ú5(Ó¦M½ëïC,‰Ë v¢Ô‰ n‘¨ , ¬s+Bl©›ðG Ærm©rG T”ž¼}Ú£Hɸùý›·smzÛz2¶s´"ž•ˆóÌU¤ëäH ê{>w)eg¾Úº`Ò·_|f 10vïÔâÐ3 ¯}ÀùøÃ‘cö9ÇìÃÆ^…O3öL5Ÿ3hà—ŸkÏR…Gà>šèÖ¼.¥gHr°“1ÞA•·ïšXõEBLËb¬ß°rÐßú_ù+?úlAŒQa,''²/ÍÆLµ.I?ÿ¿åç9Ýd7{§ÕŸ'=ºøË2£?ô]£sËÊ^¿©lj®yóÖí~ä¤ÍÖ£þ4›³Ó,9Å•UÊþKûILöÏ,¨“Éþwð8ÛsËóʼn–i—Ö?Ò³xXï¡}{ø8ZÐs¯ˆ888lØ´–x$.O˜k½Î[äæU`]`Q`[iI!ݸ"VÞ"o+QçöäÊÅ+ѧmŠ’Œ!¿þØz‘’1ù\hûÅ“Œ[¢W¯ž¹E)¼Ë*9.øõ¡C¾ù§Ó¡ñ‡¦—Ç[Qù˜Ž9§UÔÞ¨«¹^c§?Ýí÷a~fw÷AIJæu =7’“1‚(2“±íÑí?}ßûìÁ…y§ò£Îä¿2*`˜Æ™ý/§/Ͱ+Ͳ‡ n¿ü°õr7߸ÜèòڸʺdVCfÍ뼺Ƣ†¦‡Œ¬™;Æ­µ»ÆøÆC'ö…ßËÙr~cq™ý3 ÈÇ܃ÇÂ13ÑÝ@Ï·â»Ø¾}û–Tdï…dm)É ¬óy‡yXXX篴_îcò>,¥¥"¥Q¼r»µÚS¹ÉØ%¢Ì1„éú¢Ô3²Ì7¦âABåÓVX+"³&’O(†gV?Oc&W݉­¸]~ùåGçc2–†iYŒ¾J}éO/ò1ãÇ«>½K,1ðñ-WÕzOìßÓséð䳋*3>9ö©ÌrL6ùýޚѿÒ?³³%¦WLÆ‚ˆ;×µäïã‡îÿ=ãÀ´ÌÛzùц1ÿ;½˜™rb1ûZÅYö©ñËô­gëÙ;>O fV‡–ÔD”ÕÆTÔ%TÕ§V7¼È.ùóÄUUmËËŒ\ìËÊ8W}gKß&”›ÙÇ9?C®äœY‘÷è0cÏÔC“ûüòóÝËæ4ÔÖÐ3­èÌ™3ÛÇ÷ ñ^HÖÿÅ8q꼄T%°(°Î_áUÈ(Ê–&ç¯óWZ*Rò⯯·A¡ÇŒ›JëšËêÞ¶`sqÍaÇŒùž m§ü ÿÑ]…£º!>Ë—/¿èjO,1®WlN éûÅ‚!ß<Ò“znYy’Mæ¥MÏ6ª¬Ù÷ÛÝ]LýÛæ£çž…ÅÙô ‰@iHH×3ÆdŒ ŠJqI6±á5æÅ½É?+Mì×Ósɰ¸¿å-Œ§sN/æÄâ²Ç†–¿,3³Öb·KÀ£üŠç%Õ/JkØ÷¾Ï¯8à䯺ž=võ±si™싽ӲïÊù2ûgò1Ž‹^œ‰?>Ç|öO{u_¥1FÁ~f×*œ_®,'Þ ÉÚRŒkµ.p,(p”ÀbKuáE¢NØR7þ"…·È[ç-¶Tçyë¼Å¶Ùr2~[ÕTÀz#DˆÎY˜Œ;LËsÆ[u¶Ò]IÓÎ`*ŸK`QtŒŒŒvïÙA,1B‹›!o÷íÃÕÊ;ÇOýÌŽèÓ-œvKåx˜ŒDQ9b²6œñ€UW(Dö7û>_,ÖûáºÑÉÖË ¢ ©Æ¥ìÓ‹Ës/Tä;_»k7v­±ò*3mK£[!%5F>¡ê›Î)ÿi:ý/ó 7úîGÉ‘©|\~¥,É:Éh‘›ÖÐqß~1sÄ€ØÇôŒv%è_®ð½”'ƉT…ŒŽX¥Î/Ñ“_¢?¯­v>9WáBÉíӹɨíU¹[T™WL…o|åƒdÖ“tVpFu«eT?JeÝI¬º[ñq2¶$žm¿Û¶o¶²²¢?º’¦ÁT,>—À¢èøùùÍ_ð±Äøe2Sw-™þõ??Ý:bvF1¶b2FD< ¿`<¨ª+lÕ3ÛWüêŸðÍþå¶ñiÎÚÌkê€qyžSEKeÑ¥’W³KçG­2³Æ\yµü?v±‹¯;Ó÷åÞ‰3üá¾GÔÏ+óÓìÿ¼ûçÈ9ƒ¾bŸ^æqžÅ.É€b“‰·@‚r“œˆuPÈ(PàXE!u;Š+ÑAˆÄ„”D°¥ÂëÜ&o‘èÓfÇ :f•_WôZD… üÇŒm-‰'BÛïªÕË]]]éÏ­¤ig0 Ï%°(:AAAS¦N"–XkŽÉA±=ƒYQ;Oÿö‹ÏNh d욘sk7uÀ¸²ðbU±kU‰;«ìraÞe}›ó#—8Ú_­ýßA9÷:âõÃ}ª™îÙžo·jdŸ¾ú—ó™CôÌuaÖ­[‡9¦Ë*(×BÒË9u˜Œ;À™³¦ûûûÓŸÛv@eP ¢IAuè6ôèèÒè*þ Uˆ¦¸¤¤¤ ò3±Ä:XLÆ‚ˆ'߯ª+ݤؠß?\v'ö€F~ðñÊ¢KUL7Véåêrê öìjY^ô¡â™˜s!÷w<¢¯ëîSSå{gßËmj;Ç÷mÏNé¬ê:?³Ž««ëªÕˉ%v¹ÉøbdÙ…P¦kxé•ȶx9â£kSp’1ù\h;2uRPPý¹müé“Û2Š‚êÐmD…q…À¢èP'†K¬ƒÅdŒ ˆx´!S>ö»¨:ð›‰ýzÞ\©œxj“aÁ*ƒd|¥¦òjM•ûⱘ}‹|:s®ËC_çfqÈ騽S gü8ðËÏÿZ¤¡0wx– CYy±ÌÑ."ï1cßDÖ½”êû©5m1¥“±´…Ï)|ZéÏm›’>…ŒâBô!š¼+°(:,«g¯žÄë`1#"ìds¯²6¿mzØøÕ¿– ëóhݘ4›¹N5UžìXLŸDÁÅì4̹(û¢î¥¯ÌâMwZ8tð7ÿÔòŸô˜—ôÜ hllìÞ½;±´Ñ."o2öI`ù&µÃDÖun2v° žm¿ôÏÉÉ¡?·mBHú2Š Ñ‡hò"d p¬À¢XÀäÄë`­]†DܧçF0#HW§É˜’úqÞŽñß¿ÔQ˺úW5Ó­ŽsÀ˜‹éLüÚ·2Ã!ñÔ\ߕʿþð¥êÀo–ΙâååEÏŠ,Aí(è’8´yB^† ü2:ˆXÔhW›Œ=㪼ãYí5“±m2¨-]âÀ_á‡èC4y2 8V`Q,`rb‰u°Eåô¬ˆ†¨É8æ nü©ÃTAE",Ú¿ 4…Ø”´ÁÌôÈ­ó'~ûÅg†3~dìž’{{}nñ‡X\]x)õܲGÚc– gÿÌÎÅx?L¥ª6.<<œž)жͺXS ì,°(.sæÌ¾î{™w!£]Dn2†D+=˜Œ¥å”)Ûyž1j»Ðí–·$Tz„ÐQ€À±‹¢CgL,±–U_LÏhˆšŒßÖ×½o~CµQ$šß5Û‘ö˜Èx:wÌìç-»o3ܰ¾áfMÉå,×Í/uT·ŒûöÛÝÏl_Éíß³WO‹EÏŠhÛf]¬©vX==ݳÆÜe…v!O^°RkëV±|ã‚M{ˆ¢@‰ÉXJ0Sðn=x‡)¨ @·ùúM^„ŒŽXENÆ‚(*Íï+kó$k çÇy?|ys¥rÜ¡éŒÝ“M™xçâ_‹Š“x{¶g›+ mÛ¬‹5•À΋âb``pàïâB»ˆžNðÖ‹âåasçÍ&Š-ÉH #žm¿óhúù‰ñ¯VáÝzðS´Z!š¼+°(:Ô/‰‰%ÖÁ²ê‹è¹ LÆÒÕd\Q›' ÷ÿðÕ¿ë=ðËî+¦ŽL`D…h+°(.˜ŒÑVíßÿ{øET"&§G™[;ögÝ?›1s$ãÛ÷½‰> |35J™þÐ"³hÑBø A,±“1‚ âÁIÆw*jr:Þ˜„‰$c€J¢t‰…þ>D…h+°(.œd¼”Xh(ÊkŸ¾½“Ó#‰¢bX÷ºœþ$tMÍuð¼áQ80bä0X¶Úþ¼|õï\ñ[Tš*ÁßK(C† †åI,±Öü‚®T’qÌAÝd Cª ˆ"É84æNyMNÇ›”Ù·o_z>$•GºÝrB¥ê¼Ð#8¢I p¬À¢¸pNÑ[M,4åV3¢¢0v|2~Ú¯ÿ÷?þIw÷¶ ÐûÄüqðŸSRRèGA>À¹‘çgIJêx¥•Œñx¢¨tb2Ûùá„Rúay‡)¨ @·[îC7uàEàXEq100د¯K,1åµgÏ™ùqDQ1ìød“B̆(®ús©««+ý(Ȩ S˪ãÅdŒ ˆxp’ñíòšìN±ýñ‘ÞTÊ;Lц Ñ$8V`Q\>$cr¡¡(×~ìóŒŸEŰîuýIè(›ëˆyÑóö¦ëÖ­¥ùûº“2°³¾´?$â>=O¢!Òµ)0#ˆ¢Ò¹É¸oß>L&“ž•¶BdPÞTÊ;LAT¨&@·9´Z!†y›‹â²Ug«™Õib‰¡(¯“&«ßºïICHÆšššV¬XIKÈæ"<<œê èèè´áîmNÆ™ù±={õ¤ùÀ´iSeaå,,ïD—¢€»"]Ï“1‚(*aQþy% Ħ¤Ãœ1ëWzVÚ•D)èÒè*_–¥à6©Qü€*RÐ¥ÐU¾§ meüx5ÿÇ7‰%†¢¼Î?Ûýª#QT !çä°O¸²²²òòò‚X )¹W¯^-E^WWWè›HÆ0 SÑãD£ÍÉ©<‚Á`ЄpN2†o …¥)Ä‚êx«ê„e\~D½&cQTšß6–Ugw–[ÿÚ»=zVýJF^,±ÄP”׫–ØØ›EŰöC2†°K$þû_mmmÇSɘnü÷¿êêêЙf±X¼“ÀÃBn£†a*ìá7uÄ<ˆ®îîm†Fx¡‚ÿßOfÌú•XJbe-&cAÄ¡ùíë²ê¬ÎÒÌòôV­ô¬ <À޼oß>ÄâBQÂýú»@¢¨ LÆð¹èÞ½»À¯ÓD2:t¨¾¾>5l`` ¡¡A 0 ðóóƒœ Mø³ñM-1¢ùhÈÁÔã#ÀòåË.\´&–R§XY›GÏ“h`2F®Nç&co×9sfÓ³‚ð®ª6ŽX\(JxÚèèÖm뉢b(0£GÖÑÑ¡%&cAÄã0'—Vgv–ø#<~¨Ÿuç—$Ë Eù8y¼ß½«DQ¬”Œá[4T^ІHƉ¹ÓBVWW§ê7SÀcŽ=â5$cbĶf–V–ôãvU8¿½Ó –L'ŠÉAñ`'ãh¿RVFgévÅ~Ñ¢…ôÜ `w>yÊbA¡¨@·lÓ>mx„(*€5 d2:t(u„˜"A“:SÂËË«{÷î, †!·Á0•Œ¹çQ888À#7¾©!æA\Ò^àacØžÃVX2¨Ù…]RIÆ1uãO¦Ú‚(žŒÓs_á©Æ°ÛÞ¯¿‹XP(*PH!¿Í›E@HÆ\!ÝŽ=ZCCƒ:wBOO ¸ü@ü…РÎ3†¬L‚ÇQRR8p ºº:$`è1ȯPÔÒÒ‚ÇìÕ«$éö'c}ØØ²ë6f0#•‡ˤs•V2~[_÷¾ù ÕFD‘èôd Θ¥!ðÏ£]–!C‡F>$–Š ”ýݲg¢¨B2†ÏBxxx >-‰™êIAhÇ•¡ž’’’ù€1üoee™˜:Y"É8:.>¿]ö°±¬0¥•ŒQT ?ö-a¥w¢Ž­–/_FÏP—‡:èB,"∑¿<~~›(Ê»5 %ôG¢£xý¦†˜‡¶¹õ¯õ]ó°1u†1±4:Ýs÷…DܧgQ0#HWG’q^IBß¾}Zú#iWC_ÿàƒ}Ä"BQ!šXž\·~%Q”wå7íÿ€~Ü“˜»C† ŽŠ "–F§›W’HÏ¢h”†A2n(vç½!u¹V~“1x¡ëýìøñãûõwËA,¯aŸ9#Y0#ˆ"óæíkb;Ò)>~~{Ô(ezžº0~~~sçÏ!жêòUXÛE¹V®“1¨¢6¶Õs£†”””‘Ê#dó»&cAÄãÍÛ†Vš,8wþlÈ…ôluUàëÁãç·ˆ%ƒ¢­ê{×câd58exø·y3yGÉ©ñiaÓgL×Âù 9Vö…ÙVUS¡~ê§Ø°X,55ÕÐHb Ȉå5bÜZDØÉ¯M Š ;W¥É‚ŸÝêâ‡9Œg‹EEô›Þ_ÿç[¥nݺ úùGb”Üimk¯Âó† Q—/a›¦6^Uá¯S1{Î,/Ù}§¹K8³“qÌAÝD£TAE"-‡Á¬J•»øacåQÊŸùËE[õÞ£kãTFA&¦è×ÿ;¢ƒ™–©9oæòU¿Ã1Ju½b·pÑú®ˆèêî:mx˜xÕ2¥©ãN±®Ú& ìdŒ÷ÀCEå°ÉšgQ7‰MIg ;xµñjôœu1<==_¼€X (Úªž7œéDüùMÆ7ï\ôó®¶D]®=b°ïàÁôç\±°··×Þð'ñzeMLÆ‚ˆ‡L%cp݆U°µ¥g®ËÐØØ8`@ÿ¸ÔPbi ¨(ÂWÊ>}¾¡s1¢ƒì›ËŒÛ©·eâd5…üÀ—^øêKÚ…   ³4à#^¬¬Ù†dü¶¾ŽjLÆ¢È@2Žò)®J‘Ss"ù¹«] TWw×)ÃCÄ¢@QÑ…ÎòU¿Ó¹¸[7b¬Œû<âþ8•Q‡ï!ê c3vò” Št©Šœœœ‘Ê#`­#^© *n2. ¸+ÒõŒ1#ˆ¢B'ãÊÙÑÁÅbÙò¥ôüu †ŠêØœâXb9 ¨¸^½îD<æ®N>wÜœ-öØ>wÞì‘ÊéÜ,þúMš¢¾SoËɳ‡`ªÔìHîÃJUcsƒá#‡ûu36%DUm|Øé½<±XMMåùËûÄk”MÅMÆ¢Þ“1‚(*œd|£¸2Y¦TÈ?> ¤±±Qm¼j`°/±P´m¦fG̘=mûÎÓgNƒ¼;uÚ”åË—øùù ew‚‚‚ŒŒŒtõtaªž={B’Þ²mÝ%óð˜Ä³HÄœbÆ¢?æ®]¿ˆQ i#¾ûúúÒK\> ©ýD“1‚tud3ƒ°ãTUSQ쎌^^^³çÌ©<ü¢‡ 1?- óÉÛ46?>a’jJöKÞbWÓÁÅ6nòò;n-\´àÔÙCÄ« KXéôË LÆÒÕij®'¶#2¥•íÙµëÖÐóª@P¿ºË.Ž!^/ŠŠ"7»ººÒ«TÇ+0D%Qòñ£à›}ú|C…cXá׬_âšÂ7U5ÙÿM^NNΔ©“/yœ'æ_^ÄdŒ ˆx°“qE¢,»eÛ:KK zv&“ {Ä—1‰WŠ¢­š]ôj§Þ–ÁC~î¬LÌ •§Ïœú,ü.1Ÿ\7ë¬éƹùH$#pÂ$UcóãD‡®,làòqƒã2û—1{{{øþó(؇˜s9“1‚ âɸ°"QÆ4Y=((ˆžc9vS§N¹p•x(Úª.—­ûèghxV¦‚”¿¿¿ò¨‘׳Š^3 •=¿à\®ÛçÿüÜçŽÑ…E´÷ÀvøªÓþ_LJ–ððpµñªkׯLÎ 'æY¾¼t1-+–~U" j2Ž9¨›laHµQ$R³c +dÜä¬*ªcà:nL&sêÔÉW®;/E…›U½bÕ -Í3S!©C^ÿyð àð;¼³mçlJÅb “yÇ¢\ÃcNŸ9uöœY²prENNκukUTÇp¾À“³*wšHéªm‚(*‡MÖE^#6%2($­Åó”ã?^Á>OUmœbìlÐŽ”‘4iŠú¥Ké5IV¡nÁûÅoÂ$U:`ÙÊEܱ(á•k#”‡Á÷ŸÎÊÇT&îß¿Ÿåù3ļɯ˜ŒHÆO#½ *âåÂÃÇwÃnCJ×¥’*žžž*ªcÃcˆW„¢Â ¾_¨äåÚÞðÙœ=gÖ‰³aÎam§ã0‡±*ÊÛu7Þ¸ÂûêP~].Ÿ£ò±ŸŸy®­egâï-ÏŸ&æGÞ5qÜŽÉA1`'ãï‚òxyÑå²µššJJJ ýäãÇiý1/³0Šx-(*\;gSXÛsrrè5INØ¥»síú•:Ûµ¿ï÷íÒ‹ìœL“2È—† ×ÅýÜoóföíÛgëÖ-Òû^R]=ÝúOš<ž‰ùfC7—†A2n(̧ۂÀdŒ ŠÌ‡d'G½¸¥¢:V.~“×ØØ¸pÑ‚½þ"^ж*¬6òx? {{»ÉS'$e†/ ˘¤'†fGUTÇ@x]»n­««kû¿&±X,///܃‡üüóàŸ Îìñ'žW‘7‹&cQdä1ƒ°Ç4y¼Œßd5%%EMMÅåò9bæQT¸™…‘¿Í›uüø1zM’Oà»ëHåáð=–xuh„ðjyþÔòU¿÷ïÿ=¤dø¾m``àçç'Êádƒáïïý![+Ù³g­?æAàî"o &cAÄCN“1å½Mƒ‡ü,ƒ§`²X¬ƒü<ø§€ ëÄ<£¨p!Oš¬îëëK¯LòLNN޲ò Ç’R²‹»Õîý:¿Í›©¢:†>»[7µñªS§MáJW»u1rØôS¡?dë.¸E²q?qŸ^#%;¿­¯{ßü†j#¢H@2~á_'§ÂNwœê˜-[·ÈÎÏòüüü ¯:¦GÌ*ŠŠâòU¿_ºt‰^™äÇ#•‡'f†/•¸·ü=®ÝºÈ•Ûu-ö[º¶ÁNÆ1uãO¦Ú‚(ŒGÙÌÈüòX¹ÖÐühÿýêìÜ«½B&V5RsÞÌWI‰9DQQ}{C€´*íßõS×<ê۷ϲ•ZOÃüˆ™AåHxû®ù9ƒNî–zû· ×ÝÛŽê|Ëÿ2ñ8Â=Áø³Ï>…ÿ‰:3 ª¦"×ê‘]º; Î ^5ŠJÛ¢Ê$z™ÆÒz¨0#ˆ"£`ɘ+dH«Y×®[ëçç'ÙÀÁ`0¬¬¬¨k?½/:ñ1ñì¨ )Ö ÈµKW,TŸ¤BIÿF©[·Ÿ~HUæüö+o¨Æô‰Tç1ãFÒÓwëÖ£çTú@ü…è _ÕxçaãÖ?©Î«×-á•ñ|¤òp¹»n±¸À÷ÕË×칯E;ÀÏ]Ó²béUPŠîŠt=cLÆ¢¨¤f¿Ê+‹QT2ž™ÛœÐœ7£gÏÊ£FêêéBJnÃW™LfPP‘‘ìÚ»wÿlÄÈa›¶®~æK<*#Â[sÍÏéàÑ·®‚¨ú}¿o!ŒBŠ…D ™V KILØae£žeáïsà©?ûìSxj ÷? àc60?щԴÓgL}ðྲྀ).ðUUM?8hGêäm •{àa2FEå°ÉšÇ/¯æ–Å(¼žz?½R2äZÈ%ƒ‡ü ½ÉÎRû¶p5fä_ü{ÙÊEÜŠ¹õ è“–NL‹RÒëPÂûìžxž5=ôçº%ÃF ùì³OÕ'Žƒ·ÌÝë||z0o7ÙV9:’ñ„I*ÃG £_^×¾À—bá ¨”tò>ŽÉA1è²ÉX Td6WÉ ô:ÔD%<‚¯+ ´æÀ;b1„cˆÈČɦç/Rx̸‘Ûviû=pƒâ8•Ñ2x«©ÒØØ¨¬<¾'ð.•’˜ŒLƼªOçíçDÑ–¤×¡¤wŸ¯—¬XS¿ÀÀªEÌùY³#[¶n¡_[WÂßßÿ×S¸ËE¥'&cAă“Œ¯ä–F£à¶]Úì ŠhKÂúÓØØÄA” äåää@Ïö"%f@®MËÑ¿ÿ÷{ošNdá¢NnæÄ2AQ‰kå&cAÄ’qàË+9¥Ñ(xÁÍ|öoDmIXttt¨3%%¥+V‰zÐîÚ­ÛÀÛ‘‰kŸÞ§«»‹~a]ƒ1bä/Ä2AQ‰kì°ýeÌczµLÆÒÕá$cœÒ(ŒKÚ£çDmIX´9À‹Åò÷÷:th¯^½Z Lj]]]a:kiiA8¦ê¢CÌ€üššÖ•S,\´à‚›±dPT²•‹w`LA6æ[8(*AkJéN4DMÆ‚(*oj³K"Q® ´f[;ž!Ѝ@aý!’1)¶W¯^tãcx“±ŽŽÎèÑ£©aˆ¿0Š{8èCN†ˆÜ½{w===xLuuu¨3 §¦ä…âcŠ…‹8ºšËE%eç¼²ˆ^ÛD“1‚tu0=µ{Ö•D(¬?üɘ:,ðvÜP:t¨††dbHÏAAAT½¥d П*R3 §B„DH¿¤®——×Âß5‰åƒ¢1½0lç±ùÙù)ôÚ&˜Œ¤«Óø¦&»$åzï±Ç°ƒ‰"*PXZJÆ~~~œó ØPAêúúúˆ¡¦¦&„ã”öN«¥dÌ`0 ®££ÃEÌ€œª9w:,êuq`=éÓ·OlÚcb¡hûu¹f`x~;½ª‰LeLÔóeó óé¶ 0#ˆ"“’UrMÎ éÑó øŸ¨£üÂúߌ!û*))Á€Àd ©—Ó‹ÍСC©i[JÆ@xx8dh*C“˜y”‘ö² ÷¤jdíº5¦ÖLj¥„¢íôi”§öîɬêrz=“(˜ŒD‘9l¼úÑ ÷,æK”ë˜q#|î¹E”_Xˆd b1„cºý1D2ÖÐÐÐÒÒ‚&“ £¸'WXYY—­ CJ&f@=mr`k—¼»GKÀû>q’±”P´=>g\_·{RLB½’ILÆ¢È`2æwýæGOéE”_X kjjB¸¼¼¼ :´¥¢n©³)###hrO*à^Ѥ{÷îT2†±Ôä””è ù˜˜yR ¼|ökF>гgFZ ± P´mÞ|h­½{²X×0v2~êz[_GµQ$0óëèj2KsQDù…õ‡{§ˆ³|…œ'ñ—ê @öå=~ìïïuuuuîÙTþ700€ 1rgrîsHx*ÁÂ… àCG,+mƒÅ•Én7L ‹³éuK:°“qÌAÝøS‡©6‚ Š$ã‡/Ü2™á(×àHßïûý‡(¢üÒëPBÌ€ÜésÏYMM…~1Ȭ¬¬6lYI,«6€ü&ä<åvH-xNŒ¥”F&ÆR´ Ù‡Ûd¤c)…÷ñºoîä},£(¼¼&»¡IÀ%q¤;ã=ðDQé"ɸ®±âõ›Ñ}ñ"ŒU[NŲ "ž˜Å“^‡:bäÎ#§ôðü0Œá#!–•TÍ*‰(©Î¨ª+Ì+N…m ¿Ñ‰ÁÜÏrf^<1–R}`˜K Ó*^‹ ÌòlnŸ'a>D°Õ>æŽ{¼nÛÖ7ÔÒ+S‡€ÉAزt…düö]ý‚;Š¢Ê$bOú¥v Ä Èsðzm-гg˜´@bqIÜ„œ§¶Ü|LègEA4––ÐC-€ÉAv2sÍ,~¡ØvB2®H$æAñ¤_jBÌ€ÜÙ§oo¼õ@¦N|ÕÇ–X\’õy̵û5.HéJ^ˆbPp7Xkvë×3ÆdŒ ŠJ|jXJþ³ŒâŠms‡'ãŠDbÏÌ초ȗÅ%Ü¿uJ[bäËÄœàϺF¯ÈÇlݺå”ñ~b‰IЀ0×U;TC"îÓχ - ê=ð0#ˆ¢òúMuFq˜Â ɘÉdRwðóó ~}€ððpª3ô¤îÓ&.œdLΆÂxã®Óƒlݱ&1'ˆ…¶ä@7eå‘ôú!çp®/ÂŽ’ÂÊÊjý–Ä“”‘I·7îÿ5!•}?s&céêtdlddD]YL]]ºTè¥ÀtSRR‚ÿ謧§GEMƯRjo^>zÜð‡Ï=‰Q¨p­N.[¾”^?¤ '¸J2¹Hüñýýý5¦O"–˜¤¹‹¨£¢¸sïX éõCÊH<¹HüñáóÕ¯ÿwÄ“”vîÇè§AÖÀdŒ ]øÔÐä¼ ô¢PÅ–?zzzJJJtãcx“1ì³!„‡Ó—b nùF §¤¤@O‹}M(Â(jÂÆÆFv2æ› ù5!ûéËæÎÔœƸCŒBEtëŽ5BþL!Y$ž\ $þøL&³OßÞÄ“ˆÅ•m9 é²`2F®ÎaãÕþaÓ‹BÛæwüɲ,ìÝžFÌMÆquttFÍ=/&áÞ¼zˆÎ@÷îÝ¡x"ØÍTÄó ¿Þ~té—á?[&ê¨X.Y>Ÿ÷¶’‚Q?@4)¨nÝæÁ=‚]ú]åyª.)à‰%&ë+è'@ÀdŒ ]®œŒ©ƒÁÜÀ¼@2æìúixÿMþd¼äR˜d|Òxïè±Ãž]!꨸J<³WÍã)·)dÕ Ûˆ 1ÌmRÃÕ”ð€Äk¿Åaÿýž~LÆÒÕ9ÄIÆiE!Š­ÀdLÅYƒÁþõϨ  Ô1c‹åååÕ½{w}}}ÎDìý72nll„I””” •!ó wF¥øÏÔœ²rV|öbÚ'LRø5¬ÍÀºÐ2Š Ñ‡hòÂ?Š¿Ò~úè÷ôå b¡µÓÏ/57wô¹FÔd ±8ÕÆœj#¢Hp’±KZÑsÅV`2¶²²‚½;„ZȲY(¨\ËMÆzzzC‡¥†©³&¨aèɘjúùù©««÷êÕ+%%¥ "Ž˜ùòÚ]‡_†ÿlwñ,QGÛì„I*°ÂP«ŠDà¤SQ³,?D¢É ÿ(þJûá$ãëÄBk§‡MWâÅÚ±5#¢¨tÙd XII©¥Ë±ÉxÅŠÜd ÂýDdÈÜdL=¡ƒ\'㽇¶Ž;\â1¥‹;û7 ‰ßšJ¨t‰…¢Ñä…¥ýôìÙ#*å±ÐکΡY…ÅÙô ˆ`2F®;‡º¤>Sl©dÜ«W/øÐ×ׇaMMMê²ü@2=z4ÕYGGB€——5Šº¢…¿¿?Ä_€QŒa,ô ‚èÓ½{w[PG̃\Ÿ8SsÊ–íu´ýJéx'¦²¡Û-'WªÎ =BhØåÅ_i?ð€Äk¿÷k”–ÒO€ "€ÉAº:Œ„º¤>Sl!Cf…¼ @ †+üèt :ŒyÿaZ[[êð?db`2™)))ZZZœîÔ5’óËãˆy}Ÿ„_5v˜­Ë¢ŽJÄÅRKƼi•w˜‚ªt›¯Ñä…¥ýÀK¬ýb2FÄ¥2&êù²ù¥%t[^õ‘)âRCsS ƒ[HÆô î(òËc‰yqýº@,†ÿ‰:*)wìцo\ôú!xÓ*ï0E«¢É ÿ(þJ;/™ýúG,±ö‹É‘’\õ‘5šXľD!Åd,\ »ã‹C¾D• ‡NìÐÕÝE¯’@HZå¦ *T Ûˆ 1ÌmRÃÕ” cøˆ¡Äk¿˜Œi ÉUAY’qJaÂÛñÉ8¯œÃ̺çÐæ¹‹¦Çf=$ê¨dµu9½`á|zýtDå@—>@WùÒ-·IâB)èÒèjËÓ¶??¿Ù¿iK¬ýb2F¤{Õêz[_GµQ$^¿©I)Rx›ßvx2.có ƒÆf>œ»púæí«ˆ:* ï¹ü3½~ cdd´eûŸÄk¿ÁQ>ô ˆä`'㘃ºñ§SmA‰¢’¬ 7R ž*¶¥¬Œòšœ6[T–fjjRR•IÔ…˜É|ì™tGmÂh »£D•žŸuÿŒ{›q„—eË—JcU,®pïwi'ìdŒ÷ÀCEåæ}gk7=bw‚ò;wátE ‘T,ö¾mKÔQ©:|ÄPƒAö”•Gø8‹«ýb2F¤&cQdÒ²b·š™\𮓇ñd 5¢(§Fpb±o€QG¥íËçIõÂmrJcccÏž=™Äâj¿˜Œ‘6P“–Lµ&cQpÖíž–p=¹à *Üozõ<æQ”;ï>È7àQG;À³–Ö®]Cð©OR!–•D|îÙÜÜD? ‚ˆ@QÀÝ`­ÙÂÃ1&cQpQ—e!û '“£ÐN±OŸo˜L&ý©ëòL™:Ùíš9±ˆ$%&cD\0#BRóº4"éVpÌÕÄü@”ßs&Y;Ÿ Š2kx¢ßÐá?=|q…¨£èæí« éÏ[×¾!À÷bùHPLƈ¸`2FD1 !›L‡Š™Ëv›Ë{®?´¸ûÜLÌ{Ä50ÒF:];ÜjŸË·Ïðö‡åK=LÈÛG¦´v:1cÎD¢(›¾J¿§ª>êê-k¢Žv®>þÊÊ#è[×¾!lÞ¾’X>“1".˜Œi‘Ââì;ܽnÛr­o¨¥ÇÁf!æ1ï(®­ö¹yß™÷ £OBýxÇž±ÙvÈxµ¾Ñªü²ØÔÂgÄ~NFü¦÷WÁÑÞDQ¬¡êè~–(¢²à°Cðfx|C€ï Ä‘  ÷ègBÑ5C,Nµ1§Ú‚ Fu=3½(„ØÛuºë¬ÙøQ”5—­žüì.¢ˆÊˆÖN',œO¯å]??¿YšS‰%#Y‹*é'CÑ5#‚tU¬Òuzï<·OÈ{(#F§ßý¢Ç¿á¢.;nÓ[ ET¦ÄÃÆ#•GÜð—î纓1"&˜Œ‘Ò²b×ížäy߈Øíu¢k6þ~à¸Q”Ϙï]´t6QDeMk'ƒ®|ؘ:`L,‰‹ÉLƂȅÅÙ«v¨>м˜ E{}ÓûK¢( :¸Ÿ™>gbtú¢ŽÊ ]ö°qccãHåáwž: Dâ¾ðàýU‚´JeLÔË-kÞ°ªè¶ 0#"¼Œy¼ý¨f|n€Œ8}öÄsNljbçzû‰óÐa?E¥Ý!ê¨lzýÝÈ.y‘г†g7o_E, i¸aÿ4¼6"q0#"+œ±Ùvñæ±ø\YbÍÐa?ÅN4*í6ÌÏí'ND•e×nZliiI¯ß]&“Ù¯ÿw°º‹Bb2F¤&cAdØÉÁ®ŽØùu¢D!ÅÎRsÁ4ÓóúD•q! BLìR·Ä[°p>ç-䢆˜ŒiÀNÆ¥%ÂO¹@é JR‰_' ;øé³'ÅNñÀñ­«7hET.tp?;kö zýVt.]ºôDz߈% =1#Ò€Œ£wo‹?u˜j#‚t"ÿý>!ïq6a'úMï/ŸFyÅöŠß9Uue<½X~=h°mç®ô*®¸„‡‡«ORéÈ“1" ØÉï‡ ˆìZøŒØÿu¢œƒµ¿ÅŽr¹ò˜¡¡îD•/W®Ñ²³·£WqE$''gœÊèþ‰É‘˜Œ‘-²K"rdÄè´;ì»~¤Ýᇡî7ØñŽW˜ü’·iK)UÕ•[…Ê‘° M›>1((ˆ^Ë‹ÆÆFUµqíüt´Á˜Œñ©IK¦‡Z“1‚ 2«ºÜåš±ÿë\×lø}ë®U‹–ÌêÖ­Ûó½ÄX!B’†þÛôVOýU}؈!0ùHåS¦NjIèЧÏ7êǭݸøÀñ­WýÎmܶ\ïàzâaQ95(ÊkœÊ蜜z]W ,œgít‚x½àË„»ô ˆhÜ Öš-<c2FD†HHÜwf1q=ÿNôa˜»æ‚iY)ØÉ˜¯¯AÑ^Ðçe¿õëÿmÿýÖ®]c``àïï/â˜LfPP•••®î®ÑcF}òÉ'S§«C8¾áoG<*Âû¨ª6®±±‘~¿‚ýöí9´™x¥cae="¢ÞO“qPð‹? ûäÝa_õïßÌ™þ›‚mk¤k"SÉxÙêyT æÒR2ŽN¿£ ÅöéÛÒ°«««D Âf Rµ¾þÁ‘Ê# jëéo€¤N<5*_Z;ŸX°pýË?žžžóµf¯±ÃÄdŒˆ‹Â&ããG {|©t@{}FX`cYóÅé“fÕ»_B~HD¾ùŒÊ‚7üíø©Š9p’ñG}†]þcÙo={ö€@ )–~R¢öÁƒúèùÛÚÉ€˜ TŽÔ7ø Â1‹Å¢ßZ¹ÅÂÒbÚô Ñéw‰ØaV&Ò³‚ ¢¡˜Éøîmÿ_öM{x½¾ íMUéÛúêwõðÿùã' ã‘c‘kd*ƒ/nþ¶PƒÎÅ'cÈͳ4§BTuuuíÈ-äï ç1Äôü!îÌ ò¥µó‰ÉS&Èï9ǰÂoÞ²iÝæ¥Äëê`_\inn¢ç AD@1“ñ7J-ôþªIõº$ñû榿߽ƒÿax¢Ú¯‹~Ó¢û!"‡ÈZ2¦Ry„ŸŸ=߃ÁX¶lI¿þß:¸ŸåUT^„oVÃGþN¿£ò‹Åš5{º¡ñŠ:Þû5ðÚˆX(`2NKLþG÷WDޯ͊k,+xÛPû÷»·P‡pü®±ÞÉÔ¼×WJTO±¨®­ÓÖÙ=mÚT=]]º„ Hg@%ã¦çÌŠÆzÛw¯©<\ª'NˆNNNάÙ3!¦G{3ŒÊ¾á 7Õ'©È×uŽSRRF(s½fF¼–N“1". ˜Œ-,ûöí_~ëÇŒß4B8¦Ž—d$þ¿Oþî*n׺÷ì³fÏYãÓsæÌV€Ó¿DNÉÎO9f±&1ï‘ jéplÐÏ??~LÖÎÚ‚˜Þ¿ÿ÷{ô73ŒÊ¾¯Òï­\«µKNî÷àÁƒq*£†y¯¢³ÄdŒˆ‹&coGç_*•†Ü`%†Öå&5UC ~ÛP ÿ¿©*M‹xþÉ?ºÓ]Åa©öö!S´ÖXÞÑ1¾æ}ÍsêÔ© yÉI‘ rJ£‰ýŸ,¸çЖY³g¦¤¤Ðs)c@X?ppÿ´éÂ|‰9GeßCÛ§LÌd2é·S&9kxVÖV0Lƈ¸ˆšŒ“- Óm¨¶ŒSžùÿ>ù¿¢våá·ªS" ¿.Ém,+ë Ònß5 ÿº«8 ¦:[ç„I`òf‡‡ëN]}诪¦&âõG‘,²–Œ_¥ß›¯5ëÀ}ôüÉ0Ô!½»O//•}]¯™ úùGKKKú½”%ÂÃÕ•GhÌP÷ñw f»sÅdŒˆ‹¨ÉX¾øê›ï¬v®c>vƒpÌJ ­IU›ÿÃðˆ¡£6¬Ü@÷‡|Þãôµgoß{DäìºôÇ!×gÑÑ*jª°›¡{ ÒQÈT2~æ1VEÙÓÓ“ž9™'%%EEu¬óIâ… ²ox‚ïºMKTÕTdçgy,kÍÚÕcÆ„Lü—ÞšÏ>ûôøÙ]Älw¢˜ŒqQÌd|ç’k/•’<Ž=°+ ¹ù¸"ò>¨ûçªNw‡ðWñŸ}ñU@rq «!»æõ­ø‚^/æîu~“ 1s¦½=ÝAAv’ñÕ[Ö*ªcäîÏGf,œwÈ`;ñrP¹Ö:H¢ ÎïÜÉd?~¬_ÿïÏZì§f ’1uíB­¥³_¥ßãÎp'ŠÉÅLÆÀÚek•‡(gÛ^àg ù8ßãŒ÷_þ£ûñŒ¶Üé㤙ÝKdVûTY%¥T5„d•ð‹ÖÜã|7*iåúõ;ñ‚ÒQÀ~ÎÁëHb~`§k`´{ÊÔI2~ê§víÚ1_kÖ«ŒûÄëBåBkçÃF ™5{fPPýŽv°ÎïÒÝÙ¯ÿwÛ÷¬ OôãÎÒ_»éd þÓÝ KÜQeDÂ}z¦D4*c¢^nYó†UE·!—ÉxÑj¿ŒÞ´"å÷ à«Z·M–­ßÞý‹¯ç.]_PTB÷‰³þÿû暦·N©L³øB0¢´6¹¤æÜÄyû/yÇî;}vþÂExéØWm;»˜Øÿu¼+×þ¾yË&yÿÔ{zzŽUQæ 7¨|éxÙP}â¸ÁƒYYYuÀ—4//¯ ç÷éóÍAƒ¿ø¿S-[=ŸÎžèñoˆïDŸ¶¨2‰žu‘r–ŒKÊÊû 3Xuú¹»{œƒvº<ÛçqôNltv~uMiZZâœ?Vÿû+%3ÛKô"ðõwƒ¶™\‚dLÅâ=w^„¤úçW屜Ÿ§-<änyë…‰«Ç855ù=z„ ò•Œ“ò;QíÍËΞ¥gHÎa0Žc2#ï]Z·i ÖY³gBx•쥓à럿¿ÿ–­[zöì1_k¦ó âÙ¹j-M‡b£Æþ"¤sLjÉ‘ò”Œáky`ÍÚÕôÜ(¾¾¾³›F¼LT½pÙÂk¿þßõÐoíÚ5®®®mÛ%A¶†4¬¯PUMå³îŸM›>ḡîËD?âé¹Éøÿø‡µ“1¶SÄdŒHv2n,-~Ê…Œ0h„ê*}³gÅ•¼É8¯ª<»²:¡¬6¥²>ÕYÝáøŒãU¥¿Ð“ Åïþã÷îw=4õ·íf³·›ëÝ Ÿ·Ãüá#c£%§=ÜÓKKê›ü vž¿·Éüæ•—±ª“§à+Dz|HÆ;EÏ[ç§L¤x§N;~tÇžõÄ‹Eå×G/®œµØÿÇò¹ÃG ¨:xð )S'|ÀÏÏ/ˆ###ª¾lûÂв5¤áÝú›`…'YˆZKç̘3ÉíšÅAƒmk7vÚ‡”×;Oœš››èµA$;GîØê0Õ–Ytöž2~Æ“¢*"ÇU½*®Ž+­M*¯K­jHg½Î¨~[Tþÿ‰v3¼U[vž8ÏånäîÓFñ¡Æ¿éYkìr´p2g&[9¹™B8¶Of–4¼y‘UvürðÊÓÞ7²Æà¥ŽDjtb2~öêúˆ‘Ãõ¤© çw9E¼dT1¼ä õ¯Ýk)giN3nä°áƒÕ'Ž7o_IÕÍlˆ… _&Þ¢à“òÝ÷J1¸£:K¼6" ØÉXöïÇb±>ïÕ×%„Á›ŒË*ÂrÊÂò*# Y fM<„ãŠú”ʆ4N>†¯ÅôÄB1~Ƭõû­¯=?ie|ÈÂÂ÷¾í~K›Çϳ¯dFšl?m®s9È*¡¨°¾)&·b¥§OpÜÀñy"%:+Ãn~Â$ºŽ¬Äill©<"ñÂQÅóÑ‹+\wëo"êtùêÇ u‰bNjɑò‘Œ˜|?rŠstî­Ì*¿*­|’Qò8½ôyvyx~eT+¶„ŽËë¢ó+!ÿß§ŸgæÑÓóQ›ºËëç=¾¶ò 2òz6i½ñœÖ¿n1ËÉ»ZËòN]:%ßi{Ò+뉛,Œcò’«’‹XS·Ú…Ç@2¦A‰“ŸrÌb ±ÿëçkÍ’£Ûy´ øJ?|ä/Ü#¨BJÅânݺAx%FIЛþ†ÿ‰(v¼˜Œqy[_'übÆ€$ãÆÆÆï˜¡súøƒÐ9*’qX~ù½¤¢Gi%A™e!9/ ª 3˜5q¥µ+O\õÌìþÅ×á¯âé‡àÍÒÒ¢ÿ÷J?ÿøÝÊK?ùôó¨¼JÛ'ñ¿í0þ—Õ˜5fáqWën–<:]gSši¿ù¸Ù6¯ºæwâr5tl]´µµéÇúƒÁ Ïç ²²²200€ÿévPžz b‘[öŠØÿIÛC'vìÚµƒ~z…¶HÓ'ŠòwpˆVD…:´Ú‡«(=Åz@~[V¬Ç§:üEÞŠd¥ ê¢ËÅ€ê„QÄXÉ:YCõÂe#¢ØÁb2FÄ¥(àn°Öìš´dº-YOÆ^^^ƒú+õ¦qæQ¢Ñ“ðd@’_J‘O\þ­øÂûÉÅŽƒ³Øá8<Ÿ}ZÅ«âêÅGÜ=CSzõííö#úQ>6xÐÀ+§Ä=<v|ˆùC]žÇjpK,¿Î$6îâ½g® ¯}ÁºÚ;]#¸?óð4Y|£éíûó~á:&7׬]{îÜ9xˆ¿C‡… ÐèÑ£a˜BOOFÁÿt[CÆBè©©© £`Z¼ú‚!¯,&9ÿI‡éu뼪š ýÜ]€³†g·îXM,BN²êF EéC)bO» T”iEéCÙRÏ–ê±ýîëá‹ÿ¦€ˆLt¬çO͘3‰(v°˜Œqõx2›Œ×®\úÛô‘ Œs¿,3²y‘y><Ë:,óìã›çéW£so0Øáø'e–=Ï.Ë«„|üûaw÷gIÃÇÏPVŸÉ=¡"((Hyä/‹~S õÛ~27üxÚòìjS?H½V EkÏ^¶t;_Ãò®¯ók«¯™ú/;}ÿ¶É‚½ö• oöÚܱºқÊ+ ãÂcR§dˆôô÷÷‡©`Z%%%ÊžýüüïWðÒN:232üǪŒêRØmŽªÚ8HQÄ¢àrE»ñ¹ÃÜ ¢I!°›ð"w˜[‚ÀnD‘hráÖù¥{ph©BÞ"·¸±˜‚è#q!|¾¸J;RLƈ¸È}2ž¢6ÎIkhè1-Õµ&còÜâò#sŒž¤X93e£EYíë™Ûm]}ý4h5o킲•••––¤dmmmÍôéòtd2Þ{h˃2ý+ i>vœ2±(¸ò‡*Þ w˜;À•¿"°È_¡l©rGqxå/òWZ*r厥(xëÄ0¿ücy+‡y¡êmÐÜöèÜ…¿þß?þ~ nݤ[õ þZ»q1QìH1#â"÷Éx©ÖÃ?øoš:AÛÔ3©È'£ò1$có§)qÖ Ü#Çw‹¨ƒÇé%6Ûèߌʩ¬‡G((*™óÇ:•…[=®çGæGŸ¥:“uf׳UF×.Gd—5¼¹˜VròÁËEºf GŸÈ}Z×§0S­ Æž—O¬=êœYÌRѶ¹Ñ¿ÿ÷]óï6k×®6´Ñ2lpúõÿN`¦áÏgœÌÖz‘¿B‰ %Õ *›¼ÃÜ&ÀmrGñJõx›Ü±DEÜa~ùÇòVZæ´Ç{A®?êO Ã?;JJÎ]ø«¹]}•å·¸JØï¨„IÆ4çì´áMÆ&)kÍnA8v Ët}™å™ÃÍǾñÜdLéõòUmm1„Ý#²BBò¬‹-.¸›jèX,7öѿ͠’q°aŽëŽÂ;–åá·X !7olN¾›XTßô:x>|5}‡ÃË;þÑáYq¶t‹“,‹“­ŠSÎe2¬NØXNÕ±Útá $ãÛ÷\ó¯Ÿ(y걸¡(«©ªä—eFXX»˜¯:íy76ÿEFIlvé’Ãîæ7²ËjKÞØ'3!¿}ÿ÷‹ÄÜ…û6nß1uÊÄŽ¿/´¿¿¿ºº:^¼é"PÉ8¥à©TݺãOCCCú)»0ð­{ÐÏ? •S!Ûºœ"Š ÷mÛQc‡ÅޝMˆ‹¨ÉXf¡’qô–q6í_ozÃ6<Ë&,óL`2$ÝÓ’B²Ê ¶ÖÖ7ž÷~:]Çâê5sÄÌTkfª 3ͦ$ý|Iºml”ͪÖӶÛ@ž¶Þø²‹{]NÄâ ÈäÅ/-ÜkôØòWË}v÷âò*JªÊk_ï4÷;åú¸ ²ŽÕôÖ¿ ªñí{{ŸÐõf¾¯ŽFË~þñûƒû÷tð/äàé455­¬¬è6‚(.’qTíÓç¼á…ššŠ÷m;bù rgHÌÍï¾WŠÍ|HÔ;FxêÇá^D±ÄdŒˆ‹Ü'cÈ‚Žù’qøÕ…;l&éØír{fò$•:Atx_ZÁ¾úD9«~å íãç3ãlJ2lK3ìJ3AûÒ,¶a!ç˲V±^|Ä"òÊce0+E¤n9eðÐjò&³É›­v˜^³ö~~þzÈÚ“ž[MnäWÖ64¿kl~·ÕÐó {pb”[NØñ”§ú·Íêÿ½’¥¥5“†žžžŽŽþýQl²óSŽ[¬%ö’ÕÉÃdÖì™ôóuyV®Õ"*wîÑß¼yû*¢Øanß½$Š &cD\ä>»ºº.RîÉÜcâõçÙ[3v9ÍÞçºëê n8oDÅÕ7”5¿.½çí ²ÆxþNÓ{þ6e9Ž`v²}NŠCyîðyˆ­Ú+×°Ÿˆô’×oÞ¾KÊfÎÓ=I:ù•Íf3ÕµF›)G­6×:p± ¬¦®±Ye¹íãÄì—F9/ g‡ñÝ©¥Ò±úpzF; ؇ijjvðké`òÊÄþO²ÎךåååE?Y—¶'}úöî¬c¨¤üqPÿûAîD±Ã„§† Š &cD\*c¢¢wo{ê¢Û‚§d¼Ù2ଫßÕë³·›/;ëuÀ÷7Þ~ží¾1ÏóPñ#—ð;ÞÚ‡.]³·¿j;}›ÕômçιÛg;Uä;? ¶W[o9iËùû]vXøÎÙåèz͆}\9ÓòqIÚyfªuqò¹â$Ë¢‹ûwMï·¿–ˆ(J[Lƈ4`'ã†ÂüÆÒª-S?6gÂðüðSù¯Œ8±Ø´(Áœ}]¶”sì«OdØ–fÚ¿µ]wüÜâC6; -§¬7³p¼ú2ÖþÆóu§½Ë›«ß¼­k~÷úíû¨Ô‚y{œT×Yš]:_UâVÅt«*v%ò1}ð˜sf÷ÈqÖ½=z“w¨}÷íŸÜº’ž3€Édª««wüUäDªÀ~ÎÑë±ÿ“ ÃG e0ô“!è? ßãpobYÉšT2v©ôˆ­jƒ».³%(^8^»iÉ¡;ˆb§½ƒ×¢„¬çÔ Œ „Œ#wlŒ?u˜jËþþþƒþqÍÒÉ OŽæGÄS±¸ˆ}¹âs[KÒÙ±‚lyî…œTÇÓìfí´1»nàþdù±ËÇÝg±êK_¿©hlÎ.«Þjt]MÛrø ããö¶¥Å—Y¥l«Jܹù¸‚“? Çé¶ùÏOÆi8ãÈÄÛN«(. gNf€<„c›cD^̾‘;[sb±‹í®œŸ¦cuÄ#ðYQedymlE]RU}FõëܺÆlV½¡Ç•u¿,3\}Ô*%ͽºâjuÅ•êrЃUæÁÍÇ•épì á˜É0M2^taþàÁ_ÿsÞØAùéÉôœÉ~~~ZZZtAä©&cç+¦x½6XYYio^F,.Y“7ßH`ÝLϽWþwúÙ½GOGŽSU€p|èÄŽu›¤õyW »ãóM'ŠR“1" d1o˜£¾`È7ÁÇ%Û­Ì‹¦"2äãˬÒá¸èRiªMšÃÏÅ¿Lì×Cõ‡Þáþ·èy’aŒŒŒôõõé‚È9RMÆ#tuwÑÏ„ðàïï¯1}"±¸dMÞdì“Àº›\}/E ^}I¿Za/#‡•ûpü]?¥'áÞD±³„…٣ǿ‰¢TÅdŒˆËÛú:á3d17ÔÖZ³pà—Ÿš< fç„ ožƜӋ³ìËr Ï _aÉÕ#Þô«ý;p|Ÿx"yÑÖåÌLÍ)D±s5vص;DQzÞ rinn¢ßN¢€»ÁZ³kÒ„ ‹É˜"?=yÕ´Ñ{u??÷çØ}S³ì§8¢"ß91ÞéÏ#¿,3³ÎrƒùM÷—iKŽ{¨pN)6p´/.¾Z[}mÍ©f5;"Sù˜:xœí«÷l‹ê–qÿù¶Çg&ºé9ñRˆb Õd¬1}b‡]î°m¶Õ©ZíÐfzöì!ã‘??ͬ}‘['¢—ò݂RÀ‹OSœ'Û&Ù>LZ{ÄjÚ¬_‰'ÅŠÚÜN×á‚]bZQ”¸ÌªTâµ Þ#(JO¼6".¢ÞO6“1ElÈãɃ¿÷ížK†'h懢—ç9W¸T]ªbº= sÖÜi6b…ÉXö)ÅF«K˺ ¸®æZ]íuÒ›½ŠBN1ô&šÒà—Ÿï^¦ÙP[C?±¼Ád2ˆ¿ÆCä©&ã>}zÃ'…~&)Ó¶ÛêT­vh3ÊÊ#ü^$–˜LÉMÆîÑ•F -Ù_ -¹ü¢T ^‘¥±… „‘yõAYµR«o&²ŽÅT¾Yßð?YÓíþ¼»zÔìA_ íÛã¾ÇúqMMMº òIAy|Zás‰û4üzÿýèçü9•Û2Š‚êÐmÂ+Ô0@5%Ë–­[Ní%šL¹cÏz*_~Uiõ¤È>¸Ø%”yõe©OtÙý¸ŠÀÄÊ'I-ú8±Ò?®Â÷Uô¿Ƽ[ñq2&Ÿ«Ué¥&Ý»w—ë44U¯]¸ë6-=|b'Q”’˜ŒqQÀdL‘Ÿž¼xÂð!ßüÓiÑÐØ}ÓŠ^œùpÀøãXÜp³­/XWé™åºùÙ&íÑJ?|õ/ç3‡èÇR8´´´üüüè‚È!ñiEÏ%®ÿ³+ƒ‡üL?‡„’S…ŒâÂ߇¨M€¿") z M¦ä&câ˜qóuNUSI]3ëõ»jAB½¬®9Õò1c¾çjUz©‰¼_AAAtC&àîêêJ7ø`'c¾—/DK{ƒy‹fE)‰É…MÆáþ·Tè=±/ß•Ê '~+K¶aÿº®–s7¿ö­«¹‘wSïå¶ñ{'öû¶Çg‡Ö.’ߟىƒÁ=z4Ý@9DJÉØÃÇzÊÔÉôsH!9UÈ(.ü}ˆ Ñø+’B’qMã»ú7ïø„:&c!À[?pà@ºÁ‡¸É¾‚þ8¨?Q”’˜ŒqQðdLáãhñÃWÿZ¥Ü÷‘ö˜T«¥¬b7ê$ *fèM¶˜3hà—Ÿo˜3¡³îðÜêάÕb‡ùösÖnûˆýŸD”F2¨/]âÀ_ᇿQ!šER(F2~ÝüþÍÛ¿y…Š”’±ƒƒoä…˜““C {yyikk±X,x¿x»…‡‡ëëëëèèp/B›kÞ Ò§¤¤ÀCÑ Äš¿3àêêÊ`0`f c©ë´@ ^½zÁ´ðŠ u˜s(Ró/n2{ôøwTÊ¢( “²B8¯ ADEÔdœjcžy‘ýy_Né¬úá«îÿýKµ,­µWK^Åî×pZ4td_öÏìÒc>ºûQÓêάÕb‡ù…sÕ¶¥iE!WgSp¡>ÂÝnùCMÕy¡Gp *Dà¯HŠɘ\n²#7Wm»ò²ôæ«ò{ìS«ž&³ž¥²BÒhŸ§± õûq~1垥ÐßîY±WLùÇɘ|®V…%6pà@Þ ï •€­¬¬`Ò'„cèíM!ƒBâ„ •””¨« A(r¯¹ B*¥†¹ˆ5¡ÀÎŒ¨Ù†}u¡Oˆ×P„þP¤îƒux(¨ÃüÃÞ„“ŒÉ—/\µ c<|lˆ¢4,a¥S/ ADDÔd¬T¬›¡2°×ç†3|¥£rs¥²Æ_ªþÐû©ÏºGçÁÞ•IggÖêêêrý›¤Ë"‘dlduøÇAÖm^ærÅœ[|úò†ÄGÀûI禠*Ýn¹Ý¡ƒÑÕÝÅþáÏb”5EèC»åE¬ v†aHÀˆ©aˆ¼0-µ›€W¯…Ý›sÀê¼×8jC2Þ²cõÞC[‰¢4ÄdŒˆK×JÆ|Í5584ìÏ{uÿá«]±tÀgŸ}:SsÊIã½>÷ûöíC?t ž‘˜¢ ¢ ðW$ÅÚukŒ­KR¦Ü)µdL<‘(˜Œ©d ¹“®~¨Ã5JOO¦`šT6ÕÔÔ\±b @Onå"Ö„B:C2† û9@š1(r“1 ®®ÙÚÕÕ•:ýº©šxí­jÑ>tDQb2FÄ¥k%cø Ǚڠ´íŰ™  GpFÑC<è¸j€‚EÁ_i'°q„-÷¯l"/H$'d?ùô³O©—_†ÿ ÿÓO#!ˆä<]ᦠ*T Û„W¨a€jJùJÆž U7“YwÓ«g׆ä×E3Ê_§W5eU¿É«{SðA† ÔË_Ç”4„ÔAÿ{ÕwSYRJÆ9990À{Ì•ªÃ5Š›Y###j+MÙ…a---ÎtÿC¬ …t1Cg+++¨À.^H’1|5…Q”†÷‚.677Ñó "™8öèþ7¬*º-©la;Ø@P_Ûl#è!ФàmRü£ºÝ£T šASSÓßߟn ˆœð!‡¶Á€gW!Ûý±lîƒ|òÉÿG}¬(V®ÑJÈ~Ú§¯äïM?ºôº*hSp›Ô( þ @î0U—,jj*×ï^ ©L¹sÏQŽ7¿ûûíû¾ñ˜1ù\­ K ‚&÷OsT… I²åžÌÀ­Ã0$N¸Y†Qpa*Þ!¸ˆ5¡Î-%cˆÎ09U䣮®/“ŒÉ—/Ü蔀=þM¥!^›‘RÙÂv ‚°Õø‡)ø+¼c‰&@Tˆ¦D€­d{¾* H§ V2†°ëásÓÔ_ÇÞ1ÄbÇ‘a€úX}öÙ§Vö'¨þcUFáù÷-ѳg4Üe+ƒŠ˜Œ›Þ~¤ô’±žžJêj„„JMêtÞ””Å[`›L‚aÍ® Tˆøð п É„Ïc¬N˜Œi á4ÖYÀ÷'º³Wý(¼ÒCˆ&gÒ¦åmD…hJ‹Ë„n ˆœPXœ}Æf3±ÿãv·öV®ÑúeøÏzÇOÉ励>|b|¦ +CJæW­ýƒÿ·M1¨OßÞÜ%›r“ñFÕ嘊kq•~IUiÕAY5/óëÅõÉ¥¯SËõØâúÈ‚ºgÙ5ÐÿVrÕíäªö'cØÆjiiÁjÁÔßßþ§¾wÁÂÔÔÔ„úСC¡ÿs¿Á$:::ba,lŸ![Suú@‘·Â‹X¶ÔæÊÈȈ 5c°ë„Ð ÕÕÕ!IÃÿ0 ÀƒÀ¨¶%câ£'%1#Ò€Æ óKK¨¶œ_|áƒM7$µi 7y‡Bt šQ!š’¾ñ3x~‚ rAaE±ÿo?rÝ{HgôØá=zü›ó‹º}P!úðêás~Þ¢™ ÙOy‹—uuwÑOƒð›Ð “Ty—• Úê1㪆·,AB]ÇŒ»mKÆSßW‰¢ÄÅdŒHv‹Ü±1þÔaª-§p í€J¨¼Ð#>¯¼Ã\¨"/ôAý‰ o“æBÛ†žžu}J‘# +Ò‹CÁ°Ø;ÆV‡!àBþeøÏ[w¬fŸËÕ6]®ZÌž3“~„‡Uëþ —¬¹s¯°d̬ySRÛÜ’ÌÚæ¼ª–“1ßsµ*½àº¯ßT¯]W®Õ‚¯¯DQâb2F¤;xÉõ=ð(444¸'rµ :‡¶g‰ NG6t›oZ¢ ¢))üüüøæŒ 2Î;—´7/ûqЀÞ}¾úcù\+‡Ñ©Ä±mÂãôìÙƒ~„úÂ|KL¦’Œ³*s+›r«„™]‰É¸-´-ï=¬_e‰¢ÄÅdŒHvS€dÌûヶÁŸM[ªPÐ%­Vˆ&@Tˆ¦¤ÀS9¾Üêèè())M˜¤zä䮇Ï=3ŠÃ$îð‘Cñ#~úèáC,+Y“›Œ/¿ª´}VìÊtYr=ºì6£ü^\ù½øŠV½[ý/G”øÄUð$ãñĉ"½àºŒ‰×.ŠV'ç/šI%î¶Ã³0#bñ¶¾®üe+Ÿ_v“÷dÌd2ù¯8#.D6¥šÝæ@—ø",Q¤šÝæë¢)A`ÉÀò¡"{p±†††ƒƒCBÃÚm?±ÿ“ ë·¬ÀSŒrrrúõÿŽXP2hKÇŒc D72ÿ˜q[’1äEi[ßÈŠŠŽ¬¨bõ–U_L¼vQ¼âc;~â¢(yó#é•AD#ï¦w°Öìš´dº-v“÷dìïﯩ©I7ÚO)¸MjE€ªSp›Ô(€hD…hJ¼ª1"›˜ûýºj¹ÿ“œö—Œ.\@=BáêêºdÅbAÉ “qXN¸eJ w€ðzA¢(/vL2.­Î Wb Qï'ïÉØÊʪ¥+Ý ø#ìÓ··d¯ð(ï¬]·ÆÄê± dPn2¾Â¨º[y#¡Ê7‰å—̺%¦~I¬;ɼ÷À“Åd|ã®$ËÄœ ¢./Âü7œ(J\Lƈ¸t•d¬££ƒ×(m X2°|è‚t€áÚСCÕÕÕ]]]…œáó!¿žsæN÷ó󣟯Ë_zö쑘L,%tç޼njï$WßMi£0íÍÞdL>Wç oÇ/Ã~øÜ‹¨Ë‘Á7¿ë÷¢(qK«3éõAD£«$ãö_˜B‘Ô©&Ò6¨{1*))éééQ7åN$cûKÆxB—§RKIåMÆ7X>‰íAf“±öæåGNêEù²c’ñç›››èUAD“1"•{  H«@†( b1„cº*Œs‚ûõÿ™J1uêä+>¶Ä"’My“±W«ýÂãÈ`2朡;–(Ê“Œw›‡×¦@Ä¢«$c¼Ó›`ÉÀò¡"eÔÕÕ‡jeeÕ†ôÙÉܰe¥¥¥%ý”]˜W¥ —lÊMÆ—U’RÖ’ñ«ÔG¿ ÿb%Q—G»uëFT$îÆý¿²ªËéµAD «$ãÂön K–Ý@©kš¾¾¾’’’ŽŽNxx8]ŸÂâì3ç·d¿ªá±wû臿ÃÓÕÝuô”±pdÖ}‡·)}ß_Ym²pGªLTê7(¶ä¿Œœüëdâ‰:ÑÅËç™X!Šr*$c¢"qWíP¥We LÆ&cDê@^±b¬fFFF,‹®¶ƒ¢Ê¤Ìâpi‹¿Ãc2™ýúŸ”óŒX22+ÌêU»Vú«úÖíkˆ¢ÃcïOÔYšX…dLåWN2&‹ôa˜Ûþ3ËèµADCÔdœjcžíq‘jË#˜Œ…€É‘®®®£GVWWëLâVa'cf¸´½ûø²²òHú)»$œÆ»‰Å"ïž29°jíïDQ.ô¹ç<~âØ¤ÜgD]N Žôý¾ßˆ¢dµº´Çë¶-½6#ˆhˆšŒåLÆBÀdŒH&“i`` ¤¤¤­­-Sü;&ƒ]ù°1}ÀXQr¥ü†Ë˜´@öéÅ‘¾D]~•v2f¤l“Œñé*É'ü"Aà‹uŸHÆÒ»¶C‡%ã‡!ÞÊÊ#ºæÙÆŠwÀX®ÃåÔ_Õ/yZE¹VÚÉØ7ÐÖçž½6#ˆÈt•dŒWm^µ ‘T&8p ««+]’¥å…6nˆ½ ôÔÝ·åøñcôswà óð‘¿‹BÞ¥9ÕÁÕ„(Ê…»ön‰¢¼{õ¦Ýø‰c‰¢mh’ÀO.HMZrìÑýoXUt[˜Œ¼ÓÒN:,Sp®Ú¶,“ù²cLÊ}>bä0QnA¢H(+¼ûø2±(äÚ]{7DQ.¼äynê¯êDQ¼zÓž“ŒÉºD¬ª+¢We‘ŠŒÛvwènè†â‚w‡FÚ ƒÁÐÒÒê°LLÉøÀÙeYÌ—¦çMû©S'ÓOß°´´Ô˜1ÉÑÕ„Xò+¼ƒˆ¢\ßO`ÎiD]”Ò›Âð©i(¡We‘Š ­¬¬ôôôè†Èt‘d K–Ý@Ñ 2ñèÑ£;þjŸŒA½.sNExxøÄÉjæç `ëףNj—Ï“÷ˆ,¿áòQȵ1ãF(d,Mσµ‹(¶Ç¨ä{–ët ´èUA¤;Ö¤%×egRmy¤m' t‘d K–Ý@ÖèÄLLÑ)ÉÔœ;Ã××—ž …ÉdŽTþ2î~rîóÏ>û”Úò‘å7?‹ôƒ9‡ÿ‰zÛ|θþè…;abÎS‰ôI+áíó4ò*Ñäï³bÍü Û– ïÃ;–R`×›§öœü]{÷ä€ ozUFiÂŽ†‘;6ÆŸ:Lµå‘¶]˜ŒÚ%Ð Å– ,º -SWWwïÞ=GGÇÎ=é¶³’1„EÅ>ḱ±qÊÔÉ>÷\¨×;_kµ ä²kï&îÒ å7Ã<>XÜX ©ñæCk#ûm«v¨ºÞ0¬o¬zý¦¬`6^ÍïMÿ T‡vöyæÃí“™OŒ¥äï3lÔ@­•“…÷áW`›1 !ôzŒ Ò‡ åýx@÷îÝŽôµ3àT ·ÈàBU¸ÐÕ–{Ò -U(èºÄ.‰ ,X2tAäÉ8¢ã}yk¤òp‰ÜÆOY¶l©µãYî‹ut5¥7.8±ø‹Böµv8Ã9á1Q—}až!Ðß}ìAÔ…ë}ßbÝîI'-7yËÅE|ÕÕÕÛs—xéDØ‘K’q.OAï>„NÞa ªÐíEÞ&ï0…ð ï0…ÀQ¼Ãb—lCäŽÂâì3ç·± Ãô¼é0köLݱ¥¥ÅÆ­ò¾ÒäÜ=¾ ¶-ßõûÏÑS»yÇʲ0ç«Öþ1_k £dß6ÄâÄœ ýg—î?³<;_žþ ¡¤¤$½Kž#H›y[_Ç|òn´;o)@26à@7DƒÚ%Ð D…hR´Z$ÆòŽ¢àmò¥à¯óWDA__ßÈȈn ˆœP\•’UÑY;½g—îNzV‚ü:c ñ2ÁÅËçÃVe×¾MÉy!ë·¬˜:}û,_7™òeüH–§Mu¹ðYÔ­a#³c1ß(!>yéåâmÔÜÜD¿òþ¹‘YònzkÍ®IK¦Û‚`‡-HÆm88ÊŸ5‰ ѤhµÈ?LAUb˜·É…¿Î_¼; "tn2Wk/µ··£çFÎIII¡÷/r+²¦©õ1ŸC!B 3n„c¢.ܢʤ¿ÿ~O¿—ò¬uC‡¥"Kˆz<HÆð µW¯^bý ”?k¢IÑj‘æàSP ºôq‘z´h°X,X&t‘s¼¼¼è• [7x[µ´´„üVLGG‡îÚ­›’’LK˜U©Ù%‘hJ^è¢ßS€#Ǿ¾¾*ªcžGÝ&^`KFÄûCz;pd;,bTç 36mú„%ËçǦ=!FÉ…ž¾êÇÁ« êÂÍ+c¼}'O‡Š¹øùùÁ6Šn ˆ,Ñ…’1 îåɨÜ@78¢I!J‘;,d€ê¼Mj¸Í@Z±bÝ@äWWWj•€/q„{¢.¢«v¨––Òoª¼ûbùýc ¢Øt­d @àîõ[…=9ð6©a þ Q'šüEªÐmüMn…w˜‚h ÇÁÁAGG‡n ò2†TþY>ð½HƒÄbÞÛƒ LÆÜªÞ¹|HÆQ²clÚS͹3,--èY”aÂÃÃUTÇÞ{|•x í10ôÆ’åó9ÇnµŸGÝ!ÆJVkdz³ÓèÝçkx®ˆøb¬ oÁ°ƒ=}‰ºˆ> ÷Øfý¦Êøó;D–érÉX¬+T@¸Uç"°HAâBWyà¯óW(¨:]ú]ý] XøÇ,E‚?3™L¨øûûC¢5øui}HÆššš°@&¦~ß”¨©&cÙP‡uF®Ì/ƒÉ˜rãÖÕ³çÌ’ÙK´666ž8y¼”%<,u÷§A!¶J0|ÃSkƒZ³{ôüþwt5#:ȧMônjٞoWn›¸xËëyºøD–érÉÀë”°`9Ð D!àOÆÔÙ)è\Ì“ŒªáXII‰nél |0I¯^½ÔÕÕ;7üÉl2]=m~<(=¯2¬ ÊÊ#þÚµ!%/Œ˜g‰z"ò°ƒ!%ÏþMCwßfO_GH·D7áB°¶v< $â%ËçCSÜ‘Aáûôéþ\·¸o„­Ç›÷éwWÞÐÓÓãý;‚È¢&ãÌ‹y7½©¶¼ƒ‹ð·wŠ2†w¹¥ï?D2†Ä̽~Ÿ††4©aˆÔÜdLA<ævè¨dœS%›Æ¥=…ª¢:NFþ&_cÖ®]3q²ÚãÐĬJÛ¨;\Í «Oé¶wŸ¯ar!Tº@köÿßÞ¿@×–õèÉÀ1º¸‡­ ÚD$<ˆÛj7Jã€.Iˆš€¸!´è‘‡n¸6  c'ƒÌǶ”Ûvî›ÛÂ$•&ת†®RÇȦmT¸ÊVa“:¦ìX.p|\åòÕÕF·(êªê¹?íoj2Ï\½ö^½ÿßXc9¿5×ÚkÍ5ç÷ýç\k¯MZš, ûÎ_ÿWÑn»»¼ñç®~å_^ü¥·¿9²O±ìþ¯xï=]õ᚟m¦¨2îï–š0î%¦ŒQc†½µ-ëá‡P3FZXXð77I,--¡€Ä´SÆ´ÛÕùù9k¥Œó¤Ï×ý_ÿKÄè7|ÃßäZ¸ãn.Ü˾ë¥_öe_ò¦Ÿû©èg²|ðþwíßù TN$ˆýòæ_øi °Dö`áÜíuË œ¢UÓ-ÝUÆ Õü*!ZÈ@•ñÀrÒ„q/¡U£b••ZxÎ3á?}ÌÏÏoooû·”œœœ,..bGXÛ>XÈ;ŠŠ_Y]]íÊí­m7ùP;µ´òõ_CzÿàÐÇå«_Üð«îÉ ‰¿ô˾ä?wõã½?<<-Í/?ù†üÊ,þÒþ›#{™å·>x{G_LÁ>¼a%DÛ¨2†ÁN#qŠÿQ –ð Ïñññl5±çôì›ünÛ–ßûý#{àR;û¯¿÷íÿÍw|ëóž÷¼ïû¾ï­Õí0ºvíÚöÂ|Õ‹ÿs4±?-³ZÞúKoú’/ý ßý½ÿ¶­*¹|úÿˆ»ê]CS3¢å<ñðC÷¿þÇž<û#—O£ŸÊx˜OœŸŸÏüe´B”§…ÊøÎ߸þü/úB“Å*c[ÐFÿýÿðZ4+ÊõÕÛ¯FT5Ì`IJ»»ûÍßòM_ôçŸÿòïûÎ÷Üó?G_­¥ù…öðW¿æÅßü­«wÿî;¢U•,UÆÄ ………– °…˜š~*càÏc9_ÎÚe„è,mSÆoü¹«ŸÿùÏv¢xDRûåú“oøÁµÿÛý¼ç=÷¯|õ‹QÉtÌ£££ârÂûûûW¯^E ÏÍ}þ×¼dùÿônÖÇߥ¥ùå×ßûv1²q­ªpé¨2ÖMKÑz«Œ¼...ÎöýSM™r¾þqR!ºKÛ”ñÇúb7Î8uAE±áË¿ï;¿þo|-B™û ßð7ý‚ö…¿ÿþž·|Ù—;§ð_ÿ›_÷=¯ø †ùêhŸZfµì¼õo}ã׿è«þ³·þÒ›¢U•/»y÷çžøc׺ƒÞ×&úÁ…2~ì=þÉOX¾O3~‚Xä9Ó¬7ˆ>ñÄO|èCzä‘G\¾w þ»Ÿ}ù§ùwm[îÿƒßFÚþ§Ï¿ÐÇ(ãhm‘凎o?øE¿lÿð÷²¼å­?ã-÷üî;­$ ì;¿ëïú µÌvá½è«^ÈEÿåý·D«jZþû·|ïÝ¿óë®Kt‡………áÌF‰s¡Œ?øª—÷æŸ>"òž αá_Ç‹™pýúõååå““—ï)ë/Q$Ú°¼î ?ôÝßûQ·$þÎú·Dk+\ÐÇ£‰é+¿ñKÑ*-M.\ˆýñò%_ú¸Ü¿ñÞ·Gkk]~ägþÁÿáî?t=J!zÃ…2îÓà%ÙÙÙÙÝíêßl³ã]Fô”óós{Kñî|ÿ¯Ý÷ñwEZaæË‹¾ê…ÍÈ£WüÓMSÆ|c´JKËý¿×›~î§V¾þkÐÄ\ ?‘ßäò_ó_ýÿuý¡#àüÿk Ñiú¯ŒammíððÐeúÅþóoœœœ,//'êì½íê/ßùÓ‘V˜íòËûoù[ßøõ‘±Žå‡ŽÃš_÷†Š h©oyË[æï¬ËÅ¿UoüÛ~1ZÛØòþßùÊÿV×:#öùùù!ŒÛE×yúsŸ¾÷Ý.“Á ”ñÙÙÙÊÊJÿîA߸qƒó’3ê7ûûû½l½9Ü{ß{^õ“;’ ³]¾ù[Wñ—þyd¬cá[œ(JûÐG~3*£¥Úåà7~é;¿ëïRÕÈbÄq´¶ùå­û¯û•;þ¹ë a .Šðà·ßµþÍ=ð1—OcÊì¿pûôžÅO}êS_ýÕ_ú— ¢0æÙÚÚ"Þ ðý ¯¾úß¼óè?õÈ}mX>ô‘ÿõù_ô…‘±¦åo}ã×;Q|Éè™æ¸˜–òËo¼wû‡¿ïK¾ô/üÕ¯yñOÿ?vÿïÿvT`&ËG>q´ù£sRôøÆ¬èEÿoÊì÷ý˜aµYð¼à;;;š3î%Œy¸Ä{{{.?0î½ï=[¯ý¦Oýo÷µaùîïû‡¯{ÃGÆ:–ûÿà·WþúKX^õÿøó>ïó~ô'þ‰½³"*¦eêåCý_ßôæŸú;ßqñÈÄ‹¾ê…Û¯ù¾{þÝ¿ÊÌv¹þk?ýoÞýK®t„ÓÓÓ……—¢ÝHÇØé®Ï³šf²úîîîââ¢ë=ãøø˜K<ð¿3ü½“<üÙFº¡ùåOßû%_úPT‘½îå+_°øÞ÷ßµL±pùöŸgxC•>ÿ‹¾ðïn|û[ÞºË $*Ö†åÁG?üÔÿÙ½™{3·ËÑn¤ŒS°gs»ûêߣ££è½]Œ××ÖÖÖ××õ"É~pýúõÕÕU]Mxòé?yøÿȃ(†Ù-ozóOý½oŒ ,ßþß‚€‹ŒZŠ/‡¿µÿº7üðê7~ýçþ³ù$ý[ï¿3*Óªå¡G?ü'O>æš~w8ØŸj‰®#eœ}åÑÅ_úïíí!‚SŸ:=88XZZÒÿuííí¼š­ ÿç3OþÑÇüßnÌjyÑW½ðð·nŒ ,?úÿôûÿé?ŠŒZò—ÿåðm(àoùÖÿêù_ô…\¸—ßwþòíÿï¨L —þ‡»nœ¼‡q kô‚HŠËr!Z”q&(:3*Äå»G»µµ•£™PÌ”éÁã"Ä˷¾¾®±M’§žú?þ᫾öŸÿ‹Wà÷þ—HRÔ½ ´þê×¼826³üâ/ÿ¿Px‘QK´|äÞ‡öe±ò×_råÊ.j˜ªûݾ'*ÙÎåCý·î¿nóþÆ{î¾Ó5÷®±¼¼<ðç¾D·2*$k ¶Up„gAÍt||Œ«Ò_âu {vüààÀåÅ­|î‰?¾ã×ÿÅ÷ÿø·~×üõù™°÷ÿùñ_Øÿ‰ü¦Wˆf,É¥d™—|ýñÿÉwä—ñK‘2ðý¾Ì{ï}{´Ö+ƒ¶{þ}a~™"ûi¾Ì;ïúÑZ–Û~õõÿáÿûÁJÊüæû~õMoþïþÞÆ·å ŸóŸ|ÁñÕ_¹¾ñ?ø“/ ËTõ]cËüò?ã °üêáÏEX’eþù¿Ü¦¿âǾù{~俾íöÝν‰Âc¿ˆp!º€”ñxéØGGG.ß>86Žp¢ßØ¡¤766677Û/ú$ŸY<òèÃùøïþ_ßÿ7?\vÖ‘Kr)Sæm¿ví žóá3§L¸Œ-ƒ¸ê©ÿÕøÿñ“DXÂ2óóóþè£,“îš,Ê °¼ã7Ù­1i™ŸüéWo¼ìÛ¿ú¿|RxiiÉþn.SÇwù¥H™÷ÞsËhöÞûÞ`I–ÁÂÞ¨Lgê,ëëëÏ‹nQTâ¶½ï¸Ýò÷ºººÚByzzŠÀ娦ÓLD$uwk8rž³eæ^@ßoó ½Vð~Ô¿=våʪbggçððP=¥=ܸqƒ!½ËÑŠ*cèÈÅÅÅ–¼>öüüüÚµkKKK%Š@RWvww]^´Œ«W¯æ?;.f#–Ù¾]8¨çÎQZœ/£\ñ ²Û·M‹."e<gggÈœ2îxVbÅ41Ç@P¬jvdggG³’-dssS/m-§§§tÃÙZ†ð«ÿ££#zÁêêêÜÜÜòò2~o_¿!n?š0EÊxpÊxgÓÇM¾£‘ï"B,,,ð핯=N­xÓÐ[ëëëè—ícooq²ËÌü@/ÿZìøøxww—áº=&ßCëÎI·Ð„±è(O<üÐý¯ÿ±'ÏþÈåÓ2N‡˜„B%,Ñÿk}Ïñ€ýó-|B3»½‡KŒ&PPi9-yÆw¹/¯Äâ,L ÏÍÍ1J·‡†Ý:Ñ54a,ú”ñP0óóóök誦]ONNìV©=N'4q ùgU]n¾¸¸è23…Ñ2¸L×°ç†öã?R¦†57Ü4a,ú”q!ÎÎÎLÈ/ÁžEßk%‰ l…F ûi’x8 ·VVVú1Øovww‘q.3Sp2 ¥\¦ ØM0œ›©áíím$”¼\ŸÐ„±è=Êøì#¿÷س¼ úE‹Æ%b!p¯\¹‚› m Xŧ˯®²–2”\[[ca£òLjEû±p¢G½;Á‡>ô¡ÇÜef ³ýÊ’†½···¾¾>77Çà_cþ£ cÑ{.”ñ_õòÁþÓG% zŽ.¹víò—O—?:šÉ!â!nÒÜŽ'ç&&bÝJâò(,õV9‡‡‡T¯´‚˜„f­?u(ÃÉÉ ƒ!ßâââÖÖ–SïÑŸÞ‰!p¡Œ‡üx}E~åÊ‚ØO —––²žýØÜÜ$¶±‰ýDf~~¾øS"b,Èb†’Åb:ì9.—i žq,¸ ¡„†‘B—[ô)ã~bÊØeFÏIãѲÆúÄÝÕËgÏÏÏçææ®û½„,&‹õ«£ú8==í÷¨ƒddë23AŒg°b±ÆÏCãààoæ2Bt“§?÷øƒwÜþÌSOº|RÆý$RÆ€SÃ’ªe“Êøúå«v±- ì=“ @Ú/l(l‘D²¸h¢m›R­œÙNÔ1ðÀ!0®FoookÊp°hÂXôdñ]ëßœÿã:)ã~’TÆè3,©ퟦØßß_[[ Å\–2&ì#q:b´RÜ‚dq3´_£,i e&¶ˆ"I]¦AhÃÔퟺA4p4a,úAÑÿÀ“2îIe D¸ÝÝ]— ìÍÏÏ£0€2‹‹‹~Kª2¦üÎÎŽd_’ÅAm¹2¾yó&§Ì[|Ò+kNö1ßÈ8ùúõëjÆ‚6 7NŠ~ e<\’Êøìì ã~[e˜êEX /¬ éüsYʘOÊk8#$‹+MN”’ö¿ÓüìÕ+”‰”1Á{cc#Ÿö ¬öS¶\Ö&cüD& Ãþþ>ÃÅããc;6;Zv‚¤ ãð‰‘ÂìÐz°Ö¶ -ª7?Ðt9x{Œ¸Œ”=cVw-„¨)ãáBÄ%»ÌÂ6–ÔÍDʵAI{FûÖÖ–ÙÁ+cƒ’óóó¡t’ÅUA» ›%iSÀ¨RÚ!ÍÒÞ6HÚ+cVÙïGi¥hVÒ4c®Eøè<[‘.ÐD¦¦ÌXqii‰MXË¡eYe]OÊ#‹9r›4kýÎÇbšÕeª†£ÚÛÛ³Ib¾ÈY…A¦Uû!¢FÊx¸w Æ.3z‰1;kÐ)c"7…}Ú¿Ñ‚(Î>Ùs¨*Ø!è2ƒG²¸B²”±MÖš‘ª&f{e¶FÓµ(TÒlâe%i°´g¢ ³ sÀ ýT+Çz\ô4;œnú^i¼B8*ú8š˜OM‹ThðŇpB´)ãábÊH‚wÎcÁ8>ÿœ1…Iûy#Û±)°¼¼LlÆ‚Î&”²C<{Ö$“!Y\-YʘvÈ*3‚·b”–yÑŽGø\êÙÙ™ Ùd‹hìÂ|b¿ØÝVÙEÊøàà€]ѧ&}:‚ýÛU™cà€÷öö4(²ø÷ÿþßÿ÷¿ËÑ}¤Œ‡ œÈçÉ—kÄlWnD&oܸA<¾>ú!AÔf­(fqZóLµ„`ʯg1»»»ˆ`—)¦ŒYEÚš±a͕렎6¼¿¿O"y™&Ú0«0GUDi–èã‰&Ù*ÜÿÔpÀ$GBÑo¤Œ…h‚““d±&Þª=:??oi$©jbcc#|˜!|šÂnnX:bkk‹µlK™&Ú0«p¾2N¾õŒq&âØeŠÁW—‘³Èz qìLB1$Š*ãOþÊmŸ~Ç–BLº©ÎŠJ8==E8îìì ç¨aT² M¤!BŠÉÂèm»¶‰ý6nwww4“{ĶXl”UT¨0цY…³”1PmÍsR$86D^SÆ dŸÍ]fLóþ¼„b€UÆBˆé8;;CÜHmÔ„ib µºyûÚèQ]ŒÞèWì¶Õööv8‘.Ì‘•m˜ZÙê¥0 ˜9fK³Ê £Œ9ZKSØÃ(Îùù9â{¢­øjiâ~CSL¾t(Õ(„2¢F)„Ÿ#ݘ ‚°.øt2-seeM/MÜo®\¾LÓžÛa,…!RÆ¢2þÙ?ûgú½N²Xu"æ´Àÿá¡ìQ ÿˆè1¡&aw0¤Œ…H則úø›ßôÌSOº|RÆ¢(›z«e€jCÌŠíàï#PE¬E:Ôÿ‡y¢%ä(ã““û´µFV ŸÂU¤Ã§tXk«ÂÂ|ÚZÒf·¬ae |Iˆ."e,&`½‘¿¨m?WG¸ŒÍ’:mLvww{–h}%TÆ>M‚Ñûüüüòò2iÿÈ;Å}±jeô¿åöô…½‹Ð Àê忟Zá…… ÌÍÍáÿÙÊ^{O+ ØÍBI¡qFÊXLÑÿ8p¯WüAOÑ üñ7½éM.Ó¢[‡‡‡ˆ•M× Äkª2F§Z{°÷ú)aV­­­ÙÈ ùKË!tFÚš‘’á~|aSÒÖðŽIÛã:”gÿ¦°ï]Ïx¢àBŸ}ä÷{àc–"d1âØåA‚'\Fô‚ƒƒƒÎò7nÐ I Jhàït‹¡áUl˜¸k²þv_¸ #Y¦†í‡ þ/ß?¾zëßR²ÊT2šUM1µí7¢‹\t‰¾êåú§Q¼axßm80*XÑ?zôŽðõj5ÿÒ—¾ ¢ŸœP¼úthÚ‰¿É®"AÖÒ¨^sìx9ÿ´XX8KcŸ››c•g˜Bô†‹.¡ÿÀ“ÝÌâååeÍÌõy¨!:Áññ1ZçÏÿù??Ø»7ŠWŸ9sÆ$ÈZ—>??Ïø‹d.,œ3g¼°°`F!z€”±˜\-.rPï„ZÓ;ÚzÊÜÜ\‡ô%#4Æ¥+++ö\“ÞU,BñêÓ$hvƒkww7zÎ8UÓ lê—Öe g)c{Î8œ+Ñ ‚h-Oîñï¸}ü[Û¤ŒÅàû¼çí=9ïÉÆ?°Û ámñ.>!-*'¯>Mbkk‹Ošw$[Ãò¡2† ²{{{.ká,e ö#?Ö‚~£,Z ²¸Ð?}H‹éH`ÆéÛ ŒDÿèÊŵ_ÚmnnFcQ„ˆ.b˜Ð|«ðik'£÷ûÙb#,OÂJ”$ÞB ß¼y3¼Gq||ÝÇm¦èàI‹©éýd*ŽEÒ¡»íb":ñÄüþþþÒÒRêÃ<ÝšóBˆÙ"e,j§ÖŽÙ-šàj6¬E¹ºÒ50¨'Fz†M†ííí¹¶’Zó…/|a›§¸8 ´ûÆÆFN#\×ÿï!D1¤ŒEœžž"£vÓRÙÙÙAj/..^¹reeeMàTL¬¥0%H°-{¨j~—ýp^þ7Ú¢åÐ)¡#i sssö\[[[®­¤±¶¶öüç?ŸöÃ…&½»»Ûª•Ò˜—––ÆNikÚX! "e,âèèâ2£›¿.U¤'ŠÄ®©ö6éO›íñ8¶e¨"$5*¹¤¨íÄ}vÁußÞÞFA2:b¤Ä%Ã2éèqi£2Z’z}}}oo¯’ÁÞtØT1¹à1hÚX!Š e,šÃ&ᣦ-œ5Bþµk×–——í_mQ3nE£’Ù3˜Tg iâ2¢} eÄv¯€†TáÔ>ª•¹µµÅÎѦ´„†Ÿ2§/Ðn'•iÚXLG/‘CCÊX4ÊW|ÅW<ûÙϾ2"ç±H¢øúúº½Ù‡´³Ö‚ Ù½¸¸ˆÄ).¾Ù ѰA2rA¹@Äõº§u 1Ò£­63…l÷O¦ø.M‹‰ ÑäåÄÐ2 q||¼´´dšØHÃ3MŒ¦i>„#q0VëñâÖbš©:ÅM€2Ð$Pá|u­ú˜Á$½ƒœËOˆ„Ž˜ ¥Ä0)ªŒ)÷éwÜiy!&ÅB²SÄ— DÝêH™Yiâd±éãá«Ç‹[ˆý­yMâõ1â5ç–Èt0¶,ß;î¡Ë‘ jmmÍe„E•±%A4ìîîÎÏÏ;]ü‹«®^½Š¬iÏü„=Ç™ªoÐÄz¼¸U0îbLEoÉ,¾5uÚÏD?3͇ÎÂh­¼è×´±(-„<ÃA¦3DÊX4 ÙþPÃXLƒ’na´6}êu=^Ü6¸FH½Þó¥©oll¬¬¬”Ôë46öC¯©ªÕé7Ub,¿•ùg!ˆ”±˜ÇÇÇ(†ÍÍͽ½½µµµ6ÏLØ|¤ ¤‰/n&wvvÚIš*O“Æ|‹Y’ ¹#µp‡òøSˆ±UÆBd°Æ²q£3zµj¦?ò‘<úè£.3S’b4Už)ZŒÈ’,Éý©…;„žû")cQ%íœ0&ŒåG²±&BÓÆSÓûÿ…K¦[…õMpù ‹K]¦S×’ðD–0kiOh´4X¶£­®®ºŒ"”±¨’vN fc L„¦§Cõf´aÚXô˜ßÖb,O<üÐ'nÛ{æ©']> )cQˆÖNET+|‹°¹¹yýúu—ÅÐ\»ÁØ`aa¡äK*Ø<ù÷©F1(ðKx'—BL‹”±(Dkå`óÊxPÿÿW zŠ ¤ü»QÀ¾ÍŸœœ¬¯¯SáQ õ2!ªBžTŒŸ;??ϧËOÎH¾Þ‚[1ZåRQ·b„·X°UFÒRž……E âh,RþöK(‚mo6a\y;âê—B”à“>zïûóFgccÃe&' ØBuD˜µ´ZRWËgì , Q¶ôlÀDè·w‹‹‹eþ0UK=Ç/D…\xRýÓ‡ÈgmmíððÐeª`$V]ÓFÒ­²Y¢l% ö§àììlaaA1;¤ä†"øÆ6mFk™T8£ÿ43eÈòIGF?mnnr9ÐRlþZË0¾0kÙƒ@ sÀsssQù““vHìùÌýøAˆ ¹p¯RÆ"âbÉG)’ŒÄê-âÕ¥FDوѦ·lf!²DÙª N¼ò9 üßaRAÅšœŽPû´%¾ˆTÒKKK¾ÚÉ¢k±°ŠËAÓEébGC[¹lÎgX±EíË®°£˜I[y„5«Ð÷l²··Gš¶jRø^3Œ.%ÆÁpeào ¢Z.¼”±È€W&Ч’Œ‚>¦S‰ DYˆ,Q¶*Ð z~ôäI’’£Mº¤oÒ>m tªÙѵdmðf«üó\ëÑ\¯t¯^½jê**ŒzöÝæWQ>t ¤§~+ût©[Ó¢ òEBçü‘?¼¹ÿ¶ñom“29TõVb^„[q«x Ó3†¸iå#K˜µ´ÇŒÓQUµô4bËeÄ%eªÅÄk”€~%k*6ZåEíéé)v;Œ¥¥%{À#*JÞpŸí9 ƒôÔ?² ¿,Ic³öiDi#Ê‚Y LwM 1Þq{¡ú29”Ÿ°.iq™>›´.ŸØ6ÊBd‰²U¡€T„:Åéï˜öM¡xõéHÑQÆ€œÝÚÚ²G)ìu+Å•1«(ì±Í§ ü:°¬7¦®õ„YÒQ6J€¥sVuM 1EÿOÊXäPþZBNu²,†3k‰²Y¢l…èQã±è§ŠY”y“2Ô7iŸ¶„ÿ¥ìõë×ý“ay•1Åì±co‰ g)ãÝÝÝ¥¥%³—$ü:°¬7’ðiÓ\~¤CÌb« iéŸ 1)RÆ¢,§§§>¾NÍE8J T.?™ñ)2Z\>Q"K”­b‘ÉeD{{{[[[.#Êô¬,eŒÆeŸÈÜk×®™ÞÊ¡2>??ŸáßlPPÛñ#îFp•§~}Møu`Ù¤1ËîR·n˜´‡dmÕ94a,ĤH‹²T5çGàñø¬­ò¤Áì†ÏÚ*ˆ²Y¢l…Ø[®\F¤1“ŸßÕwŪö?õ=´)’(J“ ¶ONNh–Öð^ay@†rpXëŸx‰ ïîîú]q´¬òÇL‚µHg QÉÓ>¬a³Dö"ÛFY[ hÂXˆ)2eÑœ_¨?-'RA¢Uû&ì" tjÕ:UíŸ1'#O—0VŸ†3jÕì`YpùÜ.u‰0,kv#ÊvF)š0bR¤ŒEYf2ç×KáìšH²´´äß#Ö#ýS£Ö©jÿºçÐjm*õ¡ c!¦CÊX”eS­”ÍQ ozî3‘#áÚe|u„ˈQkS©­­­½½=—BFÊX”e&wûÂÉÉIU?Ïï+•ÈŽ‘½e?Yði³{Ìh8Ó%I DÆÑvø´ÙË e,¦ãôôtqqQ/Cb Š*ãO¿ãÎϼë–"dUÓÍÍ›7‰O.#TòbH*ÑÈf- –5BK˜6ÆZ¬¥Á²eháŸÅ<ýôÓÿò_þË©K'šA¹ 15E•±YèÅd9œÍÏÏ»ŒHPÕÈ!©DCK˜6"K”…Èe!´„i#i™Žëׯû¢µ==ßr4a,D¤ŒEYpÁè— *QH}¥+Êr²£²c6ŸŽv*cÐx¸Í´ðVƒBÊX”EÊ8ŸJR_i•2NâV-a¢,$-ÓÑZe¬Ÿ–¶–óósú”wbjžxø¡Oܶ÷ÌSOº|øwÑcôœqU)¿¾ÒÚ9ã$a™0 Q’–éh­2†~¼+×_¦J®WЄ± Ð!jBÊ8)ã|쟇]¦#!š©MôY¢l¾LTx´õËt´ùŸbÚüºÜâ•ïKVr½ÚÀÂÂB…ÆzXYˆTzâ/DMèo–rÐËöÇR‰"a'Ñ~BK˜6"K”ÍŠÎ4¢ˆe:ZþÖ¶fú¾U&de½Ñ[k« ³€O›ÝcF³ÞÊØUàÓf/CûoÈÔýÞܨ쥓–0ùéä&$BÂUà-¤ŽÿjíÉ 1[.:†”±ÈbccÃe&¤Ía¦Z>ç×h<4!—·‚ÐAî¸L+©ûÕ¹‘³–NZÂä§“›„eŒTKÒ8sꥴð4…hCÊXdQæŽj;L…è!ì±´ùGf³¥ªŸ'ÖM­ÓÆ‘³–NZÂä§“›„eŒ¤R3„Ö²°°pvvæòѶÓ¢оµMÊXdQ&~ãvûíyÛ?ç7sÊÜsè7uܯƒ¦¬¬% J[64‚ÙÁg£ŒÖ;,kv0#¸|k¨éÁ­ž©uóà·ú§)c‘ÃDúÏâ ø´Ù ³„¸—8ëg ðvKá*Ã,g½ÄY/ñFK¶*Ÿ®Ìùͽ¾#‹Êß3Pu?m,ÆRÕóú}}¢èàI‹Š¿ž"”•–ËB˜[ .Ÿ¶¹¥=f„( ‘ŲŸ³@j6½˜¢ óóó•ßî’›uO‹|ês5E=CÊXT@ÁêJ ʤ%",¦ò–0퉌Q’–TʼµcPèiìTºõ(ަgH}?cÍrtæÁå…èRÆ¢ þÓ[Ò“&-a0m”·„iOdŒ²´¤RÓýÍþ¢BW¹ŒѹOÔ´ñ¬8;;[XX¨©æS]h,â Ë0ö„¨)cQ Eb¹Ëd€;‹L«Ð´ñL¨õ¥é©Ž.Ëh¸üe±Ð˜´€eÁåKˆ[wëÚÈbY!J"e,ª׌ƒv™ ’Î+i3†„vKg‘,“o ÓžÈe!iI¢W.LÄÂÂÂéé©Ëˆn¾æYÓÆ3auuõèèÈeª&ÕÑ%¡Å§I„é,‹e!i(k$·2R 1)RÆ¢ŽWVV\&ÜVä¹"‹eÁåo-¦³H–É·„iOdŒ²´$©üoZûþ%q9??ßE‰©ÿmi˜ºŸºIutIchñé¤1Õâíž(k„Ƭ´S#e,*cì†p[‘çŠ,QBK˜Î"Y&ߦ=‘1ÊBҡɳIéÊ»{›¡‹Rz0¦aêþ™oª£KC‹O'ù#²$ @r?Fja!&¥¨2þÌ»ÞyúÞw[^ˆTŠü™žË;/Kƒe!5 .Ÿ(É줖ü,±DÔúä__YYY9>>v™aÓéùoa•‰¹á0öe(ò2¤:ºÐhé¤%L@V±ÐbD–dHîÇH-,ĤUÆBŒ¥àD)ÎËði³f1|ÖVfñ8ë%Icq‹ÇY/I“–½•b 4œ0èG .ÓAò§ŽŽœ`lùùy.Ó8õ½«»È3l%ÉrtæÁå3,.u™N]KÂY¬¥=¡ÑÒ`Y!J"e,ª¤ÈïðzO‡þº¬U0¦bd¥ßáõà íyA5Í íØ×7f4ào%7Å)úïÐBAO‚&Œ§F/6îÇê-ñÇÇÇF_Ñ¡‘,,,Ôýç‘RÆb€œ?ò‡7÷ßöÌSOº|êbþfš0.ƒ¦{óJà™©Æ•••·¥ÃÃõµ5—© )c!RQÇ0ðicM—¤Èï8ûJ?&Œ¾Ïûì쌯îý͇ÍÍÍëׯ»L=P“RÆB¤¢Ž!&c°Ok¸<èÂ¥¥¥ZnßZúô2àYMÿŸœœ¬¬¬ áÊóóóõ=JAÄ/,,H ‘ÊEÇxäî£ÏÞ÷!Ë ‘þq3´{âz *†9À8::Z]]u™^`ofp™F@///ç¿U½Ô×ZŽé}¸²½½=|𔱩\t ýÓ‡˜ˆîþUÁÔè_Ü*d€O«÷ï9œ&á[¶··766šùº™SÇ}9Æ+#®WPK@‹¡!e,¦;œ?n899AÙ $*7¢jii©îßÝ·‡¾>]ÝÌó!´–ÕÕÕ¾¾š-†U=qD/£êØ!+é±S%oÒè-$³uªÊŒŒY’´…Z|'Y‡!Ĥ<ñðCŸ¸moü[Û¤ŒÅܸqÞû™ÔNÿ—o›ÂëQ$ËËË=ž¯õ)‘ÝÝÝÕÕÕáÜ[0Ê?ÀÍÙØØ@ÓÅòk/UeFÆ,IšÜ‹‘šK{£%Œ0{Q4-%`´þ‚"Y!Œ¢ÿ'e,¦£÷¿¦Bº ê6nÃôéu I†p·¡¦ŸPu8–a¾àoiiiê6ƒCf,Áp¥`·JU¡1+ ų>M"LûO#?í-ÉDe L "e,j§ü GkÙÛÛÛÚÚrQçççò¾Þ+ÂÝ®àbÕïl¹1ziúÐ~£i ‰QÆ.S®Ί ÁpâèèÈY *³ôeT¸xÖ§“ÆÔbS2L„„«À…ˆ2MÐËW ôøñÐVÑ×Ö‡s·¡Ú×¶\¿~½—í¡ “>bD÷¡þÃOQi©Ú14f¥¡xÖ§“ÆÔbS2™ð¤Z’F1p¤ŒE  f}z)Uïmý{`}Pwg‹U¼¾=Pi›››CŽŸe899¡®ÐÄ K¦öT©ªÑ£µSg}:iL-9%“ OÒ©F1d¤ŒEC %{óHeŸÎ¥+ìïï£ ú!‰ìáûAÉ»òw¬ÓUþßÎQä¯ïì½.KKKåÿ>:_J¦JO3¸Áæ†e!5=*r‘öÃìà³QFë–5;˜\^ˆK¤ŒEs %‰m]°òøø¸ÖŸÛ‹,쟪}bµy®]»64Y 6p™ÉÁi¨ÓAþ_ßѨÂâ Ÿ_ÏŽÒ”¢¯H‹F9;;Ãkw÷ñJÏßÕì§WýAÂess³÷ï¡K…s_XX˜®ã\½z§¡NY¿ff¸ˆ}qq‘Ví½,)c1@¤ŒÅ @lmmunÚŒÃø3ŽmÀà¤s¯rCØqØåïnwºü¤ÏBX¥¥jÁaBmDo–¸yó&‹&¦–ê¸"ù+HQeü™w½ó‘»'xÕ‹ù#;4ùÊq®¯¯ë½Å-ÁÉÆÆF‡&_;=Õ]ùO$±Ç–&zÅX¿¡ÙÏÏÏû‘95ƒSBãKë®K‹RT Q9]y`÷æÍ›Dôþ½u®ëØ»í\Ñrh?´"—0ȸ‚õ`#gUZH8´XYYiÆ)I‹"e,f ‘¿»»ÛÚG_úéOkApuZû›NTûÖÖV'ä{3\<$;îÑ»!Ðŧ­êf;xvc¿C•2äÁ;n—2³„ø·³³ÓB}cSÚD#Éš6ÃàjmDÛæ1v§ÛåEÿo³¡rÿ¼ 3¢K‹rþÈÞÜÛ3O=éòi¨cˆÚ!d"nÖ××Û0;KxÞØØ Bkª¸+0¬Br]½zµ ÛÉÖ˜*ºUÎóÖGGGêt©Ð–æçç]¦A¤Œ…HEC4„IŠêc4ñæææââ¢f­:Çùù9ÊØ~¤?+I:óÜ~®MøçÆÂ`ì·¶¶æ2 29.Dû‘2âå gªŸ££#ÓÄC~¯V@›>ÞÙÙiìO å4iâ"œVôOÑCƒV .Ó ºï!D*Êø3ïzçé{ßmy!YŒÎXXXØÚÚªïuWˆ§íím¾euØïší„óÝÝÝ¥×®]«ïdZ鯯Æüü<Ã*i€U÷máá™!D>ÊXÿô!fÂéééÞÞÞÊÊÊâèï¯å”Íö÷÷Ü^95öCoÑ06ò¡ñ؃¿²ò³•ì“6ƒRAóI[Ò èDPc '\FƒÆ¦é[!Úƒ”±˜=7oÞD#޳DÖ«W¯™N¾qã%)ÏVlËH ¸»Û.fm9»¶¶677Ç@‹qíáèèhl@õRŒ¶Gy6·æ‡È¦EI©LUŠÎÓp¢8x?Fw.#„hRÆ¢] fö÷÷Q*ëë먜+—^]]ý†oø>ÁY¯\±@)ÏVRÂÑã"Ú™k­fÍÆxá _hv”4Y†d”?<<Ôí…J Jo¸Œ‡fÙ…h’'~èão~Óø·¶I‹öƒâ!Ü~í×~í»ßýnÅ]1ggg´Ï+_ùJäˆ['ªFo¨˜êŠs!DÍý<)cÑ ®\¹¢("J²¸¸¸°° âšÐã‘ÿh!DµH‹^±¼¼Œ2FÓè)F15GGG£')®lnn:“¨”q}ï éz,[ˆ†‘2ýÁ&Œ M‹©A»ftåŠÞ/VzB  7nÜ`Àï2Bˆú‘2ýÁ&Œ M‹é ÙÌÍ͹ftåÊ¢þ–¢ŽŽŽVWW]Fd³···µµå2Bˆú‘2=!œ064#%¦àúõë®]¢ßŠÕ(ˆ&×…h)cÑ cCÓÆb Vƒ÷ý½à/ »±±¡†Tú_·"è/…h)cÑ74.%ÄäŒ^Úv„Ö êFÏ AïH¢a¤ŒEß2•`ÿFî2¢ôî¶±œÍÏÏ»Œ¢Š*ãGî>úì}²¼mFÊXTÂîîîÎΎˈzX^^¾qã†ËˆÇÇÇ+++.#„h„¢ÊXˆ® e,*áàà`}}ÝeD=0ö`â2"Áõë×õRm!FÊXô )cQ '''KKK.#êAïnËG#!šçÁ;n—2½BÊXTÂùèÅÆ.#êJÖ»ÛrÐë;„hž§?÷8âø™§žtù4¤3D—2U±´´trrâ2¢ô¨qjB´é !ÄÑŒ]èŸ,²Ð] !Z‹”±bˆ¼ë]ïÒ+ëF?2ËâÆËËË.#„hÊø3ïzçé{ßmy!„¢ôVã,ôv!ZË…2Ö?}!„¨ƒùùù³³3——\»vm{{Ûe„mBÊX´—+ ÜŠI „+./„h=ÏŠ^Ù&Dk‘2í%)jC‹¥£2>›,)„hž«#\F\²±±±¿¿ï2Bˆ¦xâá‡>þæ7k›”±h'©Š6Ò¾Q™h­Õrvvvttä2¢‡‡‡kkk.#.Y]]UC¢yŠþž”±h/j½~ Óž¤¼1™€T£UšQÓšÆóóó.#.Y\\¼yó¦Ë!šBÊX´´…—QÚF”5’…ÃbQ:ÌŠ>qxx>½º··çÿQâôôôÚµkW¯^õw«‘³°4œŸŸ³Öÿ,,YØ?[ùU&bŽ777iTXÀJ²gÒÓï̲Ðÿ}$‘kb&H‹–F…¬4¤oLÝ0¹IêND×A¡†/Ê]\\¼~ý:‰“““……Vmoo“°ûøÈ\šŸ¢£¤ÿ×âÔò€…4»%±´´dåÑÊdÙ•WÆÈ>Ò[[[ˆcÛVDPivu„Áˆ‹Ææ2Bˆ‘2-%T«Yiˆ²†7¦n˜¿‰è ˆ-p™@¯¯¯û—aݸqƒKoçñ¯@òúm³ÊS€Ml¦óììlnnÎf”£§)ø^ýŽj,{{{Œ\FŒn>¬¬¬¸Œ¢A¤ŒEK µEV¢¬á©…ó7½a$ŒS”ñüüKÛÜ3ÅNOOÏÎÎÌë#,-’p]fðд|ËB4IQeüÈÝGŸ½ïC–Bˆöƒ~5AŒÒ½víÚêêªI ìÈìv¯ŒONN°D¼f•¿ÐÅiÊ(CI¾eÌÞlCö†Hekk+k.€hœ Ĭ(ªŒ…BˆúØÝÝ b8 ®üo@…M"e,„böèm !KKKºÃ ÄLxðŽÛQÆ=ð1—OCÊX!D½èÅm!ú=¢³âéÏ=Ž8~æ©']> )cÑ%V/_G „ègggóóó.3xæææüãïBˆ¶!e,ºÄ½‚MÔÃÝwßýôÓO»Œ¨………ÓÓS—6òcB´õOÑ%QDMlllèžkeeeÅþy{à0<`à2Bˆöq¡3>ýŽ;?ó®wZ^ˆ6#e,jâúõëákÚDåhìaܼysqqÑe„íãBgèŸ>DW25¡™¼ºý÷öU—0ÇÇÇ+++.#„hRÆ¢KH‹úÐÿ/ÔŠf壣#ý’Xˆ6#e,º„”±¨I·ZÑ\©±¿¿¿±±á2Bˆfyâá‡îý=ý¹Ç]> )cÑvÎGÿ¼:el ÐïÜEµÐÒæççõ:­j¡ŸÚs/{ÙËžýìgÓs—F¸ÕƒáððÐç¾°°€O£NtBˆ†)úxRÆ¢åloo£‰CôZ¢ô+±:@º~{ɵk×ܺÁÀˆ AìÎÿ’7n¸ÕBˆF2=áôôtnnΓŠ(¢‘q.#*âää$쿤‡yÃgww×UÁ=S!DóH‹þNkÂXÔÄùùùââ¢Ô©œ«W¯ºÞ;`EŽð;<b¶H‹þM‹úØÚÚÚÛÛsQ6ä°þ{ttä¬ÃÃðI8“¢A¤ŒE¯° ¢ cQ+z…BMÐøÛ»ákÂXˆY!e,z…M‹ºY\\¼yó¦Ëˆê`X;ÀßÞE¨„˜!RÆâ‚½÷fóí_Úýà•í»:¿üÝŸ-Z²—ç½öž/ø‘»#£–ñË7þà•o}]lÔR~ùž;/–È8´åeo»òªßŒZÊ-8ºù×Þ§X^÷µ·Þõðæé?é"¨èRÆCçÆÃ¯üì}[¿öû×çôä‘'œUô.7!œûÁýÊ¿ !zŽîðä³8=\ÐY'§ªýˆÖRT?zïûó ‰.‚,¦oê1—ÃàÚ]ŸÞxÛÇÎþäi—BˆÁ€ëÃî¾çA—ŸÛîÔåE_(ªŒE/YùÙû$‹‡ÆÍÏž3:ú—Bˆ$üÝx8ïï‹À~p§8U—½@Êx¸ìß÷ã]—ƒ‹Î¥w!„$‡'Ÿ]{ëý.SEÒþñà·£Œ{ ï²J÷“íƒOè6ÐY|ý½šáB œ³?yzþµ÷¸L p§8U—½àéÏ=~úÞw»LRÆýdù¿[þ^’è§üäÂë>à2B1`ªš&À©êwÌCCʸŸhîp€hzC!Œª‚ ‚é‘2î'êÌDÊX! )c15ÊøæþÛ>ýŽ;-/ú:ó‘2BCÊXLÍ…2Ö?}ôuæ"e,„†”±˜)ã~¢Î<@¤Œ…Â2S#eÜOÔ™ˆ”±˜ˆ+Ûw±¸L&*Ü6:}ðb ¤ŒE*O<üÐý¯ÿ±'ÏþÈåÓ2î'êÌDÊxh˜´NóMºU”eNAGÊX¤Rô?ð¤Œû‡:ó‘2¡Î›BóuQ—<†6œ‚h )c‘Š”ñpQg Rƃ¢I™ØMYþ0$އƒ”±HEÊx¸¨3)ãA‘*òBcT€lêâVßZÀ™.‰ŠÙg²d²˜O„…ÃtˆÙ'Z›j±ÏÈn$-¢¯H‹T¤Œ‡‹:ó‘2Iñf³ÒFΆI¢]EY—ÊH‡å£´%Œü¬‘SƧI¤Ú(+zŒ”±HEÊx¸¨3)ãa’*þÆ*±BµùZ6«|Vò³Fù2©›ˆ^"e,R‘2.êÌDÊx°„‚ÏÒI ˆÅŸµ„'*£©Ù¬òYi -¡Ñ—±„eü2©›ˆ^"e,R‘2.êÌDÊx°„‚ÏÒ9°`ЭMÍf•ÏJC”M¥È&ùeŠ|‹èRÆ"•¢Êøƒ¯z¹”qÏPg Rƃ"yé?[›S&¹*´Dkó CÖ¶c÷“¤ÈžI¤Ú(+zŒ”±H¥¨2~ôÞ÷çCy€H SIµ—j‰ Ge’BBcT Y>§@Î~K¸8ë­D«’…-´I‹è+RÆ"•¢ÊXôuæ"e,Œ:ô_W4eÎqJ )c‘Š”ñpQg RÆjÒRÆ¢[H‹T¼ãv”ñc|ÌåÓ2î'êÌDÊX@}ú¯Ê2ë %‹‡†”±HåéÏ=~úÞw»LRÆýDy€H !„!e,¦FʸŸ¨3)c!„0¤ŒÅÔH÷uæ"e,„†”±˜š eüÉ_¹íÓï¸Óò¢”ïÌWÒÞy$ÚŒ”±BRÆbj.”±þ¯ìÌ9òWʸsH !„!e,¦FʸŸ”WÆ¢‹Ì¿öž³?yÚe„bœ?ý ÎO—/”ñ‘2î'RÆÃdí­÷ž|Öe„b걕Ÿ½ÏeÊ!eÜ3{àc(Þ'ÏþÈåÓ2î'å•q´Êg-᳞KÎ*Q-Gp¶úóv!„$oûØþ}¸L9¤Œ{FÑÿÀ“2î5)co‰²e!´Dk£’¢B6ßþñkw}Úe„b`àQÆ.S)ãž!e<\ê›3öLT +-*çüégÇko½ÿèÎô̱b àúpz¸>`%ORÆ=CÊx¸´Mƒe#£¨‰Ã“Ï®þü‡ç_{OXíZ´hÑÒ×eî5ïÃéUþC )ãž!e<\Z¨Œ!iB!Z‹”qÏ2.mVÆ‘Q!„h'RÆ=CÊx¸´P[62 !„­EʸgH—¶)㬴BÑZ¤Œ{FQeüÁW½\ʸgÌJ{Kj6™B!Z‹”qÏ(ªŒ)ñØ•½üO´‰”q´„«, Q²,ÞžLx’!„¢mH÷Œ¢ÊXôuf!„¢$ ¦=CÊx¸¨3 !„%Q0íÞq;Ê8ÿA )ã~¢Î,„B”DÁ´g<ý¹Ç½÷ý.“”q?QgB!J¢`:@¤Œû‰:³BQÓ"eÜOÔ™…Bˆ’(˜ eü‰Ûö¼ãvË‹~ Î,„B”DÁt€\(cý^ÿPgB!J¢`:@¤Œû‰:³BQÓ"eÜOÔ™…Bˆ’(˜öŒÇøŠ÷ɳ?rù4¤Œû‰:³BQÓžQô?ð¤Œû‡:³BQÓž!e<\Ô™…Bˆ’(˜ö )ãá¢Î,„B”DÁ´gHuf!„¢$ ¦=CÊx¸¨3 !„%Q0íRÆÃEY!„(‰‚iÏ2.êÌB!DIL{FQeüÁW½\ʸg¨3 !„%Q0íE•ñc|ìñO~Âò¢¨3 !„%±`zpÿ£×çÔ™D—)ªŒEÿ2B!JbÁôêáMg]FÊx¸H !„%‘2îŸ~Ç(ãÇø˜Ë§!eÜO¤Œ…Bˆ’H÷Œ§?÷ø£÷¾ße22î'ËoüÝ?î2B!„˜œ…×}àôŸ”2RÆýdóí×Ï„Bˆ©9û“§ç_{ )ãA!eÜOöÞÿ™­_û}—B!Ä„Üÿèúm%!e<(.”ñ'nÛ{ðŽÛ-/úÁùÓÏ,í~P !„Ó±ò³÷ê1RƃâBë?ðzÉÑœ­þü‡Ïþäi—B!D1vþí'·Ü_=H )ã>³ß#‹¯¿÷Ú]Ÿ¶Q¯B!r¸9ú_•Ÿ½eìLRÆCʸçœýÉÓŒzéäW¶ïŠ–ÿäGïŽ,ýXúz^m[žýCï‹,ýXæ~¸Ÿç¥¥gËswî‰,ýXfîX_ïúm¦“*Qƶ—·âëßåëäÿà'ÏþÈeÒ2.}}ç±\Oس:.Ó#®ÿÎéæÛ?î2B´: ÝÐezD;Ï+U{1.n]c ´„æÓ¾±±ï=ûÈïÿ<)ãa"e,¦FÊXˆÙ"eÜ$S(ã( IˬÈ?’‰Žs¢ÂYT²“âH‹L¤ŒÅÔH 1[¤Œ›$G»L‚äÚüòMRá‘T²« §RÆ")c15RÆBÌ)ã&™TÛ*¿„Æh­­ò¤®òÙÐ}6$uUh ížÐîÓ–ðY#4†vHµûlh·´_rŒž±öhUd—2™H‹©‘2b¶H7I…Ê8¹ØZˆì,©öT£_l-Dv–T»CB»/-©kÍ‘%ÕžeŒ,¶Xa(hgɲK‹L¤ŒÅÔH 1[¤Œ›áä‘'8ÜB–2·bD–%4†Ùhm˜ Óž¤1ÌFkS³–N’,ÎÏ‚Y¼1̆éÈ×Z:ÌúDd÷ÙФ¥ŒE&³RÆÖL£ÆšJñ’!Ó1Ce\¼U/é 2&ðï½ÿ3,7~Ü™*Å3UJÝûo-³Ra·ÊïY‹EÌä¼NÿøI¾ôðä³67Ì1°Ì½æâýqK»$M¸¬J»ÌˆÐ’³6¹ ’ÆÐ2éÚü’‘elð–ä*#Ën„k³J&íÞb‰p­”±Èd&Ê8leCŠ—Œ(^RLͬ”qÝíghʘ`ÿÜ{¾àGîfyÞkïùŽõÑó§ŸqëªàŸÜù~ÿ$ȺQ÷þÛÌL$ìJÓu@¨õ¼Ø3Ëî{¤å¯ßöQ“¼ÛÂë>@zí­÷›2¶ba_0»Ë\†9ç•\›o±tr‰Šy’ÆÐbé䮵t’ü’‘%«@rñ«¬XHÒn–p í–ñÅ¢%¹–l!eüÁW½\Êx€´ái ßp“D«rJF/)¦†°1eQyû”2&ÒG‹ó¬úmı[]šú7ŸxÎܲÿ¹×¼ï»÷+«^tðçßú·,d‡#Ž¥Œ³¸ñðãìaïýŸ¡…oýÚï³Ãå7þ®µÒ,;ÿö“¬:¸ÿQŠ ‚)cKG¤®JCKrmHñµÉ’‘el¬U‘='­òdÙC¬ K!eüØ{ü“îÏÁÅp˜Hû&ÅâL#£}´'‰¶ 7 WA”Í¡xI15“*c»¬¶8Óå•*nOmn®‚(›Åp”1}þÇRþD c%&ŸµÿçîÜSÉcì„]E;g©jÿíg"V‘3]vŠâv#i1|y+‹²9­ ×&KF–¬I£}Fv#²çd-f}"´– V¦2ä¸2Nmm–WµGä—‰,Y;IR¼¤˜š‰”qÖ¥$®k7lm~ˆ,É© Gïß÷Ès_›¢,?ÿ‡ßwí®O»B%ÈÚÿüÈ݈W¨ì„]E;g©jÿí§ ‚ªÅ¥Fø¬Õ˜¥a¬Ý°µùe ²$ dž×ùÓÏf1ajÏ?,¼îìøE öÝ÷²,&Líù‡Ó?~ÒJ6ÉÊ"cV—a¿„FK{’Æ,‹_œuDªÑíÉ2Y–,c¸*L‡$íf1cþÚoÖFFdñc|ÌV¥"e<\&RÆÑâí–0ÆÚC’F,¶ø¬%Œdù,Š—S3©2Žo·„1Öž$\EÚŸµ„e³ÐÓzš¢+L¤Œ£ÅÛ-aŒµ' W‘¶Åg-aDÙŠŸW“¤*cÑQžþܘ§k¤Œ‡ËÔsÆž,ß7Ö'fíаµcw’Eñ’bj&UÆ.u+Y—8Ëž$u•‹ï$dP¿ÀKþBîY?ôÛßòÖûÝêÒ|ÏÿôÀ³~ð·£ýç¿>q«KîØa¸ý/jÆ¥n%²ûl–=Iê*3ßI„”±˜9RÆÃ¥Ve®Ê*–…˜t+Oñ’bjêVÆáª¬4DYÃŒEJ&”2†ð­g•¿µ]!ŽýþIÔ½½µ-•¬ÆŸÕGH„«²Òe 3)™Š”±˜9RÆÃ¥¸2üZ¸x£%ŒÈn%“e¢%i7 ¤Ç2Qa1)c/¥¿@>aDv+•oW7æ34e 'ú§n2‘‚ û‚ï>aDv+•oW7ŽEÊXÌœ eüñ7¿é“¿r›åÅp˜HOÄD~°rfûíaRe<3¼‚TÆ¢£Ô§ gëB¥ŒÅ̹PÆ÷~ïKïûÑW[^ )c15RÆBÌ)ã&‘2Êø£»?õ¾¿ÿmÏ<5ƒ7¡ˆ"e,¦FÊXˆÙ"eÜ$RƃâBßÜÛ]ëßüÙû>d&1êSƳEʸjUÆ3DÊXt…v*ÈòH‹úxæ©'?øª—â¶=—ÏàB?ñðCïûûßvï÷¾TÓÆƒBÊXL”±³EʸI¤ŒûÁ'å¶»Ö¿ùÁ;nwù .”1Ø´ñ‡â5çü¡YDï‘2S#e,Äl‘2n)ã®óÌSO"ˆº|ÕËÇÎ;eL¹¿ùMló¾¿ÿmÜ}ô™w½ÓþW:gyòìlÛ§?÷xT,uIý’.nK%DÅR—ÔË0óm¿üê=CPÆ ö|…ä,®ô­<ñðCQ±ä’õ'“]ÜöñO~"*™\(CÉ+ãïü…ß Ï—útën…: ‹¥.ulK]²&8¢b©K«¶ÅƒEÅR—Ô`ÔÅm' F}UÆëçþÝ;óß…ç›Èò—ª¶½v×§í_Ó ”Ôï-¸­+}+³ÚvVÁ¨ÂmOßûîOܶwï÷¾Ôd±…°|œ26>{߇>ü¯¹ÿõ?Æöc—îþ”Û,€¯Š¥.©ytq[*!*–º|úwº f¾í×lÿ*"²¡åK–cKmËâëïu§:âwà¾Br–Gï}¿Û àžïüo£b©KjOëܶ„Û¨LÖBÉ“Gž˜{Íû¢š¯kyÙÛ®ü•oµ-ßý=ÿÏðd©OWAÔ^X&k©|[ZiT,u¡Í» º¸-,*–º¤£.n;Q0Ú|ûÇ£¦[ãB¤FÆz–ÏÿÁßzÛßû¿‡ç›Èò—Ê·-P¹ûÈmPpÛÔ`4«mgŒ òm?ðþÁ'å¶ÔK’[”±'”ÛYKê¿àP,u¸\fÛ‚C¢Ym›:ß\fÛ"Ã)–Ym›l|W®\™z[ õGÅR—YmëJßJ™m‹Ì)²¸Ò·R÷¶YÃô¨Xê2ݶïüW·ý¿ö’2ß›ê=!*–º¤nKk‰Š¥.³Ú–¾æ6(³-Þ *–ºÌjÛÔ`TfÛ‚ÁhVÛ¦£2ÛŽ Ft@ºátÛÚRfÛÔ`TfÛ‚ÁhVÛ– F³ÚÖ•¾•2ÛVÈR›_éÊXˆjA»”rtt´ººê2BˆÆ¡Ò ]Fˆ¾#½"š@ÊXL”±³EÊX éÑRÆbj¤Œ…˜-RÆbPH¯ˆ&2S#e,Äl‘2ƒBzE4”±˜)c!f‹”±Ò+¢ ¤ŒÅÔH 1[¤ŒÅ ^M e,¦FÊXˆÙ"e,…ôŠh)c15RÆBÌ)c1(¤WDH‹©‘2b¶H‹A!½"š@ÊXL”±³EÊX éÑRÆbj¤Œ…˜-RÆbPH¯ˆ&2S#e,Äl‘2ƒBzE4”±˜)c!f‹”±Ò+¢ ¤ŒÅÔH 1[¤ŒÅ ^M e,¦FÊXˆÙ"e,…ôŠh)c15RÆBÌ)c1(¤W*àôôôúõëW¯^ÝÜÜ㬬¬ ÅÅE,ÀZ¸qã†Ûf`P.%Ä„H 1[è€ÃTÆ÷½½=b÷ÆÆ• îkkk¬ÝÝÝlpï%Ò+ÓsóæÍk×®Ñ7ÐÄtô1îãøøØ••ÁºøêÕååeºÓÖÖÖáá¡+1 ð#.%Ä„Ð}¤Œ…˜!CSÆ'''wt0ÁxMìÞßß§’ÁPÎÚ îÛÛÛƒª¨¾"½2 (॥¥éº݉1(Íùùyô4CR·¢×H‹©¡‹I 1C†£ŒÄw ¸‡:¸~²ŒàÎæ î½Dze2¢hS:Ó´œ¡°M^“vÖž"e,¦FÊXˆÙ2e¼¿¿oḒàn ûêÕ«½î½Dz¥( ñëëëÕ>Nt~~N¢Cîîî’vÖÞ!e,¦FÊXˆÙÒoeÌ©-//ollܼyÓ™ªMŒ2FâIté•Bìì쬬¬ÔçèB|]¨ühµH‹©‘2b¶ôUŸŸŸÛïæëûýÜéééöö6Ê[Wté•1ÐsÖ××www]¾N°¢¿[þã<ûÁÃëðWºÆüü<.†ê¢n4œg­K 1!‘2fipmm »µ=ÏÜÜÆ­­-ýZ\ˆª O…ʘžµ··G/þ¸¸èúÞ%X°ÓC¯_¿Þæg 86|éòuB!Žå‘º‚ôJ&U\¾~¬¯¶ðæ •€FûÚKjö÷÷“¿Nààñž”¤žÂ&”D"ã.]¡lØ– ÍÛâDÌÉió³T‹€;{ÞÜÜ´é\÷دh˜““Î"é@êƒkßãr¸¼h1RÆ™ÔiÇ3‰©xüŽËÌ|õ€¥KOêÝØŒ:ù‹ñ/:Ó­°C+cgjÞ6¬vÒægíž%9˜fúb Цöö†F´.q‘+FP:ì¬åšÚ{S…÷<ï{ßûþÚ_ûk´"ÊLzG…/å`ø ÚŒô±4u"ÈÒÒ= ïJ/ÃÍÒãÜê‘J#k³¦_éªôǬÇùþößþÛ/yÉKèG”™ô©¾×MÿeÛÔÞ<ç›ÔôuÃés9š¹-Ê eœž…ž3Ãç8,—™xIô ‚59=<T£yÆp?hâsÜinJâЩ¶Å­O*ÖE}؅溕‘°Å£ 2ÔA¼²môSTÚF%?ñ¦Í×i63ìÔBÔ íœÈES'‚Lô«œ3—¾†ÃÇô;\4û …õ°‹ôhgš6ç5ÃÀAàṌh%RÆ)Ðgð,Í(#ð# ²]¦qll0é]Ô'>MšóÂE–|ÿ‚+×üqàjr-¸²eBÛÚOQ-|’&*W¨ei*´ê™wm!*ŒƒexYr"ìÇì“þR¡–eŸ6‡íòCßçŒf><æ21†qÑ>¤ŒcÐjkkk%ÇÇUÁ‘T¨M‹ƒ,ÆÉV>ª¦n·¶¶ýU퇧¢ÊˆlQI¢b«º¦D/§Í;SuØMâXô †|Œ'+ [ìpqq9ÑÄsAPÆ3ÇÄÎ(œŸí9‘Š”qŒý°Àef R£&ßr||Œþ6oeÔ!‹=uŒ×9þZYdÄD×áâë›×1qìÚú$…¢”è(4à:½C½ÕçQí˜Gï„8}°nÞª™Z¼Pf¯E*RÆ·@ÿ¬ÃÅ”YŒò¨vB”½íïïÛO4®Œ^°6ú±¿§‹Óž«¶¢D>¸u꼋ó¯³kë#Êô2ë ;;;3¹Q#ÄìîîÒb]¦Sm]÷©dúàüüüÂè—'{{{•{û"—i 8k—mBÊøO!Ì[8†«°Kãnp£ÞûÔ=Ý0Ý eVOûÔã+ZçeÏrhFG´™£_ƒôi:€g³6‹‹‹ëëëU¹—:&˜*!#—­AÊøO™í/Þr ?ã&JivB¤_ZZâ³ÇO'…ãXKhæ¯rhE—oz¢ª©g·¢wÐÝeÑ_Úól•€È#NÑqtÄ…?„«aŒ åç[û‹7ü ±Oc›~ eìhyÅ"Ü]frð,Œ˜‰ô½ïˆþ£Ñ=9p©óóóh¸’ƒ ÄãÑk§‰1Ô­=èÖõšêŸ1@“¢+ª±€>HìÙM¿T9å2« .Ó>Z;%7d¤ŒDý™¿g1-áyºéó Cð¡I4¸Ô’ŽUìîîÒ„è Ü۰GØ+ðQˆ‚à½iH¨aºqÜÎææ&^Èå'„ª+ù&»Z!Hg„æ>iIDAT¡4}Ó*¤Œ/hù˜Ò888X__w™ÂØ© |Æ‹ÓDZR{Ãve êˆ+C¸ÛIE/Ñ<ŒêñámÖvÍÀÀ` q<]Ül˜’÷„EåH_@àŸz<ÚH“………I5..uº™æþ±×‚¿ì4¹µÞýÆnj:bEϰ×hHoLÎZ~7Ø`ÈMpwѤŒ/X\\ìÄ­R„ÝDO {ü©)Ð8a"Ô~"4N #—bÏ5¹LQÌÏÏwb\Áøg¿Üè RÆ÷I—––\¦Ý0öeì2Pg‹Ô› ÁÆ¥ºí cR!8&šÆÂ}áÄ\¦Ýìê•£mBʸ/ÏbÒð•+º¾· e3Œ‡ù‹Ÿ,Éf—¢fìQ —#&ºq:é]Ö"ßÒ*¤œÚþÃÕˆ‰¦ñ¤Œ#:ñ@y{èÄ#zM¢è%šDí-ÉDbwaa¡C¿šÕLD{ºrûäû•í»üâL#B»_ܺ[[ÀS¤ÌD?bèÆS«¨CÓŸWñ‡)ƒÂ^lâ2¢¦1¡æðDì·õ/*ÆRS,þˆ`‘gQ|hŽ"oh÷‹[w+ù’ö¬’ ·‡¡+ãœØµ`ËzK´ÆZ,•ñøµYŒ‰~ _p:–ôb5ùµ‚ÿöâ%;tg­=yÖè¢õt­ýŒùOqÖqLú ¿%¹yó&ZpìGj3.Þ°ë ÿÛS×9àâ«ù±2 µQðÖÂX‹eSË„ÆdÞÝÖfÙsÚ@ÎúÉZ²Ö†Æ¬2.s+¶*§€1Ño2 ÊhóG‘W뤊x±©)¾ó‚%OOO©·ŽÎ¾Ì½½=šPþ£ív ¢ 1öº¿ÄS0é·<½(@4 Ž e<ö :kÀ“¶ê‚Í~:&ýö"3ÑÄPþï­“¡6´d­ Ye\æ’ИZÀ£w{¨±ct‚¬Ÿdeµàü&S @–ÑìY[y&}ò ?2VšKšÔOqdSSpç©GžÅÙÙµ¡ÛUA W`ì/>S¯ÂØ‹RðªMǤß^ä`4£#†aXñû~þÓ3i/¨–";÷eŠžôq¸œû]YqÖS „ÆÔeô‹3¥¡‡µÚC£duž±8µ@hL-ÅDNäÎvvvrÆÍFª{ŠÒž, L{œé²@hô Ó¶*§@åz¦¢¨À¬¸1öª‘ödY `ÚãL—B£O@˜öDÆÔ2kkk{{{.#DÍ ——— þPdlË'íɲ@Á´Ç™. „FŸ€0’Z8 FKKKÅÿë*çñ¹ñA6­@hL-…»É¤Ó^¢>Æ7Ç~ƒjLvcqjИZ ‹‰6,þ{[\ú¦àœ_˜€,Ÿ•e‡ÔÍ!k“О³ÊIÂU9Å"p¬¸×âÃd¢ßÞ¥^¬T#dÙ!usÈÚ$´ç¬²DÄh‹?ÅYs¡å0¬*8Z¢$½Æ·á°1§!Ë©›CÖ&¡=g•%Rm—W äðð°øh!ç=NcƒljИZ ‡"å‰Ôsss.#fJÑÙW²†•cÛqjИZ +mè³IžûÜçš7Éç9ÏyÎÞð·M.v© LYvÃŒÉO#ÚÄg#;$·M®Í/¡Ç¹ÆRð®‘z!’ #Ën˜1ùiD›øld‡ä¶Y»KÖXZˆ ÉùL*aöédÂȲfL~Ñ&>Ù!¹m*[&ääää‹¿ø‹m«|žýìg¿ûÝïv›ÝJ~„…Ô¡1µ@V¸È&¶K‰™2ôË0霱7¦© k«äâV'(2g<Ýœøt˜ˆðvK„˜1ùiD›øld,IcˆˆpëÆ¡›VùœÍÏÏ»Lš÷é0áí–1còÓˆ6ñÙÈX’ÆÔM\*—Ivb FOùMpw"l½>&"¼Ý!fL~Ñ&>ÙKÒá Œ-Rð‰ÿ)挽1µ@hL-Y[…‹[‘@sÆía‚æØK²þ’1µ‡–¬µQŸ5²,EŒFÁÎ3ÑÌVä•,;Ög¥ÚÃmÃO#ÚÄg#;$·ÈÚ¤r@ùL:rˆj>ºvY×%Õn~Ñ&>Ù!¹mDÖ&EÐÿňº™ôÖDÔz£öŸÕ¶Síá¶á§mⳑ’Û&ÉÙ<‡òA05†–¬µQŸ5’©<'''KKK.#fÊͱ—d½›¢FlYo‰ÖÂX‹eóËx²ìUKñסCä•"–峊أ2YÙä®rVAª²ìr@c)X“FTزޘµ«"ö¨LV6¹«œUFÎ&cÉqBTB™9c°¬7fµí"ö¨LV6¹«œUFÖ®Š@û´q~FÖ²Þ­…±Ë旤ţwS´‡ Úb/Éo‹Öˆýâ¬#¢U¶¸u ¸L@–½`ç)£ŒKh´¬ÇYÅŒ¨€K]2ÚÂáLÅ\jÜÚ,{„^¿5–"/ûó¤^ ÐhY³&ŠQ—ºd´…Ù2йTÚZ¸ØþVÜŠH‹º™èq8H6à¨U[Ö㬉bFTÀ¥.máp¦Œb.5n­'Õ˜Je<¶ŸZõ‹³ŽˆVÙâÖ,à2#²J‚K{(ÚûJç¦ þz å§÷¦bïBÒs¢ùØ‹TÏǽÕd˜èGx¢IH%—xï……—ÉfÒ_1Μ¬g;Eó ]ŸÑÇ:¤ v|ÇD¯~\è‰fÓ‡ âOInÞ¼IÏÒ˜AÔ mŒ1æ”ñŒ¨†]ñ-„ØL5ÎÏÏ㸩ÆFèýý}ZÑúúº¦ÏÅL0Ù„ÃÿÓ‡Ö‰ÑÛÛÛ„éI§ÏíÆ©Ë´›ü7iˆ†‘2¾ +#òœW—çcbnnŽOº_ïoÙ<ìdQršä« ú™Î‚Jîý@‹fÃÉÒ„®\¹²±±¡V$f‚iHkD"3NsÖþríÚ5NA ¤§{¸ŸºêÄ@biiIiµ)ã :1\Ã/ H¦óÛ"hð5àL=AÓ{é6C¦¸wÑ9–——QÆjE¢ û=ÁqxxˆXÜßß/yšxªö?¨Ý¡×AÊøÆ”´Ë2¢³ªz ¯Í»L)?„ù ÁÓ„ôhµh-|e¨jø}cÂ7CϽeµmH;ZÞ4Ñyh‘Jî U¸«vÒ{éߺrr::MÅéÐǧc¢?ʧÂ]ÕA'&憆”±ƒÖ¹ÔâWû¼GUÓÏíDR4@¿¨hy(!Åà­¯O¦V;4eo+-þߢÞOÿw)ã?¥µz±Ž1e_åcµC‘C_zïgãD?@#û7×X‡èoí=aÝžj'RÆ ²ÔÑ«9M†Ñ=›ãtºõw ÆXÏ^â†,¦ õOmˆ^ÂÐtcc£OÍ•À´ºº:Ý+˜r¨cv©t{ªHßm´moÃAyà)êèÒgggœìææf S°³³ÃéHÓ4‰…±~ü£éÉÉ íG³Å¢[Ðûèƒýðá×®]«o°ÝÂ{Â-ÔÂ2ŽÙÝÝEc¹Ì¬¹yóæÊÊJ­^ïúõë ¦Å®à¨*Ó—ÿ‚?Õy̆"4ž¶=œC“(Þk(Ìøpii‰„3 Ñ’´Þ«W¯¶çw27nÜ(.p (ÈVÂʵÖ`‡§jÏ0¾åO?)ãƵ!FÒg Ü­æ‹lâß´±±ÄDå„ßK jÏ… ¦ØÚÚZ…óâ÷÷÷'õæO9®^ÆYÅ, yÐ~€Ö¤á™òl._d.Mbaañ’½“Âb0Åæççù¬ðÛ…h:íÙÄåÎMØêòå`o¸ûK  Ëzœ÷ |Yì-Åì°«úö1D.¾ÚågUwjà”ÅtH§@ÿ¡»Î\láSЋ.ÓtT¾Ñþú‹¸r i,ø”¨ÿaÅpg“ÊÙ»Ï~8Ys£|ž+k·!ƒœ8nÅüi_ÝE¸p¨RÓš\#†RY3@ 33mÊVÀÕ÷±s:ØÜ"±eéÂ| h0®5_¹b޶Tò…h4fü§÷{{{9+ô¼=Å(Ì&æTËß dt.ëV|òtmëqôtë€|#YìmS?!§9[IÚ.²2N‡žCÿ™¡êBwâª\¦­ ;¨¥)¦{ 6dsï Ø‰yjœæÜÜÔ¹a\ª©®™[D6zÁû/,,pÕˆˆî*®®.--y <¼9ÃÕçŠO› xbvÎNœIˆ¡BüBtnmmù <­u@0_ŠÅf"¼L¤Ñgé•Ó=Ò€Of‡ìÁdqk!¬S-³Ç„9†ºIÕr¤Œ3±þÓüc¸Ä_Wžš¥–ð† ¼pÁÑ?. çË&l˜ï‚í)c:ñ-ÚMÚ]Å££üÑ&k‰¬ŒhÿÃ3JšHCsâsºˆ.DïA¶º8ΗÒõÌ9£Òé× Jm–ÄYÛ µÁÑ6<] Ô#Éâö#eœGó"Õ89^*joo#Ç?"P®ŽpnxÄí·ßNVQ`"Ý#ˆ;Ñ¢¶·· ·®° i oÒ–PÒD œ„¨:Ýó±xÛm·¹î7)L´û{”!‘?âm!Ä,¾ÉäÍJŽ‹)2Aš4Éåkƒ1:=§s.&„ƒG¦˜p¹¸owÉ•+W¶¶¶XÕé³ C!¡¿ì˾̵¤ÕU{œȳº%*Ä8==¥—ÿÌŸù3aD Ó雨dW´ƒ0œ&*q. Œ«‰}Œó5Ô¤Œ ÁzqqGPSb4Iø‡ô÷L@»”Bô%»Œ¢qžõ¬g½ò•¯t™~AX'¸×÷„î M¼¹¹©ûZBz¥(hV—•ÿÄçæÍ›ËUü(¸ÍH‹©‘2b¶ôXÃéé)Ê•(\ísŒ'£?Âwé ŠÎ!½2^È2Ä,9¿KoÙÚÚȯ饌ÅÔH 1[ú­Œ7n˜%"— î¸,ûQMç~2$ é•i  ÑîíÍS{{{=ìHWa[ûuÛä‹”±˜)c!fË”±·Ù½\•L€.þd0¡Ü‚» ƒúÏ ½R zÑÖÖÖâèMék£¿…FœØ=»»»é0ô–¹¹9ú}fhOâK‹©¡I 1C†£Œ=È\‚ûÂè¯4}pÇhaÝ0£wÓåo&‹6 ½R 7nÜ8<<´~ˆ“~âÙýg†Ž4Øgð¥ŒÅÔÐqèG.#„hœ*cOÜíq Ü{‰ôŠh)c15RÆBÌ–!+c1@¤WDH‹©‘2b¶H‹A!½"š@ÊXL”±³EÊX éÑRÆbj¤Œ…˜-RÆbPH¯ŒçììŒØ,Ê e,¦†ö#e,¦æüüܼ˜šÏû¼Ï[__w!zùýÒ+ãÁ#,//›ÅÔH‹©ÁùÒ„\Fˆ ±ÿ.5G$¦cnnîE/z‘Ë0??ïzZ^מØì2b*¤ŒÅÔH‹2lnnêoJ¢ (²èep—^œBy¤ŒÅÔH‹2H—GAPd!e„àUð-ÛÛÛ8豓‘b ¬«rAñ |’®ûæOUÊ8“ËôÚŒJôˆúBR¿ƒàòèÑ©ºƒ`_!äÑÔið½ î¹ûYÄi&¸wCÓsp=tÎüN’ZBgJHÆÉâ hxŠª¾šFóò†ú¬µ·DÔÚy"¼w0•LeÊÖ‡vHkôj¸É<_Mt™úÁ·•‰Íö=¥±3í+Œ1è’T©WÃyT¨UGD²OKOÁèV—`8AÖRSì+8ªËâµö¾wÎËw"ž¤¦3m»2¦KØXa†Ý£I§â]!?8õ ^OZk3K“'•ŒÊÁÃrúTf%!jPÐêh{´@Úá¬4"½ow™fá«m<€š•ÿé4t@*Ð6ŽÈY›¥Ie‚·Aûø5õ, OA°dì+4lÆ`8Â\ÝÓ¨YÌ*¸#fl²¨¼ƒ·Ws™Ñst œ‹3͈Y9U±³³ƒk õ;S1Žé3ëë뇇‡Î4#fÕy<ô"â ½hVṋ“hu´½Yù\ƒÞ7+eìÁ q 0ÛªèˆÓÄ´¢Ù šY)c§O%à|¶··'œ+z¦‚}…˜Nmж›¼uỄ;5@=п*l¢-UÆ7nÜ`0s=gÌÜ)¸W+-ÀåÇa±¼%̼ó¸WÆ »»»./²AÖ@&iÚ Œ Ž„¡¦Î K†tù™2sel ‰QuTKÁ>¥ ˜dÒ ØWhH ™Z2JoIp?99¡•VÕÓÛ¨Œ¹Þx„–\uh‰S0踗Ɇ¦ç´çá–tÇÚ’˜ÝZvF¯œt™YÓe ÄfÄq{¼S ÁíàÀÛ3~h‰26PºE<³‚`ƒ`_±Gt\¦´*¸¯¯¯Wr[¸Ê˜Üž¨ »»»­ú ÎüüüØY‡ªÚGU´Ê‘o]F$ lÓÆÚ3¬âzµêêÀóXð<ø—i(‰™ßq)¢2Ûió­  ‚}¥m·­Zå ©êÇeJÐFeÌ¿UN¡màÆ:)”Ÿ¨Í‚ÚkÕ0·mP?sssíQÆm• û¹9à½[¥ŒÛF‘{¾ ‚ù ‚}…ॺ² f*™öj©2¦ÝëÚ§ÂøŒŽQD/--µjª»% øÖÖÖ¤Œs°‘ƒÄ_*''' ªœt´ÌSAS9E”±‚`ƒ`_áÜ\ vÊ<ꄚé³2Æ·®¬¬ð©‰+^Ig¯Ð*¢Œq¾ËËËíyÀ® R'ûûû8g h]´Ÿk×®Ñ[uz¶à‹¬NðKRÆ9P?hœŸœ{¨ŠK Î+FLû Á‹FÛÐà3„ÚX½›µÏÊÇ;À)p’|ܽ­¯¯#éìwÊEœ‚•¡Þ¨Lk.w¯æJðªöFg)ãL“@[¥µäò³âìò¥ÎH=Òx')ãðØ@‚Îg«Ä«|û>‡CûÁ ‘µg«²PŒ˜"ö ^4**ÿ<¶-õú5@à6è#>x•¤½ÊØÒœ*NaaaðŒ¼3Y3pDb.óêêj8:,â(ãëŠÁi~~Þæ*†ãa1ÔÍÉþ©!œþ”2Î!r.´CzZéá ±¨|—¾³³³ã;ˆFei‘MuYš>h³8„.Fƒz<‡CUpâ h3¾ãWÆ–V¤ýLû ÁË·%ÚMÅ'ã.º›Ù{gÊùÖíÙ6ïXz®Œ‰Á.s =#.ÆTN_#cœ ^€ÖLþ"˜1777Ö)Ð8¨"—A_¢%Q·T MäíŸJ65Œ3% á/5ÉÁÞDÊ8ZõUFZm’æ·²²‚ôé¥Jæ4MøÒƒ€nÆcƒs§€Ëˆx0ZˆË\Bè¢Þ|¯Ä³õR%ã[ønBU$OÓ„²Ëd  X>öª…–à2#¨ZM‚ÎE#üáµ0ºÕ}„hñÁóM¨ü¶Ë” ¥Ê˜Óæœ]þV¬v¼ƒ ÿЋhc߃ÓBèÛæ8esv:YçB# ¨TÆÎ’†àOñª6Þ¢Ù'_J_êb¬â\è ?§S$îRuD,)ã¨"ÚM1Ë·FáŸÖk¶‹±ŠÃÆ{püœ¯õNKÖ¹ÐMè5”qy‘€Ê¤úiãU8v¥Ú)‰;âBtn Å¹pØxN‡±î?h$rqÖE”±‚ +q+Ń`_¡Šè;Yƒ" ’*¢ýPŒ°HeÒ »8ˆâ\ð?§ã„9Á;§Œwù´T›n£:ò[?"êTTŠõ+Àw°¶ /É‘5õh‹85ÊpÕmH4Ö)PùÂÎqÙÑä_õãû·^Gú¢G=Škƒ² v`4¸t$.wV‡1¸ è9ÎŽblè¬"—›öCUS·ôDgÍ€fFˤþillEÅ’ …c¡¶í’µAñÐÎ9kê©GËÚüvÎZJÒ5¨Î*X uK­:kÖ©i'ˆ< #wLç!.ÙܼbÖð¾a8 †KÏ™Fy ²8vÖæ·s à|(IãáÔœ5Ê(&™(ö* jw$èAΚ‘Ž@1B$Μ ÙÊ´2ØåÈ—Í`mìÀ’R¿ß†©Šqvì„¶á¬%h©26ÇAup’\T®®­ ½Å×/-€.g Hæv S~! mÎmp‰9MþìÛmð¡¬-â<œ‘ƒz ZÌq Vg„²m‹NÍ *ŸÃfœ……0%jxõRP ¹Òæ4 º„}Á2üv[K“(~Ft*B/ûáÒ›+a‡¶J$¡åpÅIÐ|½mr®#-œkÄVvÉhÿT8û´¬wÍ@¯±Kï)xYýHÉC_v; ¾ìÛ鉤ÍÏ募ÝcæøÍ/ñI‡²U"‰Õ0 *™:'\Qc}‚ÅH¼ {0ñd` qq9/½¨Á¥¹¸ö/«”BLC¦ÔÁ"´ÿvì¬5œ/J<£‘[󳡻p9ø2 ‚ÀMû •ɧiAê6Y|äƒwõíÜ.‡…W‘‚÷`í<Âí(? ±Nm˜®Âzøí¶–6o=¥”¤¨6ä{i¤Ýº´wÎØeFŽÐǨAì\Ô‚6‰»>WÈc e,xL·Á%æ4 ŽÓ¾«x“àzÓ.ù.s¤KŠ8Êø¶EsÁ áÙ1╨ςQ*Âk‘, â1-2W: 5敹â~hQ>’qú|»¥EZ½ÀeFbÅ­‘DñI»µ š*A<^ åãGJž¬ÛÔ­ˆÏወ¡.¡7aw‘€ú§#»Ì([5âÆ©º±¾+ öc4uüì¡¡º&’ >Ámp ‡çv ØŠGè~Âá¹ýƒ1"“¶žE¥0¸Ó*0Zº -UÆ8—¹„3Ç›àè´4z`ÑN̘3åzsá“~™ OËëh´?—¹„­Ø?~†‹þ ¥N­’[ †§RK|’~™ÎÏZ— ¬%ÛÚ -‡öC+*£’Û N†¶A¿£÷™ Æç$}7 \F$ â¢#ÇBM½áš€D•ÜNL ‡AŠæäÖ]B=°¶ˆ2V,û çžl?´=²|ôAB¡[× è8È?GÚI² P3¸—)AK•1çF—pù¡êˆúÂBSèÎã,lò•±y2s8Ód06ØÄböX§@²Ïÿ‹¦¡#™JÆÉÚ|~¶[Z§ic\<)5SÄ)°Êº–Ë‹´. Kɘdx'eí–OÚ0ÕŽ‘&ê µ΂ƀß0o‹'15ŒÃÍ:q ° ;— h 4j2ËÓÀpMæç©s*¿Gµs9²\_ ±i6¼( •S0Gš¯V9;<-År<³¡ X>ö sÔ›Ë'  P¥”±ú¤'’¥JsÜZ AŠÐ•8rë2~:†³s%pŽ”¤ã¸| ZªŒq8JN’SuÖ ’îÉÜ„µ`-Ìp`m79#Æb°¹Î®¸åøñ´rü¾ˆ2¦$5É×ñíΚAÒ=qllÈæ­- ƒ–M‚ã± MULÚí©4Nm9¶uV‘€Š²6FõÒêÆöÈ…Q·´"ê™m1Òò¹v3q™‹¬ à%ð$hNÖÂÇž&mŒ ±3¢k8«H`õLuÑŠè³ùBÎHåSŒÖÂå@ñ˜V6ßhû¡>ÊmÐ8|5`-86ð¦ e-çH…PÒœ[‘‚`Ž¢ 8ìùe/{YŽèÍ+^ñŠòe¦†z³!×wlԣ퇦°šÔ?M# ÌÚÆ¬FS|¯GÂñ˜bñA_‘jÛý9M_!´ g-A{•1 ªƒSå<éóÅ»¹ k @Ež‚º¶ê6 ÐE­ Pûv…&·èvüæ¢oä¤(`1x"•À‰°[öFÓ±æ[Ä)ø2|ßNSã8‹{F ¡#"§czÂðÑ ¼€žëWÜíýòÁ²¦´,õœ'¡R'ø_3²OKˆ$´ï\¸4´=ZàØðb0tvÁb¶aŽÏ°Ë:ô\·‹[ŸzO~£•¡ °Uñ¾@m橇hNœ”­I¬žI˜”¡þ ÃuØð²šC°` × sísÔ&ÆK^0eiøo$AÖ·UÛª¸’ 0»¥8ZN ‹p9(†L'…=á~!GûÜç>—ªpÖ[¡b-*•,Söl ª…î@ÝÒT¬ižè/mÒ®aÝÚ Ô Õ®çTûè:O—ÛíåÖ'Úí½œ°¾Ì‰°ÉXì±àN PVÕað*C«•±Á©Ò{9[Nžºãü‹{¥$³¬k66ö2,èÅCoZ5@=ÐRi ^ÏEœBT†šwæ€Í1¹“CåÛ ×ÂyÆOñ ’áŠ÷ö$ôú3µGR“‘‡òÎE$I:Z ¯I\jqÏ•Äb¶‘%J&"kx6Qè°FÎñ9Tm;ìMÖ7]F$°ká2£èE›±š¤§ÓÁËøF‹ë† ½ Ú§k’5<+ÙÈiÛÞå²ÛЕE.•¨Œ‚àApRhZÏ{Þó ÏzÖ³~üÇÜYo…“âxÊ—)C¼¸š¾–èeü­ttÝ.ðÃ*ðz ²†^%0íÖ‡¤ð”“Ák:: Œ=4\*—ÞË8ÁÕ¢·”ñ_íÁ"±)9œéÒÒ’UBª4,â²Êð-&aé]Öji¬eüW{°^µº·[}+‘s!YÎ…¦MiŸæˆM‰–ñqíOBk¡Í¬Œ~bHï -ásÜê Ø.#\ÄÀ@{hWI—ZÃ]„ðŒæ¤è;œ ‚›lªkÍ p!Ye"Ap èûˆã/þâ/Ή‰ù/ÿe¼_ù2S“¼’î‹úL­½ÎA5Ú4œIÎé’ܳ‚פtI‡à èBxúÕ” ý©¾ö» søM©xkʦäÆö¥"N¡HŽeƒ“õ€è1­SG®ÓÁ´*#Ç  kcµš”qEœ õO[µLã¡ ‘ k³ní×Êx sµø ¼íOBH¦EtÁ;á£\F$ VÁe2 þ­‘wT>ŸT>[Ѩ:¡•i$HRÿécþ““ëx‹8AW.ƒš”1P«cÝW¿’2Ó16xYÝÒ¨O©XZ ööke®>lj¡Cqä_èhœvW(ƒ"Á«]UÆI?e­0k©S¨©±&Á7Ù7r9NŠƒ±\R†V¥ŒC¬/…~Šãd'¤­E‹2cuC…à¤.jðò·SÄNŽ'¼¾S÷ö±ÎeÈLá\’¾ŒFî¦åsí¬©Cc1Ûš4˜g¬mÓŠ¬a³vÒ&m’ÎeD«m—)=÷ÂVôqÓÊæ$>ŽÝF\ÐX\·& |5` 86(#C‹8Á|& p}‚Ús©b$g‘Ø-„´ GfL™&¥4W¾Ñ¦'À‚;—•c+3Ï2EðJ¥'Ê8‰µ°zÇ‹Q×`1Û ‹š¸ VrRüžÁ‚®a-lÏœS‰¬,â*qìcŽÚ®ù5ÃG/ðz lφõ[äX1“æ•H+vîR"AUÎÅ6´|®¸Àb¶aÐ01~ÏÚ8ýž9˜4'ጤŒs°Úv™iñ":{óÂ4Õ?€ è)ð{†ÔÆIŠyi5)ã$‚}…Šu©ؠņ£@øæ2Ù ÔðÃ*ðz lÏF²áyÙ`Á½Ñ"e\µ6¨?‡òA·)ã±øè^@Oõ £™9!z¦K‰U9—±øi9ð3»“âg£¡’ ;)ã|캸Lø{J`z ¼ä…fnh4¦Œ‹Ðã ØWš ^~X^@OÍF ܬ2.íQÆÝEÊ8‡Æ”qG‘2ÎÇ"¢Ëˆ­RÆEÊX¤"e<\¤ŒË#ç’ƒ”q>RÆùHç#e\)c‘J¯”1'síÚ5|=Œò_ðþ¯íímD3wØ»‚”ñ]¾ÐFeO£†Ï9±Ö•RÆ ÎÏÏ÷÷÷ýÍx§?ûgÿ¬¥ñZø®fî·–ÓÓS¼´ý¾þÜŸûsÏyÎs,½µµµ7É )ã) ‹íîîúšqà/xÁ ,?88hàNý¬¸1zU_ø;{ÂÞ‚×a &Ò]ú Œq—\W®1gb xô,ÊÅ“¦œž¥M1Ï^ž*OaHçøø˜Øl-s"4*{Õ°³Ç£YKZÛpÈH{ð9xž¹¹9B² ™úñOØSßµ4‚†4( H%àŸ}ß!muB%ø‡‘ÅÖñóÐH‡¾FÿÂÑ¿šúšYe¿!ú‘@'E"Û†=Aìûçh¿Æ±àeOØ[ðBsî ^FUÁk6ÊÿȵXÂ5vÖ\h\{ιOM:ЍÞ"eú §»d„]pÊŠ2„"Â<[l“}¥*çÒið3$|NA‡CœÆ›QoÄï!è?º'‹j)éS6„@÷xz¯RÆEÀ!ûS$–Ù¼7ݶë‘ó¥p"ƒÇ‚(xU¼f Œi¾¦‰§ˆ&wh1C¾}YDõ)ÓWp‘6Í0Ý Ê$ƒõÁÆïªœKGáô§ Ñfvww©=Bš3õŽããcNR$fGx¹3äù-)ã± †§DÑmM#NÑ>ÛCkÎ}º«O·"xmll öæLUÁ«ieÌU粕l²4ý•••Á>^Ã…—2΀OÄ¥ºü´ lhcõ­%²2Fö•Ÿs¢"Áå{„MM•ô-lÎNz“Á˜,L„„k ËzŠÜôƒ0›µ*²çPŸs›ˆ¬‡:¢ ³Y«"{9?@Œ.Oç$B0& $†eÃÏ ©êúV|Xž¬ßšDW1Ìf­ŠìùŒU„É«•“Ò©$ך%$ÕXž±çEÊL?¯" Jr­YBRFÖ/¢f³VEöZ2åP+uM‹$€t*ɵf‰Hµ×='šEÖ\QÛÈ ¢uÖOþ£Å@:•äZ³„x£ð˜±$3QƆ?…" Jr­YB’ÆÈRëM¿R¶aòâw™[³Y«"{c»¯™" Jr­Y ŸM®*OUÁ«âÃòd€è*†Ù¬U‘¢lH–" /Fñ„ÆÐn$-!ùk§¦ˆê-RfRüéL”€Ðڤœ³ ²~ 5˜0›µ*²C”õTÕñÚLMçè¯æD ¡ÝHZ"R äœcÔÂlÖªÈQÖ“åÛFÖá :Ù0›µ*²C”õ$¡¿|% 4†v#i ™´üD4¯ŒýÁO”€ÐÚ¤%dlù¬sŒZK˜ÍZÙ!ʆäv3Ô:­e=YNÏWÈD ¡ÝHZ om”^fjª ^–‡O»w™€è*†Ù¬U‘’ÏBÆ;Yì€Ë_Z|Âð«,f7’ÙÐbYÙF8Supá¹ü.“A‘2“âÎ'Q9>aøU–³Élh±¬áL#œéVŠü‚ ÂlÖªÈI‹‘3—Öªr.îZ&†O~•%ÀìF2Z,k8ӭƬUÑ¥³Y«";$-FW~Á™so$:µ0›µ*²CÒb$ƒ]DpùK‹O~•%ÀìF2Z,k8ÓgeË0el¸ü¥Å' ¿Ê`v#™ -–5œiD˜µµž¬QbÔ6ÂlÖªÈI‹‘s³¨IêD#ómN—¸ü¥Å' ¿Ê`v#™ -–5|6¹ª8qÚ=wç^ŸøëÇ„M}ñµëÆâ0À¦CMôŒ~|øÔ4»Aø£k·üXM?B¨uLÛ*¸Ê 08ÙªÔNCÊhÕÞæzwñG-“Bã&²...r᧸ê8â……Â~Ù™z íñbµâئN[îUëƒ*¥íÑ~Z´Cgí)xTHµÃHF³½¹'ιàF*&±+ÚUïRÂ{0:¯¯¯OáIph¸5¼ž¼ßcTήòir*¼C <-ÇYmŒæ¬Ùgïƒ~¿DªvÞ½9e 8Áª|« †zü´§†(Yc-°÷÷÷q:óóóÄiÒÕ†ÿö`ƒ¥ª|+U¹Tê"Ô*UjM‘6‰'êk„¶P%qÔd_Ï~UVa¨¶«¶Ú~Q‹&¦×”(ì ×'€Ri}¦VÛk*ìÎ @ FW%ïÙ=–ÅÔ'HSAãFê¸éÔ¨2ÎÆJ“uù©xË[Þò_ñèìž9Ô0'Eƒ¾rå Ÿ8ÖjOötýúu`¡rhX=SÉœà¯þê¯~Ý×}ÝK_úÒ2çÅ~¨œjgÈz­‘6‰'𛛣÷O%HÞøÆ7þ¥¿ô—øt¦©°Ùš^Ê>S%'·h68ðW¾ò•]Q-15vjO¾†ÇÆ5á½íN`ÿT2AðÛ¾íÛ^ò’—”9/*Š QÕ(®I K¯W¿úÕ/~ñ‹ÿõ¿þפµûP'Öøm2D´Û õ]ߦ•1à,ðœØMŒSø®ïú®Ûn»Í„‰H{TƒNÕ¡AG‹N5)¼¸¸h'bgáJÔ •OÃ2•L;ã«é“ Xùö®t'ÜGËY8~;vÿÄOüUJeNz.쓭ض—²¦BB@uYÓ¥=7Óz+_ÁÑrØxŽŸȉþÅ_üÅ—¿ü大˜Š`Ÿô)|wÉÁ›áéeøð)®5ÕBUÓ7}Ó[ßúVEµ³+* bîÎ#8ljßHcÃE/zñáœg4…ôa‡ìŠ ÑÀU¨ÇOï˜"xQol‹2&A³$ ©RjƒˆFmw¥N8wŽ–³ 8~; 63œž26LãrñhÇc[îÒº å“®3µw^£5p¥›…ŽÍWã+9àøÃƒl‰/K¶B¸¨ÁQâMñÌj‚‡+η[;0ú šŒW‰FȇzKêßY³a”¤<[Mê’­—Ú£ÞB‰IÝ|qÁFЇ³jä¨1¾?À‘X £5oWúŽGŒÏ¡ÀX¹Fƒa'”gŸSèé.b-ˆß{eé›Ô¿•OV5»¢Ò¨gŒé<ŠQØÜ£õn˜IP7ßÃ%æÀLó‰?Ljkš•oôp„ý ‚œŨsšÇØ«Ï% ZQ~:=Ý*¼Æå\h]Κ µ‡g£|RO[åÓzi Ô6AÓ´2Øå n)0«)Wo·îv`^p:ÔšÈ3SÆWkoµ`UcQ ¨²´u|%PGÅ/žíÁb¡5 æv S~!ÖPÆb‡bNÓ ñÙwY+ôóeÛYI„âDñ€kÄYX<“¤†?µSBcq¥Ìi´ û:®>Y B`Ûô}œÍ€ƒdoæ‚ÁÏ{jß)Ùu—Úháa SÿT¯ÅBð®è5VÆã•P>~¤ä¡/»Ž˜}X7‰`~ÜŽŸÃVæ‚ÌWøÃ3!¾uá:ÜfƒQk!ç`µákØÜ/Pÿ\qê£Ûl6˜±ö—·c”ï2 ðíö-Aòñ‡çñ‘¸Üö]æÀ®¾E‡™ëà"„§ÙÝ ˆFä`Ø-Âû x–åì¬Ò7Û[‹Cð¢1øÂàeµ/«^®EÁàeZÙמ]²dƒsClÛ|üá…X0¢¾ì¯¬mÞ¼N2ceì¡:¬j¸ÒVwæòhë•fÌí¦üB¾CÒ#‡Þ¤ß oåγ@ÈÁ•°noTÛ=ÌËØ·ØáyO½ ]q8x× ô» žPàæôÚ¡¤¦»/«¼ù x>VÑ\¡ƒX´ÚÀ1ZýØP*¿6Ïd$Å™ ùøÃó "ÝNg7…Ö ‚&‘í[üáY– ×'Aœ$ ^6y¯jë™ïr×/mö¬æóñ‡‚ŸtûmÿCÿñ?þÿIqÃf\“íIEND®B`‚linux-nvme-nvme-stas-a8026bb/doc/man.xsl000066400000000000000000000021751440613556600202240ustar00rootroot00000000000000 .TH " " " " "" "nvme-stas " " " linux-nvme-nvme-stas-a8026bb/doc/meson.build000066400000000000000000000101231440613556600210530ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # if want_man or want_html or want_readthedocs docbklst = find_program('genlist-from-docbooks.py') dbus2doc = find_program('dbus-idl-to-docbooks.py') dbusgen = find_program('gdbus-codegen', required: false) # Needed by dbus2doc if not dbusgen.found() error('gdbus-codegen missing: Install libglib2.0-dev (deb) / glib2-devel (rpm)') endif # Get the list of DocBook files to process. The result will # be saved to variable docbooks as a list of tuples as follows: # docbooks = [ ['file1', 'manvolnum-from-file1.xml', 'file1.xml'], # ['file2', 'manvolnum-from-file2.xml', 'file2.xml'], ... ] docbooks = [] rr = run_command(docbklst, check: true) output = rr.stdout().strip() if output != '' foreach item : output.split(';') items = item.split(',') stem = items[0] manvolnum = items[1] fname = items[2] deps = items[3] if deps == 'None' deps = [] else deps = deps.split(':') endif docbooks += [ [stem, manvolnum, fname, deps] ] endforeach endif # Generate DocBooks from IDL queried directly from the D-Bus services. out_dir = conf.get('BUILD_DIR') / 'man-tmp' env = environment({'PYTHONPATH': PYTHONPATH}) idls = [ 'stafd.idl', 'stacd.idl' ] foreach idl : idls rr = run_command( dbus2doc, '--idl', conf.get('BUILD_DIR') / 'staslib' / idl, '--output-directory', out_dir, '--tmp', meson.current_build_dir(), env: env, check: true) output = rr.stdout().strip() if output != '' foreach stem : output.split(';') docbooks += [ [stem, '5', out_dir / stem + '.xml', []] ] endforeach endif endforeach xsltproc = find_program('xsltproc') if xsltproc.found() manpage_style = 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl' if run_command(xsltproc, '--nonet', manpage_style, check: false).returncode() != 0 error('Docbook style sheet missing: Install docbook-xsl (deb) / docbook-style-xsl (rpm)') endif endif xslt_cmd = [ xsltproc, '--nonet', '--xinclude', '--stringparam', 'man.output.quietly', '1', '--stringparam', 'funcsynopsis.style', 'ansi', '--stringparam', 'man.th.extra1.suppress', '1', '--stringparam', 'man.authors.section.enabled', '0', '--stringparam', 'man.copyright.section.enabled', '0', '--stringparam', 'nvme-stas.version', '@0@'.format(meson.project_version()), '-o', '@OUTPUT@', ] man_xsl = files('man.xsl') html_xsl = files('html.xsl') html_files = [] # Will be used as input to readthedocs foreach tuple: docbooks stem = tuple[0] sect = tuple[1] file = files(tuple[2]) deps = tuple[3] if want_man man = stem + '.' + sect custom_target( man, input: file, output: man, depend_files: deps, command: xslt_cmd + [man_xsl, '@INPUT@'], install: true, install_dir: mandir / ('man' + sect) ) endif if want_html or want_readthedocs html = stem + '.html' html_file = custom_target( html, input: file, output: html, depend_files: deps, command: xslt_cmd + [html_xsl, '@INPUT@'], install: want_html, install_dir: docdir / 'html' ) html_files += [ [stem, html_file ] ] endif endforeach endif if want_readthedocs subdir('readthedocs') endif linux-nvme-nvme-stas-a8026bb/doc/nvme-stas.xml000066400000000000000000000163541440613556600213640ustar00rootroot00000000000000 nvme-stas nvme-stas Mr Martin Belanger Dell, Inc. nvme-stas 7 nvme-stas NVMe over Fabrics STorage Appliance Services Introduction This page describes the services provided by the nvme-stas package. nvme-stas is composed of two services, stafd8 and stacd8, running on a host computer (the NVMe Host). STorage Appliance Finder (<code>stafd</code>) The tasks performed by stafd include: Register for mDNS service type _nvme-disc._tcp with Avahi, the service discovery daemon. This allows stafd to automatically locate Central or Direct Discovery Controllers (CDC, DDC) with zero-configuration networking (zeroconf). stafd also allows users to manually enter CDCs and DDCs in a configuration file (/etc/stas/stafd.conf) when users prefer not to enable mDNS-based zeroconf. Connect to discovered or configured CDCs or DDCs. Retrieve the list of NVMe subsystem IO Controllers or Discovery Controller referrals from the Discovery Log Page using the NVMe command "Get Log Page". Maintain a cache of the discovery log pages. Provide a D-Bus API where other applications can interact with stafd. This API can be used, for example, to retrieve the list of cached discovery log pages. STorage Appliance Connector (<code>stacd</code>) The tasks performed by stacd include: Read the list of storage subsystems (i.e., discovery log pages) from stafd over the D-Bus API. Similar to stafd, stacd can also read a list of storage subsystems to connect to from a configuration file: (/etc/stas/stacd.conf). Set up the I/O controller connections to each storage subsystem. Provide a D-Bus API where other applications can interact with stacd. For example, an application could retrieve the list of I/O controllers that stacd connected to. System configuration A host must be provided with a Host NQN and a Host ID. nvme-stas will not run without these two mandatory configuration parameters. To follow in the footsteps of nvme-cli and libnvme, nvme-stas will use the same Host NQN and ID that nvme-cli and libnvme use by default. In other words, nvme-stas will read the Host NQN and ID from these two files by default: /etc/nvme/hostnqn /etc/nvme/hostid Using the same configuration files will ensure consistency between nvme-stas, nvme-cli, and libnvme. On the other hand, nvme-stas can operate with a different Host NQN and/or ID. In that case, one can specify them in /etc/stas/sys.conf. A new optional configuration parameters introduced in TP8010, the Host Symbolic Name, can also be specified in /etc/stas/sys.conf. The documentation for /etc/stas/sys.conf can be found /etc/stas/sys.conf.doc. See Also stacctl1, stacd.conf5, stacd.service8, stacd8, stafctl1, stafd.conf5, stafd.service8, stafd8, linux-nvme-nvme-stas-a8026bb/doc/readthedocs/000077500000000000000000000000001440613556600212015ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/doc/readthedocs/Makefile000066400000000000000000000012201440613556600226340ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # .DEFAULT_GOAL := help # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". .PHONY: help help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). .PHONY: Makefile %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) linux-nvme-nvme-stas-a8026bb/doc/readthedocs/conf.py000066400000000000000000000022151440613556600225000ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- project = 'nvme-stas' copyright = 'Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved.' author = 'Martin Belanger ' master_doc = 'index' version = '@VERSION@' release = '@VERSION@' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autosummary', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['Thumbs.db', '.DS_Store'] linux-nvme-nvme-stas-a8026bb/doc/readthedocs/environment.txt000066400000000000000000000000161440613556600243030ustar00rootroot00000000000000sphinx==5.3.0 linux-nvme-nvme-stas-a8026bb/doc/readthedocs/index.rst000066400000000000000000000012051440613556600230400ustar00rootroot00000000000000Welcome to nvme-stas's documentation! ===================================== What does nvme-stas provide? * A Central Discovery Controller (CDC) client for Linux * Asynchronous Event Notifications (AEN) handling * Automated NVMe subsystem connection controls * Error handling and reporting * Automatic (zeroconf) and Manual configuration .. toctree:: :maxdepth: 2 :caption: Contents: installation.rst nvme-stas.rst stafd-index.rst stacd-index.rst stasadm.rst sys.conf.rst stas-config.target.rst stas-config@.service.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` linux-nvme-nvme-stas-a8026bb/doc/readthedocs/installation.rst000066400000000000000000000015161440613556600244370ustar00rootroot00000000000000Installation ============ Debian / Ubuntu: ---------------- .. code-block:: sh $ apt-get install nvme-stas Fedora / Red Hat: ----------------- .. code-block:: sh $ dnf install nvme-stas Python Version -------------- The latest Python 3 version is always recommended, since it has all the latest bells and whistles. libnvme supports Python 3.6 and above. Dependencies ------------ nvme-stas is built on top of libnvme, which is used to interact with the kernel's NVMe driver (i.e. drivers/nvme/host/). To support all the features of nvme-stas, several changes to the Linux kernel are required. nvme-stas can also operate with older kernels, but with limited functionality. Kernel 5.18 provides all the features needed by nvme-stas. nvme-stas can also work with older kernels that include back-ported changes to the NVMe driver. linux-nvme-nvme-stas-a8026bb/doc/readthedocs/make.bat000066400000000000000000000014401440613556600226050ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd linux-nvme-nvme-stas-a8026bb/doc/readthedocs/meson.build000066400000000000000000000027001440613556600233420ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # pandoc = find_program('pandoc', required: true) components = [ 'conf.py', 'Makefile', 'make.bat', 'index.rst', 'environment.txt', 'installation.rst', 'nvme-stas.rst', 'org.nvmexpress.stac.debug.rst', 'org.nvmexpress.stac.rst', 'org.nvmexpress.staf.debug.rst', 'org.nvmexpress.staf.rst', 'stacctl.rst', 'stacd-index.rst', 'stacd.conf.rst', 'stacd.rst', 'stacd.service.rst', 'stafctl.rst', 'stafd-index.rst', 'stafd.conf.rst', 'stafd.rst', 'stafd.service.rst', 'stas-config.target.rst', 'stas-config@.service.rst', 'stasadm.rst', 'sys.conf.rst', ] foreach component : components configure_file( input: component, output: component, configuration: conf, ) endforeach foreach tuple: html_files stem = tuple[0] html_file = tuple[1] rst = '_' + stem + '.rst' custom_target( rst, input: html_file, output: rst, build_by_default: true, command: [ pandoc, '-f', 'html', '-t', 'rst', '-o', '@OUTPUT@', '@INPUT@' ] ) endforeach linux-nvme-nvme-stas-a8026bb/doc/readthedocs/nvme-stas.rst000066400000000000000000000001561440613556600236520ustar00rootroot00000000000000========================== STorage Appliance Services ========================== .. include:: _nvme-stas.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/org.nvmexpress.stac.debug.rst000066400000000000000000000002421440613556600267470ustar00rootroot00000000000000========================= org.nvmexpress.stac.debug ========================= .. module:: org.nvmexpress.stac.debug .. include:: _org.nvmexpress.stac.debug.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/org.nvmexpress.stac.rst000066400000000000000000000002041440613556600256600ustar00rootroot00000000000000=================== org.nvmexpress.stac =================== .. module:: org.nvmexpress.stac .. include:: _org.nvmexpress.stac.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/org.nvmexpress.staf.debug.rst000066400000000000000000000002421440613556600267520ustar00rootroot00000000000000========================= org.nvmexpress.staf.debug ========================= .. module:: org.nvmexpress.staf.debug .. include:: _org.nvmexpress.staf.debug.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/org.nvmexpress.staf.rst000066400000000000000000000002041440613556600256630ustar00rootroot00000000000000=================== org.nvmexpress.staf =================== .. module:: org.nvmexpress.staf .. include:: _org.nvmexpress.staf.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stacctl.rst000066400000000000000000000001101440613556600233600ustar00rootroot00000000000000======= stacctl ======= .. module:: stacctl .. include:: _stacctl.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stacd-index.rst000066400000000000000000000003271440613556600241400ustar00rootroot00000000000000STorage Appliance Connector =========================== .. toctree:: :maxdepth: 1 stacd.rst stacd.conf.rst stacd.service.rst stacctl.rst org.nvmexpress.stac.rst org.nvmexpress.stac.debug.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stacd.conf.rst000066400000000000000000000001271440613556600237550ustar00rootroot00000000000000========== stacd.conf ========== .. module:: stacd.conf .. include:: _stacd.conf.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stacd.rst000066400000000000000000000000771440613556600230350ustar00rootroot00000000000000===== stacd ===== .. module:: stacd .. include:: _stacd.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stacd.service.rst000066400000000000000000000001461440613556600244710ustar00rootroot00000000000000============= stacd.service ============= .. module:: stacd.service .. include:: _stacd.service.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stafctl.rst000066400000000000000000000001101440613556600233630ustar00rootroot00000000000000======= stafctl ======= .. module:: stafctl .. include:: _stafctl.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stafd-index.rst000066400000000000000000000003211440613556600241350ustar00rootroot00000000000000STorage Appliance Finder ======================== .. toctree:: :maxdepth: 1 stafd.rst stafd.conf.rst stafd.service.rst stafctl.rst org.nvmexpress.staf.rst org.nvmexpress.staf.debug.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stafd.conf.rst000066400000000000000000000001271440613556600237600ustar00rootroot00000000000000========== stafd.conf ========== .. module:: stafd.conf .. include:: _stafd.conf.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stafd.rst000066400000000000000000000000761440613556600230370ustar00rootroot00000000000000===== stafd ===== .. module:: stafd .. include:: _stafd.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stafd.service.rst000066400000000000000000000001461440613556600244740ustar00rootroot00000000000000============= stafd.service ============= .. module:: stafd.service .. include:: _stafd.service.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stas-config.target.rst000066400000000000000000000001771440613556600254420ustar00rootroot00000000000000================== stas-config.target ================== .. module:: stas-config.target .. include:: _stas-config.target.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stas-config@.service.rst000066400000000000000000000002111440613556600257010ustar00rootroot00000000000000==================== stas-config@.service ==================== .. module:: stas-config@.service .. include:: _stas-config@.service.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/stasadm.rst000066400000000000000000000001101440613556600233570ustar00rootroot00000000000000======= stasadm ======= .. module:: stasadm .. include:: _stasadm.rst linux-nvme-nvme-stas-a8026bb/doc/readthedocs/sys.conf.rst000066400000000000000000000001151440613556600234720ustar00rootroot00000000000000======== sys.conf ======== .. module:: sys.conf .. include:: _sys.conf.rst linux-nvme-nvme-stas-a8026bb/doc/stacctl.xml000066400000000000000000000177761440613556600211150ustar00rootroot00000000000000 stacctl nvme-stas Mr Martin Belanger Dell, Inc. stacctl 1 stacctl STorage Appliance Connector (STAC) utility program stacctl OPTIONS COMMAND OPTIONS Description stacctl is a tool that can be used to communicate with the stacd 8 daemon to retrieve operational data. Commands The following commands are understood: ls Show the list of I/O controllers. This will list all the I/O controllers configured in stacd.conf 5 as well as those discovered by the stafd 8 daemon. Options The following options are understood: Exit status On success, 0 is returned; otherwise, a non-zero failure code is returned. Examples List I/O controllers $ stacctl ls --detailed [{'connect attempts': 0, 'device': 'nvme1', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': '8d22fa96da912fb13f5a', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aedead', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'connect attempts': 0, 'device': 'nvme2', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': 'a9987ae2fd173d100fd0', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aebeef', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'connect attempts': 0, 'device': 'nvme3', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': '13e122f1a8122bed5a8d', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}] Disable tracing $ stacctl troff Show <citerefentry><refentrytitle>stacd</refentrytitle> <manvolnum>8</manvolnum></citerefentry> operational status. $ stacctl status {'config soak timer': '1.5s [off]', 'controllers': [{'connect attempts': 0, 'device': 'nvme1', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': '8d22fa96da912fb13f5a', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aedead', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'connect attempts': 0, 'device': 'nvme2', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': 'a9987ae2fd173d100fd0', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aebeef', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}, {'connect attempts': 0, 'device': 'nvme3', 'host-iface': '', 'host-traddr': '', 'hostid': '3e518ec3-72ec-46a5-a603-2510e3140e29', 'hostnqn': 'nqn.2014-08.org.nvmexpress:uuid:13730573-e8d7-446e-81f6-042a497846d5', 'model': 'Linux', 'retry connect timer': '60.0s [off]', 'serial': '13e122f1a8122bed5a8d', 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}], 'log-level': 'DEBUG', 'tron': True} See Also stacd.conf 5 , stacd 8 linux-nvme-nvme-stas-a8026bb/doc/stacd.conf.xml000066400000000000000000001016501440613556600214630ustar00rootroot00000000000000 stacd.conf nvme-stas Mr Martin Belanger Dell, Inc. stacd.conf 5 stacd.conf stacd 8 configuration file /etc/stas/stacd.conf Description When stacd 8 starts up, it reads its configuration from stacd.conf. Configuration File Format stacd.conf is a plain text file divided into sections, with configuration entries in the style key=value. Spaces immediately before or after the = are ignored. Empty lines are ignored as well as lines starting with #, which may be used for commenting. Options [Global] section The following options are available in the [Global] section: nr-io-queues= Takes a value in the range 1...N. Overrides the default number of I/O queues create by the driver. Note: This parameter is identical to that provided by nvme-cli. Default: Depends on kernel and other run time factors (e.g. number of CPUs). nr-write-queues= Takes a value in the range 1...N. Adds additional queues that will be used for write I/O. Note: This parameter is identical to that provided by nvme-cli. Default: Depends on kernel and other run time factors (e.g. number of CPUs). nr-poll-queues= Takes a value in the range 1...N. Adds additional queues that will be used for polling latency sensitive I/O. Note: This parameter is identical to that provided by nvme-cli. Default: Depends on kernel and other run time factors (e.g. number of CPUs). ignore-iface= Takes a boolean argument. This option controls how connections with I/O Controllers (IOC) are made. There is no guarantee that there will be a route to reach that IOC. However, we can use the socket option SO_BINDTODEVICE to force the connection to be made on a specific interface instead of letting the routing tables decide where to make the connection. This option determines whether stacd will use SO_BINDTODEVICE to force connections on an interface or just rely on the routing tables. The default is to use SO_BINDTODEVICE, in other words, stacd does not ignore the interface. BACKGROUND: By default, stacd will connect to IOCs on the same interface that was used to retrieve the discovery log pages. If stafd discovers a DC on an interface using mDNS, and stafd connects to that DC and retrieves the log pages, it is expected that the storage subsystems listed in the log pages are reachable on the same interface where the DC was discovered. For example, let's say a DC is discovered on interface ens102. Then all the subsystems listed in the log pages retrieved from that DC must be reachable on interface ens102. If this doesn't work, for example you cannot "ping -I ens102 [storage-ip]", then the most likely explanation is that proxy arp is not enabled on the switch that the host is connected to on interface ens102. Whatever you do, resist the temptation to manually set up the routing tables or to add alternate routes going over a different interface than the one where the DC is located. That simply won't work. Make sure proxy arp is enabled on the switch first. Setting routes won't work because, by default, stacd uses the SO_BINDTODEVICE socket option when it connects to IOCs. This option is used to force a socket connection to be made on a specific interface instead of letting the routing tables decide where to connect the socket. Even if you were to manually configure an alternate route on a different interface, the connections (i.e. host to IOC) will still be made on the interface where the DC was discovered by stafd. Defaults to false. [I/O controller connection management] section Connectivity between hosts and subsystems in a fabric is controlled by Fabric Zoning. Entities that share a common zone (i.e., are zoned together) are allowed to discover each other and establish connections between them. Fabric Zoning is configured on Discovery Controllers (DC). Users can add/remove controllers and/or hosts to/from zones. Hosts have no direct knowledge of the Fabric Zoning configuration that is active on a given DC. As a result, if a host is impacted by a Fabric Zoning configuration change, it will be notified of the connectivity configuration change by the DC via Asynchronous Event Notifications (AEN). List of terms used in this section: Term Description AEN Asynchronous Event Notification. A CQE (Completion Queue Entry) for an Asynchronous Event Request that was previously transmitted by the host to a Discovery Controller. AENs are used by DCs to notify hosts that a change (e.g., a connectivity configuration change) has occurred. DC Discovery Controller. DLP Discovery Log Page. A host will issue a Get Log Page command to retrieve the list of controllers it may connect to. DLPE Discovery Log Page Entry. The response to a Get Log Page command contains a list of DLPEs identifying each controller that the host is allowed to connect with. Note that DLPEs may contain both I/O Controllers (IOCs) and Discovery Controllers (DCs). DCs listed in DLPEs are called referrals. stacd only deals with IOCs. Referrals (DCs) are handled by stafd. IOC I/O Controller. Manual Config Refers to manually adding entries to stacd.conf with the controller= parameter. Automatic Config Refers to receiving configuration from a DC as DLPEs External Config Refers to configuration done outside of the nvme-stas framework, for example using nvme-cli commands
DCs notify hosts of connectivity configuration changes by sending AENs indicating a "Discovery Log" change. The host uses these AENs as a trigger to issue a Get Log Page command. The response to this command is used to update the list of DLPEs containing the controllers the host is allowed to access. Upon reception of the current DLPEs, the host will determine whether DLPEs were added and/or removed, which will trigger the addition and/or removal of controller connections. This happens in real time and may affect active connections to controllers including controllers that support I/O operations (IOCs). A host that was previously connected to an IOC may suddenly be told that it is no longer allowed to connect to that IOC and should disconnect from it. IOC connection creation There are 3 ways to configure IOC connections on a host: Manual Config by adding controller= entries to the [Controllers] section (see below). Automatic Config received in the form of DLPEs from a remote DC. External Config using nvme-cli (e.g. "nvme connect") IOC connection removal/prevention There are 3 ways to remove (or prevent) connections to an IOC: Manual Config. by adding exclude= entries to the [Controllers] section (see below). by removing controller= entries from the [Controllers] section. Automatic Config. As explained above, a host gets a new list of DLPEs upon connectivity configuration changes. On DLPE removal, the host should remove the connection to the IOC matching that DLPE. This behavior is configurable using the disconnect-scope= parameter described below. External Config using nvme-cli (e.g. "nvme disconnect" or "nvme disconnect-all") The decision by the host to automatically disconnect from an IOC following connectivity configuration changes is controlled by two parameters: disconnect-scope and disconnect-trtypes. disconnect-scope= Takes one of: only-stas-connections, all-connections-matching-disconnect-trtypes, or no-disconnect. In theory, hosts should only connect to IOCs that have been zoned for them. Connections to IOCs that a host is not zoned to have access to should simply not exist. In practice, however, users may not want hosts to disconnect from all IOCs in reaction to connectivity configuration changes (or at least for some of the IOC connections). Some users may prefer for IOC connections to be "sticky" and only be removed manually (nvme-cli or exclude=) or removed by a system reboot. Specifically, they don't want IOC connections to be removed unexpectedly on DLPE removal. These users may want to set disconnect-scope to no-disconnect. It is important to note that when IOC connections are removed, ongoing I/O transactions will be terminated immediately. There is no way to tell what happens to the data being exchanged when such an abrupt termination happens. If a host was in the middle of writing to a storage subsystem, there is a chance that outstanding I/O operations may not successfully complete. Values: only-stas-connections Only remove connections previously made by stacd. In this mode, when a DLPE is removed as a result of connectivity configuration changes, the corresponding IOC connection will be removed by stacd. Connections to IOCs made externally, e.g. using nvme-cli, will not be affected, unless they happen to be duplicates of connections made by stacd. It's simply not possible for stacd to tell that a connection was previously made with nvme-cli (or any other external tool). So, it's good practice to avoid duplicating configuration between stacd and external tools. Users wanting to persist some of their IOC connections regardless of connectivity configuration changes should not use nvme-cli to make those connections. Instead, they should hard-code them in stacd.conf with the controller= parameter. Using the controller= parameter is the only way for a user to tell stacd that a connection must be made and not be deleted "no-matter-what". all-connections-matching-disconnect-trtypes All connections that match the transport type specified by disconnect-trtypes=, whether they were made automatically by stacd or externally (e.g., nvme-cli), will be audited and are subject to removal on DLPE removal. In this mode, as DLPEs are removed as a result of connectivity configuration changes, the corresponding IOC connections will be removed by the host immediately whether they were made by stacd, nvme-cli, or any other way. Basically, stacd audits all IOC connections matching the transport type specified by disconnect-trtypes=. <emphasis>NOTE</emphasis> This mode implies that stacd will only allow Manually Configured or Automatically Configured IOC connections to exist. Externally Configured connections using nvme-cli (or other external mechanism) that do not match any Manual Config (stacd.conf) or Automatic Config (DLPEs) will get deleted immediately by stacd. no-disconnect stacd does not disconnect from IOCs when a DPLE is removed or a controller= entry is removed from stacd.conf. All IOC connections are "sticky". Instead, users can remove connections by issuing the nvme-cli command "nvme disconnect", add an exclude= entry to stacd.conf, or wait until the next system reboot at which time all connections will be removed. Defaults to only-stas-connections. disconnect-trtypes= This parameter only applies when disconnect-scope is set to all-connections-matching-disconnect-trtypes. It limits the scope of the audit to specific transport types. Can take the values tcp, rdma, fc, or a combination thereof by separating them with a plus (+) sign. For example: tcp+fc. No spaces are allowed between values and the plus (+) sign. Values: tcp Audit TCP connections. rdma Audit RDMA connections. fc Audit Fibre Channel connections. Defaults to tcp. connect-attempts-on-ncc= The NCC bit (Not Connected to CDC) is a bit returned by the CDC in the EFLAGS field of the DLPE. Only CDCs will set the NCC bit. DDCs will always clear NCC to 0. The NCC bit is a way for the CDC to let hosts know that the subsystem is currently not reachable by the CDC. This may indicate that the subsystem is currently down or that there is an outage on the section of the network connecting the CDC to the subsystem. If a host is currently failing to connect to an I/O controller and if the NCC bit associated with that I/O controller is asserted, the host can decide to stop trying to connect to that subsystem until connectivity is restored. This will be indicated by the CDC when it clears the NCC bit. The parameter connect-attempts-on-ncc= controls whether stacd will take the NCC bit into account when attempting to connect to an I/O Controller. Setting connect-attempts-on-ncc= to 0 means that stacd will ignore the NCC bit and will keep trying to connect. Setting connect-attempts-on-ncc= to a non-zero value indicates the number of connection attempts that will be made before stacd gives up trying. Note that this value should be set to a value greater than 1. In fact, when set to 1, stacd will automatically use 2 instead. The reason for this is simple. It is possible that a first connect attempt may fail. Defaults to 0.
See Also stacd 8
linux-nvme-nvme-stas-a8026bb/doc/stacd.service.xml000066400000000000000000000043561440613556600222030ustar00rootroot00000000000000 stacd.service nvme-stas Mr Martin Belanger Dell, Inc. stacd.service 8 stacd.service Systemd unit file for the stacd service /usr/lib/systemd/system/stacd.service Description stacd 8 is a system service used to automatically connect to I/O controllers discovered by stafd 8 . See Also stacd 8 , stafd 8 , stas-config.target 8 linux-nvme-nvme-stas-a8026bb/doc/stacd.xml000066400000000000000000000203301440613556600205320ustar00rootroot00000000000000 ]> &daemon; nvme-stas Mr Martin Belanger Dell, Inc. &daemon; 8 &daemon; &deamondesc; &daemon; OPTIONS Description &daemon; is a system daemon that can be used to automatically connect to NVMe-oF I/O Controllers using the discovery log pages collected by stafd 8 . It can also be manually configured with &daemon;.conf 5 to connect to I/O Controllers that otherwise cannot be found automatically. Options The following options are understood: Specify a different configuration file than &daemon;.conf 5 (default: /etc/stas/&daemon;.conf). Send messages to syslog instead of stdout. Use this when running &daemon; as a daemon. (default: false). Trace ON. (default: false) Print D-Bus IDL to FILE and exit. Exit status On success, 0 is returned, a non-zero failure code otherwise. Daemonization &daemon; is managed by systemd. The following operations are supported: Command Description $ systemctl start &daemon; Start daemon. $ systemctl stop &daemon; Stop daemon. The SIGTERM signal is used to tell the daemon to stop. $ systemctl restart &daemon; Effectively a stop + start. $ systemctl reload &daemon; Reload configuration. This is done in real time without restarting the daemon. The SIGHUP signal is used to tell the daemon to reload its configuration file. Note that configuration parameters that affect connections (e.g. kato), will not apply to existing connections. Only connections established after the configuration was changed will utilize the new configuration parameters.
Design &daemon; use the GLib main loop. The GLib Python module provides several low-level building blocks that &daemon; requires. In addition, many Python modules "play nice" with GLib such as dasbus (D-Bus package) and pyudev (UDev package). GLib also provides additional components such as timers, signal handlers, and much more. Configuration &daemon; can automatically set up the I/O connections to discovered storage subsystems. However, &daemon; can also operate in a non-automatic mode based on manually entered configuration. In other words, storage subsystems can be entered in a configuration file named /etc/stas/&daemon;.conf. This configuration file also provides additional parameters, as log-level attributes used for debugging purposes. D-Bus API The interface to &daemon; is D-Bus. This allows other programs, such as &control;, to communicate with &daemon;. The D-Bus address is org.nvmexpress.stac. See Also &daemon;.conf 5 , &daemon;.service 8 , stacctl 1 , stafd 8 , org.nvmexpress.stac 5 .
linux-nvme-nvme-stas-a8026bb/doc/stafctl.xml000066400000000000000000000152241440613556600211020ustar00rootroot00000000000000 stafctl nvme-stas Mr Martin Belanger Dell, Inc. stafctl 1 stafctl STorage Appliance Finder (STAF) utility program stafctl OPTIONS COMMAND OPTIONS Description stafctl is a tool that can be used to communicate with the stafd 8 daemon to retrieve operational data. Commands The following commands are understood: ls Show the list of discovery controllers. This will list all the controllers configured in stafd.conf 5 as well as those discovered with mDNS service discovery. dlp Show discovery log pages. adlp Show all discovery log pages. Options The following options are understood: Values Exit status On success, 0 is returned; otherwise, a non-zero failure code is returned. Examples List all the discovery controllers $ stafctl ls [{'device': 'nvme0', 'host-iface': '', 'host-traddr': '', 'subsysnqn': 'nqn.2014-08.org.nvmexpress.discovery', 'traddr': '::1', 'transport': 'tcp', 'trsvcid': '8009'}] Enable tracing $ stafctl tron Show discovery log pages from a specific discovery controller $ stafctl dlp --transport tcp --traddr ::1 --trsvcid 8009 [{'adrfam': 'ipv6', 'asqsz': '32', 'cntlid': '65535', 'portid': '1', 'subnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28', 'subtype': 'nvme', 'traddr': '::1', 'treq': 'disable sqflow', 'trsvcid': '8009', 'trtype': 'tcp'}, {'adrfam': 'ipv6', 'asqsz': '32', 'cntlid': '65535', 'portid': '1', 'subnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aedead', 'subtype': 'nvme', 'traddr': '::1', 'treq': 'disable sqflow', 'trsvcid': '8009', 'trtype': 'tcp'}, {'adrfam': 'ipv6', 'asqsz': '32', 'cntlid': '65535', 'portid': '1', 'subnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34aebeef', 'subtype': 'nvme', 'traddr': '::1', 'treq': 'disable sqflow', 'trsvcid': '8009', 'trtype': 'tcp'}] See Also stafd.conf 5 , stafd 8 linux-nvme-nvme-stas-a8026bb/doc/stafd.conf.xml000066400000000000000000000300051440613556600214610ustar00rootroot00000000000000 stafd.conf nvme-stas Mr Martin Belanger Dell, Inc. stafd.conf 5 stafd.conf stafd 8 configuration file /etc/stas/stafd.conf Description When stafd 8 starts up, it reads its configuration from stafd.conf. Configuration File Format stafd.conf is a plain text file divided into sections, with configuration entries in the style key=value. Spaces immediately before or after the = are ignored. Empty lines are ignored as well as lines starting with #, which may be used for commenting. Options [Global] section The following options are available in the [Global] section: ignore-iface= Takes a boolean argument. This option controls how connections with Discovery Controllers (DC) are made. DCs are automatically discovered using DNS-SD/mDNS. mDNS provides the DC's IP address and the interface on which the DC was discovered. There is no guarantee that there will be a route to reach that DC. However, we can use the socket option SO_BINDTODEVICE to force the connection to be made on a specific interface instead of letting the routing tables decide where to make the connection. This option determines whether stafd will use SO_BINDTODEVICE to force connections on an interface or just rely on the routing tables. The default is to use SO_BINDTODEVICE, in other words, stafd does not ignore the interface by default. Defaults to false. pleo= Port Local Entries Only. Takes a string argument enabled or disabled. This option is sent in the LSP field (Log SPecific) of the Get Discovery Log Page (DLP) command. It is used by stafd to tell Discovery Controllers (DC) whether the response to a Get DLP command should contain all the NVM subsystems or only those reachable by the host on the interface where the Get DLP command was issued by the host. This parameter was introduced in TP8010. When pleo=enabled, then the DC shall return records for only NVM subsystem ports that are presented through the same NVM subsystem port that received the Get Log Page command. When pleo=disabled, then the DC may return all the NVM subsystem ports that it holds, even those that can only be reached on NVM subsystem ports that did not receive the Get Log Page command. In other words, the host may not even be able to reach those subsystems. Defaults to enabled. [Service Discovery] section The following options are available in the [Service Discovery] section: zeroconf= Enable zeroconf provisioning using DNS-SD/mDNS. Takes a string argument enabled or disabled. When enabled, the default, stafd makes a request with the Avahi daemon to locate Discovery Controllers using DNS-SD/mDNS. Discovery Controllers that support zeroconf advertize themselves over mDNS with the service type _nvme-disc._tcp. Defaults to true. [Discovery controller connection management] section The following options are available in the [Discovery controller connection management] section: persistent-connections= Takes a boolean argument. Whether connections to Discovery Controllers (DC) are persistent. When true, connections initiated by stafd will persists even when stafd is stopped. When false, stafd will disconnect from all DCs it is connected to on exit. Defaults to false. zeroconf-connections-persistence= Takes a unit-less value in seconds, or a time span value such as "72hours" or "5days". A value of 0 means no persistence. In other words, configuration acquired through zeroconf (mDNS service discovery) will be removed immediately when mDNS no longer reports the presence of a Discovery Controller (DC) and connectivity to that DC is lost. A value of -1 means that configuration acquired through zeroconf will persist forever. This is used for the case where a DC that was discovered through mDNS service discovery no longer advertises itself through mDNS and can no longer be connected to. For example, the DC had some catastrophic failure (e.g. power surge) and needs to be replaced. In that case, the connection to that DC can never be restored and a replacement DC will be needed. The replacement DC will likely have a different NQN (or IP address). In that scenario, the host won't be able to determine that the old DC is not coming back. It won't know either that a newly discovered DC is really the replacement for the old one. For that reason, the host needs a way to "age" zeroconf-acquired configuration and remove it automatically after a certain amount of time. This is what this parameter is for. Defaults to 72hours. See Also stafd 8 linux-nvme-nvme-stas-a8026bb/doc/stafd.service.xml000066400000000000000000000036761440613556600222120ustar00rootroot00000000000000 stafd.service nvme-stas Mr Martin Belanger Dell, Inc. stafd.service 8 stafd.service Systemd unit file for the stafd service /usr/lib/systemd/system/stafd.service Description stafd 8 is a system service used to automatically locate NVMe-oF Discovery Controllers using mDNS service discovery. See Also stafd 8 , stas-config.target 8 linux-nvme-nvme-stas-a8026bb/doc/stafd.xml000066400000000000000000000213711440613556600205430ustar00rootroot00000000000000 ]> &daemon; nvme-stas Mr Martin Belanger Dell, Inc. &daemon; 8 &daemon; &deamondesc; &daemon; OPTIONS Description &daemon; is a system daemon that can be used to automatically locate and connect to NVMe-oF Discovery Controllers using mDNS service discovery. It can also be manually configured with &daemon;.conf 5 to connect to Discovery Controllers that cannot be located using mDNS. Options The following options are understood: Specify a different configuration file than &daemon;.conf 5 (default: /etc/stas/&daemon;.conf). Send messages to syslog instead of stdout. Use this when running &daemon; as a daemon. (default: false). Trace ON. (default: false) Print D-Bus IDL to FILE and exit. Exit status On success, 0 is returned, a non-zero failure code otherwise. Daemonization &daemon; is managed by systemd. The following operations are supported: Command Description $ systemctl start &daemon; Start daemon. $ systemctl stop &daemon; Stop daemon. The SIGTERM signal is used to tell the daemon to stop. $ systemctl restart &daemon; Effectively a stop + start. $ systemctl reload &daemon; Reload configuration. This is done in real time without restarting the daemon. The SIGHUP signal is used to tell the daemon to reload its configuration file. Note that configuration parameters that affect connections (e.g. kato), will not apply to existing connections. Only connections established after the configuration was changed will utilize the new configuration parameters.
Design &daemon; use the GLib main loop. The GLib Python module provides several low-level building blocks that &daemon; requires. In addition, many Python modules "play nice" with GLib such as dasbus (D-Bus package) and pyudev (UDev package). GLib also provides additional components such as timers, signal handlers, and much more. &daemon; connects to the avahi-daemon using D-Bus. The avahi-daemon, or simply Avahi, is an mDNS discovery service used for zero-configuration networking (zeroconf). &daemon; registers with Avahi to automatically locate Central Discovery Controllers (CDC) and Direct Discovery Controllers (DDC). When Avahi finds Discovery Controllers (DC), it notifies &daemon; which connects to the DC with the help of the libnvme library. Once a connection to a DC is established, &daemon; can retrieve the discovery log pages from that DC and cache them in memory. Configuration &daemon; can automatically locate discovery controllers (DC) with the help of Avahi and connect to them. However, &daemon; can also operate in a non-automatic mode based on manually entered configuration. In other words, DCs can be entered in a configuration named /etc/stas/&daemon;.conf. This configuration file also provides additional parameters, such as log-level attributes used for debugging purposes. D-Bus API The interface to &daemon; is D-Bus. This allows other programs, such as &control;, to communicate with &daemon;. The D-Bus address is org.nvmexpress.staf. See Also &daemon;.conf 5 , &daemon;.service 8 , stafctl 1 , org.nvmexpress.staf 5 .
linux-nvme-nvme-stas-a8026bb/doc/standard-conf.xml000066400000000000000000000636751440613556600222020ustar00rootroot00000000000000 tron= Trace ON. Takes a boolean argument. If true, enables full code tracing. The trace will be displayed in the system log such as systemd's journal. Defaults to false. hdr-digest= Enable Protocol Data Unit (PDU) Header Digest. Takes a boolean argument. NVMe/TCP facilitates an optional PDU Header digest. Digests are calculated using the CRC32C algorithm. If true, Header Digests are inserted in PDUs and checked for errors. Defaults to false. data-digest= Enable Protocol Data Unit (PDU) Data Digest. Takes a boolean argument. NVMe/TCP facilitates an optional PDU Data digest. Digests are calculated using the CRC32C algorithm. If true, Data Digests are inserted in PDUs and checked for errors. Defaults to false. kato= Keep Alive Timeout (KATO) in seconds. Takes an unsigned integer. This field specifies the timeout value for the Keep Alive feature in seconds. Defaults to 30 seconds for Discovery Controller connections and 120 seconds for I/O Controller connections. ip-family= Takes a string argument. With this you can specify whether IPv4, IPv6, or both are supported when connecting to a Controller. Connections will not be attempted to IP addresses (whether discovered or manually configured with controller=) disabled by this option. If an invalid value is entered, then the default (see below) will apply. Choices are ipv4, ipv6, or ipv4+ipv6. Defaults to ipv4+ipv6. queue-size= Takes a value in the range 16...1024. Overrides the default number of elements in the I/O queues created by the driver. This option will be ignored for discovery, but will be passed on to the subsequent connect call. Note: This parameter is identical to that provided by nvme-cli. Defaults to 128. reconnect-delay= Takes a value in the range 1 to N seconds. Overrides the default delay before reconnect is attempted after a connect loss. Note: This parameter is identical to that provided by nvme-cli. Defaults to 10. Retry to connect every 10 seconds. ctrl-loss-tmo= Takes a value in the range -1, 0, ..., N seconds. -1 means retry forever. 0 means do not retry. Overrides the default controller loss timeout period (in seconds). Note: This parameter is identical to that provided by nvme-cli. Defaults to 600 seconds (10 minutes). disable-sqflow= Takes a boolean argument. Disables SQ flow control to omit head doorbell update for submission queues when sending nvme completions. Note: This parameter is identical to that provided by nvme-cli. Defaults to false. [Controllers] section The following options are available in the [Controllers] section: controller= Controllers are specified with the controller option. This option may be specified more than once to specify more than one controller. The format is one line per Controller composed of a series of fields separated by semi-colons as follows: controller=transport=[trtype];traddr=[traddr];trsvcid=[trsvcid];host-traddr=[traddr],host-iface=[iface];nqn=[nqn] Fields transport= This is a mandatory field that specifies the network fabric being used for a NVMe-over-Fabrics network. Current trtype values understood are: Transport type trtype Definition rdma The network fabric is an rdma network (RoCE, iWARP, Infiniband, basic rdma, etc) fc The network fabric is a Fibre Channel network. tcp The network fabric is a TCP/IP network. loop Connect to a NVMe over Fabrics target on the local host
traddr= This is a mandatory field that specifies the network address of the Controller. For transports using IP addressing (e.g. rdma) this should be an IP-based address (ex. IPv4, IPv6). It could also be a resolvable host name (e.g. localhost). trsvcid= This is an optional field that specifies the transport service id. For transports using IP addressing (e.g. rdma, tcp) this field is the port number. Depending on the transport type, this field will default to either 8009 or 4420 as follows. UDP port 4420 and TCP port 4420 have been assigned by IANA for use by NVMe over Fabrics. NVMe/RoCEv2 controllers use UDP port 4420 by default. NVMe/iWARP controllers use TCP port 4420 by default. TCP port 4420 has been assigned for use by NVMe over Fabrics and TCP port 8009 has been assigned by IANA for use by NVMe over Fabrics discovery. TCP port 8009 is the default TCP port for NVMe/TCP discovery controllers. There is no default TCP port for NVMe/TCP I/O controllers, the Transport Service Identifier (TRSVCID) field in the Discovery Log Entry indicates the TCP port to use. The TCP ports that may be used for NVMe/TCP I/O controllers include TCP port 4420, and the Dynamic and/or Private TCP ports (i.e., ports in the TCP port number range from 49152 to 65535). NVMe/TCP I/O controllers should not use TCP port 8009. TCP port 4420 shall not be used for both NVMe/iWARP and NVMe/TCP at the same IP address on the same network. Ref: IANA Service names port numbers nqn= This field specifies the Controller's NVMe Qualified Name. This field is mandatory for I/O Controllers, but is optional for Discovery Controllers (DC). For the latter, the NQN will default to the well-known DC NQN: nqn.2014-08.org.nvmexpress.discovery if left undefined. host-traddr= This is an optional field that specifies the network address used on the host to connect to the Controller. For TCP, this sets the source address on the socket. host-iface= This is an optional field that specifies the network interface used on the host to connect to the Controller (e.g. IP eth1, enp2s0, enx78e7d1ea46da). This forces the connection to be made on a specific interface instead of letting the system decide. dhchap-ctrl-secret= This is an optional field that specifies the NVMe In-band authentication controller secret (i.e. key) for bi-directional authentication; needs to be in ASCII format as specified in NVMe 2.0 section 8.13.5.8 'Secret representation'. Bi-directional authentication will be attempted when present. hdr-digest= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. data-digest= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. nr-io-queues= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. nr-write-queues= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. nr-poll-queues= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. queue-size= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. kato= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. reconnect-delay= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. ctrl-loss-tmo= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section. disable-sqflow= See definition in [Global] section. This is an optional field used to override the value specified in the [Global] section.
Examples: controller = transport=tcp;traddr=localhost;trsvcid=8009 controller = transport=tcp;traddr=2001:db8::370:7334;host-iface=enp0s8 controller = transport=fc;traddr=nn-0x204600a098cbcac6:pn-0x204700a098cbcac6
exclude= Controllers that should be excluded can be specified with the exclude= option. Using mDNS to automatically discover and connect to controllers, can result in unintentional connections being made. This keyword allows configuring the controllers that should not be connected to. The syntax is the same as for "controller", except that only transport, traddr, trsvcid, nqn, and host-iface apply. Multiple exclude= keywords may appear in the config file to specify more than 1 excluded controller. Note 1: A minimal match approach is used to eliminate unwanted controllers. That is, you do not need to specify all the parameters to identify a controller. Just specifying the host-iface, for example, can be used to exclude all controllers on an interface. Note 2: exclude= takes precedence over controller. A controller specified by the controller keyword, can be eliminated by the exclude= keyword. Examples: exclude = transport=tcp;traddr=fe80::2c6e:dee7:857:26bb # Eliminate a specific address exclude = host-iface=enp0s8 # Eliminate everything on this interface
linux-nvme-nvme-stas-a8026bb/doc/standard-options.xml000066400000000000000000000112051440613556600227260ustar00rootroot00000000000000 Print the help text and exit. Print the version string and exit. tron Trace ON. Enable code tracing, which is to say that lots of debug information will be printed to the syslog (e.g. systemd-journal). troff Trace OFF. Disable code tracing. status Show runtime status information. Print additional details. NVMe-over-Fabrics fabric type (default: "tcp"). Discovery controller's network address. Transport service id (for IP addressing, e.g. tcp, rdma, this field is the port number). Network source address used on the host to connect to the controller. This field specifies the network interface used on the host to connect to the controller. This field specifies the Controller's NVMe Qualified Name. This field is mandatory for I/O Controllers, but is optional for Discovery Controllers (DC). For the latter, the NQN will default to the well-known DC NQN: nqn.2014-08.org.nvmexpress.discovery if left undefined. TRTYPE rdma, fc, tcp, loop. TRADDR IP or Fibre Channel address. E.g. 10.10.0.100. TRSVCID E.g., 8009. IFACE Network interface name. E.g., eth1, enp0s8, wlp0s20f3. NQN NVMe Qualified Name. linux-nvme-nvme-stas-a8026bb/doc/stas-config.target.xml000066400000000000000000000046001440613556600231400ustar00rootroot00000000000000 stas-config.target nvme-stas Mr Martin Belanger Dell, Inc. stas-config.target 8 stas-config.target Used to synchronize the start of nvme-stas processes /usr/lib/systemd/system/stas-config.target Description This target is used as a synchronization point before starting stacd.service8 and stafd.service8. It ensures that /etc/nvme/hostnqn and /etc/nvme/hostid are present before starting stacd.service8 and stafd.service8. See Also stacd 8 stafd 8 linux-nvme-nvme-stas-a8026bb/doc/stas-config@.service.xml000066400000000000000000000042161440613556600234150ustar00rootroot00000000000000 stas-config@.service nvme-stas Mr Martin Belanger Dell, Inc. stas-config@.service 8 stas-config@.service Used for auto-generation of nvme-stas configuration files. /usr/lib/systemd/system/stas-config@.service Description This service is used for the automatic run-time generation of NVMe configuration located in /etc/nvme (e.g. /etc/nvme/hostnqn). This is needed by stacd.service8 and stafd.service8. See Also stacd 8 stafd 8 linux-nvme-nvme-stas-a8026bb/doc/stasadm.xml000066400000000000000000000166451440613556600211060ustar00rootroot00000000000000 stasadm nvme-stas Mr Martin Belanger Dell, Inc. stasadm 1 stasadm STorage Appliance Services admin functions stasadm OPTIONS COMMAND OPTIONS Description stasadm is used to configure nvme-stas. The configuration is saved to /etc/stas/sys.conf. Although nvme-stas' configuration is saved to /etc/stas/sys.conf, it's still possible to interoperate with the configuration of nvme-cli and libnvme. nvme-stas allows one to save individual parameters such as the Host NQN and ID outside of /etc/stas/sys.conf. This allows, for example, using the same default Host NQN and ID defined by nvme-cli and libnvme in /etc/nvme/hostnqn and /etc/nvme/hostid respectively. To tell nvme-stas that you want to use the those files, simply use stasadm's option. Commands The following commands are understood: hostnqn Generate the Host NQN. This is typically used as a post installation step to generate /etc/nvme/hostnqn. The NVMe base specifications says: An NQN is permanent for the lifetime of the host. For this reason, the host NQN should only be generated if /etc/nvme/hostnqn does not exist already. hostid Generate the Host ID. This is typically used as a post installation step to generate /etc/nvme/hostid. Although not explicitly specified in the NVMe specifications, the Host ID, like the Host NQN, should be permanent for the lifetime of the host. Only generate the Host ID if /etc/nvme/hostid does not exist. set-symname [SYMNAME] Set the host symbolic name. The symbolic name is an optional parameter that can be used for explicit registration with a discovery controller. clear-symname Clear the host symbolic name. Options The following options are understood: By default, hostnqn and hostid save the values to /etc/stas/sys.conf. This option allows saving to a separate file. Traditionally, nvme-cli and libnvme retrieve the default Host NQN and ID from /etc/nvme/hostnqn and /etc/nvme/hostid respectively. The option can be used to tell nvme-stas that it should use those same configuration files. Exit status On success, 0 is returned; otherwise, a non-zero failure code is returned. Examples Generate <filename>/etc/nvme/hostnqn</filename> $ stasadm hostnqn --file /etc/nvme/hostnqn Generate <filename>/etc/nvme/hostid</filename> $ stasadm hostid -f /etc/nvme/hostid Configure the host's symbolic name $ stasadm set-symname LukeSkywalker See Also nvme-stas 7 linux-nvme-nvme-stas-a8026bb/doc/sys.conf.xml000066400000000000000000000115751440613556600212110ustar00rootroot00000000000000 sys.conf nvme-stas Mr Martin Belanger Dell, Inc. sys.conf 5 sys.conf nvme-stas 7 configuration file /etc/stas/sys.conf Description When stafd 8 and stacd 8 start up, they read the system configuration from sys.conf. Configuration File Format sys.conf is a plain text file divided into sections, with configuration entries in the style key=value. Whitespace immediately before or after the = is ignored. Empty lines and lines starting with # are ignored, which may be used for commenting. Options [Host] section The following options are available in the [Host] section: nqn= Takes a string argument identifying the Host NQN. A value starting with file:// indicates that the Host NQN can be retrieved from a separate file. This is a mandatory parameter. Defaults to: file:///etc/nvme/hostnqn. id= Takes a string argument identifying the Host ID. A value starting with file:// indicates that the Host ID can be retrieved from a separate file. This is a mandatory parameter. Defaults to: file:///etc/nvme/hostid. symname= Takes a string argument identifying the Host symbolic name. A value starting with file:// indicates that the symbolic name can be retrieved from a separate file. This is an optional parameter. There is no default value. See Also nvme-stas 7 linux-nvme-nvme-stas-a8026bb/docker-compose.yml000066400000000000000000000011311440613556600216000ustar00rootroot00000000000000version: '3.7' x-stas: &default-stas image: ghcr.io/linux-nvme/nvme-stas:main build: context: . volumes: - /run/dbus:/run/dbus - /etc/nvme:/etc/nvme privileged: true network_mode: host services: stafd: <<: *default-stas environment: RUNTIME_DIRECTORY: /run/stafd XDG_CACHE_HOME: /var/cache/stafd PYTHONUNBUFFERED: 1 command: -u /usr/sbin/stafd stacd: <<: *default-stas environment: RUNTIME_DIRECTORY: /run/stacd XDG_CACHE_HOME: /var/cache/stacd PYTHONUNBUFFERED: 1 command: -u /usr/sbin/stacd linux-nvme-nvme-stas-a8026bb/etc/000077500000000000000000000000001440613556600167225ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/etc/dbus-1/000077500000000000000000000000001440613556600200155ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/etc/dbus-1/system.d/000077500000000000000000000000001440613556600215635ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/etc/dbus-1/system.d/meson.build000066400000000000000000000012011440613556600237170ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # dbus_conf_dir = datadir / 'dbus-1' / 'system.d' configure_file( input: 'org.nvmexpress.staf.in.conf', output: 'org.nvmexpress.staf.conf', configuration: conf, install_dir: dbus_conf_dir, ) configure_file( input: 'org.nvmexpress.stac.in.conf', output: 'org.nvmexpress.stac.conf', configuration: conf, install_dir: dbus_conf_dir, ) linux-nvme-nvme-stas-a8026bb/etc/dbus-1/system.d/org.nvmexpress.stac.in.conf000066400000000000000000000020441440613556600267700ustar00rootroot00000000000000 linux-nvme-nvme-stas-a8026bb/etc/dbus-1/system.d/org.nvmexpress.staf.in.conf000066400000000000000000000020441440613556600267730ustar00rootroot00000000000000 linux-nvme-nvme-stas-a8026bb/etc/stas/000077500000000000000000000000001440613556600176745ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/etc/stas/stacd.conf000066400000000000000000000357141440613556600216530ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # ============================================================================== # STorage Appliance Connector Daemon (stacd) - configuration file # # In this file, options that are commented represent the default values used. # Uncommented options override the default value. [Global] # tron: Trace-ON. Enable additional debug info # Type: boolean # Range: [false, true] #tron=false # hdr-digest: Protocol Data Unit (PDU) Header Digest. NVMe/TCP facilitates an # optional PDU Header digest. Digests are calculated using the # CRC32C algorithm. # Type: boolean # Range: [false, true] #hdr-digest=false # data-digest: Protocol Data Unit (PDU) Data Digest. NVMe/TCP facilitates an # optional PDU Data digest. Digests are calculated using the # CRC32C algorithm. # Type: boolean # Range: [false, true] #data-digest=false # kato: Keep Alive Timeout (KATO): This field specifies the timeout value # for the Keep Alive feature in seconds. The default value for this # field is 30 seconds (2 minutes). # Type: Unsigned integer # Range: 0..N # Unit: Seconds #kato=30 # nr-io-queues: Overrides the default number of I/O queues create by the # driver. # Type: Unsigned integer # Range: 1..N # Default: Depends on kernel and other run time # factors (e.g. number of CPUs). # nr-write-queues: Adds additional queues that will be used for write I/O. # Type: Unsigned integer # Range: 1..N # Default: Depends on kernel and other run time # factors (e.g. number of CPUs). # nr-poll-queues: Adds additional queues that will be used for polling # latency sensitive I/O. # Type: Unsigned integer # Range: 1..N # Default: Depends on kernel and other run time # factors (e.g. number of CPUs). # queue-size: Overrides the default number of elements in the I/O queues # created by the driver. # Type: Unsigned integer # Range: 16..1024 # Default: 128 #queue-size=128 # reconnect-delay: Overrides the default delay, in seconds, before reconnect # is attempted after a connect loss. # Type: Unsigned integer # Range: 1..N # Unit: Seconds # Default: 10 (retry to connect every 10 seconds) #reconnect-delay=10 # ctrl-loss-tmo: Overrides the default controller loss timeout period in # seconds. # Type: Unsigned integer # Range: -1, 0..N where -1 means retry forever # Unit: Seconds # Default: 600 (retry to connect for up to 10 minutes) #ctrl-loss-tmo=600 # disable-sqflow: Disables SQ flow control to omit head doorbell update for # submission queues when sending nvme completions. # Type: boolean # Range: [false, true] # Default: false #disable-sqflow=false # ignore-iface: This option controls whether connections with I/O Controllers # (IOC) will be forced on a specific interface or will rely on # the routing tables to determine the interface. # # See the man pages for details: man stacd.conf # # Type: boolean # Range: [false, true] # Default: true #ignore-iface=false # ip-family: With this you can specify whether stacd will support IPv4, IPv6, # or both when connecting to I/O Controllers (IOC). # # See the man pages for details: man stacd.conf # # Type: String # Range: [ipv4, ipv6, ipv4+ipv6] # Default: ipv4+ipv6 #ip-family=ipv4+ipv6 # ============================================================================== [I/O controller connection management] # This section contains parameters to manage I/O controller connections. # For example, parameters are provided to control disconnect policy. In other # words, whether stacd will disconnect from IOCs on DLPE removal and which # connections will be affected. # # Also, what should stacd do when a DLPE NCC bit (Not Connected to CDC) is # asserted. Should stacd stop trying to connect to an I/O controller after a # certain number of unsuccessful attempts. # # See the man pages for details: man stacd.conf # disconnect-scope: Determines which connections, if any, will be the target of # a potential disconnect on DLPE removal. # # Type: String # Range: [only-stas-connections | all-connections-matching-disconnect-trtypes | no-disconnect] # Default: only-stas-connections #disconnect-scope=only-stas-connections # disconnect-trtypes: Specify which connections should be audited based on the # transport type. This parameter only applies when # "disconnect-scope = all-connections-matching-disconnect-trtypes". # # Type: String # Range: [tcp, rdma, fc, tcp+rdma, tcp+fc, rdma+fc, tcp+rdma+fc] # Default: tcp #disconnect-trtypes=tcp # connect-attempts-on-ncc: The NCC bit (Not Connected to CDC) returned in a # DLPE indicates whether a connection is currently # established between the CDC and the subsystem listed # in the DLPE. # # When the NCC bit is asserted, it may mean that the # subsystem is offline or that fabrics connectivity is # momentarily lost. If the host is also unable to # connect to the subsystem, then there is no point in # continuing to try to connect. In fact, the CDC will # monitor the situation an inform the host (AEN) when # connectivity is restored. # # This field is used to tell stacd how to behave when # the NCC bit is asserted. How many times should it try # to connect before give-up, or whether to keep trying # indefinitely. # # Type: Integer # Range: [0, 2..N], 0 means "Never stop trying". A # non-0 value indicates the number of attempts # before giving up. This value should never be # set to 1. A value of 1 will automatically be # increased to 2. That's because a single # failure may be normal and a mimimum of 2 # attempts is required to conclude that a # connection is not possible. # Default: 0 #connect-attempts-on-ncc=0 # ============================================================================== [Controllers] # controller: I/O Controllers (IOC) are specified with this keyword. # # Syntax: # controller = transport=;traddr=;trsvcid=;host-traddr=;host-iface=,nqn= # # transport= [MANDATORY] # This field specifies the network fabric being used for a NVMe-over- # Fabrics network. Current string values include: # # Value Definition # ------- ----------------------------------------------------------- # rdma The network fabric is an rdma network (RoCE, iWARP, Infiniband, basic rdma, etc) # fc The network fabric is a Fibre Channel network. # tcp The network fabric is a TCP/IP network. # loop Connect to a NVMe over Fabrics target on the local host # # traddr= [MANDATORY] # This field specifies the network address of the Controller. For # transports using IP addressing (e.g. rdma) this should be an IP- # based address (ex. IPv4, IPv6). It could also be a resolvable host # name (e.g. localhost). # # nqn= [MANDATORY] # This field specifies the Subsystem's NVMe Qualified Name. # # trsvcid= [OPTIONAL] # This field specifies the transport service id. For transports using # IP addressing (e.g. rdma) this field is the port number. # # Depending on the transport type, this field will default to either # 8009 or 4420 as follows. # # UDP port 4420 and TCP port 4420 have been assigned by IANA # for use by NVMe over Fabrics. NVMe/RoCEv2 controllers use UDP port # 4420 by default. NVMe/iWARP controllers use TCP port 4420 by # default. # # TCP port 4420 has been assigned for use by NVMe over Fabrics and TCP # port 8009 has been assigned by IANA for use by NVMe over Fabrics # discovery. TCP port 8009 is the default TCP port for NVMe/TCP # discovery controllers. There is no default TCP port for NVMe/TCP I/O # controllers, the Transport Service Identifier (TRSVCID) field in the # Discovery Log Entry indicates the TCP port to use. # # The TCP ports that may be used for NVMe/TCP I/O controllers include # TCP port 4420, and the Dynamic and/or Private TCP ports (i.e., ports # in the TCP port number range from 49152 to 65535). NVMe/TCP I/O # controllers should not use TCP port 8009. TCP port 4420 shall not be # used for both NVMe/iWARP and NVMe/TCP at the same IP address on the # same network. # # host-traddr= [OPTIONAL] # This field specifies the network address used on the host to connect # to the Controller. For TCP, this sets the source address on the # socket. # # host-iface= [OPTIONAL] # This field specifies the network interface used on the host to # connect to the Controller (e.g. IP eth1, enp2s0, enx78e7d1ea46da). # This forces the connection to be made on a specific interface # instead of letting the system decide. # # dhchap-ctrl-secret [OPTIONAL] # NVMe In-band authentication controller secret (i.e. key) for # bi-directional authentication; needs to be in ASCII format as # specified in NVMe 2.0 section 8.13.5.8 'Secret representation'. # Bi-directional authentication will be attempted when present. # # hdr-digest [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # data-digest [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # nr-io-queues [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # nr-write-queues [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # nr-poll-queues [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # queue-size [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # kato [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # reconnect-delay [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # ctrl-loss-tmo [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # disable-sqflow [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # Multiple DCs may be specified on separate lines like this (this is # just an example and does not represent default values): # # controller = transport=tcp;traddr=localhost;nqn=nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28 # controller = transport=tcp;traddr=2001:db8::370:7334;host-iface=enp0s8;nqn=nqn.starship-enterprise # controller = transport=fc;traddr=nn-0x204600a098cbcac6:pn-0x204700a098cbcac6;nqn=nqn.romulan-empire # ... # Type: String # # Default: There is no default controller. STAC will not try to # connect to a default I/O Controller. #controller= # exclude: Excluded controllers. This keyword allows configuring I/O # controllers that should not be connected to (whatever the # reason may be). # # The syntax is the same as for "controller=", except that the key # host-traddr does not apply. Multiple "exclude=" keywords may # appear in the config file to define the exclusion list. # # Note 1: A minimal match approach is used to eliminate unwanted # controllers. That is, you do not need to specify all the # parameters to identify a controller. Just specifying the # host-iface, for example, can be used to exclude all controllers # on an interface. # # Note 2: "exclude=" takes precedence over "controller=". A # controller specified by the "controller=" keyword, can be # eliminated by the "exclude=" keyword. # # Syntax: Same as "controller=" above. # Type: String # # Example: # exclude = transport=tcp;traddr=fe80::2c6e:dee7:857:26bb # Eliminate a specific address # exclude = host-iface=enp0s8 # Eliminate everything on this interface #exclude= linux-nvme-nvme-stas-a8026bb/etc/stas/stafd.conf000066400000000000000000000314311440613556600216460ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # ============================================================================== # STorage Appliance Finder Daemon (stafd) - configuration file # # In this file, options that are commented represent the default values used. # Uncommented options override the default value. [Global] # tron: Trace-ON. Enable additional debug info # Type: boolean # Range: [false, true] #tron=false # hdr-digest: Protocol Data Unit (PDU) Header Digest. NVMe/TCP facilitates an # optional PDU Header digest. Digests are calculated using the # CRC32C algorithm. # Type: boolean # Range: [false, true] #hdr-digest=false # data-digest: Protocol Data Unit (PDU) Data Digest. NVMe/TCP facilitates an # optional PDU Data digest. Digests are calculated using the # CRC32C algorithm. # Type: boolean # Range: [false, true] #data-digest=false # kato: Keep Alive Timeout (KATO): This field specifies the timeout value # for the Keep Alive feature in seconds. The default value for this # field is 30 seconds. # Type: Unsigned integer # Range: 0..N # Unit: Seconds #kato=30 # queue-size: Overrides the default number of elements in the I/O queues # created by the driver. # Type: Unsigned integer # Range: 16..1024 # Default: 128 #queue-size=128 # reconnect-delay: Overrides the default delay, in seconds, before reconnect # is attempted after a connect loss. # Type: Unsigned integer # Range: 1..N # Unit: Seconds # Default: 10 (retry to connect every 10 seconds) #reconnect-delay=10 # ctrl-loss-tmo: Overrides the default controller loss timeout period in # seconds. # Type: Unsigned integer # Range: -1, 0..N where -1 means retry forever # Unit: Seconds # Default: 600 (retry to connect for up to 10 minutes) #ctrl-loss-tmo=600 # disable-sqflow: Disables SQ flow control to omit head doorbell update for # submission queues when sending nvme completions. # Type: boolean # Range: [false, true] # Default: false #disable-sqflow=false # ignore-iface: This option controls whether connections with Discovery # Controllers (DC) will be forced on a specific interface or # will rely on the routing tables to determine the interface. # # See the man pages for details: man stafd.conf # # Type: boolean # Range: [false, true] # Default: true #ignore-iface=false # ip-family: With this you can specify whether stafd will support IPv4, IPv6, # or both when connecting to Discovery Controllers (DC). # # See the man pages for details: man stafd.conf # # Type: String # Range: [ipv4, ipv6, ipv4+ipv6] # Default: ipv4+ipv6 #ip-family=ipv4+ipv6 # pleo: Port Local Entries Only. If enabled and supported, when connected to a # Direct Discovery Controller (DDC), stafd will ask the DDC to return # records for only NVM subsystem ports that are presented through the same # NVM subsystem port that received the Get Log Page command. When disabled # or not supported by the DDC, the DDC may return NVM subsystems that are # not even reachable by the host, including those using a transport # different from the transport used for the Get Log Page command (e.g. Get # Log Page using TCP and receiving FC subsystems). This configuration # parameter has no effect if the DDC does not support PLEO (see PLEOS). # # Type: String # Range: [disabled, enabled] # Default: enabled #pleo=enabled # ============================================================================== [Service Discovery] # zeroconf: Control whether DNS-SD/mDNS automatic discovery is enabled. This is # used to enable or disable automatic discovery of Discovery # Controllers using DNS-SD/mDNS. # # Type: String # Range: [disabled, enabled] # Default: enabled #zeroconf=enabled # ============================================================================== [Discovery controller connection management] # persistent-connections: Whether connections to Discovery Controllers (DC) # are persistent. If stafd is stopped, the connections # will persists. When this is set to false, stafd will # disconnect from all DCs it is connected to when stafd # is stopped. # Type: boolean # Range: [false, true] #persistent-connections=true # zeroconf-connections-persistence: DCs that are discovered with mDNS service # discovery which are later lost (i.e. no mDNS # and TCP connection fails), will be purged from # the configuration after this amount of time. # Type: Time specs. # Unit: Takes a unit-less value in seconds, # or a time span (TS) value such as # "3 days 5 hours". # Range: -1, 0, TS. # With "-1" equal to "no timeout" and # 0 equal to timeout immediately. # Default: 72 hours (3 days) #zeroconf-connections-persistence=72hours # ============================================================================== [Controllers] # controller: Discovery Controllers (DC) are specified with this keyword. # # Syntax: # controller = transport=[trtype];traddr=[traddr];trsvcid=[trsvcid];host-traddr=[traddr];host-iface=[iface];nqn= # # transport= [MANDATORY] # This field specifies the network fabric being used for a NVMe-over- # Fabrics network. Current string values include: # # Value Definition # ------- ----------------------------------------------------------- # rdma The network fabric is an rdma network (RoCE, iWARP, # Infiniband, basic rdma, etc) # fc The network fabric is a Fibre Channel network. # tcp The network fabric is a TCP/IP network. # loop Connect to a NVMe over Fabrics target on the local host # # traddr= [MANDATORY] # This field specifies the network address of the Controller. For # transports using IP addressing (e.g. rdma) this should be an IP- # based address (ex. IPv4, IPv6). It could also be a resolvable host # name (e.g. localhost). # # nqn= [OPTIONAL] # This field specifies the Discovery Controller's NVMe Qualified # Name. If not specified, this will default to the well-known DC # NQN: "nqn.2014-08.org.nvmexpress.discovery". # # trsvcid= [OPTIONAL] # This field specifies the transport service id. For transports using # IP addressing (e.g. rdma) this field is the port number. # # Depending on the transport type, this field will default to either # 8009 or 4420 as follows. # # UDP port 4420 and TCP port 4420 have been assigned by IANA # for use by NVMe over Fabrics. NVMe/RoCEv2 controllers use UDP port # 4420 by default. NVMe/iWARP controllers use TCP port 4420 by # default. # # TCP port 4420 has been assigned for use by NVMe over Fabrics and TCP # port 8009 has been assigned by IANA for use by NVMe over Fabrics # discovery. TCP port 8009 is the default TCP port for NVMe/TCP # discovery controllers. There is no default TCP port for NVMe/TCP I/O # controllers, the Transport Service Identifier (TRSVCID) field in the # Discovery Log Entry indicates the TCP port to use. # # The TCP ports that may be used for NVMe/TCP I/O controllers include # TCP port 4420, and the Dynamic and/or Private TCP ports (i.e., ports # in the TCP port number range from 49152 to 65535). NVMe/TCP I/O # controllers should not use TCP port 8009. TCP port 4420 shall not be # used for both NVMe/iWARP and NVMe/TCP at the same IP address on the # same network. # # Ref: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=nvme # # host-traddr= [OPTIONAL] # This field specifies the network address used on the host to connect # to the Controller. For TCP, this sets the source address on the # socket. # # host-iface= [OPTIONAL] # This field specifies the network interface used on the host to # connect to the Controller (e.g. IP eth1, enp2s0, enx78e7d1ea46da). # This forces the connection to be made on a specific interface # instead of letting the system decide. # # hdr-digest [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # data-digest [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # kato [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # reconnect-delay [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # ctrl-loss-tmo [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # disable-sqflow [OPTIONAL] # See definition in [Global] section. This is used to override # the value specified in the [Global] section. # # Multiple DCs may be specified on separate lines like this (this is # just an example and does not represent default values): # # controller = transport=tcp;traddr=localhost;trsvcid=8009 # controller = transport=tcp;traddr=2001:db8::370:7334;host-iface=enp0s8 # controller = transport=fc;traddr=nn-0x204600a098cbcac6:pn-0x204700a098cbcac6 # ... # # Type: String # # Default: There is no default controller. STAF will not try to # connect to a default Discovery Controller. #controller= # exclude: Excluded controllers. Using mDNS to automatically detect # and connect controllers, can result in unintentional connections # being made. This keyword allows configuring the controllers that # should not be connected to (whatever the reason may be). # # The syntax is the same as for "controller=", except that the key # host-traddr does not apply. Multiple "exclude=" keywords may # appear in the config file to define the exclusion list. # # Note 1: A minimal match approach is used to eliminate unwanted # controllers. That is, you do not need to specify all the # parameters to identify a controller. Just specifying the # host-iface, for example, can be used to exclude all controllers # on an interface. # # Note 2: "exclude=" takes precedence over "controller=". A # controller specified by the "controller=" keyword, can be # eliminated by the "exclude=" keyword. # # Syntax: Same as "controller=" above. # Type: String # # Example: # exclude = transport=tcp;traddr=fe80::2c6e:dee7:857:26bb # Eliminate a specific address # exclude = host-iface=enp0s8 # Eliminate everything on this interface #exclude= linux-nvme-nvme-stas-a8026bb/etc/stas/sys.conf.doc000066400000000000000000000055371440613556600221370ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # ============================================================================== # STorage Appliance Services (stas) - System configuration file # # In this file, options that are commented represent the default values used. # Uncommented options override the default value. [Host] # nqn: The host's unique Non-Qualified Name. A value starting with "file://" # indicates that the Host NQN can be retrieved from a separate file. # Typically, nvme-cli saves the Host NQN in /etc/nvme/hostnqn. For # compatibility with nvme-cli, nvme-stas defaults to looking for the # existance of this file and will read the NQN from it. Otherwise, you # can overwrite the default NQN by specifying its value here or # specifying another file that contains the Host NQN to use. # Type: string # Default: file:///etc/nvme/hostnqn #nqn=file:///etc/nvme/hostnqn # id: The host's unique Identifier (ID). A value starting with "file://" # indicates that the Host ID can be retrieved from a separate file. # Typically, nvme-cli saves the Host ID in /etc/nvme/hostid. For # compatibility with nvme-cli, nvme-stas defaults to looking for the # existance of this file and will read the ID from it. Otherwise, you # can overwrite the default ID by specifying its value here or # specifying another file that contains the Host ID to use. # Type: string # Default: file:///etc/nvme/hostid #id=file:///etc/nvme/hostid # key: The host's DHCHAP key to be used for authentication. This is an # optional parameter only required when authentication is needed. # A value starting with "file://" indicates that the Host Key can # be retrieved from a separate file. Typically, nvme-cli saves the # Host Key in /etc/nvme/hostkey. For compatibility with nvme-cli, # nvme-stas defaults to looking for the existance of this file and # will read the Key from it. Otherwise, you can overwrite the default # Key by specifying its value here or specifying another file that # contains an alternate Host Key to use. # Type: string # Default: file:///etc/nvme/hostkey #key=file:///etc/nvme/hostkey # symname: The host's symbolic name. This can be a string or the name of a file # containing the symbolic name. A value starting with "file://" # indicates that the Symbolic Name can be retrieved from a separate # file. # Type: string # Default: There is no default. The symbolic name is undefined by # default. #symname= linux-nvme-nvme-stas-a8026bb/meson.build000066400000000000000000000125731440613556600203210ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # project( 'nvme-stas', meson_version: '>= 0.53.0', version: '2.2.1', license: 'Apache-2.0', default_options: [ 'buildtype=release', 'prefix=/usr', 'sysconfdir=/etc', ] ) fs = import('fs') #=============================================================================== prefix = get_option('prefix') datadir = prefix / get_option('datadir') etcdir = prefix / get_option('sysconfdir') bindir = prefix / get_option('bindir') sbindir = prefix / get_option('sbindir') mandir = prefix / get_option('mandir') docdir = datadir / 'doc' / 'nvme-stas' cnfdir = etcdir / 'stas' want_man = get_option('man') want_html = get_option('html') want_readthedocs = get_option('readthedocs') buildtime_modules = [] if want_man or want_html or want_readthedocs buildtime_modules += ['lxml'] endif python3 = import('python').find_installation('python3', modules:buildtime_modules) python_version = python3.language_version() python_version_req = '>=3.6' if not python_version.version_compare(python_version_req) error('Python @0@ required. Found @1@ instead'.format(python_version_req, python_version)) endif # Check if the runtime Python modules are present. These are not needed # to build nvme-stas, but will be needed to run the tests. missing_runtime_mods = false py_modules_reqd = [ ['libnvme', 'Install python3-libnvme (deb/rpm)'], ['dasbus', 'Install python3-dasbus (deb/rpm) OR pip3 install dasbus'], ['pyudev', 'Install python3-pyudev (deb/rpm)'], ['systemd', 'Install python3-systemd (deb/rpm)'], ['gi', 'Install python3-gi (deb) OR python3-gobject (rpm)'], ] foreach p : py_modules_reqd if run_command(python3, '-c', 'import @0@'.format(p[0]), check: false).returncode() != 0 warning('Missing runtime module "@0@". @1@'.format(p[0], p[1])) missing_runtime_mods = true endif endforeach if missing_runtime_mods and get_option('rt_pymods_reqd') error('Please install missing runtime modules') endif #=============================================================================== conf = configuration_data() conf.set('VERSION', meson.project_version()) conf.set('LICENSE', meson.project_license()[0]) conf.set('BUILD_DIR', meson.current_build_dir()) conf.set('STAFD_DBUS_NAME', 'org.nvmexpress.staf') conf.set('STAFD_DBUS_PATH', '/org/nvmexpress/staf') conf.set('STACD_DBUS_NAME', 'org.nvmexpress.stac') conf.set('STACD_DBUS_PATH', '/org/nvmexpress/stac') #=============================================================================== stafd = configure_file( input: 'stafd.py', output: 'stafd', install_dir: sbindir, copy: true, ) stacd = configure_file( input: 'stacd.py', output: 'stacd', install_dir: sbindir, copy: true, ) stafctl = configure_file( input: 'stafctl.py', output: 'stafctl', install_dir: bindir, copy: true, ) stacctl = configure_file( input: 'stacctl.py', output: 'stacctl', install_dir: bindir, copy: true, ) stasadm = configure_file( input: 'stasadm.py', output: 'stasadm', install_dir: bindir, copy: true, ) #=========================================================================== install_subdir( 'etc/stas', install_dir: etcdir, ) #=========================================================================== foreach component : [ 'nvme-stas.spec', '.coveragerc', 'coverage.sh', ] configure_file( input: component + '.in', output: component, configuration: conf, ) endforeach #=========================================================================== # Make a list of modules to lint modules_to_lint = [stafd, stafctl, stacd, stacctl, stasadm] # Point Python Path to Current Build Dir. # This is used by other meson.build files. PYTHON_SEARCH_PATHS = [ conf.get('BUILD_DIR'), conf.get('BUILD_DIR') / 'subprojects' / 'libnvme', ] PYTHONPATH = ':'.join(PYTHON_SEARCH_PATHS) #=========================================================================== subdir('staslib') subdir('etc/dbus-1/system.d') subdir('usr/lib/systemd/system') subdir('test') subdir('doc') #=========================================================================== summary_dict = { 'prefix ': prefix, 'etcdir ': etcdir, 'cnfdir ': cnfdir, 'bindir ': bindir, 'sbindir ': sbindir, 'datadir ': datadir, 'mandir ': mandir, 'docdir ': docdir, 'dbus_conf_dir ': dbus_conf_dir, 'sd_unit_dir ': sd_unit_dir, 'build location ': meson.current_build_dir(), 'libnvme for tests ': libnvme_location, } summary(summary_dict, section: 'Directories') summary_dict = { 'want_man ': want_man, 'want_html ': want_html, 'want_readthedocs ': want_readthedocs, } if meson.version().version_compare('>=0.57.0') # conf.keys() foreach key : conf.keys() if key not in ['BUILD_DIR', 'VERSION', 'LICENSE'] summary_dict += { key + ' ': conf.get(key) } endif endforeach endif summary(summary_dict, section: 'Configuration', bool_yn: true) linux-nvme-nvme-stas-a8026bb/meson_options.txt000066400000000000000000000013041440613556600216020ustar00rootroot00000000000000# -*- mode: meson -*- option('man', type: 'boolean', value: false, description: 'build and install man pages') option('html', type: 'boolean', value: false, description: 'build and install html pages') option('readthedocs', type: 'boolean', value: false, description: 'to be used by Read-The-Docs documentation builder') option('libnvme-sel', type: 'combo', value: 'subproject', choices: ['subproject', 'pre-installed'], description: 'Select the libnvme to be used for testing. Either libnvme built as a "subproject", or libnvme already installed on the system.') option('rt_pymods_reqd', type: 'boolean', value: false, description: 'Make sure all run-time python modules are installed') linux-nvme-nvme-stas-a8026bb/nvme-stas.spec.in000066400000000000000000000045561440613556600213570ustar00rootroot00000000000000Name: nvme-stas Summary: NVMe STorage Appliance Services Version: @VERSION@ Release: 1%{?dist} License: @LICENSE@ URL: https://github.com/linux-nvme/nvme-stas BuildArch: noarch BuildRequires: meson BuildRequires: glib2-devel #BuildRequires: libnvme-devel BuildRequires: libxslt BuildRequires: docbook-style-xsl #BuildRequires: systemd-devel BuildRequires: systemd-rpm-macros BuildRequires: python3 #BuildRequires: python3-devel #BuildRequires: python3-pyflakes #BuildRequires: python3-pylint #BuildRequires: pylint #BuildRequires: python3-libnvme #BuildRequires: python3-dasbus #BuildRequires: python3-pyudev #BuildRequires: python3-systemd #BuildRequires: python3-gobject-devel BuildRequires: python3-lxml Requires: avahi Requires: python3-libnvme Requires: python3-dasbus Requires: python3-pyudev Requires: python3-systemd Requires: python3-gobject %description nvme-stas is a Central Discovery Controller (CDC) client for Linux. It handles Asynchronous Event Notifications (AEN), Automated NVMe subsystem connection controls, Error handling and reporting, and Automatic (zeroconf) and Manual configuration. nvme-stas is composed of two daemons: stafd (STorage Appliance Finder) and stacd (STorage Appliance Connector). %prep %autosetup -p1 -n %{name}-%{version} %build %meson --wrap-mode=nodownload -Dman=true -Dhtml=true %meson_build %install %meson_install %check %meson_test %define services stacd.service stafd.service %pre %service_add_pre %services %post %service_add_post %services %preun %service_del_preun %services %postun %service_del_postun %services %files %license LICENSE %doc README.md %dir %{_sysconfdir}/stas %config(noreplace) %{_sysconfdir}/stas/stacd.conf %config(noreplace) %{_sysconfdir}/stas/stafd.conf %{_sysconfdir}/stas/sys.conf.doc %{_datadir}/dbus-1/system.d/org.nvmexpress.*.conf %{_bindir}/stacctl %{_bindir}/stafctl %{_bindir}/stasadm %{_sbindir}/stacd %{_sbindir}/stafd %{_unitdir}/stacd.service %{_unitdir}/stafd.service %{_unitdir}/stas-config.target %{_unitdir}/stas-config@.service %dir %{python3_sitelib}/staslib %{python3_sitelib}/staslib/* %doc %{_pkgdocdir} %{_mandir}/man1/sta*.1* %{_mandir}/man5/*.5* %{_mandir}/man7/nvme*.7* %{_mandir}/man8/sta*.8* %changelog * Wed May 18 2022 Martin Belanger - Release 1.1 * Thu Mar 24 2022 Martin Belanger - Release 1.0-rc4 - linux-nvme-nvme-stas-a8026bb/stacctl.py000077500000000000000000000063211440613556600201630ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' STorage Appliance Connector Control Utility ''' import sys import json import pprint from argparse import ArgumentParser import dasbus.error from dasbus.connection import SystemMessageBus from staslib import defs def tron(args): # pylint: disable=unused-argument '''@brief Trace ON''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STACD_DBUS_NAME, defs.STACD_DBUS_PATH) iface.tron = True # pylint: disable=assigning-non-slot print(f'tron = {iface.tron}') # Read value back from stacd and print def troff(args): # pylint: disable=unused-argument '''@brief Trace OFF''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STACD_DBUS_NAME, defs.STACD_DBUS_PATH) iface.tron = False # pylint: disable=assigning-non-slot print(f'tron = {iface.tron}') # Read value back from stacd and print def _extract_cid(ctrl): return ( ctrl['transport'], ctrl['traddr'], ctrl['trsvcid'], ctrl['host-traddr'], ctrl['host-iface'], ctrl['subsysnqn'], ) def status(args): # pylint: disable=unused-argument '''@brief retrieve stacd's status information''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STACD_DBUS_NAME, defs.STACD_DBUS_PATH) info = json.loads(iface.process_info()) info['controllers'] = iface.list_controllers(True) for controller in info['controllers']: transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn = _extract_cid(controller) controller.update( json.loads(iface.controller_info(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn)) ) print(pprint.pformat(info, width=120)) def ls(args): '''@brief list the I/O controller's that stacd is connected (or trying to connect) to. ''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STACD_DBUS_NAME, defs.STACD_DBUS_PATH) info = iface.list_controllers(args.detailed) print(pprint.pformat(info, width=120)) PARSER = ArgumentParser(description='STorage Appliance Connector (STAC)') PARSER.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) SUBPARSER = PARSER.add_subparsers(title='Commands') PRSR = SUBPARSER.add_parser('tron', help='Trace ON') PRSR.set_defaults(func=tron) PRSR = SUBPARSER.add_parser('troff', help='Trace OFF') PRSR.set_defaults(func=troff) PRSR = SUBPARSER.add_parser('status', help='Show runtime status information about stacd') PRSR.set_defaults(func=status) PRSR = SUBPARSER.add_parser('ls', help='List I/O controllers') PRSR.add_argument( '-d', '--detailed', action='store_true', help='Print detailed info (default: "%(default)s")', default=False ) PRSR.set_defaults(func=ls) ARGS = PARSER.parse_args() if ARGS.version: print(f'nvme-stas {defs.VERSION}') sys.exit(0) try: ARGS.func(ARGS) except dasbus.error.DBusError: sys.exit('Unable to communicate with stacd over D-Bus. Is stacd running?') linux-nvme-nvme-stas-a8026bb/stacd.py000077500000000000000000000073041440613556600176260ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' STorage Appliance Connector Daemon ''' import sys from argparse import ArgumentParser from staslib import defs # ****************************************************************************** def parse_args(conf_file: str): '''Parse command line options''' parser = ArgumentParser(description='STorage Appliance Connector (STAC). Must be root to run this program.') parser.add_argument( '-f', '--conf-file', action='store', help='Configuration file (default: %(default)s)', default=conf_file, type=str, metavar='FILE', ) parser.add_argument( '-s', '--syslog', action='store_true', help='Send messages to syslog instead of stdout. Use this when running %(prog)s as a daemon. (default: %(default)s)', default=False, ) parser.add_argument('--tron', action='store_true', help='Trace ON. (default: %(default)s)', default=False) parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) return parser.parse_args() ARGS = parse_args(defs.STACD_CONF_FILE) if ARGS.version: print(f'nvme-stas {defs.VERSION}') print(f'libnvme {defs.LIBNVME_VERSION}') sys.exit(0) # ****************************************************************************** if __name__ == '__main__': import json import logging from staslib import log, service, stas, udev # pylint: disable=ungrouped-imports # Before going any further, make sure the script is allowed to run. stas.check_if_allowed_to_continue() class Dbus: '''This is the DBus interface that external programs can use to communicate with stacd. ''' __dbus_xml__ = stas.load_idl('stacd.idl') @property def tron(self): '''@brief Get Trace ON property''' return STAC.tron @tron.setter def tron(self, value): # pylint: disable=no-self-use '''@brief Set Trace ON property''' STAC.tron = value @property def log_level(self) -> str: '''@brief Get Log Level property''' return log.level() def process_info(self) -> str: '''@brief Get status info (for debug) @return A string representation of a json object. ''' info = { 'tron': STAC.tron, 'log-level': self.log_level, } info.update(STAC.info()) return json.dumps(info) def controller_info( # pylint: disable=too-many-arguments,no-self-use self, transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn ) -> str: '''@brief D-Bus method used to return information about a controller''' controller = STAC.get_controller(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn) return json.dumps(controller.info()) if controller else '{}' def list_controllers(self, detailed) -> list: # pylint: disable=no-self-use '''@brief Return the list of I/O controller IDs''' return [ controller.details() if detailed else controller.controller_id_dict() for controller in STAC.get_controllers() ] log.init(ARGS.syslog) STAC = service.Stac(ARGS, Dbus()) STAC.run() STAC = None ARGS = None udev.shutdown() logging.shutdown() linux-nvme-nvme-stas-a8026bb/stafctl.py000077500000000000000000000131231440613556600201640ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' STorage Appliance Finder Control Utility ''' import sys import json import pprint from argparse import ArgumentParser import dasbus.error from dasbus.connection import SystemMessageBus from staslib import defs def tron(args): # pylint: disable=unused-argument '''@brief Trace ON''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) iface.tron = True # pylint: disable=assigning-non-slot print(f'tron = {iface.tron}') # Read value back from stafd and print def troff(args): # pylint: disable=unused-argument '''@brief Trace OFF''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) iface.tron = False # pylint: disable=assigning-non-slot print(f'tron = {iface.tron}') # Read value back from stafd and print def _extract_cid(ctrl): return ( ctrl['transport'], ctrl['traddr'], ctrl['trsvcid'], ctrl['host-traddr'], ctrl['host-iface'], ctrl['subsysnqn'], ) def status(args): # pylint: disable=unused-argument '''@brief retrieve stafd's status information''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) info = json.loads(iface.process_info()) info['controllers'] = iface.list_controllers(True) for controller in info['controllers']: transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn = _extract_cid(controller) controller['log_pages'] = iface.get_log_pages(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn) controller.update( json.loads(iface.controller_info(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn)) ) print(pprint.pformat(info, width=120)) def ls(args): '''@brief list the discovery controller's that stafd is connected (or trying to connect) to. ''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) info = iface.list_controllers(args.detailed) print(pprint.pformat(info, width=120)) def dlp(args): '''@brief retrieve a controller's discovery log pages from stafd''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) info = iface.get_log_pages(args.transport, args.traddr, args.trsvcid, args.host_traddr, args.host_iface, args.nqn) print(pprint.pformat(info, width=120)) def adlp(args): '''@brief retrieve all of the controller's discovery log pages from stafd''' bus = SystemMessageBus() iface = bus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) info = json.loads(iface.get_all_log_pages(args.detailed)) print(pprint.pformat(info, width=120)) PARSER = ArgumentParser(description='STorage Appliance Finder (STAF)') PARSER.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) SUBPARSER = PARSER.add_subparsers(title='Commands') PRSR = SUBPARSER.add_parser('tron', help='Trace ON') PRSR.set_defaults(func=tron) PRSR = SUBPARSER.add_parser('troff', help='Trace OFF') PRSR.set_defaults(func=troff) PRSR = SUBPARSER.add_parser('status', help='Show runtime status information about stafd') PRSR.set_defaults(func=status) PRSR = SUBPARSER.add_parser('ls', help='List discovery controllers') PRSR.add_argument( '-d', '--detailed', action='store_true', help='Print detailed info (default: "%(default)s")', default=False, ) PRSR.set_defaults(func=ls) PRSR = SUBPARSER.add_parser('dlp', help='Show discovery log pages') PRSR.add_argument( '-t', '--transport', metavar='', action='store', help='NVMe-over-Fabrics fabric type (default: "%(default)s")', choices=['tcp', 'rdma', 'fc', 'loop'], default='tcp', ) PRSR.add_argument( '-a', '--traddr', metavar='', action='store', help='Discovery Controller\'s network address', required=True, ) PRSR.add_argument( '-s', '--trsvcid', metavar='', action='store', help='Transport service id (for IP addressing, e.g. tcp, rdma, this field is the port number)', required=True, ) PRSR.add_argument( '-w', '--host-traddr', metavar='', action='store', help='Network address used on the host to connect to the Controller (default: "%(default)s")', default='', ) PRSR.add_argument( '-f', '--host-iface', metavar='', action='store', help='This field specifies the network interface used on the host to connect to the Controller (default: "%(default)s")', default='', ) PRSR.add_argument( '-n', '--nqn', metavar='', action='store', help='This field specifies the discovery controller\'s NQN. When not specified this option defaults to "%(default)s"', default=defs.WELL_KNOWN_DISC_NQN, ) PRSR.set_defaults(func=dlp) PRSR = SUBPARSER.add_parser('adlp', help='Show all discovery log pages') PRSR.add_argument( '-d', '--detailed', action='store_true', help='Print detailed info (default: "%(default)s")', default=False, ) PRSR.set_defaults(func=adlp) ARGS = PARSER.parse_args() if ARGS.version: print(f'nvme-stas {defs.VERSION}') sys.exit(0) try: ARGS.func(ARGS) except dasbus.error.DBusError: sys.exit('Unable to communicate with stafd over D-Bus. Is stafd running?') linux-nvme-nvme-stas-a8026bb/stafd.py000077500000000000000000000124301440613556600176250ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' STorage Appliance Finder Daemon ''' import sys from argparse import ArgumentParser from staslib import defs # ****************************************************************************** def parse_args(conf_file: str): '''Parse command line options''' parser = ArgumentParser(description='STorage Appliance Finder (STAF). Must be root to run this program.') parser.add_argument( '-f', '--conf-file', action='store', help='Configuration file (default: %(default)s)', default=conf_file, type=str, metavar='FILE', ) parser.add_argument( '-s', '--syslog', action='store_true', help='Send messages to syslog instead of stdout. Use this when running %(prog)s as a daemon. (default: %(default)s)', default=False, ) parser.add_argument('--tron', action='store_true', help='Trace ON. (default: %(default)s)', default=False) parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) return parser.parse_args() ARGS = parse_args(defs.STAFD_CONF_FILE) if ARGS.version: print(f'nvme-stas {defs.VERSION}') print(f'libnvme {defs.LIBNVME_VERSION}') sys.exit(0) # ****************************************************************************** if __name__ == '__main__': import json import logging import dasbus.server.interface from staslib import log, service, stas, udev # pylint: disable=ungrouped-imports # Before going any further, make sure the script is allowed to run. stas.check_if_allowed_to_continue() class Dbus: '''This is the DBus interface that external programs can use to communicate with stafd. ''' __dbus_xml__ = stas.load_idl('stafd.idl') @dasbus.server.interface.dbus_signal def log_pages_changed( # pylint: disable=too-many-arguments self, transport: str, traddr: str, trsvcid: str, host_traddr: str, host_iface: str, subsysnqn: str, device: str, ): '''@brief Signal sent when log pages have changed.''' @dasbus.server.interface.dbus_signal def dc_removed(self): '''@brief Signal sent when log pages have changed.''' @property def tron(self): '''@brief Get Trace ON property''' return STAF.tron @tron.setter def tron(self, value): # pylint: disable=no-self-use '''@brief Set Trace ON property''' STAF.tron = value @property def log_level(self) -> str: '''@brief Get Log Level property''' return log.level() def process_info(self) -> str: '''@brief Get status info (for debug) @return A string representation of a json object. ''' info = { 'tron': STAF.tron, 'log-level': self.log_level, } info.update(STAF.info()) return json.dumps(info) def controller_info( # pylint: disable=no-self-use,too-many-arguments self, transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn ) -> str: '''@brief D-Bus method used to return information about a controller''' controller = STAF.get_controller(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn) return json.dumps(controller.info()) if controller else '{}' def get_log_pages( # pylint: disable=no-self-use,too-many-arguments self, transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn ) -> list: '''@brief D-Bus method used to retrieve the discovery log pages from one controller''' controller = STAF.get_controller(transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn) return controller.log_pages() if controller else list() def get_all_log_pages(self, detailed) -> str: # pylint: disable=no-self-use '''@brief D-Bus method used to retrieve the discovery log pages from all controllers''' log_pages = list() for controller in STAF.get_controllers(): log_pages.append( { 'discovery-controller': controller.details() if detailed else controller.controller_id_dict(), 'log-pages': controller.log_pages(), } ) return json.dumps(log_pages) def list_controllers(self, detailed) -> list: # pylint: disable=no-self-use '''@brief Return the list of discovery controller IDs''' return [ controller.details() if detailed else controller.controller_id_dict() for controller in STAF.get_controllers() ] log.init(ARGS.syslog) STAF = service.Staf(ARGS, Dbus()) STAF.run() STAF = None ARGS = None udev.shutdown() logging.shutdown() linux-nvme-nvme-stas-a8026bb/stasadm.py000077500000000000000000000151421440613556600201630ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' STorage Appliance Services Admin Tool ''' import os import sys import uuid import configparser from argparse import ArgumentParser from staslib import defs try: import hmac import hashlib except (ImportError, ModuleNotFoundError): hmac = None hashlib = None def read_from_file(fname, size): # pylint: disable=missing-function-docstring try: with open(fname) as f: # pylint: disable=unspecified-encoding data = f.read(size) if len(data) == size: return data except FileNotFoundError: pass return None def get_machine_app_specific(app_id): '''@brief Get a machine ID specific to an application. We use the value retrieved from /etc/machine-id. The documentation states that /etc/machine-id: "should be considered "confidential", and must not be exposed in untrusted environments, in particular on the network. If a stable unique identifier that is tied to the machine is needed for some application, the machine ID or any part of it must not be used directly. Instead the machine ID should be hashed with a crypto- graphic, keyed hash function, using a fixed, application-specific key. That way the ID will be properly unique, and derived in a constant way from the machine ID but there will be no way to retrieve the original machine ID from the application-specific one" @note systemd's C function sd_id128_get_machine_app_specific() was the inspiration for this code. @ref https://www.freedesktop.org/software/systemd/man/machine-id.html ''' if not hmac: return None data = read_from_file('/etc/machine-id', 32) if not data: return None hmac_obj = hmac.new(app_id, uuid.UUID(data).bytes, hashlib.sha256) id128_bytes = hmac_obj.digest()[0:16] return str(uuid.UUID(bytes=id128_bytes, version=4)) def get_uuid_from_system(): '''@brief Try to find system UUID in the following order: 1) /etc/machine-id 2) /sys/class/dmi/id/product_uuid 3) /proc/device-tree/ibm,partition-uuid ''' uuid_str = get_machine_app_specific(b'$nvmexpress.org$') if uuid_str: return uuid_str # The following files are only readable by root if os.geteuid() != 0: sys.exit('Permission denied. Root privileges required.') id128 = read_from_file('/sys/class/dmi/id/product_uuid', 36) if id128: # Swap little-endian to network order per # DMTF SMBIOS 3.0 Section 7.2.1 System UUID. swapped = ''.join([id128[x] for x in (6, 7, 4, 5, 2, 3, 0, 1, 8, 11, 12, 9, 10, 13, 16, 17, 14, 15)]) return swapped + id128[18:] return read_from_file('/proc/device-tree/ibm,partition-uuid', 36) def save(section, option, string, conf_file, fname): '''@brief Save configuration @param section: section in @conf_file where @option will be added @param option: option to be added under @section in @conf_file @param string: Text to be saved to @fname @param conf_file: Configuration file name @param fname: Optional file where @string will be saved ''' if fname and string is not None: with open(fname, 'w') as f: # pylint: disable=unspecified-encoding print(string, file=f) if conf_file: config = configparser.ConfigParser( default_section=None, allow_no_value=True, delimiters=('='), interpolation=None, strict=False ) if os.path.isfile(conf_file): config.read(conf_file) try: config.add_section(section) except configparser.DuplicateSectionError: pass if fname: string = 'file://' + fname if string is not None: config.set(section, option, string) else: config.remove_option(section, option) with open(conf_file, 'w') as f: # pylint: disable=unspecified-encoding config.write(f) def hostnqn(args): '''@brief Configure the host NQN''' uuid_str = get_uuid_from_system() or str(uuid.uuid4()) uuid_str = f'nqn.2014-08.org.nvmexpress:uuid:{uuid_str}' save('Host', 'nqn', uuid_str, args.conf_file, args.file) def hostid(args): '''@brief Configure the host ID''' save('Host', 'id', str(uuid.uuid4()), args.conf_file, args.file) def set_symname(args): '''@brief Define the host Symbolic Name''' save('Host', 'symname', args.symname, args.conf_file, args.file) def clr_symname(args): '''@brief Undefine the host NQN''' save('Host', 'symname', None, args.conf_file, None) def get_parser(): # pylint: disable=missing-function-docstring parser = ArgumentParser(description='Configuration utility for STAS.') parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) parser.add_argument( '-c', '--conf-file', action='store', help='Configuration file. Default %(default)s.', default=defs.SYS_CONF_FILE, type=str, metavar='FILE', ) subparser = parser.add_subparsers(title='Commands') prsr = subparser.add_parser('hostnqn', help='Configure the host NQN. The NQN is auto-generated.') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the NQN.', type=str, metavar='FILE' ) prsr.set_defaults(cmd=hostnqn) prsr = subparser.add_parser('hostid', help='Configure the host ID. The ID is auto-generated.') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the ID.', type=str, metavar='FILE' ) prsr.set_defaults(cmd=hostid) prsr = subparser.add_parser('set-symname', help='Set the host symbolic') prsr.add_argument( '-f', '--file', action='store', help='Optional file where to save the symbolic name.', type=str, metavar='FILE' ) prsr.add_argument('symname', action='store', help='Symbolic name', default=None, metavar='SYMNAME') prsr.set_defaults(cmd=set_symname) prsr = subparser.add_parser('clear-symname', help='Clear the host symbolic') prsr.set_defaults(cmd=clr_symname) return parser PARSER = get_parser() ARGS = PARSER.parse_args() if ARGS.version: print(f'nvme-stas {defs.VERSION}') sys.exit(0) try: ARGS.cmd(ARGS) except AttributeError as ex: print(str(ex)) PARSER.print_usage() linux-nvme-nvme-stas-a8026bb/staslib/000077500000000000000000000000001440613556600176105ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/staslib/.gitignore000066400000000000000000000000141440613556600215730ustar00rootroot00000000000000__pycache__ linux-nvme-nvme-stas-a8026bb/staslib/__init__.py000066400000000000000000000005211440613556600217170ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''STorage Appliance Services''' __version__ = '@VERSION@' linux-nvme-nvme-stas-a8026bb/staslib/avahi.py000066400000000000000000000424161440613556600212610ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' Module that provides a way to retrieve discovered services from the Avahi daemon over D-Bus. ''' import socket import typing import logging import functools import dasbus.error import dasbus.connection import dasbus.client.proxy import dasbus.client.observer from gi.repository import GLib from staslib import defs, conf, gutil def _txt2dict(txt: list): '''@param txt: A list of list of integers. The integers are the ASCII value of printable text characters. ''' the_dict = dict() for list_of_chars in txt: try: string = functools.reduce(lambda accumulator, c: accumulator + chr(c), list_of_chars, '') key, val = string.split("=") the_dict[key.lower()] = val except Exception: # pylint: disable=broad-except pass return the_dict def _proto2trans(protocol): '''Return the matching transport for the given protocol.''' if protocol is None: return None protocol = protocol.strip().lower() if protocol == 'tcp': return 'tcp' if protocol in ('roce', 'iwarp', 'rdma'): return 'rdma' return None # ****************************************************************************** class Avahi: # pylint: disable=too-many-instance-attributes '''@brief Avahi Server proxy. Set up the D-Bus connection to the Avahi daemon and register to be notified when services of a certain type (stype) are discovered or lost. ''' DBUS_NAME = 'org.freedesktop.Avahi' DBUS_INTERFACE_SERVICE_BROWSER = DBUS_NAME + '.ServiceBrowser' DBUS_INTERFACE_SERVICE_RESOLVER = DBUS_NAME + '.ServiceResolver' LOOKUP_USE_MULTICAST = 2 IF_UNSPEC = -1 PROTO_INET = 0 PROTO_INET6 = 1 PROTO_UNSPEC = -1 LOOKUP_RESULT_LOCAL = 8 # This record/service resides on and was announced by the local host LOOKUP_RESULT_CACHED = 1 # This response originates from the cache LOOKUP_RESULT_STATIC = 32 # The returned data has been defined statically by some configuration option LOOKUP_RESULT_OUR_OWN = 16 # This service belongs to the same local client as the browser object LOOKUP_RESULT_WIDE_AREA = 2 # This response originates from wide area DNS LOOKUP_RESULT_MULTICAST = 4 # This response originates from multicast DNS result_flags = { LOOKUP_RESULT_LOCAL: 'local', LOOKUP_RESULT_CACHED: 'cache', LOOKUP_RESULT_STATIC: 'static', LOOKUP_RESULT_OUR_OWN: 'own', LOOKUP_RESULT_WIDE_AREA: 'wan', LOOKUP_RESULT_MULTICAST: 'mcast', } protos = {PROTO_INET: 'IPv4', PROTO_INET6: 'IPv6', PROTO_UNSPEC: 'uspecified'} @classmethod def result_flags_as_string(cls, flags): '''Convert flags to human-readable string''' return '+'.join((value for flag, value in Avahi.result_flags.items() if (flags & flag) != 0)) @classmethod def protocol_as_string(cls, proto): '''Convert protocol codes to human-readable strings''' return Avahi.protos.get(proto, 'unknown') # ========================================================================== def __init__(self, sysbus, change_cb): self._change_cb = change_cb self._services = dict() self._sysbus = sysbus self._stypes = set() self._service_browsers = dict() # Avahi is an on-demand service. If, for some reason, the avahi-daemon # were to stop, we need to try to contact it for it to restart. For # example, when installing the avahi-daemon package on a running system, # the daemon doesn't get started right away. It needs another process to # access it over D-Bus to wake it up. The following timer is used to # periodically query the avahi-daemon until we successfully establish # first contact. self._kick_avahi_tmr = gutil.GTimer(60, self._on_kick_avahi) # Subscribe for Avahi signals (i.e. events). This must be done before # any Browser or Resolver is created to avoid race conditions and # missed events. self._subscriptions = [ self._sysbus.connection.signal_subscribe( Avahi.DBUS_NAME, Avahi.DBUS_INTERFACE_SERVICE_BROWSER, 'ItemNew', None, None, 0, self._service_discovered, ), self._sysbus.connection.signal_subscribe( Avahi.DBUS_NAME, Avahi.DBUS_INTERFACE_SERVICE_BROWSER, 'ItemRemove', None, None, 0, self._service_removed, ), self._sysbus.connection.signal_subscribe( Avahi.DBUS_NAME, Avahi.DBUS_INTERFACE_SERVICE_BROWSER, 'Failure', None, None, 0, self._failure_handler ), self._sysbus.connection.signal_subscribe( Avahi.DBUS_NAME, Avahi.DBUS_INTERFACE_SERVICE_RESOLVER, 'Found', None, None, 0, self._service_identified ), self._sysbus.connection.signal_subscribe( Avahi.DBUS_NAME, Avahi.DBUS_INTERFACE_SERVICE_RESOLVER, 'Failure', None, None, 0, self._failure_handler ), ] self._avahi = self._sysbus.get_proxy(Avahi.DBUS_NAME, '/') self._avahi_watcher = dasbus.client.observer.DBusObserver(self._sysbus, Avahi.DBUS_NAME) self._avahi_watcher.service_available.connect(self._avahi_available) self._avahi_watcher.service_unavailable.connect(self._avahi_unavailable) self._avahi_watcher.connect_once_available() def kill(self): '''@brief Clean up object''' logging.debug('Avahi.kill()') self._kick_avahi_tmr.kill() self._kick_avahi_tmr = None for subscription in self._subscriptions: self._sysbus.connection.signal_unsubscribe(subscription) self._subscriptions = list() self._disconnect() self._avahi_watcher.service_available.disconnect() self._avahi_watcher.service_unavailable.disconnect() self._avahi_watcher.disconnect() self._avahi_watcher = None dasbus.client.proxy.disconnect_proxy(self._avahi) self._avahi = None self._change_cb = None self._sysbus = None def info(self) -> dict: '''@brief return debug info about this object''' services = dict() for service, obj in self._services.items(): interface, protocol, name, stype, domain = service key = f'({socket.if_indextoname(interface)}, {Avahi.protos.get(protocol, "unknown")}, {name}.{domain}, {stype})' services[key] = obj.get('data', {}) info = { 'avahi wake up timer': str(self._kick_avahi_tmr), 'service types': list(self._stypes), 'services': services, } return info def get_controllers(self) -> list: '''@brief Get the discovery controllers as a list of dict() as follows: [ { 'transport': tcp, 'traddr': str(), 'trsvcid': str(), 'host-iface': str(), 'subsysnqn': 'nqn.2014-08.org.nvmexpress.discovery', }, { 'transport': tcp, 'traddr': str(), 'trsvcid': str(), 'host-iface': str(), 'subsysnqn': 'nqn.2014-08.org.nvmexpress.discovery', }, [...] ] ''' return [service['data'] for service in self._services.values() if len(service['data'])] def config_stypes(self, stypes: list): '''@brief Configure the service types that we want to discover. @param stypes: A list of services types, e.g. ['_nvme-disc._tcp'] ''' self._stypes = set(stypes) success = self._configure_browsers() if not success: self._kick_avahi_tmr.start() def kick_start(self): '''@brief We use this to kick start the Avahi daemon (i.e. socket activation). ''' self._kick_avahi_tmr.clear() def _disconnect(self): logging.debug('Avahi._disconnect()') for service in self._services.values(): resolver = service.pop('resolver', None) if resolver is not None: try: resolver.Free() dasbus.client.proxy.disconnect_proxy(resolver) except (AttributeError, dasbus.error.DBusError) as ex: logging.debug('Avahi._disconnect() - Failed to Free() resolver. %s', ex) self._services = dict() for browser in self._service_browsers.values(): try: browser.Free() dasbus.client.proxy.disconnect_proxy(browser) except (AttributeError, dasbus.error.DBusError) as ex: logging.debug('Avahi._disconnect() - Failed to Free() browser. %s', ex) self._service_browsers = dict() def _on_kick_avahi(self): try: # try to contact avahi-daemon. This is just a wake # up call in case the avahi-daemon was sleeping. self._avahi.GetVersionString() except dasbus.error.DBusError: return GLib.SOURCE_CONTINUE return GLib.SOURCE_REMOVE def _avahi_available(self, _avahi_watcher): '''@brief Hook up DBus signal handlers for signals from stafd.''' logging.info('avahi-daemon service available, zeroconf supported.') success = self._configure_browsers() if not success: self._kick_avahi_tmr.start() def _avahi_unavailable(self, _avahi_watcher): self._disconnect() logging.warning('avahi-daemon not available, zeroconf not supported.') self._kick_avahi_tmr.start() def _configure_browsers(self): stypes_cur = set(self._service_browsers.keys()) stypes_to_add = self._stypes - stypes_cur stypes_to_rm = stypes_cur - self._stypes logging.debug('Avahi._configure_browsers() - stypes_to_rm = %s', list(stypes_to_rm)) logging.debug('Avahi._configure_browsers() - stypes_to_add = %s', list(stypes_to_add)) for stype_to_rm in stypes_to_rm: browser = self._service_browsers.pop(stype_to_rm, None) if browser is not None: try: browser.Free() dasbus.client.proxy.disconnect_proxy(browser) except (AttributeError, dasbus.error.DBusError) as ex: logging.debug('Avahi._configure_browsers() - Failed to Free() browser. %s', ex) # Find the cached services corresponding to stype_to_rm and remove them services_to_rm = [service for service in self._services if service[3] == stype_to_rm] for service in services_to_rm: resolver = self._services.pop(service, {}).pop('resolver', None) if resolver is not None: try: resolver.Free() dasbus.client.proxy.disconnect_proxy(resolver) except (AttributeError, dasbus.error.DBusError) as ex: logging.debug('Avahi._configure_browsers() - Failed to Free() resolver. %s', ex) for stype in stypes_to_add: try: obj_path = self._avahi.ServiceBrowserNew( Avahi.IF_UNSPEC, Avahi.PROTO_UNSPEC, stype, 'local', Avahi.LOOKUP_USE_MULTICAST ) self._service_browsers[stype] = self._sysbus.get_proxy(Avahi.DBUS_NAME, obj_path) except dasbus.error.DBusError as ex: logging.debug('Avahi._configure_browsers() - Failed to contact avahi-daemon. %s', ex) logging.warning('avahi-daemon not available, operating w/o mDNS discovery.') return False return True def _service_discovered( self, _connection, _sender_name: str, _object_path: str, _interface_name: str, _signal_name: str, args: typing.Tuple[int, int, str, str, str, int], *_user_data, ): (interface, protocol, name, stype, domain, flags) = args logging.debug( 'Avahi._service_discovered() - interface=%s (%s), protocol=%s, stype=%s, domain=%s, flags=%s %-14s name=%s', interface, socket.if_indextoname(interface), Avahi.protocol_as_string(protocol), stype, domain, flags, '(' + Avahi.result_flags_as_string(flags) + '),', name, ) service = (interface, protocol, name, stype, domain) if service not in self._services: try: obj_path = self._avahi.ServiceResolverNew( interface, protocol, name, stype, domain, Avahi.PROTO_UNSPEC, Avahi.LOOKUP_USE_MULTICAST ) self._services[service] = { 'resolver': self._sysbus.get_proxy(Avahi.DBUS_NAME, obj_path), 'data': {}, } except dasbus.error.DBusError as ex: logging.warning('Failed to create resolver: "%s", "%s", "%s". %s', interface, name, stype, ex) def _service_removed( self, _connection, _sender_name: str, _object_path: str, _interface_name: str, _signal_name: str, args: typing.Tuple[int, int, str, str, str, int], *_user_data, ): (interface, protocol, name, stype, domain, flags) = args logging.debug( 'Avahi._service_removed() - interface=%s (%s), protocol=%s, stype=%s, domain=%s, flags=%s %-14s name=%s', interface, socket.if_indextoname(interface), Avahi.protocol_as_string(protocol), stype, domain, flags, '(' + Avahi.result_flags_as_string(flags) + '),', name, ) service = (interface, protocol, name, stype, domain) resolver = self._services.pop(service, {}).pop('resolver', None) if resolver is not None: try: resolver.Free() dasbus.client.proxy.disconnect_proxy(resolver) except (AttributeError, dasbus.error.DBusError) as ex: logging.debug('Avahi._service_removed() - Failed to Free() resolver. %s', ex) self._change_cb() def _service_identified( # pylint: disable=too-many-locals self, _connection, _sender_name: str, _object_path: str, _interface_name: str, _signal_name: str, args: typing.Tuple[int, int, str, str, str, str, int, str, int, list, int], *_user_data, ): (interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags) = args txt = _txt2dict(txt) logging.debug( 'Avahi._service_identified() - interface=%s (%s), protocol=%s, stype=%s, domain=%s, flags=%s %-14s name=%s, host=%s, aprotocol=%s, address=%s, port=%s, txt=%s', interface, socket.if_indextoname(interface), Avahi.protocol_as_string(protocol), stype, domain, flags, '(' + Avahi.result_flags_as_string(flags) + '),', name, host, Avahi.protocol_as_string(aprotocol), address, port, txt, ) service = (interface, protocol, name, stype, domain) if service in self._services: transport = _proto2trans(txt.get('p')) if transport is not None: self._services[service]['data'] = { 'transport': transport, 'traddr': address.strip(), 'trsvcid': str(port).strip(), # host-iface permitted for tcp alone and not rdma 'host-iface': socket.if_indextoname(interface).strip() if transport == 'tcp' else '', 'subsysnqn': txt.get('nqn', defs.WELL_KNOWN_DISC_NQN).strip() if conf.NvmeOptions().discovery_supp else defs.WELL_KNOWN_DISC_NQN, } self._change_cb() else: logging.error( 'Received invalid/undefined protocol in mDNS TXT field: address=%s, iface=%s, TXT=%s', address, socket.if_indextoname(interface).strip(), txt, ) def _failure_handler( # pylint: disable=no-self-use self, _connection, _sender_name: str, _object_path: str, interface_name: str, _signal_name: str, args: typing.Tuple[str], *_user_data, ): (error,) = args if 'ServiceResolver' not in interface_name or 'TimeoutError' not in error: # ServiceResolver may fire a timeout event after being Free'd(). This seems to be normal. logging.error('Avahi._failure_handler() - name=%s, error=%s', interface_name, error) linux-nvme-nvme-stas-a8026bb/staslib/conf.py000066400000000000000000000641071440613556600211170ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''nvme-stas configuration module''' import re import os import sys import logging import functools import configparser from staslib import defs, singleton, timeparse __TOKEN_RE = re.compile(r'\s*;\s*') __OPTION_RE = re.compile(r'\s*=\s*') class InvalidOption(Exception): '''Exception raised when an invalid option value is detected''' def _parse_controller(controller): '''@brief Parse a "controller" entry. Controller entries are strings composed of several configuration parameters delimited by semi-colons. Each configuration parameter is specified as a "key=value" pair. @return A dictionary of key-value pairs. ''' options = dict() tokens = __TOKEN_RE.split(controller) for token in tokens: if token: try: option, val = __OPTION_RE.split(token) options[option.strip()] = val.strip() except ValueError: pass return options def _parse_single_val(text): if isinstance(text, str): return text if not isinstance(text, list) or len(text) == 0: return None return text[-1] def _parse_list(text): return text if isinstance(text, list) else [text] def _to_int(text): try: return int(_parse_single_val(text)) except (ValueError, TypeError): raise InvalidOption # pylint: disable=raise-missing-from def _to_bool(text, positive='true'): return _parse_single_val(text).lower() == positive def _to_ncc(text): value = _to_int(text) if value == 1: # 1 is invalid. A minimum of 2 is required (with the exception of 0, which is valid). value = 2 return value def _to_ip_family(text): return tuple((4 if text == 'ipv4' else 6 for text in _parse_single_val(text).split('+'))) # ****************************************************************************** class OrderedMultisetDict(dict): '''This class is used to change the behavior of configparser.ConfigParser and allow multiple configuration parameters with the same key. The result is a list of values. ''' def __setitem__(self, key, value): if key in self and isinstance(value, list): self[key].extend(value) else: super().__setitem__(key, value) def __getitem__(self, key): value = super().__getitem__(key) if isinstance(value, str): return value.split('\n') return value class SvcConf(metaclass=singleton.Singleton): # pylint: disable=too-many-public-methods '''Read and cache configuration file.''' OPTION_CHECKER = { 'Global': { 'tron': { 'convert': _to_bool, 'default': False, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'kato': { 'convert': _to_int, }, 'pleo': { 'convert': functools.partial(_to_bool, positive='enabled'), 'default': True, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('disabled', 'enabled'), }, 'ip-family': { 'convert': _to_ip_family, 'default': (4, 6), 'txt-chk': lambda text: _parse_single_val(text) in ('ipv4', 'ipv6', 'ipv4+ipv6', 'ipv6+ipv4'), }, 'queue-size': { 'convert': _to_int, 'rng-chk': lambda value: None if value in range(16, 1025) else range(16, 1025), }, 'hdr-digest': { 'convert': _to_bool, 'default': False, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'data-digest': { 'convert': _to_bool, 'default': False, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'ignore-iface': { 'convert': _to_bool, 'default': False, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'nr-io-queues': { 'convert': _to_int, }, 'ctrl-loss-tmo': { 'convert': _to_int, }, 'disable-sqflow': { 'convert': _to_bool, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'nr-poll-queues': { 'convert': _to_int, }, 'nr-write-queues': { 'convert': _to_int, }, 'reconnect-delay': { 'convert': _to_int, }, ### BEGIN: LEGACY SECTION TO BE REMOVED ### 'persistent-connections': { 'convert': _to_bool, 'default': False, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, ### END: LEGACY SECTION TO BE REMOVED ### }, 'Service Discovery': { 'zeroconf': { 'convert': functools.partial(_to_bool, positive='enabled'), 'default': True, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('disabled', 'enabled'), }, }, 'Discovery controller connection management': { 'persistent-connections': { 'convert': _to_bool, 'default': True, 'txt-chk': lambda text: _parse_single_val(text).lower() in ('false', 'true'), }, 'zeroconf-connections-persistence': { 'convert': lambda text: timeparse.timeparse(_parse_single_val(text)), 'default': timeparse.timeparse('72hours'), }, }, 'I/O controller connection management': { 'disconnect-scope': { 'convert': _parse_single_val, 'default': 'only-stas-connections', 'txt-chk': lambda text: _parse_single_val(text) in ('only-stas-connections', 'all-connections-matching-disconnect-trtypes', 'no-disconnect'), }, 'disconnect-trtypes': { # Use set() to eliminate potential duplicates 'convert': lambda text: set(_parse_single_val(text).split('+')), 'default': [ 'tcp', ], 'lst-chk': ('tcp', 'rdma', 'fc'), }, 'connect-attempts-on-ncc': { 'convert': _to_ncc, 'default': 0, }, }, 'Controllers': { 'controller': { 'convert': _parse_list, 'default': [], }, 'exclude': { 'convert': _parse_list, 'default': [], }, ### BEGIN: LEGACY SECTION TO BE REMOVED ### 'blacklist': { 'convert': _parse_list, 'default': [], }, ### END: LEGACY SECTION TO BE REMOVED ### }, } def __init__(self, default_conf=None, conf_file='/dev/null'): self._config = None self._defaults = default_conf if default_conf else {} if self._defaults is not None and len(self._defaults) != 0: self._valid_conf = {} for section, option in self._defaults: self._valid_conf.setdefault(section, set()).add(option) else: self._valid_conf = None self._conf_file = conf_file self.reload() def reload(self): '''@brief Reload the configuration file.''' self._config = self._read_conf_file() @property def conf_file(self): '''Return the configuration file name''' return self._conf_file def set_conf_file(self, fname): '''Set the configuration file name and reload config''' self._conf_file = fname self.reload() def get_option(self, section, option, ignore_default=False): # pylint: disable=too-many-locals '''Retrieve @option from @section, convert raw text to appropriate object type, and validate.''' try: checker = self.OPTION_CHECKER[section][option] except KeyError: logging.error('Requesting invalid section=%s and/or option=%s', section, option) raise default = checker.get('default', None) try: text = self._config.get(section=section, option=option) except (configparser.NoSectionError, configparser.NoOptionError, KeyError): return None if ignore_default else self._defaults.get((section, option), default) return self._check(text, section, option, default) tron = property(functools.partial(get_option, section='Global', option='tron')) kato = property(functools.partial(get_option, section='Global', option='kato')) ip_family = property(functools.partial(get_option, section='Global', option='ip-family')) queue_size = property(functools.partial(get_option, section='Global', option='queue-size')) hdr_digest = property(functools.partial(get_option, section='Global', option='hdr-digest')) data_digest = property(functools.partial(get_option, section='Global', option='data-digest')) ignore_iface = property(functools.partial(get_option, section='Global', option='ignore-iface')) pleo_enabled = property(functools.partial(get_option, section='Global', option='pleo')) nr_io_queues = property(functools.partial(get_option, section='Global', option='nr-io-queues')) ctrl_loss_tmo = property(functools.partial(get_option, section='Global', option='ctrl-loss-tmo')) disable_sqflow = property(functools.partial(get_option, section='Global', option='disable-sqflow')) nr_poll_queues = property(functools.partial(get_option, section='Global', option='nr-poll-queues')) nr_write_queues = property(functools.partial(get_option, section='Global', option='nr-write-queues')) reconnect_delay = property(functools.partial(get_option, section='Global', option='reconnect-delay')) zeroconf_enabled = property(functools.partial(get_option, section='Service Discovery', option='zeroconf')) zeroconf_persistence_sec = property( functools.partial( get_option, section='Discovery controller connection management', option='zeroconf-connections-persistence' ) ) disconnect_scope = property( functools.partial(get_option, section='I/O controller connection management', option='disconnect-scope') ) disconnect_trtypes = property( functools.partial(get_option, section='I/O controller connection management', option='disconnect-trtypes') ) connect_attempts_on_ncc = property( functools.partial(get_option, section='I/O controller connection management', option='connect-attempts-on-ncc') ) @property def stypes(self): '''@brief Get the DNS-SD/mDNS service types.''' return ['_nvme-disc._tcp', '_nvme-disc._udp'] if self.zeroconf_enabled else list() @property def persistent_connections(self): '''@brief return the "persistent-connections" config parameter''' section = 'Discovery controller connection management' option = 'persistent-connections' value = self.get_option(section, option, ignore_default=True) legacy = self.get_option('Global', 'persistent-connections', ignore_default=True) if value is None and legacy is None: return self._defaults.get((section, option), True) return value or legacy def get_controllers(self): '''@brief Return the list of controllers in the config file. Each controller is in the form of a dictionary as follows. Note that some of the keys are optional. { 'transport': [TRANSPORT], 'traddr': [TRADDR], 'trsvcid': [TRSVCID], 'host-traddr': [TRADDR], 'host-iface': [IFACE], 'subsysnqn': [NQN], 'dhchap-ctrl-secret': [KEY], 'hdr-digest': [BOOL] 'data-digest': [BOOL] 'nr-io-queues': [NUMBER] 'nr-write-queues': [NUMBER] 'nr-poll-queues': [NUMBER] 'queue-size': [SIZE] 'kato': [KATO] 'reconnect-delay': [SECONDS] 'ctrl-loss-tmo': [SECONDS] 'disable-sqflow': [BOOL] } ''' controller_list = self.get_option('Controllers', 'controller') cids = [_parse_controller(controller) for controller in controller_list] for cid in cids: try: # replace 'nqn' key by 'subsysnqn', if present. cid['subsysnqn'] = cid.pop('nqn') except KeyError: pass # Verify values of the options used to overload the matching [Global] options for option in cid: if option in self.OPTION_CHECKER['Global']: value = self._check(cid[option], 'Global', option, None) if value is not None: cid[option] = value return cids def get_excluded(self): '''@brief Return the list of excluded controllers in the config file. Each excluded controller is in the form of a dictionary as follows. All the keys are optional. { 'transport': [TRANSPORT], 'traddr': [TRADDR], 'trsvcid': [TRSVCID], 'host-iface': [IFACE], 'subsysnqn': [NQN], } ''' controller_list = self.get_option('Controllers', 'exclude') # 2022-09-20: Look for "blacklist". This is for backwards compatibility # with releases 1.0 to 1.1.6. This is to be phased out (i.e. remove by 2024) controller_list += self.get_option('Controllers', 'blacklist') excluded = [_parse_controller(controller) for controller in controller_list] for controller in excluded: controller.pop('host-traddr', None) # remove host-traddr try: # replace 'nqn' key by 'subsysnqn', if present. controller['subsysnqn'] = controller.pop('nqn') except KeyError: pass return excluded def _check(self, text, section, option, default): checker = self.OPTION_CHECKER[section][option] text_checker = checker.get('txt-chk', None) if text_checker is not None and not text_checker(text): logging.warning( 'File:%s [%s]: %s - Text check found invalid value "%s". Default will be used', self.conf_file, section, option, text, ) return self._defaults.get((section, option), default) converter = checker.get('convert', None) try: value = converter(text) except InvalidOption: logging.warning( 'File:%s [%s]: %s - Data converter found invalid value "%s". Default will be used', self.conf_file, section, option, text, ) return self._defaults.get((section, option), default) value_in_range = checker.get('rng-chk', None) if value_in_range is not None: expected_range = value_in_range(value) if expected_range is not None: logging.warning( 'File:%s [%s]: %s - "%s" is not within range %s..%s. Default will be used', self.conf_file, section, option, value, min(expected_range), max(expected_range), ) return self._defaults.get((section, option), default) list_checker = checker.get('lst-chk', None) if list_checker: values = set() for item in value: if item not in list_checker: logging.warning( 'File:%s [%s]: %s - List checker found invalid item "%s" will be ignored.', self.conf_file, section, option, item, ) else: values.add(item) if len(values) == 0: return self._defaults.get((section, option), default) value = list(values) return value def _read_conf_file(self): '''@brief Read the configuration file if the file exists.''' config = configparser.ConfigParser( default_section=None, allow_no_value=True, delimiters=('='), interpolation=None, strict=False, dict_type=OrderedMultisetDict, ) if self._conf_file and os.path.isfile(self._conf_file): config.read(self._conf_file) # Parse Configuration and validate. if self._valid_conf is not None: invalid_sections = set() for section in config.sections(): if section not in self._valid_conf: invalid_sections.add(section) else: invalid_options = set() for option in config.options(section): if option not in self._valid_conf.get(section, []): invalid_options.add(option) if len(invalid_options) != 0: logging.error( 'File:%s [%s] contains invalid options: %s', self.conf_file, section, invalid_options, ) if len(invalid_sections) != 0: logging.error( 'File:%s contains invalid sections: %s', self.conf_file, invalid_sections, ) return config # ****************************************************************************** class SysConf(metaclass=singleton.Singleton): '''Read and cache the host configuration file.''' def __init__(self, conf_file=defs.SYS_CONF_FILE): self._config = None self._conf_file = conf_file self.reload() def reload(self): '''@brief Reload the configuration file.''' self._config = self._read_conf_file() @property def conf_file(self): '''Return the configuration file name''' return self._conf_file def set_conf_file(self, fname): '''Set the configuration file name and reload config''' self._conf_file = fname self.reload() def as_dict(self): '''Return configuration as a dictionary''' return { 'hostnqn': self.hostnqn, 'hostid': self.hostid, 'hostkey': self.hostkey, 'symname': self.hostsymname, } @property def hostnqn(self): '''@brief return the host NQN @return: Host NQN @raise: Host NQN is mandatory. The program will terminate if a Host NQN cannot be determined. ''' try: value = self.__get_value('Host', 'nqn', defs.NVME_HOSTNQN) except FileNotFoundError as ex: sys.exit(f'Error reading mandatory Host NQN (see stasadm --help): {ex}') if value is not None and not value.startswith('nqn.'): sys.exit(f'Error Host NQN "{value}" should start with "nqn."') return value @property def hostid(self): '''@brief return the host ID @return: Host ID @raise: Host ID is mandatory. The program will terminate if a Host ID cannot be determined. ''' try: value = self.__get_value('Host', 'id', defs.NVME_HOSTID) except FileNotFoundError as ex: sys.exit(f'Error reading mandatory Host ID (see stasadm --help): {ex}') return value @property def hostkey(self): '''@brief return the host key @return: Host key @raise: Host key is optional, but mandatory if authorization will be performed. ''' try: value = self.__get_value('Host', 'key', defs.NVME_HOSTKEY) except FileNotFoundError as ex: logging.info('Host key undefined: %s', ex) value = None return value @property def hostsymname(self): '''@brief return the host symbolic name (or None) @return: symbolic name or None ''' try: value = self.__get_value('Host', 'symname') except FileNotFoundError as ex: logging.warning('Error reading host symbolic name (will remain undefined): %s', ex) value = None return value def _read_conf_file(self): '''@brief Read the configuration file if the file exists.''' config = configparser.ConfigParser( default_section=None, allow_no_value=True, delimiters=('='), interpolation=None, strict=False ) if os.path.isfile(self._conf_file): config.read(self._conf_file) return config def __get_value(self, section, option, default_file=None): '''@brief A configuration file consists of sections, each led by a [section] header, followed by key/value entries separated by a equal sign (=). This method retrieves the value associated with the key @option from the section @section. If the value starts with the string "file://", then the value will be retrieved from that file. @param section: Configuration section @param option: The key to look for @param default_file: A file that contains the default value @return: On success, the value associated with the key. On failure, this method will return None is a default_file is not specified, or will raise an exception if a file is not found. @raise: This method will raise the FileNotFoundError exception if the value retrieved is a file that does not exist. ''' try: value = self._config.get(section=section, option=option) if not value.startswith('file://'): return value file = value[7:] except (configparser.NoSectionError, configparser.NoOptionError, KeyError): if default_file is None: return None file = default_file try: with open(file) as f: # pylint: disable=unspecified-encoding return f.readline().split()[0] except IndexError: return None # ****************************************************************************** class NvmeOptions(metaclass=singleton.Singleton): '''Object used to read and cache contents of file /dev/nvme-fabrics. Note that this file was not readable prior to Linux 5.16. ''' def __init__(self): # Supported options can be determined by looking at the kernel version # or by reading '/dev/nvme-fabrics'. The ability to read the options # from '/dev/nvme-fabrics' was only introduced in kernel 5.17, but may # have been backported to older kernels. In any case, if the kernel # version meets the minimum version for that option, then we don't # even need to read '/dev/nvme-fabrics'. self._supported_options = { 'discovery': defs.KERNEL_VERSION >= defs.KERNEL_TP8013_MIN_VERSION, 'host_iface': defs.KERNEL_VERSION >= defs.KERNEL_IFACE_MIN_VERSION, 'dhchap_secret': defs.KERNEL_VERSION >= defs.KERNEL_HOSTKEY_MIN_VERSION, 'dhchap_ctrl_secret': defs.KERNEL_VERSION >= defs.KERNEL_CTRLKEY_MIN_VERSION, } # If some of the options are False, we need to check wether they can be # read from '/dev/nvme-fabrics'. This method allows us to determine that # an older kernel actually supports a specific option because it was # backported to that kernel. if not all(self._supported_options.values()): # At least one option is False. try: with open('/dev/nvme-fabrics') as f: # pylint: disable=unspecified-encoding options = [option.split('=')[0].strip() for option in f.readline().rstrip('\n').split(',')] except PermissionError: # Must be root to read this file raise except (OSError, FileNotFoundError): logging.warning('Cannot determine which NVMe options the kernel supports') else: for option, supported in self._supported_options.items(): if not supported: self._supported_options[option] = option in options def __str__(self): return f'supported options: {self._supported_options}' def get(self): '''get the supported options as a dict''' return self._supported_options @property def discovery_supp(self): '''This option adds support for TP8013''' return self._supported_options['discovery'] @property def host_iface_supp(self): '''This option allows forcing connections to go over a specific interface regardless of the routing tables. ''' return self._supported_options['host_iface'] @property def dhchap_hostkey_supp(self): '''This option allows specifying the host DHCHAP key used for authentication.''' return self._supported_options['dhchap_secret'] @property def dhchap_ctrlkey_supp(self): '''This option allows specifying the controller DHCHAP key used for authentication.''' return self._supported_options['dhchap_ctrl_secret'] linux-nvme-nvme-stas-a8026bb/staslib/ctrl.py000066400000000000000000001054061440613556600211340ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''This module defines the base Controller object from which the Dc (Discovery Controller) and Ioc (I/O Controller) objects are derived.''' import time import inspect import logging from gi.repository import GLib from libnvme import nvme from staslib import conf, defs, gutil, trid, udev, stas DLP_CHANGED = ( (nvme.NVME_LOG_LID_DISCOVER << 16) | (nvme.NVME_AER_NOTICE_DISC_CHANGED << 8) | nvme.NVME_AER_NOTICE ) # 0x70f002 def get_eflags(dlpe): '''@brief Return eflags field of dlpe''' return int(dlpe.get('eflags', 0)) if dlpe else 0 def get_ncc(eflags: int): '''@brief Return True if Not Connected to CDC bit is asserted, False otherwise''' return eflags & nvme.NVMF_DISC_EFLAGS_NCC != 0 def dlp_supp_opts_as_string(dlp_supp_opts: int): '''@brief Return the list of options supported by the Get discovery log page command. ''' data = { nvme.NVMF_LOG_DISC_LID_EXTDLPES: "EXTDLPES", nvme.NVMF_LOG_DISC_LID_PLEOS: "PLEOS", nvme.NVMF_LOG_DISC_LID_ALLSUBES: "ALLSUBES", } return [txt for msk, txt in data.items() if dlp_supp_opts & msk] # ****************************************************************************** class Controller(stas.ControllerABC): # pylint: disable=too-many-instance-attributes '''@brief Base class used to manage the connection to a controller.''' def __init__(self, tid: trid.TID, service, discovery_ctrl: bool = False): sysconf = conf.SysConf() self._nvme_options = conf.NvmeOptions() self._root = nvme.root() self._host = nvme.host( self._root, hostnqn=sysconf.hostnqn, hostid=sysconf.hostid, hostsymname=sysconf.hostsymname ) self._host.dhchap_key = sysconf.hostkey if self._nvme_options.dhchap_hostkey_supp else None self._udev = udev.UDEV self._device = None # Refers to the nvme device (e.g. /dev/nvme[n]) self._ctrl = None # libnvme's nvme.ctrl object self._connect_op = None super().__init__(tid, service, discovery_ctrl) def _release_resources(self): logging.debug('Controller._release_resources() - %s | %s', self.id, self.device) if self._udev: self._udev.unregister_for_device_events(self._on_udev_notification) self._kill_ops() super()._release_resources() self._ctrl = None self._udev = None self._host = None self._root = None self._nvme_options = None @property def device(self) -> str: '''@brief return the Linux nvme device id (e.g. nvme3) or empty string if no device is associated with this controller''' if not self._device and self._ctrl and self._ctrl.name: self._device = self._ctrl.name return self._device or 'nvme?' def all_ops_completed(self) -> bool: '''@brief Returns True if all operations have completed. False otherwise.''' return self._connect_op is None or self._connect_op.completed() def connected(self): '''@brief Return whether a connection is established''' return self._ctrl and self._ctrl.connected() def controller_id_dict(self) -> dict: '''@brief return the controller ID as a dict.''' cid = super().controller_id_dict() cid['device'] = self.device return cid def details(self) -> dict: '''@brief return detailed debug info about this controller''' details = super().details() details.update( self._udev.get_attributes(self.device, ('hostid', 'hostnqn', 'model', 'serial', 'dctype', 'cntrltype')) ) details['connected'] = str(self.connected()) return details def info(self) -> dict: '''@brief Get the controller info for this object''' info = super().info() if self._connect_op: info['connect operation'] = str(self._connect_op.as_dict()) return info def cancel(self): '''@brief Used to cancel pending operations.''' super().cancel() if self._connect_op: self._connect_op.cancel() def _kill_ops(self): if self._connect_op: self._connect_op.kill() self._connect_op = None def set_level_from_tron(self, tron): '''Set log level based on TRON''' if self._root: self._root.log_level("debug" if tron else "err") def _on_udev_notification(self, udev_obj): if self._alive(): if udev_obj.action == 'change': nvme_aen = udev_obj.get('NVME_AEN') nvme_event = udev_obj.get('NVME_EVENT') if isinstance(nvme_aen, str): logging.info('%s | %s - Received AEN: %s', self.id, udev_obj.sys_name, nvme_aen) self._on_aen(int(nvme_aen, 16)) if isinstance(nvme_event, str): self._on_nvme_event(nvme_event) elif udev_obj.action == 'remove': logging.info('%s | %s - Received "remove" event', self.id, udev_obj.sys_name) self._on_ctrl_removed(udev_obj) else: logging.debug( 'Controller._on_udev_notification() - %s | %s: Received "%s" event', self.id, udev_obj.sys_name, udev_obj.action, ) else: logging.debug( 'Controller._on_udev_notification() - %s | %s: Received event on dead object. udev_obj %s: %s', self.id, self.device, udev_obj.action, udev_obj.sys_name, ) def _on_ctrl_removed(self, udev_obj): # pylint: disable=unused-argument if self._udev: self._udev.unregister_for_device_events(self._on_udev_notification) self._kill_ops() # Kill all pending operations self._ctrl = None # Defer removal of this object to the next main loop's idle period. GLib.idle_add(self._serv.remove_controller, self, True) def _get_cfg(self): '''Get configuration parameters. These may either come from the [Global] section or from a "controller" entry in the configuration file. A definition found in a "controller" entry overrides the same definition found in the [Global] section. ''' cfg = {} service_conf = conf.SvcConf() for option, keyword in ( ('kato', 'keep_alive_tmo'), ('queue-size', 'queue_size'), ('hdr-digest', 'hdr_digest'), ('data-digest', 'data_digest'), ('nr-io-queues', 'nr_io_queues'), ('ctrl-loss-tmo', 'ctrl_loss_tmo'), ('disable-sqflow', 'disable_sqflow'), ('nr-poll-queues', 'nr_poll_queues'), ('nr-write-queues', 'nr_write_queues'), ('reconnect-delay', 'reconnect_delay'), ): # Check if the value is defined as a "controller" entry (i.e. override) ovrd_val = self.tid.cfg.get(option, None) if ovrd_val is not None: cfg[keyword] = ovrd_val else: # Check if the value is found in the [Global] section. glob_val = service_conf.get_option('Global', option) if glob_val is not None: cfg[keyword] = glob_val return cfg def _do_connect(self): service_conf = conf.SvcConf() host_iface = ( self.tid.host_iface if (self.tid.host_iface and not service_conf.ignore_iface and self._nvme_options.host_iface_supp) else None ) self._ctrl = nvme.ctrl( self._root, subsysnqn=self.tid.subsysnqn, transport=self.tid.transport, traddr=self.tid.traddr, trsvcid=self.tid.trsvcid if self.tid.trsvcid else None, host_traddr=self.tid.host_traddr if self.tid.host_traddr else None, host_iface=host_iface, ) self._ctrl.discovery_ctrl_set(self._discovery_ctrl) # Set the DHCHAP key on the controller # NOTE that this will eventually have to # change once we have support for AVE (TP8019) ctrl_dhchap_key = self.tid.cfg.get('dhchap-ctrl-secret') if ctrl_dhchap_key and self._nvme_options.dhchap_ctrlkey_supp: has_dhchap_key = hasattr(self._ctrl, 'dhchap_key') if not has_dhchap_key: logging.warning( '%s | %s - libnvme-%s does not allow setting the controller DHCHAP key. Please upgrade libnvme.', self.id, self.device, defs.LIBNVME_VERSION, ) else: self._ctrl.dhchap_key = ctrl_dhchap_key # Audit existing nvme devices. If we find a match, then # we'll just borrow that device instead of creating a new one. udev_obj = self._find_existing_connection() if udev_obj is not None: # A device already exists. self._device = udev_obj.sys_name logging.debug( 'Controller._do_connect() - %s Found existing control device: %s', self.id, udev_obj.sys_name ) self._connect_op = gutil.AsyncTask( self._on_connect_success, self._on_connect_fail, self._ctrl.init, self._host, int(udev_obj.sys_number) ) else: cfg = self._get_cfg() logging.debug( 'Controller._do_connect() - %s Connecting to nvme control with cfg=%s', self.id, cfg ) self._connect_op = gutil.AsyncTask( self._on_connect_success, self._on_connect_fail, self._ctrl.connect, self._host, cfg ) self._connect_op.run_async() # -------------------------------------------------------------------------- def _on_connect_success(self, op_obj: gutil.AsyncTask, data): '''@brief Function called when we successfully connect to the Controller. ''' op_obj.kill() self._connect_op = None if self._alive(): self._device = self._ctrl.name logging.info('%s | %s - Connection established!', self.id, self.device) self._connect_attempts = 0 self._udev.register_for_device_events(self._device, self._on_udev_notification) else: logging.debug( 'Controller._on_connect_success() - %s | %s: Received event on dead object. data=%s', self.id, self.device, data, ) def _on_connect_fail(self, op_obj: gutil.AsyncTask, err, fail_cnt): # pylint: disable=unused-argument '''@brief Function called when we fail to connect to the Controller.''' op_obj.kill() self._connect_op = None if self._alive(): if self._connect_attempts == 1: # Do a fast re-try on the first failure. self._retry_connect_tmr.set_timeout(self.FAST_CONNECT_RETRY_PERIOD_SEC) elif self._connect_attempts == 2: # If the fast connect re-try fails, then we can print a message to # indicate the failure, and start a slow re-try period. self._retry_connect_tmr.set_timeout(self.CONNECT_RETRY_PERIOD_SEC) logging.error('%s Failed to connect to controller. %s %s', self.id, err.domain, err.message) if self._should_try_to_reconnect(): logging.debug( 'Controller._on_connect_fail() - %s %s. Retry in %s sec.', self.id, err, self._retry_connect_tmr.get_timeout(), ) self._retry_connect_tmr.start() else: logging.debug( 'Controller._on_connect_fail() - %s Received event on dead object. %s %s', self.id, err.domain, err.message, ) def disconnect(self, disconnected_cb, keep_connection): '''@brief Issue an asynchronous disconnect command to a Controller. Once the async command has completed, the callback 'disconnected_cb' will be invoked. If a controller is already disconnected, then the callback will be added to the main loop's next idle slot to be executed ASAP. @param disconnected_cb: Callback to be called when disconnect has completed. the callback must have this signature: def cback(controller: Controller, success: bool) @param keep_connection: Whether the underlying connection should remain in the kernel. ''' logging.debug( 'Controller.disconnect() - %s | %s: keep_connection=%s', self.id, self.device, keep_connection ) if self._ctrl and self._ctrl.connected() and not keep_connection: logging.info('%s | %s - Disconnect initiated', self.id, self.device) op = gutil.AsyncTask(self._on_disconn_success, self._on_disconn_fail, self._ctrl.disconnect) op.run_async(disconnected_cb) else: # Defer callback to the next main loop's idle period. The callback # cannot be called directly as the current Controller object is in the # process of being disconnected and the callback will in fact delete # the object. This would invariably lead to unpredictable outcome. GLib.idle_add(disconnected_cb, self, True) def _on_disconn_success(self, op_obj: gutil.AsyncTask, data, disconnected_cb): # pylint: disable=unused-argument logging.debug('Controller._on_disconn_success() - %s | %s', self.id, self.device) op_obj.kill() # Defer callback to the next main loop's idle period. The callback # cannot be called directly as the current Controller object is in the # process of being disconnected and the callback will in fact delete # the object. This would invariably lead to unpredictable outcome. GLib.idle_add(disconnected_cb, self, True) def _on_disconn_fail( self, op_obj: gutil.AsyncTask, err, fail_cnt, disconnected_cb ): # pylint: disable=unused-argument logging.debug('Controller._on_disconn_fail() - %s | %s: %s', self.id, self.device, err) op_obj.kill() # Defer callback to the next main loop's idle period. The callback # cannot be called directly as the current Controller object is in the # process of being disconnected and the callback will in fact delete # the object. This would invariably lead to unpredictable outcome. GLib.idle_add(disconnected_cb, self, False) # ****************************************************************************** class Dc(Controller): '''@brief This object establishes a connection to one Discover Controller (DC). It retrieves the discovery log pages and caches them. It also monitors udev events associated with that DC and updates the cached discovery log pages accordingly. ''' GET_LOG_PAGE_RETRY_RERIOD_SEC = 20 REGISTRATION_RETRY_RERIOD_SEC = 5 GET_SUPPORTED_RETRY_RERIOD_SEC = 5 def __init__(self, staf, tid: trid.TID, log_pages=None, origin=None): super().__init__(tid, staf, discovery_ctrl=True) self._register_op = None self._get_supported_op = None self._get_log_op = None self._origin = origin self._log_pages = log_pages if log_pages else list() # Log pages cache # For Avahi-discovered DCs that later become unresponsive, monitor how # long the controller remains unresponsive and if it does not return for # a configurable soak period (_ctrl_unresponsive_tmr), remove that # controller. Only Avahi-discovered controllers need this timeout-based # cleanup. self._ctrl_unresponsive_time = None # The time at which connectivity was lost self._ctrl_unresponsive_tmr = gutil.GTimer(0, self._serv.controller_unresponsive, self.tid) def _release_resources(self): logging.debug('Dc._release_resources() - %s | %s', self.id, self.device) super()._release_resources() if self._ctrl_unresponsive_tmr is not None: self._ctrl_unresponsive_tmr.kill() self._log_pages = list() self._ctrl_unresponsive_tmr = None def _kill_ops(self): super()._kill_ops() if self._get_log_op: self._get_log_op.kill() self._get_log_op = None if self._register_op: self._register_op.kill() self._register_op = None if self._get_supported_op: self._get_supported_op.kill() self._get_supported_op = None def all_ops_completed(self) -> bool: '''@brief Returns True if all operations have completed. False otherwise.''' return ( super().all_ops_completed() and (self._get_log_op is None or self._get_log_op.completed()) and (self._register_op is None or self._register_op.completed()) and (self._get_supported_op is None or self._get_supported_op.completed()) ) @property def origin(self): '''@brief Return how this controller came into existance. Was it "discovered" through mDNS service discovery (TP8009), was it manually "configured" in stafd.conf, or was it a "referral". ''' return self._origin @origin.setter def origin(self, value): '''@brief Set the origin of this controller.''' if value in ('discovered', 'configured', 'referral'): self._origin = value self._handle_lost_controller() else: logging.error('%s | %s - Trying to set invalid origin to %s', self.id, self.device, value) def reload_hdlr(self): '''@brief This is called when a "reload" signal is received.''' logging.debug('Dc.reload_hdlr() - %s | %s', self.id, self.device) self._handle_lost_controller() self._resync_with_controller() def info(self) -> dict: '''@brief Get the controller info for this object''' timeout = conf.SvcConf().zeroconf_persistence_sec unresponsive_time = ( time.asctime(self._ctrl_unresponsive_time) if self._ctrl_unresponsive_time is not None else '---' ) info = super().info() info['origin'] = self.origin if self.origin == 'discovered': # The code that handles "unresponsive" DCs only applies to # discovered DCs. So, let's only print that info when it's relevant. info['unresponsive timer'] = str(self._ctrl_unresponsive_tmr) info['unresponsive timeout'] = f'{timeout} sec' if timeout >= 0 else 'forever' info['unresponsive time'] = unresponsive_time if self._get_log_op: info['get log page operation'] = str(self._get_log_op.as_dict()) if self._register_op: info['register operation'] = str(self._register_op.as_dict()) if self._get_supported_op: info['get supported log page operation'] = str(self._get_supported_op.as_dict()) return info def cancel(self): '''@brief Used to cancel pending operations.''' super().cancel() if self._get_log_op: self._get_log_op.cancel() if self._register_op: self._register_op.cancel() if self._get_supported_op: self._get_supported_op.cancel() def log_pages(self) -> list: '''@brief Get the cached log pages for this object''' return self._log_pages def referrals(self) -> list: '''@brief Return the list of referrals''' return [page for page in self._log_pages if page['subtype'] == 'referral'] def _is_ddc(self): return self._ctrl and self._ctrl.dctype != 'cdc' def _on_aen(self, aen: int): if aen == DLP_CHANGED and self._get_log_op: self._get_log_op.run_async() def _handle_lost_controller(self): if self.origin == 'discovered': # Only apply to mDNS-discovered DCs if not self._serv.is_avahi_reported(self.tid) and not self.connected(): timeout = conf.SvcConf().zeroconf_persistence_sec if timeout >= 0: if self._ctrl_unresponsive_time is None: self._ctrl_unresponsive_time = time.localtime() self._ctrl_unresponsive_tmr.start(timeout) logging.info( '%s | %s - Controller is not responding. Will be removed by %s unless restored', self.id, self.device, time.ctime(time.mktime(self._ctrl_unresponsive_time) + timeout), ) return logging.info( '%s | %s - Controller not responding. Retrying...', self.id, self.device, ) self._ctrl_unresponsive_time = None self._ctrl_unresponsive_tmr.stop() self._ctrl_unresponsive_tmr.set_timeout(0) def is_unresponsive(self): '''@brief For "discovered" DC, return True if DC is unresponsive, False otherwise. ''' return ( self.origin == 'discovered' and not self._serv.is_avahi_reported(self.tid) and not self.connected() and self._ctrl_unresponsive_time is not None and self._ctrl_unresponsive_tmr.time_remaining() <= 0 ) def _resync_with_controller(self): '''Communicate with DC to resync the states''' if self._register_op: self._register_op.run_async() elif self._get_supported_op: self._get_supported_op.run_async() elif self._get_log_op: self._get_log_op.run_async() def _on_nvme_event(self, nvme_event: str): if nvme_event in ('connected', 'rediscover'): # This event indicates that the kernel # driver re-connected to the DC. logging.debug( 'Dc._on_nvme_event() - %s | %s: Received "%s" event', self.id, self.device, nvme_event, ) self._resync_with_controller() def _find_existing_connection(self): return self._udev.find_nvme_dc_device(self.tid) def _post_registration_actions(self): # Need to check that supported_log_pages() is available (introduced in libnvme 1.2) has_supported_log_pages = hasattr(self._ctrl, 'supported_log_pages') if not has_supported_log_pages: logging.warning( '%s | %s - libnvme-%s does not support "Get supported log pages". Please upgrade libnvme.', self.id, self.device, defs.LIBNVME_VERSION, ) if conf.SvcConf().pleo_enabled and self._is_ddc() and has_supported_log_pages: self._get_supported_op = gutil.AsyncTask( self._on_get_supported_success, self._on_get_supported_fail, self._ctrl.supported_log_pages ) self._get_supported_op.run_async() else: self._get_log_op = gutil.AsyncTask(self._on_get_log_success, self._on_get_log_fail, self._ctrl.discover) self._get_log_op.run_async() # -------------------------------------------------------------------------- def _on_connect_success(self, op_obj: gutil.AsyncTask, data): '''@brief Function called when we successfully connect to the Discovery Controller. ''' super()._on_connect_success(op_obj, data) if self._alive(): self._ctrl_unresponsive_time = None self._ctrl_unresponsive_tmr.stop() self._ctrl_unresponsive_tmr.set_timeout(0) if self._ctrl.is_registration_supported(): self._register_op = gutil.AsyncTask( self._on_registration_success, self._on_registration_fail, self._ctrl.registration_ctlr, nvme.NVMF_DIM_TAS_REGISTER, ) self._register_op.run_async() else: self._post_registration_actions() def _on_connect_fail(self, op_obj: gutil.AsyncTask, err, fail_cnt): '''@brief Function called when we fail to connect to the Controller.''' super()._on_connect_fail(op_obj, err, fail_cnt) if self._alive(): self._handle_lost_controller() # -------------------------------------------------------------------------- def _on_registration_success(self, op_obj: gutil.AsyncTask, data): # pylint: disable=unused-argument '''@brief Function called when we successfully register with the Discovery Controller. See self._register_op object for details. NOTE: The name _on_registration_success() may be misleading. "success" refers to the fact that a successful exchange was made with the DC. It doesn't mean that the registration itself succeeded. ''' if self._alive(): if data is not None: logging.warning('%s | %s - Registration error. %s.', self.id, self.device, data) else: logging.debug('Dc._on_registration_success() - %s | %s', self.id, self.device) self._post_registration_actions() else: logging.debug( 'Dc._on_registration_success() - %s | %s: Received event on dead object.', self.id, self.device ) def _on_registration_fail(self, op_obj: gutil.AsyncTask, err, fail_cnt): '''@brief Function called when we fail to register with the Discovery Controller. See self._register_op object for details. ''' if self._alive(): logging.debug( 'Dc._on_registration_fail() - %s | %s: %s. Retry in %s sec', self.id, self.device, err, Dc.REGISTRATION_RETRY_RERIOD_SEC, ) if fail_cnt == 1: # Throttle the logs. Only print the first time the command fails logging.error('%s | %s - Failed to register with Discovery Controller. %s', self.id, self.device, err) op_obj.retry(Dc.REGISTRATION_RETRY_RERIOD_SEC) else: logging.debug( 'Dc._on_registration_fail() - %s | %s: Received event on dead object. %s', self.id, self.device, err, ) op_obj.kill() # -------------------------------------------------------------------------- def _on_get_supported_success(self, op_obj: gutil.AsyncTask, data): # pylint: disable=unused-argument '''@brief Function called when we successfully retrieved the supported log pages from the Discovery Controller. See self._get_supported_op object for details. NOTE: The name _on_get_supported_success() may be misleading. "success" refers to the fact that a successful exchange was made with the DC. It doesn't mean that the Get Supported Log Page itself succeeded. ''' if self._alive(): try: dlp_supp_opts = data[nvme.NVME_LOG_LID_DISCOVER] >> 16 except (TypeError, IndexError): dlp_supp_opts = 0 logging.debug( 'Dc._on_get_supported_success() - %s | %s: supported options = 0x%04X = %s', self.id, self.device, dlp_supp_opts, dlp_supp_opts_as_string(dlp_supp_opts), ) if 'lsp' in inspect.signature(self._ctrl.discover).parameters: lsp = nvme.NVMF_LOG_DISC_LSP_PLEO if dlp_supp_opts & nvme.NVMF_LOG_DISC_LID_PLEOS else 0 self._get_log_op = gutil.AsyncTask( self._on_get_log_success, self._on_get_log_fail, self._ctrl.discover, lsp ) else: logging.warning( '%s | %s - libnvme-%s does not support setting PLEO bit. Please upgrade.', self.id, self.device, defs.LIBNVME_VERSION, ) self._get_log_op = gutil.AsyncTask(self._on_get_log_success, self._on_get_log_fail, self._ctrl.discover) self._get_log_op.run_async() else: logging.debug( 'Dc._on_get_supported_success() - %s | %s: Received event on dead object.', self.id, self.device ) def _on_get_supported_fail(self, op_obj: gutil.AsyncTask, err, fail_cnt): '''@brief Function called when we fail to retrieve the supported log page from the Discovery Controller. See self._get_supported_op object for details. ''' if self._alive(): logging.debug( 'Dc._on_get_supported_fail() - %s | %s: %s. Retry in %s sec', self.id, self.device, err, Dc.GET_SUPPORTED_RETRY_RERIOD_SEC, ) if fail_cnt == 1: # Throttle the logs. Only print the first time the command fails logging.error( '%s | %s - Failed to Get supported log pages from Discovery Controller. %s', self.id, self.device, err, ) op_obj.retry(Dc.GET_SUPPORTED_RETRY_RERIOD_SEC) else: logging.debug( 'Dc._on_get_supported_fail() - %s | %s: Received event on dead object. %s', self.id, self.device, err, ) op_obj.kill() # -------------------------------------------------------------------------- def _on_get_log_success(self, op_obj: gutil.AsyncTask, data): # pylint: disable=unused-argument '''@brief Function called when we successfully retrieve the log pages from the Discovery Controller. See self._get_log_op object for details. ''' if self._alive(): # Note that for historical reasons too long to explain, the CDC may # return invalid addresses ('0.0.0.0', '::', or ''). Those need to # be filtered out. referrals_before = self.referrals() self._log_pages = ( [ {k.strip(): str(v).strip() for k, v in dictionary.items()} for dictionary in data if dictionary.get('traddr', '').strip() not in ('0.0.0.0', '::', '') ] if data else list() ) logging.info( '%s | %s - Received discovery log pages (num records=%s).', self.id, self.device, len(self._log_pages) ) referrals_after = self.referrals() self._serv.log_pages_changed(self, self.device) if referrals_after != referrals_before: logging.debug( 'Dc._on_get_log_success() - %s | %s: Referrals before = %s', self.id, self.device, referrals_before, ) logging.debug( 'Dc._on_get_log_success() - %s | %s: Referrals after = %s', self.id, self.device, referrals_after, ) self._serv.referrals_changed() else: logging.debug( 'Dc._on_get_log_success() - %s | %s: Received event on dead object.', self.id, self.device ) def _on_get_log_fail(self, op_obj: gutil.AsyncTask, err, fail_cnt): '''@brief Function called when we fail to retrieve the log pages from the Discovery Controller. See self._get_log_op object for details. ''' if self._alive(): logging.debug( 'Dc._on_get_log_fail() - %s | %s: %s. Retry in %s sec', self.id, self.device, err, Dc.GET_LOG_PAGE_RETRY_RERIOD_SEC, ) if fail_cnt == 1: # Throttle the logs. Only print the first time the command fails logging.error('%s | %s - Failed to retrieve log pages. %s', self.id, self.device, err) op_obj.retry(Dc.GET_LOG_PAGE_RETRY_RERIOD_SEC) else: logging.debug( 'Dc._on_get_log_fail() - %s | %s: Received event on dead object. %s', self.id, self.device, err, ) op_obj.kill() # ****************************************************************************** class Ioc(Controller): '''@brief This object establishes a connection to one I/O Controller.''' def __init__(self, stac, tid: trid.TID): self._dlpe = None super().__init__(tid, stac) def _find_existing_connection(self): return self._udev.find_nvme_ioc_device(self.tid) def _on_aen(self, aen: int): pass def _on_nvme_event(self, nvme_event): pass def reload_hdlr(self): '''@brief This is called when a "reload" signal is received.''' if not self.connected() and self._retry_connect_tmr.time_remaining() == 0: self._try_to_connect_deferred.schedule() @property def eflags(self): '''@brief Return the eflag field of the DLPE''' return get_eflags(self._dlpe) @property def ncc(self): '''@brief Return Not Connected to CDC status''' return get_ncc(self.eflags) def details(self) -> dict: '''@brief return detailed debug info about this controller''' details = super().details() details['dlpe'] = str(self._dlpe) details['dlpe.eflags.ncc'] = str(self.ncc) return details def update_dlpe(self, dlpe): '''@brief This method is called when a new DLPE associated with this controller is received.''' new_ncc = get_ncc(get_eflags(dlpe)) old_ncc = self.ncc self._dlpe = dlpe if old_ncc and not new_ncc: # NCC bit cleared? if not self.connected(): self._connect_attempts = 0 self._try_to_connect_deferred.schedule() def _should_try_to_reconnect(self): '''@brief This is used to determine when it's time to stop trying toi connect''' max_connect_attempts = conf.SvcConf().connect_attempts_on_ncc if self.ncc else 0 return max_connect_attempts == 0 or self._connect_attempts < max_connect_attempts linux-nvme-nvme-stas-a8026bb/staslib/defs.py000066400000000000000000000026021440613556600211030ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger ''' @brief This file gets automagically configured by meson at build time. ''' import os import sys import shutil import platform from staslib.version import KernelVersion try: import libnvme LIBNVME_VERSION = libnvme.__version__ except (AttributeError, ModuleNotFoundError): LIBNVME_VERSION = '?.?' VERSION = '@VERSION@' LICENSE = '@LICENSE@' STACD_DBUS_NAME = '@STACD_DBUS_NAME@' STACD_DBUS_PATH = '@STACD_DBUS_PATH@' STAFD_DBUS_NAME = '@STAFD_DBUS_NAME@' STAFD_DBUS_PATH = '@STAFD_DBUS_PATH@' KERNEL_VERSION = KernelVersion(platform.release()) KERNEL_IFACE_MIN_VERSION = KernelVersion('5.14') KERNEL_TP8013_MIN_VERSION = KernelVersion('5.16') KERNEL_HOSTKEY_MIN_VERSION = KernelVersion('5.20') KERNEL_CTRLKEY_MIN_VERSION = KernelVersion('5.20') WELL_KNOWN_DISC_NQN = 'nqn.2014-08.org.nvmexpress.discovery' PROG_NAME = os.path.basename(sys.argv[0]) NVME_HOSTID = '/etc/nvme/hostid' NVME_HOSTNQN = '/etc/nvme/hostnqn' NVME_HOSTKEY = '/etc/nvme/hostkey' SYS_CONF_FILE = '/etc/stas/sys.conf' STAFD_CONF_FILE = '/etc/stas/stafd.conf' STACD_CONF_FILE = '/etc/stas/stacd.conf' SYSTEMCTL = shutil.which('systemctl') linux-nvme-nvme-stas-a8026bb/staslib/gutil.py000066400000000000000000000401401440613556600213050ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''This module provides utility functions/classes to provide easier to use access to GLib/Gio/Gobject resources. ''' import logging from gi.repository import Gio, GLib, GObject from staslib import conf, iputil, trid # ****************************************************************************** class GTimer: '''@brief Convenience class to wrap GLib timers''' def __init__( self, interval_sec: float = 0, user_cback=lambda: GLib.SOURCE_REMOVE, *user_data, priority=GLib.PRIORITY_DEFAULT ): # pylint: disable=keyword-arg-before-vararg self._source = None self._interval_sec = float(interval_sec) self._user_cback = user_cback self._user_data = user_data self._priority = priority if priority is not None else GLib.PRIORITY_DEFAULT def _release_resources(self): self.stop() self._user_cback = None self._user_data = None def kill(self): '''@brief Used to release all resources associated with a timer.''' self._release_resources() def __str__(self): if self._source is not None: return f'{self._interval_sec}s [{self.time_remaining()}s]' return f'{self._interval_sec}s [off]' def _callback(self, *_): retval = self._user_cback(*self._user_data) if retval == GLib.SOURCE_REMOVE: self._source = None return retval def stop(self): '''@brief Stop timer''' if self._source is not None: self._source.destroy() self._source = None def start(self, new_interval_sec: float = -1.0): '''@brief Start (or restart) timer''' if new_interval_sec >= 0: self._interval_sec = float(new_interval_sec) if self._source is not None: self._source.set_ready_time( self._source.get_time() + (self._interval_sec * 1000000) ) # ready time is in micro-seconds (monotonic time) else: if self._interval_sec.is_integer(): self._source = GLib.timeout_source_new_seconds(int(self._interval_sec)) # seconds resolution else: self._source = GLib.timeout_source_new(self._interval_sec * 1000.0) # mili-seconds resolution self._source.set_priority(self._priority) self._source.set_callback(self._callback) self._source.attach() def clear(self): '''@brief Make timer expire now. The callback function will be invoked immediately by the main loop. ''' if self._source is not None: self._source.set_ready_time(0) # Expire now! def set_callback(self, user_cback, *user_data): '''@brief set the callback function to invoke when timer expires''' self._user_cback = user_cback self._user_data = user_data def set_timeout(self, new_interval_sec: float): '''@brief set the timer's duration''' if new_interval_sec >= 0: self._interval_sec = float(new_interval_sec) def get_timeout(self): '''@brief get the timer's duration''' return self._interval_sec def time_remaining(self) -> float: '''@brief Get how much time remains on a timer before it fires.''' if self._source is not None: delta_us = self._source.get_ready_time() - self._source.get_time() # monotonic time in micro-seconds if delta_us > 0: return delta_us / 1000000.0 return 0 # ****************************************************************************** class NameResolver: # pylint: disable=too-few-public-methods '''@brief DNS resolver to convert host names to IP addresses.''' def __init__(self): self._resolver = Gio.Resolver.get_default() def resolve_ctrl_async(self, cancellable, controllers_in: list, callback): '''@brief The traddr fields may specify a hostname instead of an IP address. We need to resolve all the host names to addresses. Resolving hostnames may take a while as a DNS server may need to be contacted. For that reason, we're using async APIs with callbacks to resolve all the hostnames. The callback @callback will be called once all hostnames have been resolved. @param controllers: List of trid.TID ''' pending_resolution_count = 0 controllers_out = [] service_conf = conf.SvcConf() def addr_resolved(resolver, result, controller): try: addresses = resolver.lookup_by_name_finish(result) # List of Gio.InetAddress objects except GLib.GError as err: # We don't need to report "cancellation" errors. if err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED): # pylint: disable=no-member logging.debug('NameResolver.resolve_ctrl_async() - %s %s', err.message, controller) else: logging.error('%s', err.message) # pylint: disable=no-member # if err.matches(Gio.resolver_error_quark(), Gio.ResolverError.TEMPORARY_FAILURE): # elif err.matches(Gio.resolver_error_quark(), Gio.ResolverError.NOT_FOUND): # elif err.matches(Gio.resolver_error_quark(), Gio.ResolverError.INTERNAL): else: traddr = None # If multiple addresses are returned (which is often the case), # prefer IPv4 addresses over IPv6. if 4 in service_conf.ip_family: for address in addresses: # There may be multiple IPv4 addresses. Pick 1st one. if address.get_family() == Gio.SocketFamily.IPV4: traddr = address.to_string() break if traddr is None and 6 in service_conf.ip_family: for address in addresses: # There may be multiple IPv6 addresses. Pick 1st one. if address.get_family() == Gio.SocketFamily.IPV6: traddr = address.to_string() break if traddr is not None: logging.debug( 'NameResolver.resolve_ctrl_async() - resolved \'%s\' -> %s', controller.traddr, traddr ) cid = controller.as_dict() cid['traddr'] = traddr nonlocal controllers_out controllers_out.append(trid.TID(cid)) # Invoke callback after all hostnames have been resolved nonlocal pending_resolution_count pending_resolution_count -= 1 if pending_resolution_count == 0: callback(controllers_out) for controller in controllers_in: if controller.transport in ('tcp', 'rdma'): hostname_or_addr = controller.traddr if not hostname_or_addr: logging.error('Invalid traddr: %s', controller) else: # Try to convert to an ipaddress object. If this # succeeds, then we don't need to call the resolver. ip = iputil.get_ipaddress_obj(hostname_or_addr) if ip is None: logging.debug('NameResolver.resolve_ctrl_async() - resolving \'%s\'', hostname_or_addr) pending_resolution_count += 1 self._resolver.lookup_by_name_async(hostname_or_addr, cancellable, addr_resolved, controller) elif ip.version in service_conf.ip_family: controllers_out.append(controller) else: logging.warning( 'Excluding configured IP address %s based on "ip-family" setting', hostname_or_addr ) else: controllers_out.append(controller) if pending_resolution_count == 0: # No names are pending asynchronous resolution callback(controllers_out) # ****************************************************************************** class _TaskRunner(GObject.Object): '''@brief This class allows running methods asynchronously in a thread.''' def __init__(self, user_function, *user_args): '''@param user_function: function to run inside a thread @param user_args: arguments passed to @user_function ''' super().__init__() self._user_function = user_function self._user_args = user_args def communicate(self, cancellable, cb_function, *cb_args): '''@param cancellable: A Gio.Cancellable object that can be used to cancel an in-flight async command. @param cb_function: User callback function to call when the async command has completed. The callback function will be passed these arguments: (runner, result, *cb_args) Where: runner: This _TaskRunner object instance result: A GObject.Object instance that contains the result cb_args: The cb_args arguments passed to communicate() @param cb_args: User arguments to pass to @cb_function ''' def in_thread_exec(task, self, task_data, cancellable): # pylint: disable=unused-argument if task.return_error_if_cancelled(): return # Bail out if task has been cancelled try: value = GObject.Object() value.result = self._user_function(*self._user_args) task.return_value(value) except Exception as ex: # pylint: disable=broad-except task.return_error(GLib.Error(message=str(ex), domain=type(ex).__name__)) task = Gio.Task.new(self, cancellable, cb_function, *cb_args) task.set_return_on_cancel(False) task.run_in_thread(in_thread_exec) return task def communicate_finish(self, result): # pylint: disable=no-self-use '''@brief Use this function in your callback (see @cb_function) to extract data from the result object. @return On success (True, data, None), On failure (False, None, err: GLib.Error) ''' try: success, value = result.propagate_value() return success, value.result, None except GLib.Error as err: return False, None, err # ****************************************************************************** class AsyncTask: # pylint: disable=too-many-instance-attributes '''Object used to manage an asynchronous GLib operation. The operation can be cancelled or retried. ''' def __init__(self, on_success_callback, on_failure_callback, operation, *op_args): '''@param on_success_callback: Callback method invoked when @operation completes successfully @param on_failure_callback: Callback method invoked when @operation fails @param operation: Operation (i.e. a function) to execute asynchronously @param op_args: Arguments passed to operation ''' self._cancellable = Gio.Cancellable() self._operation = operation self._op_args = op_args self._success_cb = on_success_callback self._fail_cb = on_failure_callback self._retry_tmr = None self._errmsg = None self._task = None self._fail_cnt = 0 def _release_resources(self): if self._alive(): self._cancellable.cancel() if self._retry_tmr is not None: self._retry_tmr.kill() self._operation = None self._op_args = None self._success_cb = None self._fail_cb = None self._retry_tmr = None self._errmsg = None self._task = None self._fail_cnt = None self._cancellable = None def __str__(self): return str(self.as_dict()) def as_dict(self): '''Return object members as a dictionary''' info = { 'fail count': self._fail_cnt, 'completed': self._task.get_completed(), 'alive': self._alive(), } if self._retry_tmr: info['retry timer'] = str(self._retry_tmr) if self._errmsg: info['error'] = self._errmsg return info def _alive(self): return self._cancellable and not self._cancellable.is_cancelled() def completed(self): '''@brief Returns True if the task has completed, False otherwise.''' return self._task is not None and self._task.get_completed() def cancel(self): '''@brief cancel async operation''' if self._alive(): self._cancellable.cancel() def kill(self): '''@brief kill and clean up this object''' self._release_resources() def run_async(self, *args): '''@brief Method used to initiate an asynchronous operation with the Controller. When the operation completes (or fails) the callback method @_on_operation_complete() will be invoked. ''' runner = _TaskRunner(self._operation, *self._op_args) self._task = runner.communicate(self._cancellable, self._on_operation_complete, *args) def retry(self, interval_sec, *args): '''@brief Tell this object that the async operation is to be retried in @interval_sec seconds. ''' if self._retry_tmr is None: self._retry_tmr = GTimer() self._retry_tmr.set_callback(self._on_retry_timeout, *args) self._retry_tmr.start(interval_sec) def _on_retry_timeout(self, *args): '''@brief When an operation fails, the application has the option to retry at a later time by calling the retry() method. The retry() method starts a timer at the end of which the operation will be executed again. This is the method that is called when the timer expires. ''' if self._alive(): self.run_async(*args) return GLib.SOURCE_REMOVE def _on_operation_complete(self, runner, result, *args): '''@brief This callback method is invoked when the operation with the Controller has completed (be it successful or not). ''' # The operation might have been cancelled. # Only proceed if it hasn't been cancelled. if self._operation is None or not self._alive(): return success, data, err = runner.communicate_finish(result) if success: self._errmsg = None self._fail_cnt = 0 self._success_cb(self, data, *args) else: self._errmsg = str(err) self._fail_cnt += 1 self._fail_cb(self, err, self._fail_cnt, *args) # ****************************************************************************** class Deferred: '''Implement a deferred function call. A deferred is a function that gets added to the main loop to be executed during the next idle slot.''' def __init__(self, func, *user_data): self._source = None self._func = func self._user_data = user_data def schedule(self): '''Schedule the function to be called by the main loop. If the function is already scheduled, then do nothing''' if not self.is_scheduled(): srce_id = GLib.idle_add(self._func, *self._user_data) self._source = GLib.main_context_default().find_source_by_id(srce_id) def is_scheduled(self): '''Check if deferred is currently schedules to run''' return self._source and not self._source.is_destroyed() def cancel(self): '''Remove deferred from main loop''' if self.is_scheduled(): self._source.destroy() self._source = None linux-nvme-nvme-stas-a8026bb/staslib/iputil.py000066400000000000000000000136101440613556600214710ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger '''A collection of IP address and network interface utilities''' import socket import logging import ipaddress from staslib import conf RTM_NEWADDR = 20 RTM_GETADDR = 22 NLM_F_REQUEST = 0x01 NLM_F_ROOT = 0x100 NLMSG_DONE = 3 IFLA_ADDRESS = 1 NLMSGHDR_SZ = 16 IFADDRMSG_SZ = 8 RTATTR_SZ = 4 # Netlink request (Get address command) GETADDRCMD = ( # BEGIN: struct nlmsghdr b'\0' * 4 # nlmsg_len (placeholder - actual length calculated below) + (RTM_GETADDR).to_bytes(2, byteorder='little', signed=False) # nlmsg_type + (NLM_F_REQUEST | NLM_F_ROOT).to_bytes(2, byteorder='little', signed=False) # nlmsg_flags + b'\0' * 2 # nlmsg_seq + b'\0' * 2 # nlmsg_pid # END: struct nlmsghdr + b'\0' * 8 # struct ifaddrmsg ) GETADDRCMD = len(GETADDRCMD).to_bytes(4, byteorder='little') + GETADDRCMD[4:] # nlmsg_len # ****************************************************************************** def get_ipaddress_obj(ipaddr): '''@brief Return a IPv4Address or IPv6Address depending on whether @ipaddr is a valid IPv4 or IPv6 address. Return None otherwise.''' try: ip = ipaddress.ip_address(ipaddr) except ValueError: return None return ip # ****************************************************************************** def _data_matches_ip(data_family, data, ip): if data_family == socket.AF_INET: try: other_ip = ipaddress.IPv4Address(data) except ValueError: return False if ip.version == 6: ip = ip.ipv4_mapped elif data_family == socket.AF_INET6: try: other_ip = ipaddress.IPv6Address(data) except ValueError: return False if ip.version == 4: other_ip = other_ip.ipv4_mapped else: return False return other_ip == ip # ****************************************************************************** def iface_of(src_addr): '''@brief Find the interface that has src_addr as one of its assigned IP addresses. @param src_addr: The IP address to match @type src_addr: Instance of ipaddress.IPv4Address or ipaddress.IPv6Address ''' with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW) as sock: sock.sendall(GETADDRCMD) nlmsg = sock.recv(8192) nlmsg_idx = 0 while True: if nlmsg_idx >= len(nlmsg): nlmsg += sock.recv(8192) nlmsg_type = int.from_bytes(nlmsg[nlmsg_idx + 4 : nlmsg_idx + 6], byteorder='little', signed=False) if nlmsg_type == NLMSG_DONE: break if nlmsg_type != RTM_NEWADDR: break nlmsg_len = int.from_bytes(nlmsg[nlmsg_idx : nlmsg_idx + 4], byteorder='little', signed=False) if nlmsg_len % 4: # Is msg length not a multiple of 4? break ifaddrmsg_indx = nlmsg_idx + NLMSGHDR_SZ ifa_family = nlmsg[ifaddrmsg_indx] ifa_index = int.from_bytes(nlmsg[ifaddrmsg_indx + 4 : ifaddrmsg_indx + 8], byteorder='little', signed=False) rtattr_indx = ifaddrmsg_indx + IFADDRMSG_SZ while rtattr_indx < (nlmsg_idx + nlmsg_len): rta_len = int.from_bytes(nlmsg[rtattr_indx : rtattr_indx + 2], byteorder='little', signed=False) rta_type = int.from_bytes(nlmsg[rtattr_indx + 2 : rtattr_indx + 4], byteorder='little', signed=False) if rta_type == IFLA_ADDRESS: data = nlmsg[rtattr_indx + RTATTR_SZ : rtattr_indx + rta_len] if _data_matches_ip(ifa_family, data, src_addr): return socket.if_indextoname(ifa_index) rta_len = (rta_len + 3) & ~3 # Round up to multiple of 4 rtattr_indx += rta_len # Move to next rtattr nlmsg_idx += nlmsg_len # Move to next Netlink message return '' # ****************************************************************************** def get_interface(src_addr): '''Get interface for given source address @param src_addr: The source address @type src_addr: str ''' if not src_addr: return '' src_addr = src_addr.split('%')[0] # remove scope-id (if any) src_addr = get_ipaddress_obj(src_addr) return '' if src_addr is None else iface_of(src_addr) # ****************************************************************************** def remove_invalid_addresses(controllers: list): '''@brief Remove controllers with invalid addresses from the list of controllers. @param controllers: List of TIDs ''' service_conf = conf.SvcConf() valid_controllers = list() for controller in controllers: if controller.transport in ('tcp', 'rdma'): # Let's make sure that traddr is # syntactically a valid IPv4 or IPv6 address. ip = get_ipaddress_obj(controller.traddr) if ip is None: logging.warning('%s IP address is not valid', controller) continue # Let's make sure the address family is enabled. if ip.version not in service_conf.ip_family: logging.debug( '%s ignored because IPv%s is disabled in %s', controller, ip.version, service_conf.conf_file, ) continue valid_controllers.append(controller) elif controller.transport in ('fc', 'loop'): # At some point, need to validate FC addresses as well... valid_controllers.append(controller) else: logging.warning('Invalid transport %s', controller.transport) return valid_controllers linux-nvme-nvme-stas-a8026bb/staslib/log.py000066400000000000000000000031631440613556600207460ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''nvme-stas logging module''' import sys import logging from staslib import defs def init(syslog: bool): '''Init log module @param syslog: True to send messages to the syslog, False to send messages to stdout. ''' log = logging.getLogger() log.propagate = False if syslog: try: # Try journal logger first import systemd.journal # pylint: disable=import-outside-toplevel handler = systemd.journal.JournalHandler(SYSLOG_IDENTIFIER=defs.PROG_NAME) except ModuleNotFoundError: # Go back to standard syslog handler from logging.handlers import SysLogHandler # pylint: disable=import-outside-toplevel handler = SysLogHandler(address="/dev/log") handler.setFormatter(logging.Formatter(f'{defs.PROG_NAME}: %(message)s')) else: # Log to stdout handler = logging.StreamHandler(stream=sys.stdout) log.addHandler(handler) log.setLevel(logging.INFO if syslog else logging.DEBUG) def level() -> str: '''@brief return current log level''' logger = logging.getLogger() return str(logging.getLevelName(logger.getEffectiveLevel())) def set_level_from_tron(tron): '''Set log level based on TRON''' logger = logging.getLogger() logger.setLevel(logging.DEBUG if tron else logging.INFO) linux-nvme-nvme-stas-a8026bb/staslib/meson.build000066400000000000000000000025361440613556600217600ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # files_to_configure = [ 'defs.py', '__init__.py', 'stafd.idl', 'stacd.idl' ] configured_files = [] foreach file : files_to_configure configured_files += configure_file( input: file, output: file, configuration: conf ) endforeach files_to_copy = [ 'avahi.py', 'conf.py', 'ctrl.py', 'gutil.py', 'iputil.py', 'log.py', 'service.py', 'singleton.py', 'stas.py', 'timeparse.py', 'trid.py', 'udev.py', 'version.py' ] copied_files = [] foreach file : files_to_copy copied_files += configure_file( input: file, output: file, copy: true, ) endforeach files_to_install = copied_files + configured_files python3.install_sources( files_to_install, pure: true, subdir: 'staslib', ) #=============================================================================== # Make a list of modules to lint skip = ['stafd.idl', 'stacd.idl'] foreach file: files_to_install fname = fs.name('@0@'.format(file)) if fname not in skip modules_to_lint += file endif endforeach linux-nvme-nvme-stas-a8026bb/staslib/service.py000066400000000000000000001125471440613556600216340ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''This module defines the base Service object from which the Staf and the Stac objects are derived.''' import json import logging import pathlib import subprocess from itertools import filterfalse import dasbus.error import dasbus.client.observer import dasbus.client.proxy from gi.repository import GLib from systemd.daemon import notify as sd_notify from staslib import avahi, conf, ctrl, defs, gutil, iputil, stas, timeparse, trid, udev # ****************************************************************************** class CtrlTerminator: '''The Controller Terminator is used to gracefully disconnect from controllers. All communications with controllers is handled by the kernel. Once we make a request to the kernel to perform an operation (e.g. connect), we have to wait for it to complete before requesting another operation. This is particularly important when we want to disconnect from a controller while there are pending operations, especially a pending connect. The "connect" operation is especially unpredictable because all connect requests are made through the blocking interface "/dev/nvme-fabrics". This means that once a "connect" operation has been submitted, and depending on how many connect requests are made concurrently, it can take several seconds for a connect to be processed by the kernel. While connect or other operations are being performed, it is possible that a disconnect may be requested (e.g. someone or something changes the configuration to remove a controller). Because it is not possible to terminate a pending operation request, we have to wait for it to complete before we can issue a disconnect. Failure to do that will result in operations being performed by the kernel in reverse order. For example, a disconnect may be executed before a pending connect has had a chance to complete. And this will result in controllers that are supposed to be disconnected to be connected without nvme-stas knowing about it. The Controller Terminator is used when we need to disconnect from a controller. It will make sure that there are no pending operations before issuing a disconnect. ''' DISPOSAL_AUDIT_PERIOD_SEC = 30 def __init__(self): self._udev = udev.UDEV self._controllers = list() # The list of controllers to dispose of. self._audit_tmr = gutil.GTimer(self.DISPOSAL_AUDIT_PERIOD_SEC, self._on_disposal_check) def dispose(self, controller: ctrl.Controller, on_controller_removed_cb, keep_connection: bool): '''Invoked by a service (stafd or stacd) to dispose of a controller''' if controller.all_ops_completed(): logging.debug( 'CtrlTerminator.dispose() - %s | %s: Invoke disconnect()', controller.tid, controller.device ) controller.disconnect(on_controller_removed_cb, keep_connection) else: logging.debug( 'CtrlTerminator.dispose() - %s | %s: Add controller to garbage disposal', controller.tid, controller.device, ) self._controllers.append((controller, keep_connection, on_controller_removed_cb, controller.tid)) self._udev.register_for_action_events('add', self._on_kernel_events) self._udev.register_for_action_events('remove', self._on_kernel_events) if self._audit_tmr.time_remaining() == 0: self._audit_tmr.start() def pending_disposal(self, tid): '''Check whether @tid is pending disposal''' for controller in self._controllers: if controller.tid == tid: return True return False def info(self): '''@brief Get info about this object (used for debug)''' info = { 'terminator.audit timer': str(self._audit_tmr), } for controller, _, _, tid in self._controllers: info[f'terminator.controller.{tid}'] = str(controller.info()) return info def kill(self): '''Stop Controller Terminator and release resources.''' self._audit_tmr.stop() self._audit_tmr = None if self._udev: self._udev.unregister_for_action_events('add', self._on_kernel_events) self._udev.unregister_for_action_events('remove', self._on_kernel_events) self._udev = None for controller, keep_connection, on_controller_removed_cb, _ in self._controllers: controller.disconnect(on_controller_removed_cb, keep_connection) self._controllers.clear() def _on_kernel_events(self, udev_obj): logging.debug('CtrlTerminator._on_kernel_events() - %s event received', udev_obj.action) self._disposal_check() def _on_disposal_check(self, *_user_data): logging.debug('CtrlTerminator._on_disposal_check()- Periodic audit') return GLib.SOURCE_REMOVE if self._disposal_check() else GLib.SOURCE_CONTINUE @staticmethod def _keep_or_terminate(args): '''Return False if controller is to be kept. True if controller was terminated and can be removed from the list.''' controller, keep_connection, on_controller_removed_cb, tid = args if controller.all_ops_completed(): logging.debug( 'CtrlTerminator._keep_or_terminate()- %s | %s: Disconnecting controller', tid, controller.device, ) controller.disconnect(on_controller_removed_cb, keep_connection) return True return False def _disposal_check(self): # Iterate over the list, terminating (disconnecting) those controllers # that have no pending operations, and remove those controllers from the # list (only keep controllers that still have operations pending). self._controllers[:] = filterfalse(self._keep_or_terminate, self._controllers) disposal_complete = len(self._controllers) == 0 if disposal_complete: logging.debug('CtrlTerminator._disposal_check() - Disposal complete') self._audit_tmr.stop() self._udev.unregister_for_action_events('add', self._on_kernel_events) self._udev.unregister_for_action_events('remove', self._on_kernel_events) else: self._audit_tmr.start() # Restart timer return disposal_complete # ****************************************************************************** class Service(stas.ServiceABC): '''@brief Base class used to manage a STorage Appliance Service''' def __init__(self, args, default_conf, reload_hdlr): self._udev = udev.UDEV self._terminator = CtrlTerminator() super().__init__(args, default_conf, reload_hdlr) def _release_resources(self): logging.debug('Service._release_resources()') super()._release_resources() if self._terminator: self._terminator.kill() self._udev = None self._terminator = None def _disconnect_all(self): '''Tell all controller objects to disconnect''' keep_connections = self._keep_connections_on_exit() controllers = self._controllers.values() logging.debug( 'Service._stop_hdlr() - Controller count = %s, keep_connections = %s', len(controllers), keep_connections, ) for controller in controllers: self._terminator.dispose(controller, self._on_final_disconnect, keep_connections) def info(self) -> dict: '''@brief Get the status info for this object (used for debug)''' info = super().info() if self._terminator: info.update(self._terminator.info()) return info @stas.ServiceABC.tron.setter def tron(self, value): '''@brief Set Trace ON property''' super(__class__, self.__class__).tron.__set__(self, value) # ****************************************************************************** class Stac(Service): '''STorage Appliance Connector (STAC)''' CONF_STABILITY_LONG_SOAK_TIME_SEC = 10 # pylint: disable=invalid-name ADD_EVENT_SOAK_TIME_SEC = 1 def __init__(self, args, dbus): default_conf = { ('Global', 'tron'): False, ('Global', 'hdr-digest'): False, ('Global', 'data-digest'): False, ('Global', 'kato'): None, # None to let the driver decide the default ('Global', 'nr-io-queues'): None, # None to let the driver decide the default ('Global', 'nr-write-queues'): None, # None to let the driver decide the default ('Global', 'nr-poll-queues'): None, # None to let the driver decide the default ('Global', 'queue-size'): None, # None to let the driver decide the default ('Global', 'reconnect-delay'): None, # None to let the driver decide the default ('Global', 'ctrl-loss-tmo'): None, # None to let the driver decide the default ('Global', 'disable-sqflow'): None, # None to let the driver decide the default ('Global', 'ignore-iface'): False, ('Global', 'ip-family'): (4, 6), ('Controllers', 'controller'): list(), ('Controllers', 'exclude'): list(), ('I/O controller connection management', 'disconnect-scope'): 'only-stas-connections', ('I/O controller connection management', 'disconnect-trtypes'): ['tcp'], ('I/O controller connection management', 'connect-attempts-on-ncc'): 0, } super().__init__(args, default_conf, self._reload_hdlr) self._add_event_soak_tmr = gutil.GTimer(self.ADD_EVENT_SOAK_TIME_SEC, self._on_add_event_soaked) self._config_connections_audit() # Create the D-Bus instance. self._config_dbus(dbus, defs.STACD_DBUS_NAME, defs.STACD_DBUS_PATH) # Connect to STAF D-Bus interface self._staf = None self._staf_watcher = dasbus.client.observer.DBusObserver(self._sysbus, defs.STAFD_DBUS_NAME) self._staf_watcher.service_available.connect(self._connect_to_staf) self._staf_watcher.service_unavailable.connect(self._disconnect_from_staf) self._staf_watcher.connect_once_available() def _release_resources(self): logging.debug('Stac._release_resources()') if self._add_event_soak_tmr: self._add_event_soak_tmr.kill() if self._udev: self._udev.unregister_for_action_events('add', self._on_add_event) self._destroy_staf_comlink(self._staf_watcher) if self._staf_watcher is not None: self._staf_watcher.disconnect() super()._release_resources() self._staf = None self._staf_watcher = None self._add_event_soak_tmr = None def _dump_last_known_config(self, controllers): config = list(controllers.keys()) logging.debug('Stac._dump_last_known_config() - IOC count = %s', len(config)) self._write_lkc(config) def _load_last_known_config(self): config = self._read_lkc() or list() logging.debug('Stac._load_last_known_config() - IOC count = %s', len(config)) controllers = {} for tid in config: # Only create Ioc objects if there is already a connection in the kernel # First, regenerate the TID (in case of soft. upgrade and TID object # has changed internally) tid = trid.TID(tid.as_dict()) if udev.UDEV.find_nvme_ioc_device(tid) is not None: controllers[tid] = ctrl.Ioc(self, tid) return controllers def _audit_all_connections(self, tids): '''A host should only connect to I/O controllers that have been zoned for that host or a manual "controller" entry exists in stacd.conf. A host should disconnect from an I/O controller when that I/O controller is removed from the zone or a "controller" entry is manually removed from stacd.conf. stacd will audit connections if "disconnect-scope= all-connections-matching-disconnect-trtypes". stacd will delete any connection that is not supposed to exist. ''' logging.debug('Stac._audit_all_connections() - tids = %s', tids) num_controllers = len(self._controllers) for tid in tids: if tid not in self._controllers and not self._terminator.pending_disposal(tid): self._controllers[tid] = ctrl.Ioc(self, tid) if num_controllers != len(self._controllers): self._cfg_soak_tmr.start(self.CONF_STABILITY_SOAK_TIME_SEC) def _on_add_event(self, udev_obj): '''@brief This function is called when a "add" event is received from the kernel for an NVMe device. This is used to trigger an audit and make sure that the connection to an I/O controller is allowed. WARNING: There is a race condition with the "add" event from the kernel. The kernel sends the "add" event a bit early and the sysfs attributes associated with the nvme object are not always fully initialized. To workaround this problem we use a soaking timer to give time for the sysfs attributes to stabilize. ''' logging.debug('Stac._on_add_event(() - Received "add" event: %s', udev_obj.sys_name) self._add_event_soak_tmr.start() def _on_add_event_soaked(self): '''@brief After the add event has been soaking for ADD_EVENT_SOAK_TIME_SEC seconds, we can audit the connections. ''' if self._alive(): svc_conf = conf.SvcConf() if svc_conf.disconnect_scope == 'all-connections-matching-disconnect-trtypes': self._audit_all_connections(self._udev.get_nvme_ioc_tids(svc_conf.disconnect_trtypes)) return GLib.SOURCE_REMOVE def _config_connections_audit(self): '''This function checks the "disconnect_scope" parameter to determine whether audits should be performed. Audits are enabled when "disconnect_scope == all-connections-matching-disconnect-trtypes". ''' svc_conf = conf.SvcConf() if svc_conf.disconnect_scope == 'all-connections-matching-disconnect-trtypes': if not self._udev.is_action_cback_registered('add', self._on_add_event): self._udev.register_for_action_events('add', self._on_add_event) self._audit_all_connections(self._udev.get_nvme_ioc_tids(svc_conf.disconnect_trtypes)) else: self._udev.unregister_for_action_events('add', self._on_add_event) def _keep_connections_on_exit(self): '''@brief Determine whether connections should remain when the process exits. ''' return True def _reload_hdlr(self): '''@brief Reload configuration file. This is triggered by the SIGHUP signal, which can be sent with "systemctl reload stacd". ''' if not self._alive(): return GLib.SOURCE_REMOVE sd_notify('RELOADING=1') service_cnf = conf.SvcConf() service_cnf.reload() self.tron = service_cnf.tron self._config_connections_audit() self._cfg_soak_tmr.start(self.CONF_STABILITY_SOAK_TIME_SEC) for controller in self._controllers.values(): controller.reload_hdlr() sd_notify('READY=1') return GLib.SOURCE_CONTINUE def _get_log_pages_from_stafd(self): if self._staf: try: return json.loads(self._staf.get_all_log_pages(True)) except dasbus.error.DBusError: pass return list() def _config_ctrls_finish(self, configured_ctrl_list: list): # pylint: disable=too-many-locals '''@param configured_ctrl_list: list of TIDs''' # This is a callback function, which may be called after the service # has been signalled to stop. So let's make sure the service is still # alive and well before continuing. if not self._alive(): logging.debug('Stac._config_ctrls_finish() - Exiting because service is no longer alive') return # Eliminate invalid entries from stacd.conf "controller list". configured_ctrl_list = [ tid for tid in configured_ctrl_list if '' not in (tid.transport, tid.traddr, tid.trsvcid, tid.subsysnqn) ] logging.debug('Stac._config_ctrls_finish() - configured_ctrl_list = %s', configured_ctrl_list) discovered_ctrls = dict() for staf_data in self._get_log_pages_from_stafd(): host_traddr = staf_data['discovery-controller']['host-traddr'] host_iface = staf_data['discovery-controller']['host-iface'] for dlpe in staf_data['log-pages']: if dlpe.get('subtype') == 'nvme': # eliminate discovery controllers tid = stas.tid_from_dlpe(dlpe, host_traddr, host_iface) discovered_ctrls[tid] = dlpe discovered_ctrl_list = list(discovered_ctrls.keys()) logging.debug('Stac._config_ctrls_finish() - discovered_ctrl_list = %s', discovered_ctrl_list) controllers = stas.remove_excluded(configured_ctrl_list + discovered_ctrl_list) controllers = iputil.remove_invalid_addresses(controllers) new_controller_tids = set(controllers) cur_controller_tids = set(self._controllers.keys()) controllers_to_add = new_controller_tids - cur_controller_tids controllers_to_del = cur_controller_tids - new_controller_tids logging.debug('Stac._config_ctrls_finish() - controllers_to_add = %s', list(controllers_to_add)) logging.debug('Stac._config_ctrls_finish() - controllers_to_del = %s', list(controllers_to_del)) svc_conf = conf.SvcConf() no_disconnect = svc_conf.disconnect_scope == 'no-disconnect' match_trtypes = svc_conf.disconnect_scope == 'all-connections-matching-disconnect-trtypes' logging.debug( 'Stac._config_ctrls_finish() - no_disconnect=%s, match_trtypes=%s, svc_conf.disconnect_trtypes=%s', no_disconnect, match_trtypes, svc_conf.disconnect_trtypes, ) for tid in controllers_to_del: controller = self._controllers.pop(tid, None) if controller is not None: keep_connection = no_disconnect or (match_trtypes and tid.transport not in svc_conf.disconnect_trtypes) self._terminator.dispose(controller, self.remove_controller, keep_connection) for tid in controllers_to_add: self._controllers[tid] = ctrl.Ioc(self, tid) for tid, controller in self._controllers.items(): if tid in discovered_ctrls: dlpe = discovered_ctrls[tid] controller.update_dlpe(dlpe) self._dump_last_known_config(self._controllers) def _connect_to_staf(self, _): '''@brief Hook up DBus signal handlers for signals from stafd.''' if not self._alive(): return try: self._staf = self._sysbus.get_proxy(defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) self._staf.log_pages_changed.connect(self._log_pages_changed) self._staf.dc_removed.connect(self._dc_removed) self._cfg_soak_tmr.start() # Make sure timer is set back to its normal value. self._cfg_soak_tmr.set_timeout(self.CONF_STABILITY_SOAK_TIME_SEC) logging.debug('Stac._connect_to_staf() - Connected to staf') except dasbus.error.DBusError: logging.error('Failed to connect to staf') def _destroy_staf_comlink(self, watcher): # pylint: disable=unused-argument if self._staf: self._staf.log_pages_changed.disconnect(self._log_pages_changed) self._staf.dc_removed.disconnect(self._dc_removed) dasbus.client.proxy.disconnect_proxy(self._staf) self._staf = None def _disconnect_from_staf(self, watcher): self._destroy_staf_comlink(watcher) # When we lose connectivity with stafd, the most logical explanation # is that stafd restarted. In that case, it may take some time for stafd # to re-populate its log pages cache. So let's give stafd plenty of time # to update its log pages cache and send log pages change notifications # before triggering a stacd re-config. We do this by momentarily # increasing the config soak timer to a longer period. if self._cfg_soak_tmr: self._cfg_soak_tmr.set_timeout(self.CONF_STABILITY_LONG_SOAK_TIME_SEC) logging.debug('Stac._disconnect_from_staf() - Disconnected from staf') def _log_pages_changed( # pylint: disable=too-many-arguments self, transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn, device ): if not self._alive(): return logging.debug( 'Stac._log_pages_changed() - transport=%s, traddr=%s, trsvcid=%s, host_traddr=%s, host_iface=%s, subsysnqn=%s, device=%s', transport, traddr, trsvcid, host_traddr, host_iface, subsysnqn, device, ) if self._cfg_soak_tmr: self._cfg_soak_tmr.start(self.CONF_STABILITY_SOAK_TIME_SEC) def _dc_removed(self): if not self._alive(): return logging.debug('Stac._dc_removed()') if self._cfg_soak_tmr: self._cfg_soak_tmr.start(self.CONF_STABILITY_SOAK_TIME_SEC) # ****************************************************************************** # Only keep legacy FC rule (not even sure this is still in use today, but just to be safe). UDEV_RULE_OVERRIDE = r''' ACTION=="change", SUBSYSTEM=="fc", ENV{FC_EVENT}=="nvmediscovery", \ ENV{NVMEFC_HOST_TRADDR}=="*", ENV{NVMEFC_TRADDR}=="*", \ RUN+="%s --no-block start nvmf-connect@--transport=fc\t--traddr=$env{NVMEFC_TRADDR}\t--trsvcid=none\t--host-traddr=$env{NVMEFC_HOST_TRADDR}.service" ''' def _udev_rule_ctrl(suppress): '''@brief We override the standard udev rule installed by nvme-cli, i.e. '/usr/lib/udev/rules.d/70-nvmf-autoconnect.rules', with a copy into /run/udev/rules.d. The goal is to suppress the udev rule that controls TCP connections to I/O controllers. This is to avoid race conditions between stacd and udevd. This is configurable. See "udev-rule" in stacd.conf for details. @param enable: When True, override nvme-cli's udev rule and prevent TCP I/O Controller connections by nvme-cli. When False, allow nvme-cli's udev rule to make TCP I/O connections. @type enable: bool ''' udev_rule_file = pathlib.Path('/run/udev/rules.d', '70-nvmf-autoconnect.rules') if suppress: if not udev_rule_file.exists(): pathlib.Path('/run/udev/rules.d').mkdir(parents=True, exist_ok=True) text = UDEV_RULE_OVERRIDE % (defs.SYSTEMCTL) udev_rule_file.write_text(text) # pylint: disable=unspecified-encoding else: try: udev_rule_file.unlink() except FileNotFoundError: pass def _is_dlp_changed_aen(udev_obj): '''Check whether we received a Change of Discovery Log Page AEN''' nvme_aen = udev_obj.get('NVME_AEN') if not isinstance(nvme_aen, str): return False aen = int(nvme_aen, 16) if aen != ctrl.DLP_CHANGED: return False logging.info( '%s - Received AEN: Change of Discovery Log Page (%s)', udev_obj.sys_name, nvme_aen, ) return True def _event_matches(udev_obj, nvme_events): '''Check whether we received an NVMe Event matching one of the events listed in @nvme_events''' nvme_event = udev_obj.get('NVME_EVENT') if nvme_event not in nvme_events: return False logging.info('%s - Received "%s" event', udev_obj.sys_name, nvme_event) return True # ****************************************************************************** class Staf(Service): '''STorage Appliance Finder (STAF)''' def __init__(self, args, dbus): default_conf = { ('Global', 'tron'): False, ('Global', 'hdr-digest'): False, ('Global', 'data-digest'): False, ('Global', 'kato'): 30, ('Global', 'queue-size'): None, # None to let the driver decide the default ('Global', 'reconnect-delay'): None, # None to let the driver decide the default ('Global', 'ctrl-loss-tmo'): None, # None to let the driver decide the default ('Global', 'disable-sqflow'): None, # None to let the driver decide the default ('Global', 'persistent-connections'): False, # Deprecated ('Discovery controller connection management', 'persistent-connections'): True, ('Discovery controller connection management', 'zeroconf-connections-persistence'): timeparse.timeparse( '72hours' ), ('Global', 'ignore-iface'): False, ('Global', 'ip-family'): (4, 6), ('Global', 'pleo'): True, ('Service Discovery', 'zeroconf'): True, ('Controllers', 'controller'): list(), ('Controllers', 'exclude'): list(), } super().__init__(args, default_conf, self._reload_hdlr) self._avahi = avahi.Avahi(self._sysbus, self._avahi_change) self._avahi.config_stypes(conf.SvcConf().stypes) # Create the D-Bus instance. self._config_dbus(dbus, defs.STAFD_DBUS_NAME, defs.STAFD_DBUS_PATH) self._udev.register_for_action_events('change', self._nvme_cli_interop) _udev_rule_ctrl(True) def info(self) -> dict: '''@brief Get the status info for this object (used for debug)''' info = super().info() info['avahi'] = self._avahi.info() return info def _release_resources(self): logging.debug('Staf._release_resources()') if self._udev: self._udev.unregister_for_action_events('change', self._nvme_cli_interop) super()._release_resources() _udev_rule_ctrl(False) if self._avahi: self._avahi.kill() self._avahi = None def _dump_last_known_config(self, controllers): config = {tid: {'log_pages': dc.log_pages(), 'origin': dc.origin} for tid, dc in controllers.items()} logging.debug('Staf._dump_last_known_config() - DC count = %s', len(config)) self._write_lkc(config) def _load_last_known_config(self): config = self._read_lkc() or dict() logging.debug('Staf._load_last_known_config() - DC count = %s', len(config)) controllers = {} for tid, data in config.items(): if isinstance(data, dict): log_pages = data.get('log_pages') origin = data.get('origin') else: log_pages = data origin = None # Regenerate the TID (in case of soft. upgrade and TID object # has changed internally) tid = trid.TID(tid.as_dict()) controllers[tid] = ctrl.Dc(self, tid, log_pages, origin) return controllers def _keep_connections_on_exit(self): '''@brief Determine whether connections should remain when the process exits. ''' return conf.SvcConf().persistent_connections def _reload_hdlr(self): '''@brief Reload configuration file. This is triggered by the SIGHUP signal, which can be sent with "systemctl reload stafd". ''' if not self._alive(): return GLib.SOURCE_REMOVE sd_notify('RELOADING=1') service_cnf = conf.SvcConf() service_cnf.reload() self.tron = service_cnf.tron self._avahi.kick_start() # Make sure Avahi is running self._avahi.config_stypes(service_cnf.stypes) self._cfg_soak_tmr.start() for controller in self._controllers.values(): controller.reload_hdlr() sd_notify('READY=1') return GLib.SOURCE_CONTINUE def is_avahi_reported(self, tid): '''@brief Return whether @tid is being reported by the Avahi daemon. @return: True if the Avahi daemon is reporting it, False otherwise. ''' for cid in self._avahi.get_controllers(): if trid.TID(cid) == tid: return True return False def log_pages_changed(self, controller, device): '''@brief Function invoked when a controller's cached log pages have changed. This will emit a D-Bus signal to inform other applications that the cached log pages have changed. ''' self._dbus_iface.log_pages_changed.emit( controller.tid.transport, controller.tid.traddr, controller.tid.trsvcid, controller.tid.host_traddr, controller.tid.host_iface, controller.tid.subsysnqn, device, ) def dc_removed(self): '''@brief Function invoked when a controller's cached log pages have changed. This will emit a D-Bus signal to inform other applications that the cached log pages have changed. ''' self._dbus_iface.dc_removed.emit() def _referrals(self) -> list: return [ stas.tid_from_dlpe(dlpe, controller.tid.host_traddr, controller.tid.host_iface) for controller in self.get_controllers() for dlpe in controller.referrals() ] def _config_ctrls_finish(self, configured_ctrl_list: list): '''@brief Finish discovery controllers configuration after hostnames (if any) have been resolved. All the logic associated with discovery controller creation/deletion is found here. To avoid calling this algorith repetitively for each and every events, it is called after a soaking period controlled by self._cfg_soak_tmr. @param configured_ctrl_list: List of TIDs configured in stafd.conf with all hostnames resolved to their corresponding IP addresses. ''' # This is a callback function, which may be called after the service # has been signalled to stop. So let's make sure the service is still # alive and well before continuing. if not self._alive(): logging.debug('Staf._config_ctrls_finish() - Exiting because service is no longer alive') return # Eliminate invalid entries from stafd.conf "controller list". controllers = list() for tid in configured_ctrl_list: if '' in (tid.transport, tid.traddr, tid.trsvcid): continue if not tid.subsysnqn: cid = tid.as_dict() cid['subsysnqn'] = defs.WELL_KNOWN_DISC_NQN controllers.append(trid.TID(cid)) else: controllers.append(tid) configured_ctrl_list = controllers # Get the Avahi-discovered list and the referrals. discovered_ctrl_list = [trid.TID(cid) for cid in self._avahi.get_controllers()] referral_ctrl_list = self._referrals() logging.debug('Staf._config_ctrls_finish() - configured_ctrl_list = %s', configured_ctrl_list) logging.debug('Staf._config_ctrls_finish() - discovered_ctrl_list = %s', discovered_ctrl_list) logging.debug('Staf._config_ctrls_finish() - referral_ctrl_list = %s', referral_ctrl_list) all_ctrls = configured_ctrl_list + discovered_ctrl_list + referral_ctrl_list controllers = stas.remove_excluded(all_ctrls) controllers = iputil.remove_invalid_addresses(controllers) new_controller_tids = set(controllers) cur_controller_tids = set(self._controllers.keys()) controllers_to_add = new_controller_tids - cur_controller_tids controllers_to_del = cur_controller_tids - new_controller_tids # Make a list list of excluded and invalid controllers must_remove_list = set(all_ctrls) - new_controller_tids # Find "discovered" controllers that have not responded # in a while and add them to controllers that must be removed. must_remove_list.update({tid for tid, controller in self._controllers.items() if controller.is_unresponsive()}) # Do not remove Avahi-discovered DCs from controllers_to_del unless # marked as "must-be-removed" (must_remove_list). This is to account for # the case where mDNS discovery is momentarily disabled (e.g. Avahi # daemon restarts). We don't want to delete connections because of # temporary mDNS impairments. Removal of Avahi-discovered DCs will be # handled differently and only if the connection cannot be established # for a long period of time. logging.debug('Staf._config_ctrls_finish() - must_remove_list = %s', list(must_remove_list)) controllers_to_del = { tid for tid in controllers_to_del if tid in must_remove_list or self._controllers[tid].origin != 'discovered' } logging.debug('Staf._config_ctrls_finish() - controllers_to_add = %s', list(controllers_to_add)) logging.debug('Staf._config_ctrls_finish() - controllers_to_del = %s', list(controllers_to_del)) # Delete controllers for tid in controllers_to_del: controller = self._controllers.pop(tid, None) if controller is not None: self._terminator.dispose(controller, self.remove_controller, keep_connection=False) if len(controllers_to_del) > 0: self.dc_removed() # Let other apps (e.g. stacd) know that discovery controllers were removed. # Add controllers for tid in controllers_to_add: self._controllers[tid] = ctrl.Dc(self, tid) # Update "origin" on all DC objects for tid, controller in self._controllers.items(): origin = ( 'configured' if tid in configured_ctrl_list else 'referral' if tid in referral_ctrl_list else 'discovered' if tid in discovered_ctrl_list else None ) if origin is not None: controller.origin = origin self._dump_last_known_config(self._controllers) def _avahi_change(self): if self._alive() and self._cfg_soak_tmr is not None: self._cfg_soak_tmr.start() def controller_unresponsive(self, tid): '''@brief Function invoked when a controller becomes unresponsive and needs to be removed. ''' if self._alive() and self._cfg_soak_tmr is not None: logging.debug('Staf.controller_unresponsive() - tid = %s', tid) self._cfg_soak_tmr.start() def referrals_changed(self): '''@brief Function invoked when a controller's cached referrals have changed. ''' if self._alive() and self._cfg_soak_tmr is not None: logging.debug('Staf.referrals_changed()') self._cfg_soak_tmr.start() def _nvme_cli_interop(self, udev_obj): '''Interoperability with nvme-cli: stafd will invoke nvme-cli's connect-all the same way nvme-cli's udev rules would do normally. This is for the case where a user has an hybrid configuration where some controllers are configured through nvme-stas and others through nvme-cli. This is not an optimal configuration. It would be better if everything was configured through nvme-stas, however support for hybrid configuration was requested by users (actually only one user requested this).''' # Looking for 'change' events only if udev_obj.action != 'change': return # Looking for events from Discovery Controllers only if not udev.Udev.is_dc_device(udev_obj): return # Is the controller already being monitored by stafd? for controller in self.get_controllers(): if controller.device == udev_obj.sys_name: return # Did we receive a Change of DLP AEN or an NVME Event indicating 'connect' or 'rediscover'? if not _is_dlp_changed_aen(udev_obj) and not _event_matches(udev_obj, ('connected', 'rediscover')): return # We need to invoke "nvme connect-all" using nvme-cli's nvmf-connect@.service # NOTE: Eventually, we'll be able to drop --host-traddr and --host-iface from # the parameters passed to nvmf-connect@.service. A fix was added to connect-all # to infer these two values from the device used to connect to the DC. # Ref: https://github.com/linux-nvme/nvme-cli/pull/1812 cnf = [ ('--device', udev_obj.sys_name), ('--host-traddr', udev_obj.properties.get('NVME_HOST_TRADDR', None)), ('--host-iface', udev_obj.properties.get('NVME_HOST_IFACE', None)), ] # Use systemd's escaped syntax (i.e. '=' is replaced by '\x3d', '\t' by '\x09', etc. options = r'\x09'.join( [fr'{option}\x3d{value}' for option, value in cnf if value not in (None, 'none', 'None', '')] ) logging.info('Invoking: systemctl start nvmf-connect@%s.service', options) cmd = [defs.SYSTEMCTL, '--quiet', '--no-block', 'start', fr'nvmf-connect@{options}.service'] subprocess.run(cmd, check=False) linux-nvme-nvme-stas-a8026bb/staslib/singleton.py000066400000000000000000000013741440613556600221710ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''Implementation of a singleton pattern''' class Singleton(type): '''metaclass implementation of a singleton pattern''' _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: # This variable declaration is required to force a # strong reference on the instance. instance = super(Singleton, cls).__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] linux-nvme-nvme-stas-a8026bb/staslib/stacd.idl000066400000000000000000000020231440613556600213750ustar00rootroot00000000000000 linux-nvme-nvme-stas-a8026bb/staslib/stafd.idl000066400000000000000000000042531440613556600214070ustar00rootroot00000000000000 linux-nvme-nvme-stas-a8026bb/staslib/stas.py000066400000000000000000000507161440613556600211450ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''Library for staf/stac. You will find here common code for stafd and stacd including the Abstract Base Classes (ABC) for Controllers and Services''' import os import sys import abc import signal import pickle import logging import dasbus.connection from gi.repository import Gio, GLib from systemd.daemon import notify as sd_notify from staslib import conf, defs, gutil, log, trid try: # Python 3.9 or later # This is the preferred way, but may not be available before Python 3.9 from importlib.resources import files except ImportError: try: # Pre Python 3.9 backport of importlib.resources (if installed) from importlib_resources import files except ImportError: # Less efficient, but avalable on older versions of Python import pkg_resources def load_idl(idl_fname): '''@brief Load D-Bus Interface Description Language File''' try: return pkg_resources.resource_string('staslib', idl_fname).decode() except (FileNotFoundError, AttributeError): pass return '' else: def load_idl(idl_fname): '''@brief Load D-Bus Interface Description Language File''' try: return files('staslib').joinpath(idl_fname).read_text() # pylint: disable=unspecified-encoding except FileNotFoundError: pass return '' else: def load_idl(idl_fname): '''@brief Load D-Bus Interface Description Language File''' try: return files('staslib').joinpath(idl_fname).read_text() # pylint: disable=unspecified-encoding except FileNotFoundError: pass return '' # ****************************************************************************** def check_if_allowed_to_continue(): '''@brief Let's perform some basic checks before going too far. There are a few pre-requisites that need to be met before this program is allowed to proceed: 1) The program needs to have root privileges 2) The nvme-tcp kernel module must be loaded @return This function will only return if all conditions listed above are met. Otherwise the program exits. ''' # 1) Check root privileges if os.geteuid() != 0: sys.exit(f'Permission denied. You need root privileges to run {defs.PROG_NAME}.') # 2) Check that nvme-tcp kernel module is running if not os.path.exists('/dev/nvme-fabrics'): # There's no point going any further if the kernel module hasn't been loaded sys.exit('Fatal error: missing nvme-tcp kernel module') # ****************************************************************************** def tid_from_dlpe(dlpe, host_traddr, host_iface): '''@brief Take a Discovery Log Page Entry and return a Controller ID as a dict.''' cid = { 'transport': dlpe['trtype'], 'traddr': dlpe['traddr'], 'trsvcid': dlpe['trsvcid'], 'host-traddr': host_traddr, 'host-iface': host_iface, 'subsysnqn': dlpe['subnqn'], } return trid.TID(cid) # ****************************************************************************** def _excluded(excluded_ctrl_list, controller: dict): '''@brief Check if @controller is excluded.''' for excluded_ctrl in excluded_ctrl_list: test_results = [val == controller.get(key, None) for key, val in excluded_ctrl.items()] if all(test_results): return True return False # ****************************************************************************** def remove_excluded(controllers: list): '''@brief Remove excluded controllers from the list of controllers. @param controllers: List of TIDs ''' excluded_ctrl_list = conf.SvcConf().get_excluded() if excluded_ctrl_list: logging.debug('remove_excluded() - excluded_ctrl_list = %s', excluded_ctrl_list) controllers = [ controller for controller in controllers if not _excluded(excluded_ctrl_list, controller.as_dict()) ] return controllers # ****************************************************************************** class ControllerABC(abc.ABC): '''@brief Base class used to manage the connection to a controller.''' CONNECT_RETRY_PERIOD_SEC = 60 FAST_CONNECT_RETRY_PERIOD_SEC = 3 def __init__(self, tid: trid.TID, service, discovery_ctrl: bool = False): self._tid = tid self._serv = service # Refers to the parent service (either Staf or Stac) self.set_level_from_tron(self._serv.tron) self._cancellable = Gio.Cancellable() self._connect_attempts = 0 self._retry_connect_tmr = gutil.GTimer(self.CONNECT_RETRY_PERIOD_SEC, self._on_try_to_connect) self._discovery_ctrl = discovery_ctrl self._try_to_connect_deferred = gutil.Deferred(self._try_to_connect) self._try_to_connect_deferred.schedule() def _release_resources(self): # Remove pending deferred from main loop if self._try_to_connect_deferred: self._try_to_connect_deferred.cancel() if self._retry_connect_tmr is not None: self._retry_connect_tmr.kill() if self._alive(): self._cancellable.cancel() self._tid = None self._serv = None self._cancellable = None self._retry_connect_tmr = None self._try_to_connect_deferred = None @property def id(self) -> str: '''@brief Return the Transport ID as a printable string''' return str(self.tid) @property def tid(self): '''@brief Return the Transport ID object''' return self._tid def controller_id_dict(self) -> dict: '''@brief return the controller ID as a dict.''' return {k: str(v) for k, v in self.tid.as_dict().items()} def details(self) -> dict: '''@brief return detailed debug info about this controller''' return self.info() def info(self) -> dict: '''@brief Get the controller info for this object''' info = self.controller_id_dict() info['connect attempts'] = str(self._connect_attempts) info['retry connect timer'] = str(self._retry_connect_tmr) return info def cancel(self): '''@brief Used to cancel pending operations.''' if self._alive(): logging.debug('ControllerABC.cancel() - %s', self.id) self._cancellable.cancel() def kill(self): '''@brief Used to release all resources associated with this object.''' logging.debug('ControllerABC.kill() - %s', self.id) self._release_resources() def _alive(self): '''There may be race condition where a queued event gets processed after the object is no longer configured (i.e. alive). This method can be used by callback functions to make sure the object is still alive before processing further. ''' return self._cancellable and not self._cancellable.is_cancelled() def _on_try_to_connect(self): if self._alive(): self._try_to_connect_deferred.schedule() return GLib.SOURCE_REMOVE def _should_try_to_reconnect(self): # pylint: disable=no-self-use return True def _try_to_connect(self): if not self._alive(): return GLib.SOURCE_REMOVE # This is a deferred function call. Make sure # the source of the deferred is still good. source = GLib.main_current_source() if source and source.is_destroyed(): return GLib.SOURCE_REMOVE self._connect_attempts += 1 self._do_connect() return GLib.SOURCE_REMOVE @abc.abstractmethod def set_level_from_tron(self, tron): '''Set log level based on TRON''' @abc.abstractmethod def _do_connect(self): '''Perform connection''' @abc.abstractmethod def _on_aen(self, aen: int): '''Event handler when an AEN is received''' @abc.abstractmethod def _on_nvme_event(self, nvme_event): '''Event handler when an nvme_event is received''' @abc.abstractmethod def _on_ctrl_removed(self, udev_obj): '''Called when the associated nvme device (/dev/nvmeX) is removed from the system by the kernel. ''' @abc.abstractmethod def _find_existing_connection(self): '''Check if there is an existing connection that matches this Controller's TID''' @abc.abstractmethod def all_ops_completed(self) -> bool: '''@brief Returns True if all operations have completed. False otherwise.''' @abc.abstractmethod def connected(self): '''@brief Return whether a connection is established''' @abc.abstractmethod def disconnect(self, disconnected_cb, keep_connection): '''@brief Issue an asynchronous disconnect command to a Controller. Once the async command has completed, the callback 'disconnected_cb' will be invoked. If a controller is already disconnected, then the callback will be added to the main loop's next idle slot to be executed ASAP. ''' @abc.abstractmethod def reload_hdlr(self): '''@brief This is called when a "reload" signal is received.''' # ****************************************************************************** class ServiceABC(abc.ABC): # pylint: disable=too-many-instance-attributes '''@brief Base class used to manage a STorage Appliance Service''' CONF_STABILITY_SOAK_TIME_SEC = 1.5 def __init__(self, args, default_conf, reload_hdlr): service_conf = conf.SvcConf(default_conf=default_conf) service_conf.set_conf_file(args.conf_file) # reload configuration self._tron = args.tron or service_conf.tron log.set_level_from_tron(self._tron) self._lkc_file = os.path.join( os.environ.get('RUNTIME_DIRECTORY', os.path.join('/run', defs.PROG_NAME)), 'last-known-config.pickle' ) self._loop = GLib.MainLoop() self._cancellable = Gio.Cancellable() self._resolver = gutil.NameResolver() self._controllers = self._load_last_known_config() self._dbus_iface = None self._cfg_soak_tmr = gutil.GTimer(self.CONF_STABILITY_SOAK_TIME_SEC, self._on_config_ctrls) self._sysbus = dasbus.connection.SystemMessageBus() GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGINT, self._stop_hdlr) # CTRL-C GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, self._stop_hdlr) # systemctl stop stafd GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGHUP, reload_hdlr) # systemctl reload stafd nvme_options = conf.NvmeOptions() if not nvme_options.host_iface_supp or not nvme_options.discovery_supp: logging.warning( 'Kernel does not appear to support all the options needed to run this program. Consider updating to a later kernel version.' ) # We don't want to apply configuration changes right away. # Often, multiple changes will occur in a short amount of time (sub-second). # We want to wait until there are no more changes before applying them # to the system. The following timer acts as a "soak period". Changes # will be applied by calling self._on_config_ctrls() at the end of # the soak period. self._cfg_soak_tmr.start() def _release_resources(self): logging.debug('ServiceABC._release_resources()') if self._alive(): self._cancellable.cancel() if self._cfg_soak_tmr is not None: self._cfg_soak_tmr.kill() self._controllers.clear() if self._sysbus: self._sysbus.disconnect() self._cfg_soak_tmr = None self._cancellable = None self._resolver = None self._lkc_file = None self._sysbus = None def _config_dbus(self, iface_obj, bus_name: str, obj_name: str): self._dbus_iface = iface_obj self._sysbus.publish_object(obj_name, iface_obj) self._sysbus.register_service(bus_name) @property def tron(self): '''@brief Get Trace ON property''' return self._tron @tron.setter def tron(self, value): '''@brief Set Trace ON property''' self._tron = value log.set_level_from_tron(self._tron) for controller in self._controllers.values(): controller.set_level_from_tron(self._tron) def run(self): '''@brief Start the main loop execution''' try: self._loop.run() except Exception as ex: # pylint: disable=broad-except logging.critical('exception: %s', ex) self._loop = None def info(self) -> dict: '''@brief Get the status info for this object (used for debug)''' nvme_options = conf.NvmeOptions() info = conf.SysConf().as_dict() info['last known config file'] = self._lkc_file info['config soak timer'] = str(self._cfg_soak_tmr) info['kernel support.TP8013'] = str(nvme_options.discovery_supp) info['kernel support.host_iface'] = str(nvme_options.host_iface_supp) return info def get_controllers(self) -> dict: '''@brief return the list of controller objects''' return self._controllers.values() def get_controller( self, transport: str, traddr: str, trsvcid: str, host_traddr: str, host_iface: str, subsysnqn: str ): # pylint: disable=too-many-arguments '''@brief get the specified controller object from the list of controllers''' cid = { 'transport': transport, 'traddr': traddr, 'trsvcid': trsvcid, 'host-traddr': host_traddr, 'host-iface': host_iface, 'subsysnqn': subsysnqn, } return self._controllers.get(trid.TID(cid)) def _remove_ctrl_from_dict(self, controller, shutdown=False): tid_to_pop = controller.tid if not tid_to_pop: # Being paranoid. This should not happen, but let's say the # controller object has been purged, but it is somehow still # listed in self._controllers. for tid, _controller in self._controllers.items(): if _controller is controller: tid_to_pop = tid break if tid_to_pop: logging.debug('ServiceABC._remove_ctrl_from_dict()- %s | %s', tid_to_pop, controller.device) popped = self._controllers.pop(tid_to_pop, None) if not shutdown and popped is not None and self._cfg_soak_tmr: self._cfg_soak_tmr.start() else: logging.debug('ServiceABC._remove_ctrl_from_dict()- already removed') def remove_controller(self, controller, success): # pylint: disable=unused-argument '''@brief remove the specified controller object from the list of controllers @param controller: the controller object @param success: whether the disconnect was successful''' logging.debug('ServiceABC.remove_controller()') if isinstance(controller, ControllerABC): self._remove_ctrl_from_dict(controller) controller.kill() def _alive(self): '''It's a good idea to check that this object hasn't been cancelled (i.e. is still alive) when entering a callback function. Callback functrions can be invoked after, for example, a process has been signalled to stop or restart, in which case it makes no sense to proceed with the callback. ''' return self._cancellable and not self._cancellable.is_cancelled() def _cancel(self): logging.debug('ServiceABC._cancel()') if self._alive(): self._cancellable.cancel() for controller in self._controllers.values(): controller.cancel() def _stop_hdlr(self): logging.debug('ServiceABC._stop_hdlr()') sd_notify('STOPPING=1') self._cancel() # Cancel pending operations self._dump_last_known_config(self._controllers) if len(self._controllers) == 0: GLib.idle_add(self._exit) else: self._disconnect_all() return GLib.SOURCE_REMOVE def _on_final_disconnect(self, controller, success): '''Callback invoked after a controller is disconnected. THIS IS USED DURING PROCESS SHUTDOWN TO WAIT FOR ALL CONTROLLERS TO BE DISCONNECTED BEFORE EXITING THE PROGRAM. ONLY CALL ON SHUTDOWN! @param controller: the controller object @param success: whether the disconnect operation was successful ''' logging.debug( 'ServiceABC._on_final_disconnect() - %s | %s: disconnect %s', controller.id, controller.device, 'succeeded' if success else 'failed', ) self._remove_ctrl_from_dict(controller, True) controller.kill() # When all controllers have disconnected, we can finish the clean up if len(self._controllers) == 0: # Defer exit to the next main loop's idle period. GLib.idle_add(self._exit) def _exit(self): logging.debug('ServiceABC._exit()') self._release_resources() self._loop.quit() def _on_config_ctrls(self, *_user_data): if self._alive(): self._config_ctrls() return GLib.SOURCE_REMOVE def _config_ctrls(self): '''@brief Start controllers configuration.''' # The configuration file may contain controllers and/or excluded # controllers with traddr specified as hostname instead of IP address. # Because of this, we need to remove those excluded elements before # running name resolution. And we will need to remove excluded # elements after name resolution is complete (i.e. in the calback # function _config_ctrls_finish) logging.debug('ServiceABC._config_ctrls()') configured_controllers = [trid.TID(cid) for cid in conf.SvcConf().get_controllers()] configured_controllers = remove_excluded(configured_controllers) self._resolver.resolve_ctrl_async(self._cancellable, configured_controllers, self._config_ctrls_finish) def _read_lkc(self): '''@brief Read Last Known Config from file''' try: with open(self._lkc_file, 'rb') as file: return pickle.load(file) except (FileNotFoundError, AttributeError, EOFError): return None def _write_lkc(self, config): '''@brief Write Last Known Config to file, and if config is empty make sure the file is emptied.''' try: # Note that if config is empty we still # want to open/close the file to empty it. with open(self._lkc_file, 'wb') as file: if config: pickle.dump(config, file) except FileNotFoundError as ex: logging.error('Unable to save last known config: %s', ex) @abc.abstractmethod def _disconnect_all(self): '''Tell all controller objects to disconnect''' @abc.abstractmethod def _keep_connections_on_exit(self): '''@brief Determine whether connections should remain when the process exits. NOTE) This is the base class method used to define the interface. It must be overloaded by a child class. ''' @abc.abstractmethod def _config_ctrls_finish(self, configured_ctrl_list): '''@brief Finish controllers configuration after hostnames (if any) have been resolved. Configuring controllers must be done asynchronously in 2 steps. In the first step, host names get resolved to find their IP addresses. Name resolution can take a while, especially when an external name resolution server is used. Once that step completed, the callback method _config_ctrls_finish() (i.e. this method), gets invoked to complete the controller configuration. NOTE) This is the base class method used to define the interface. It must be overloaded by a child class. ''' @abc.abstractmethod def _load_last_known_config(self): '''Load last known config from file (if any)''' @abc.abstractmethod def _dump_last_known_config(self, controllers): '''Save last known config to file''' linux-nvme-nvme-stas-a8026bb/staslib/timeparse.py000066400000000000000000000110411440613556600221500ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- ''' This module was borrowed and modified from: https://github.com/wroberts/pytimeparse timeparse.py (c) Will Roberts 1 February, 2014 Implements a single function, `timeparse`, which can parse various kinds of time expressions. ''' # MIT LICENSE # # 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 re SIGN = r'(?P[+|-])?' DAYS = r'(?P[\d.]+)\s*(?:d|dys?|days?)' HOURS = r'(?P[\d.]+)\s*(?:h|hrs?|hours?)' MINS = r'(?P[\d.]+)\s*(?:m|(mins?)|(minutes?))' SECS = r'(?P[\d.]+)\s*(?:s|secs?|seconds?)' SEPARATORS = r'[,/]' SECCLOCK = r':(?P\d{2}(?:\.\d+)?)' MINCLOCK = r'(?P\d{1,2}):(?P\d{2}(?:\.\d+)?)' HOURCLOCK = r'(?P\d+):(?P\d{2}):(?P\d{2}(?:\.\d+)?)' DAYCLOCK = r'(?P\d+):(?P\d{2}):(?P\d{2}):(?P\d{2}(?:\.\d+)?)' def _opt(string): return f'(?:{string})?' def _optsep(string): return fr'(?:{string}\s*(?:{SEPARATORS}\s*)?)?' TIMEFORMATS = [ fr'{_optsep(DAYS)}\s*{_optsep(HOURS)}\s*{_optsep(MINS)}\s*{_opt(SECS)}', f'{MINCLOCK}', fr'{_optsep(DAYS)}\s*{HOURCLOCK}', f'{DAYCLOCK}', f'{SECCLOCK}', ] COMPILED_SIGN = re.compile(r'\s*' + SIGN + r'\s*(?P.*)$') COMPILED_TIMEFORMATS = [re.compile(r'\s*' + timefmt + r'\s*$', re.I) for timefmt in TIMEFORMATS] MULTIPLIERS = { 'days': 60 * 60 * 24, 'hours': 60 * 60, 'mins': 60, 'secs': 1, } def timeparse(sval): ''' Parse a time expression, returning it as a number of seconds. If possible, the return value will be an `int`; if this is not possible, the return will be a `float`. Returns `None` if a time expression cannot be parsed from the given string. Arguments: - `sval`: the string value to parse >>> timeparse('1:24') 84 >>> timeparse(':22') 22 >>> timeparse('1 minute, 24 secs') 84 >>> timeparse('1m24s') 84 >>> timeparse('1.2 minutes') 72 >>> timeparse('1.2 seconds') 1.2 Time expressions can be signed. >>> timeparse('- 1 minute') -60 >>> timeparse('+ 1 minute') 60 ''' try: return float(sval) except TypeError: pass except ValueError: match = COMPILED_SIGN.match(sval) sign = -1 if match.groupdict()['sign'] == '-' else 1 sval = match.groupdict()['unsigned'] for timefmt in COMPILED_TIMEFORMATS: match = timefmt.match(sval) if match and match.group(0).strip(): mdict = match.groupdict() # if all of the fields are integer numbers if all(v.isdigit() for v in list(mdict.values()) if v): return sign * sum((MULTIPLIERS[k] * int(v, 10) for (k, v) in list(mdict.items()) if v is not None)) # if SECS is an integer number if 'secs' not in mdict or mdict['secs'] is None or mdict['secs'].isdigit(): # we will return an integer return sign * int( sum( ( MULTIPLIERS[k] * float(v) for (k, v) in list(mdict.items()) if k != 'secs' and v is not None ) ) ) + (int(mdict['secs'], 10) if mdict['secs'] else 0) # SECS is a float, we will return a float return sign * sum((MULTIPLIERS[k] * float(v) for (k, v) in list(mdict.items()) if v is not None)) return None linux-nvme-nvme-stas-a8026bb/staslib/trid.py000066400000000000000000000115121440613556600211240ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''This module defines the Transport Identifier Object, which is used throughout nvme-stas to uniquely identify a Controller''' import hashlib from staslib import conf class TID: # pylint: disable=too-many-instance-attributes '''Transport Identifier''' RDMA_IP_PORT = '4420' DISC_IP_PORT = '8009' def __init__(self, cid: dict): '''@param cid: Controller Identifier. A dictionary with the following contents. { # Transport parameters 'transport': str, # [mandatory] 'traddr': str, # [mandatory] 'subsysnqn': str, # [mandatory] 'trsvcid': str, # [optional] 'host-traddr': str, # [optional] 'host-iface': str, # [optional] # Connection parameters 'dhchap-ctrl-secret': str, # [optional] 'hdr-digest': str, # [optional] 'data-digest': str, # [optional] 'nr-io-queues': str, # [optional] 'nr-write-queues': str, # [optional] 'nr-poll-queues': str, # [optional] 'queue-size': str, # [optional] 'kato': str, # [optional] 'reconnect-delay': str, # [optional] 'ctrl-loss-tmo': str, # [optional] 'disable-sqflow': str, # [optional] } ''' self._cfg = { k: v for k, v in cid.items() if k not in ('transport', 'traddr', 'subsysnqn', 'trsvcid', 'host-traddr', 'host-iface') } self._transport = cid.get('transport', '') self._traddr = cid.get('traddr', '') self._trsvcid = '' if self._transport in ('tcp', 'rdma'): trsvcid = cid.get('trsvcid', None) self._trsvcid = ( trsvcid if trsvcid else (TID.RDMA_IP_PORT if self._transport == 'rdma' else TID.DISC_IP_PORT) ) self._host_traddr = cid.get('host-traddr', '') self._host_iface = '' if conf.SvcConf().ignore_iface else cid.get('host-iface', '') self._subsysnqn = cid.get('subsysnqn', '') self._shortkey = (self._transport, self._traddr, self._trsvcid, self._subsysnqn, self._host_traddr) self._key = (self._transport, self._traddr, self._trsvcid, self._subsysnqn, self._host_traddr, self._host_iface) self._hash = int.from_bytes( hashlib.md5(''.join(self._key).encode('utf-8')).digest(), 'big' ) # We need a consistent hash between restarts self._id = f'({self._transport}, {self._traddr}, {self._trsvcid}{", " + self._subsysnqn if self._subsysnqn else ""}{", " + self._host_iface if self._host_iface else ""}{", " + self._host_traddr if self._host_traddr else ""})' # pylint: disable=line-too-long @property def transport(self): # pylint: disable=missing-function-docstring return self._transport @property def traddr(self): # pylint: disable=missing-function-docstring return self._traddr @property def trsvcid(self): # pylint: disable=missing-function-docstring return self._trsvcid @property def host_traddr(self): # pylint: disable=missing-function-docstring return self._host_traddr @property def host_iface(self): # pylint: disable=missing-function-docstring return self._host_iface @property def subsysnqn(self): # pylint: disable=missing-function-docstring return self._subsysnqn @property def cfg(self): # pylint: disable=missing-function-docstring return self._cfg def as_dict(self): '''Return object members as a dictionary''' data = { 'transport': self.transport, 'traddr': self.traddr, 'subsysnqn': self.subsysnqn, 'trsvcid': self.trsvcid, 'host-traddr': self.host_traddr, 'host-iface': self.host_iface, } data.update(self._cfg) return data def __str__(self): return self._id def __repr__(self): return self._id def __eq__(self, other): if not isinstance(other, self.__class__): return False if self._host_iface and other._host_iface: return self._key == other._key return self._shortkey == other._shortkey def __ne__(self, other): if not isinstance(other, self.__class__): return True if self._host_iface and other._host_iface: return self._key != other._key return self._shortkey != other._shortkey def __hash__(self): return self._hash linux-nvme-nvme-stas-a8026bb/staslib/udev.py000066400000000000000000000304071440613556600211310ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # '''This module provides functions to access nvme devices using the pyudev module''' import os import time import logging import pyudev from gi.repository import GLib from staslib import defs, iputil, trid # ****************************************************************************** class Udev: '''@brief Udev event monitor. Provide a way to register for udev events. WARNING: THE singleton.Singleton PATTERN CANNOT BE USED WITH THIS CLASS. IT INTERFERES WITH THE pyudev INTERNALS, WHICH CAUSES OBJECT CLEAN UP TO FAIL. ''' def __init__(self): self._log_event_soak_time = 0 self._log_event_count = 0 self._device_event_registry = dict() self._action_event_registry = dict() self._context = pyudev.Context() self._monitor = pyudev.Monitor.from_netlink(self._context) self._monitor.filter_by(subsystem='nvme') self._event_source = GLib.io_add_watch( self._monitor.fileno(), GLib.PRIORITY_HIGH, GLib.IO_IN, self._process_udev_event, ) self._monitor.start() def release_resources(self): '''Release all resources used by this object''' if self._event_source is not None: GLib.source_remove(self._event_source) if self._monitor is not None: self._monitor.remove_filter() self._event_source = None self._monitor = None self._context = None self._device_event_registry = None self._action_event_registry = None def get_nvme_device(self, sys_name): '''@brief Get the udev device object associated with an nvme device. @param sys_name: The device system name (e.g. 'nvme1') @return A pyudev.device._device.Device object ''' device_node = os.path.join('/dev', sys_name) try: return pyudev.Devices.from_device_file(self._context, device_node) except pyudev.DeviceNotFoundByFileError as ex: logging.error("Udev.get_nvme_device() - Error: %s", ex) return None def is_action_cback_registered(self, action: str, user_cback): '''Returns True if @user_cback is registered for @action. False otherwise. @param action: one of 'add', 'remove', 'change'. @param user_cback: A callback function with this signature: cback(udev_obj) ''' return user_cback in self._action_event_registry.get(action, set()) def register_for_action_events(self, action: str, user_cback): '''@brief Register a callback function to be called when udev events for a specific action are received. @param action: one of 'add', 'remove', 'change'. ''' self._action_event_registry.setdefault(action, set()).add(user_cback) def unregister_for_action_events(self, action: str, user_cback): '''@brief The opposite of register_for_action_events()''' try: self._action_event_registry.get(action, set()).remove(user_cback) except KeyError: # Raise if user_cback already removed pass def register_for_device_events(self, sys_name: str, user_cback): '''@brief Register a callback function to be called when udev events are received for a specific nvme device. @param sys_name: The device system name (e.g. 'nvme1') ''' if sys_name: self._device_event_registry[sys_name] = user_cback def unregister_for_device_events(self, user_cback): '''@brief The opposite of register_for_device_events()''' entries = list(self._device_event_registry.items()) for sys_name, _user_cback in entries: if user_cback == _user_cback: self._device_event_registry.pop(sys_name, None) break def get_attributes(self, sys_name: str, attr_ids) -> dict: '''@brief Get all the attributes associated with device @sys_name''' attrs = {attr_id: '' for attr_id in attr_ids} if sys_name and sys_name != 'nvme?': udev = self.get_nvme_device(sys_name) if udev is not None: for attr_id in attr_ids: try: value = udev.attributes.asstring(attr_id).strip() attrs[attr_id] = '' if value == '(efault)' else value except Exception: # pylint: disable=broad-except pass return attrs @staticmethod def is_dc_device(device): '''@brief check whether device refers to a Discovery Controller''' subsysnqn = device.attributes.get('subsysnqn') if subsysnqn is not None and subsysnqn.decode() == defs.WELL_KNOWN_DISC_NQN: return True # Note: Prior to 5.18 linux didn't expose the cntrltype through # the sysfs. So, this may return None on older kernels. cntrltype = device.attributes.get('cntrltype') if cntrltype is not None and cntrltype.decode() == 'discovery': return True # Imply Discovery controller based on the absence of children. # Discovery Controllers have no children devices if len(list(device.children)) == 0: return True return False @staticmethod def is_ioc_device(device): '''@brief check whether device refers to an I/O Controller''' # Note: Prior to 5.18 linux didn't expose the cntrltype through # the sysfs. So, this may return None on older kernels. cntrltype = device.attributes.get('cntrltype') if cntrltype is not None and cntrltype.decode() == 'io': return True # Imply I/O controller based on the presence of children. # I/O Controllers have children devices if len(list(device.children)) != 0: return True return False def find_nvme_dc_device(self, tid): '''@brief Find the nvme device associated with the specified Discovery Controller. @return The device if a match is found, None otherwise. ''' for device in self._context.list_devices( subsystem='nvme', NVME_TRADDR=tid.traddr, NVME_TRSVCID=tid.trsvcid, NVME_TRTYPE=tid.transport ): if not self.is_dc_device(device): continue if self.get_tid(device) != tid: continue return device return None def find_nvme_ioc_device(self, tid): '''@brief Find the nvme device associated with the specified I/O Controller. @return The device if a match is found, None otherwise. ''' for device in self._context.list_devices( subsystem='nvme', NVME_TRADDR=tid.traddr, NVME_TRSVCID=tid.trsvcid, NVME_TRTYPE=tid.transport ): if not self.is_ioc_device(device): continue if self.get_tid(device) != tid: continue return device return None def get_nvme_ioc_tids(self, transports): '''@brief Find all the I/O controller nvme devices in the system. @return A list of pyudev.device._device.Device objects ''' tids = [] for device in self._context.list_devices(subsystem='nvme'): if device.properties.get('NVME_TRTYPE', '') not in transports: continue if not self.is_ioc_device(device): continue tids.append(self.get_tid(device)) return tids def _process_udev_event(self, event_source, condition): # pylint: disable=unused-argument if condition == GLib.IO_IN: event_count = 0 while True: try: device = self._monitor.poll(timeout=0) except EnvironmentError as ex: device = None # This event seems to happen in bursts. So, let's suppress # logging for 2 seconds to avoid filling the syslog. self._log_event_count += 1 now = time.time() if now > self._log_event_soak_time: logging.debug('Udev._process_udev_event() - %s [%s]', ex, self._log_event_count) self._log_event_soak_time = now + 2 self._log_event_count = 0 if device is None: break event_count += 1 self._device_event(device, event_count) return GLib.SOURCE_CONTINUE @staticmethod def __cback_names(action_cbacks, device_cback): names = [] for cback in action_cbacks: names.append(cback.__name__ + '()') if device_cback: names.append(device_cback.__name__ + '()') return names def _device_event(self, device, event_count): action_cbacks = self._action_event_registry.get(device.action, set()) device_cback = self._device_event_registry.get(device.sys_name, None) logging.debug( 'Udev._device_event() - %-8s %-6s %-8s %s', f'{device.sys_name}:', device.action, f'{event_count:2}:{device.sequence_number}', self.__cback_names(action_cbacks, device_cback), ) for action_cback in action_cbacks: GLib.idle_add(action_cback, device) if device_cback is not None: GLib.idle_add(device_cback, device) @staticmethod def _get_property(device, prop, default=''): prop = device.properties.get(prop, default) return '' if prop.lower() == 'none' else prop @staticmethod def _get_attribute(device, attr_id, default=''): try: attr = device.attributes.asstring(attr_id).strip() except Exception: # pylint: disable=broad-except attr = default return '' if attr.lower() == 'none' else attr @staticmethod def get_key_from_attr(device, attr, key, delim=','): '''Get attribute specified by attr, which is composed of key=value pairs. Then return the value associated with key. @param device: The Device object @param attr: The device's attribute to get @param key: The key to look for in the attribute @param delim: Delimiter used between key=value pairs. @example: "address" attribute contains "trtype=tcp,traddr=10.10.1.100,trsvcid=4420,host_traddr=10.10.1.50" ''' attr_str = Udev._get_attribute(device, attr) if not attr_str: return '' if key[-1] != '=': key += '=' start = attr_str.find(key) if start < 0: return '' start += len(key) end = attr_str.find(delim, start) if end < 0: return attr_str[start:] return attr_str[start:end] @staticmethod def _get_host_iface(device): host_iface = Udev._get_property(device, 'NVME_HOST_IFACE') if not host_iface: # We'll try to find the interface from the source address on # the connection. Only available if kernel exposes the source # address (src_addr) in the "address" attribute. src_addr = Udev.get_key_from_attr(device, 'address', 'src_addr=') host_iface = iputil.get_interface(src_addr) return host_iface @staticmethod def get_tid(device): '''@brief return the Transport ID associated with a udev device''' cid = { 'transport': Udev._get_property(device, 'NVME_TRTYPE'), 'traddr': Udev._get_property(device, 'NVME_TRADDR'), 'trsvcid': Udev._get_property(device, 'NVME_TRSVCID'), 'host-traddr': Udev._get_property(device, 'NVME_HOST_TRADDR'), 'host-iface': Udev._get_host_iface(device), 'subsysnqn': Udev._get_attribute(device, 'subsysnqn'), } return trid.TID(cid) UDEV = Udev() # Singleton def shutdown(): '''Destroy the UDEV singleton''' global UDEV # pylint: disable=global-statement,global-variable-not-assigned UDEV.release_resources() del UDEV linux-nvme-nvme-stas-a8026bb/staslib/version.py000066400000000000000000000035251440613556600216540ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # ''' distutils (and hence LooseVersion) is being deprecated. None of the suggested replacements (e.g. from pkg_resources import parse_version) quite work with Linux kernel versions the way LooseVersion does. It was suggested to simply lift the LooseVersion code and vendor it in, which is what this module is about. ''' import re class KernelVersion: '''Code loosely lifted from distutils's LooseVersion''' component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) def __init__(self, string: str): self.string = string self.version = self.__parse(string) def __str__(self): return self.string def __repr__(self): return f'KernelVersion ("{self}")' def __eq__(self, other): return self.version == self.__version(other) def __lt__(self, other): return self.version < self.__version(other) def __le__(self, other): return self.version <= self.__version(other) def __gt__(self, other): return self.version > self.__version(other) def __ge__(self, other): return self.version >= self.__version(other) @staticmethod def __version(obj): return obj.version if isinstance(obj, KernelVersion) else KernelVersion.__parse(obj) @staticmethod def __parse(string): components = [] for item in KernelVersion.component_re.split(string): if item and item != '.': try: components.append(int(item)) except ValueError: pass return components linux-nvme-nvme-stas-a8026bb/subprojects/000077500000000000000000000000001440613556600205125ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/subprojects/libnvme.wrap000066400000000000000000000001541440613556600230410ustar00rootroot00000000000000[wrap-git] url = https://github.com/linux-nvme/libnvme.git revision = v1.3 [provide] libnvme = libnvme_dep linux-nvme-nvme-stas-a8026bb/test/000077500000000000000000000000001440613556600171265ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/test/__init__.py000066400000000000000000000000001440613556600212250ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/test/meson.build000066400000000000000000000142651440613556600213000ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # test_env = environment({'MALLOC_PERTURB_': '0'}) libnvme_location = '?' # We require libnvme in order to run the tests. We have two choices, either # run the tests using a pre-installed version of libnvme (i.e. from /usr) or # build libnvme as a meson subproject and run the tests using that version # of libnvme. The decision to use one method over the other is controlled # by the option "libnvme-sel". Note that if a pre-intalled libnvme is selected # but one cannot be found, then we fall back to using the subproject libnvme. if get_option('libnvme-sel') == 'pre-installed' # Check if a pre-installed libnvme can be found rr = run_command(python3, '-c', 'import libnvme; print(f"{libnvme.__path__[0]}")', check: false, env: test_env) if rr.returncode() == 0 libnvme_location = rr.stdout().strip() endif endif if libnvme_location == '?' # Second, if libnvme is not already installed or "libnvme-sel" is not # set to "pre-installed", let's fallback to using the subproject. libnvme_dep = dependency('python3-libnvme', fallback: ['libnvme', 'libnvme_dep'], required: false) test_env.prepend('PYTHONPATH', PYTHONPATH) # This sets the path to look in the build directory rr = run_command(python3, '-c', 'import libnvme; print(f"{libnvme.__path__[0]}")', check: false, env: test_env) if rr.returncode() == 0 libnvme_location = rr.stdout().strip() endif endif if libnvme_location == '?' warning('Missing runtime package needed to run the tests: python3-libnvme.') else message('\n\n\u001b[32m\u001b[1mNOTE: Tests will be using @0@\u001b[0m\n'.format(libnvme_location)) #--------------------------------------------------------------------------- # pylint and pyflakes if modules_to_lint.length() != 0 pylint = find_program('pylint', required: false) pyflakes = find_program('pyflakes3', required: false) if not pyflakes.found() temp = find_program('pyflakes', required: false) if temp.found() and run_command(temp, '--version', check: false).stdout().contains('Python 3') pyflakes = temp endif endif rcfile = meson.current_source_dir() / 'pylint.rc' if pylint.found() test('pylint', pylint, args: ['--rcfile=' + rcfile] + modules_to_lint, env: test_env) else warning('Skiping some of the tests because "pylint" is missing.') endif if pyflakes.found() test('pyflakes', pyflakes, args: modules_to_lint, env: test_env) else warning('Skiping some of the tests because "pyflakes" is missing.') endif endif #--------------------------------------------------------------------------- # Check dependencies dbus_is_active = false avahi_is_active = false systemctl = find_program('systemctl', required: false) if systemctl.found() rr = run_command(systemctl, 'is-active', 'dbus.service', check: false) dbus_is_active = rr.returncode() == 0 and rr.stdout().strip() == 'active' if not dbus_is_active warning('Dbus daemon is not running') endif rr = run_command(systemctl, 'is-active', 'avahi-daemon.service', check: false) avahi_is_active = rr.returncode() == 0 and rr.stdout().strip() == 'active' if not avahi_is_active warning('Avahi daemon is not running') endif endif want_avahi_test = dbus_is_active and avahi_is_active #--------------------------------------------------------------------------- # Unit tests things_to_test = [ ['Test Configuration', 'test-config.py', []], ['Test Controller', 'test-controller.py', ['pyfakefs']], ['Test GTimer', 'test-gtimer.py', []], ['Test iputil', 'test-iputil.py', []], ['Test KernelVersion', 'test-version.py', []], ['Test log', 'test-log.py', ['pyfakefs']], ['Test NvmeOptions', 'test-nvme_options.py', ['pyfakefs']], ['Test Service', 'test-service.py', ['pyfakefs']], ['Test TID', 'test-transport_id.py', []], ['Test Udev', 'test-udev.py', []], ['Test timeparse', 'test-timeparse.py', []], ] # The Avahi test requires the Avahi and the Dbus daemons to be running. if want_avahi_test things_to_test += [['Test Avahi', 'test-avahi.py', []]] else warning('Skip Avahi Test due to missing dependencies') endif foreach thing: things_to_test msg = thing[0] # Check whether all dependencies can be found missing_deps = [] deps = thing[2] foreach dep : deps rr = run_command(python3, '-c', 'import @0@'.format(dep), check: false) if rr.returncode() != 0 missing_deps += [dep] endif endforeach if missing_deps.length() == 0 # Allow the test to run if all dependencies are available script = meson.current_source_dir() / thing[1] test(msg, python3, args: script, env: test_env) else warning('"@0@" requires python module "@1@"'.format(msg, missing_deps)) endif endforeach endif #------------------------------------------------------------------------------- # Make sure code complies with minimum Python version requirement. tools = [ meson.current_source_dir() / '../doc', meson.current_source_dir() / '../utils', ] vermin = find_program('vermin', required: false) if vermin.found() if modules_to_lint.length() != 0 test('vermin code', vermin, args: ['--config-file', meson.current_source_dir() / 'vermin.conf'] + modules_to_lint, env: test_env) endif test('vermin tools', vermin, args: ['--config-file', meson.current_source_dir() / 'vermin-tools.conf'] + tools, env: test_env) else warning('Skiping some of the tests because "vermin" is missing.') endif linux-nvme-nvme-stas-a8026bb/test/pylint.rc000066400000000000000000000355751440613556600210120ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger [MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. jobs=1 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable=print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, backtick, long-suffix, old-ne-operator, old-octal-literal, import-star-module-level, raw-checker-failed, bad-inline-option, locally-disabled, locally-enabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, apply-builtin, basestring-builtin, buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, file-builtin, long-builtin, raw_input-builtin, reduce-builtin, standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, delslice-method, getslice-method, setslice-method, no-absolute-import, old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, reload-builtin, oct-method, hex-method, nonzero-method, cmp-method, input-builtin, round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, zip-builtin-not-iterating, range-builtin-not-iterating, filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, div-method, idiv-method, rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, bad-python3-import, deprecated-string-function, deprecated-str-translate-call, use-list-literal, use-dict-literal, bad-option-value, R0801, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable= [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio).You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Tells whether to display a full report or only the messages reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,future.builtins [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=200 # Maximum number of lines in a module max-module-lines=2000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. # `trailing-comma` allows a space between comma and closing bracket: (a, ). # `empty-line` allows space-only lines. no-space-check=trailing-comma,dict-separator # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [BASIC] # Naming hint for argument names argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct argument names argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Naming hint for attribute names attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct attribute names attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for constant names const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming hint for function names function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct function names function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_,op,ls,f,ip,id # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for method names method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct method names method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. property-classes=abc.abstractproperty # Naming hint for variable names variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ # Regular expression matching correct variable names variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ [DESIGN] # Maximum number of arguments for function / method max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in a if statement max-bool-expr=5 # Maximum number of branch for function / method body max-branches=12 # Maximum number of locals for function / method body max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of statements in function / method body max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,TERMIOS,Bastion,rexec # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception linux-nvme-nvme-stas-a8026bb/test/test-avahi.py000077500000000000000000000030701440613556600215500ustar00rootroot00000000000000#!/usr/bin/python3 import shutil import logging import unittest from staslib import avahi import dasbus.connection import subprocess SYSTEMCTL = shutil.which('systemctl') class Test(unittest.TestCase): '''Unit tests for class Avahi''' def test_new(self): sysbus = dasbus.connection.SystemMessageBus() srv = avahi.Avahi(sysbus, lambda: "ok") self.assertEqual(srv.info(), {'avahi wake up timer': '60.0s [off]', 'service types': [], 'services': {}}) self.assertEqual(srv.get_controllers(), []) try: # Check that the Avahi daemon is running subprocess.run([SYSTEMCTL, 'is-active', 'avahi-daemon.service'], check=True) self.assertFalse(srv._on_kick_avahi()) except subprocess.CalledProcessError: self.assertTrue(srv._on_kick_avahi()) with self.assertLogs(logger=logging.getLogger(), level='INFO') as captured: srv._avahi_available(None) self.assertEqual(len(captured.records), 1) self.assertEqual(captured.records[0].getMessage(), "avahi-daemon service available, zeroconf supported.") with self.assertLogs(logger=logging.getLogger(), level='WARN') as captured: srv._avahi_unavailable(None) self.assertEqual(len(captured.records), 1) self.assertEqual(captured.records[0].getMessage(), "avahi-daemon not available, zeroconf not supported.") srv.kill() self.assertEqual(srv.info(), {'avahi wake up timer': 'None', 'service types': [], 'services': {}}) if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-config.py000077500000000000000000000253231440613556600217320ustar00rootroot00000000000000#!/usr/bin/python3 import os import unittest from staslib import conf class StasProcessConfUnitTest(unittest.TestCase): '''Process config unit tests''' FNAME = '/tmp/stas-process-config-test' @classmethod def setUpClass(cls): '''Create a temporary configuration file''' data = [ '[Global]\n', 'tron=true\n', 'kato=200\n', 'ip-family=ipv6\n', '\n', '[I/O controller connection management]\n', 'disconnect-scope = joe\n', 'disconnect-trtypes = bob\n', 'connect-attempts-on-ncc = 1\n', '\n', '[Controllers]\n', 'controller=transport=tcp;traddr=100.100.100.100;host-iface=enp0s8\n', 'controller=transport=tcp;traddr=100.100.100.200;host-iface=enp0s7;dhchap-ctrl-secret=super-secret;hdr-digest=true;data-digest=true;nr-io-queues=8;nr-write-queues=6;nr-poll-queues=4;queue-size=400;kato=71;reconnect-delay=13;ctrl-loss-tmo=666;disable-sqflow=true\n', 'exclude=transport=tcp;traddr=10.10.10.10\n', ] with open(StasProcessConfUnitTest.FNAME, 'w') as f: # # pylint: disable=unspecified-encoding f.writelines(data) @classmethod def tearDownClass(cls): '''Delete the temporary configuration file''' if os.path.exists(StasProcessConfUnitTest.FNAME): os.remove(StasProcessConfUnitTest.FNAME) def test_config(self): '''Check we can read the temporary configuration file''' default_conf = { ('Global', 'tron'): False, ('Global', 'hdr-digest'): False, ('Global', 'data-digest'): False, ('Global', 'kato'): None, # None to let the driver decide the default ('Global', 'nr-io-queues'): None, # None to let the driver decide the default ('Global', 'nr-write-queues'): None, # None to let the driver decide the default ('Global', 'nr-poll-queues'): None, # None to let the driver decide the default ('Global', 'queue-size'): None, # None to let the driver decide the default ('Global', 'reconnect-delay'): None, # None to let the driver decide the default ('Global', 'ctrl-loss-tmo'): None, # None to let the driver decide the default ('Global', 'disable-sqflow'): None, # None to let the driver decide the default ('Global', 'ignore-iface'): False, ('Global', 'ip-family'): (4, 6), ('Global', 'persistent-connections'): False, # Deprecated ('Discovery controller connection management', 'persistent-connections'): True, ('Global', 'pleo'): True, ('Service Discovery', 'zeroconf'): True, ('Controllers', 'controller'): list(), ('Controllers', 'exclude'): list(), ('I/O controller connection management', 'disconnect-scope'): 'only-stas-connections', ('I/O controller connection management', 'disconnect-trtypes'): ['tcp'], ('I/O controller connection management', 'connect-attempts-on-ncc'): 0, } service_conf = conf.SvcConf(default_conf=default_conf) service_conf.set_conf_file(StasProcessConfUnitTest.FNAME) self.assertEqual(service_conf.conf_file, StasProcessConfUnitTest.FNAME) self.assertTrue(service_conf.tron) self.assertTrue(getattr(service_conf, 'tron')) self.assertFalse(service_conf.hdr_digest) self.assertFalse(service_conf.data_digest) self.assertTrue(service_conf.persistent_connections) self.assertTrue(service_conf.pleo_enabled) self.assertEqual(service_conf.disconnect_scope, 'only-stas-connections') self.assertEqual(service_conf.disconnect_trtypes, ['tcp']) self.assertFalse(service_conf.ignore_iface) self.assertIn(6, service_conf.ip_family) self.assertNotIn(4, service_conf.ip_family) self.assertEqual(service_conf.kato, 200) self.assertEqual( service_conf.get_controllers(), [ { 'transport': 'tcp', 'traddr': '100.100.100.100', 'host-iface': 'enp0s8', }, { 'transport': 'tcp', 'traddr': '100.100.100.200', 'host-iface': 'enp0s7', 'dhchap-ctrl-secret': 'super-secret', 'hdr-digest': True, 'data-digest': True, 'nr-io-queues': 8, 'nr-write-queues': 6, 'nr-poll-queues': 4, 'queue-size': 400, 'kato': 71, 'reconnect-delay': 13, 'ctrl-loss-tmo': 666, 'disable-sqflow': True, }, ], ) self.assertEqual(service_conf.get_excluded(), [{'transport': 'tcp', 'traddr': '10.10.10.10'}]) stypes = service_conf.stypes self.assertIn('_nvme-disc._tcp', stypes) self.assertTrue(service_conf.zeroconf_enabled) self.assertEqual(service_conf.connect_attempts_on_ncc, 2) data = [ '[I/O controller connection management]\n', 'disconnect-trtypes = tcp+rdma+fc\n', 'connect-attempts-on-ncc = hello\n', ] with open(StasProcessConfUnitTest.FNAME, 'w') as f: # pylint: disable=unspecified-encoding f.writelines(data) service_conf.reload() self.assertEqual(service_conf.connect_attempts_on_ncc, 0) self.assertEqual(set(service_conf.disconnect_trtypes), set(['fc', 'tcp', 'rdma'])) data = [ '[Global]\n', 'ip-family=ipv4\n', ] with open(StasProcessConfUnitTest.FNAME, 'w') as f: # pylint: disable=unspecified-encoding f.writelines(data) service_conf.reload() self.assertIn(4, service_conf.ip_family) self.assertNotIn(6, service_conf.ip_family) data = [ '[Global]\n', 'ip-family=ipv4+ipv6\n', ] with open(StasProcessConfUnitTest.FNAME, 'w') as f: # pylint: disable=unspecified-encoding f.writelines(data) service_conf.reload() self.assertIn(4, service_conf.ip_family) self.assertIn(6, service_conf.ip_family) data = [ '[Global]\n', 'ip-family=ipv6+ipv4\n', ] with open(StasProcessConfUnitTest.FNAME, 'w') as f: # pylint: disable=unspecified-encoding f.writelines(data) service_conf.reload() self.assertIn(4, service_conf.ip_family) self.assertIn(6, service_conf.ip_family) self.assertRaises(KeyError, service_conf.get_option, 'Babylon', 5) class StasSysConfUnitTest(unittest.TestCase): '''Sys config unit tests''' FNAME_1 = '/tmp/stas-sys-config-test-1' FNAME_2 = '/tmp/stas-sys-config-test-2' FNAME_3 = '/tmp/stas-sys-config-test-3' FNAME_4 = '/tmp/stas-sys-config-test-4' NQN = 'nqn.2014-08.org.nvmexpress:uuid:9aae2691-b275-4b64-8bfe-5da429a2bab9' ID = '56529e15-0f3e-4ede-87e2-63932a4adb99' KEY = 'DHHC-1:03:qwertyuioplkjhgfdsazxcvbnm0123456789QWERTYUIOPLKJHGFDSAZXCVBNM010101010101010101010101010101:' SYMNAME = 'Bart-Simpson' DATA = { FNAME_1: [ '[Host]\n', f'nqn={NQN}\n', f'id={ID}\n', f'key={KEY}\n', f'symname={SYMNAME}\n', ], FNAME_2: [ '[Host]\n', 'nqn=file:///dev/null\n', ], FNAME_3: [ '[Host]\n', 'nqn=qnq.2014-08.org.nvmexpress:uuid:9aae2691-b275-4b64-8bfe-5da429a2bab9\n', f'id={ID}\n', ], FNAME_4: [ '[Host]\n', 'nqn=file:///some/non/exisiting/file/!@#\n', 'id=file:///some/non/exisiting/file/!@#\n', 'symname=file:///some/non/exisiting/file/!@#\n', ], } @classmethod def setUpClass(cls): '''Create a temporary configuration file''' for file, data in StasSysConfUnitTest.DATA.items(): with open(file, 'w') as f: # pylint: disable=unspecified-encoding f.writelines(data) @classmethod def tearDownClass(cls): '''Delete the temporary configuration file''' for file in StasSysConfUnitTest.DATA.keys(): if os.path.exists(file): os.remove(file) def test_config_1(self): '''Check we can read the temporary configuration file''' system_conf = conf.SysConf() system_conf.set_conf_file(StasSysConfUnitTest.FNAME_1) self.assertEqual(system_conf.conf_file, StasSysConfUnitTest.FNAME_1) self.assertEqual(system_conf.hostnqn, StasSysConfUnitTest.NQN) self.assertEqual(system_conf.hostid, StasSysConfUnitTest.ID) self.assertEqual(system_conf.hostsymname, StasSysConfUnitTest.SYMNAME) self.assertEqual( system_conf.as_dict(), { 'hostnqn': StasSysConfUnitTest.NQN, 'hostid': StasSysConfUnitTest.ID, 'hostkey': StasSysConfUnitTest.KEY, 'symname': StasSysConfUnitTest.SYMNAME, }, ) def test_config_2(self): '''Check we can read from /dev/null or missing 'id' definition''' system_conf = conf.SysConf() system_conf.set_conf_file(StasSysConfUnitTest.FNAME_2) self.assertEqual(system_conf.conf_file, StasSysConfUnitTest.FNAME_2) self.assertIsNone(system_conf.hostnqn) self.assertIsNone(system_conf.hostsymname) def test_config_3(self): '''Check we can read an invalid NQN string''' system_conf = conf.SysConf() system_conf.set_conf_file(StasSysConfUnitTest.FNAME_3) self.assertEqual(system_conf.conf_file, StasSysConfUnitTest.FNAME_3) self.assertRaises(SystemExit, lambda: system_conf.hostnqn) self.assertEqual(system_conf.hostid, StasSysConfUnitTest.ID) self.assertIsNone(system_conf.hostsymname) def test_config_4(self): '''Check we can read the temporary configuration file''' system_conf = conf.SysConf() system_conf.set_conf_file(StasSysConfUnitTest.FNAME_4) self.assertEqual(system_conf.conf_file, StasSysConfUnitTest.FNAME_4) self.assertRaises(SystemExit, lambda: system_conf.hostnqn) self.assertRaises(SystemExit, lambda: system_conf.hostid) self.assertIsNone(system_conf.hostsymname) def test_config_missing_file(self): '''Check what happens when conf file is missing''' system_conf = conf.SysConf() system_conf.set_conf_file('/just/some/ramdom/file/name') self.assertIsNone(system_conf.hostsymname) if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-controller.py000077500000000000000000000246251440613556600226540ustar00rootroot00000000000000#!/usr/bin/python3 import logging import unittest from staslib import conf, ctrl, timeparse, trid from pyfakefs.fake_filesystem_unittest import TestCase class TestController(ctrl.Controller): def _find_existing_connection(self): pass def _on_aen(self, aen: int): pass def _on_nvme_event(self, nvme_event): pass def reload_hdlr(self): pass class TestDc(ctrl.Dc): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._connected = True class Ctrl: def __init__(this): this.name = 'nvme666' def connected(this): return self._connected def disconnect(this): pass self._ctrl = Ctrl() def _find_existing_connection(self): pass def _on_aen(self, aen: int): pass def _on_nvme_event(self, nvme_event): pass def reload_hdlr(self): pass def set_connected(self, value): self._connected = value def connected(self): return self._connected class TestStaf: def is_avahi_reported(self, tid): return False def controller_unresponsive(self, tid): pass @property def tron(self): return True stafd_conf_1 = ''' [Global] tron=false hdr-digest=false data-digest=false kato=30 queue-size=128 reconnect-delay=10 ctrl-loss-tmo=600 disable-sqflow=false ignore-iface=false ip-family=ipv4+ipv6 pleo=enabled [Service Discovery] zeroconf=enabled [Discovery controller connection management] persistent-connections=true zeroconf-connections-persistence=10 seconds ''' stafd_conf_2 = ''' [Discovery controller connection management] zeroconf-connections-persistence=-1 ''' class Test(TestCase): '''Unit tests for class Controller''' def setUp(self): self.setUpPyfakefs() self.fs.create_file( '/etc/nvme/hostnqn', contents='nqn.2014-08.org.nvmexpress:uuid:01234567-0123-0123-0123-0123456789ab\n' ) self.fs.create_file('/etc/nvme/hostid', contents='01234567-89ab-cdef-0123-456789abcdef\n') self.fs.create_file( '/dev/nvme-fabrics', contents='instance=-1,cntlid=-1,transport=%s,traddr=%s,trsvcid=%s,nqn=%s,queue_size=%d,nr_io_queues=%d,reconnect_delay=%d,ctrl_loss_tmo=%d,keep_alive_tmo=%d,hostnqn=%s,host_traddr=%s,host_iface=%s,hostid=%s,disable_sqflow,hdr_digest,data_digest,nr_write_queues=%d,nr_poll_queues=%d,tos=%d,fast_io_fail_tmo=%d,discovery,dhchap_secret=%s,dhchap_ctrl_secret=%s\n', ) self.NVME_TID = trid.TID( { 'transport': 'tcp', 'traddr': '10.10.10.10', 'subsysnqn': 'nqn.1988-11.com.dell:SFSS:2:20220208134025e8', 'trsvcid': '8009', 'host-traddr': '1.2.3.4', 'host-iface': 'wlp0s20f3', } ) default_conf = { ('Global', 'tron'): False, ('Global', 'hdr-digest'): False, ('Global', 'data-digest'): False, ('Global', 'kato'): None, # None to let the driver decide the default ('Global', 'queue-size'): None, # None to let the driver decide the default ('Global', 'reconnect-delay'): None, # None to let the driver decide the default ('Global', 'ctrl-loss-tmo'): None, # None to let the driver decide the default ('Global', 'disable-sqflow'): None, # None to let the driver decide the default ('Global', 'persistent-connections'): True, ('Discovery controller connection management', 'persistent-connections'): True, ('Discovery controller connection management', 'zeroconf-connections-persistence'): timeparse.timeparse( '72hours' ), ('Global', 'ignore-iface'): False, ('Global', 'ip-family'): (4, 6), ('Global', 'pleo'): True, ('Service Discovery', 'zeroconf'): True, ('Controllers', 'controller'): list(), ('Controllers', 'exclude'): list(), } self.stafd_conf_file1 = '/etc/stas/stafd1.conf' self.fs.create_file(self.stafd_conf_file1, contents=stafd_conf_1) self.stafd_conf_file2 = '/etc/stas/stafd2.conf' self.fs.create_file(self.stafd_conf_file2, contents=stafd_conf_2) self.svcconf = conf.SvcConf(default_conf=default_conf) self.svcconf.set_conf_file(self.stafd_conf_file1) def tearDown(self): pass def test_cannot_instantiate_concrete_classes_if_abstract_method_are_not_implemented(self): # Make sure we can't instantiate the ABC directly (Abstract Base Class). class Controller(ctrl.Controller): pass self.assertRaises(TypeError, lambda: ctrl.Controller(tid=self.NVME_TID)) def test_get_device(self): controller = TestController(tid=self.NVME_TID, service=TestStaf()) self.assertEqual(controller._connect_attempts, 0) controller._try_to_connect() self.assertEqual(controller._connect_attempts, 1) self.assertEqual( controller.id, "(tcp, 10.10.10.10, 8009, nqn.1988-11.com.dell:SFSS:2:20220208134025e8, wlp0s20f3, 1.2.3.4)" ) # raise Exception(controller._connect_op) self.assertEqual( str(controller.tid), "(tcp, 10.10.10.10, 8009, nqn.1988-11.com.dell:SFSS:2:20220208134025e8, wlp0s20f3, 1.2.3.4)", ) self.assertEqual(controller.device, 'nvme?') self.assertEqual( controller.controller_id_dict(), { 'transport': 'tcp', 'traddr': '10.10.10.10', 'trsvcid': '8009', 'host-traddr': '1.2.3.4', 'host-iface': 'wlp0s20f3', 'subsysnqn': 'nqn.1988-11.com.dell:SFSS:2:20220208134025e8', 'device': 'nvme?', }, ) self.assertEqual( controller.info(), { 'transport': 'tcp', 'traddr': '10.10.10.10', 'subsysnqn': 'nqn.1988-11.com.dell:SFSS:2:20220208134025e8', 'trsvcid': '8009', 'host-traddr': '1.2.3.4', 'host-iface': 'wlp0s20f3', 'device': 'nvme?', 'connect attempts': '1', 'retry connect timer': '60.0s [off]', 'connect operation': "{'fail count': 0, 'completed': False, 'alive': True}", }, ) self.assertEqual( controller.details(), { 'dctype': '', 'cntrltype': '', 'connected': 'False', 'transport': 'tcp', 'traddr': '10.10.10.10', 'trsvcid': '8009', 'host-traddr': '1.2.3.4', 'host-iface': 'wlp0s20f3', 'subsysnqn': 'nqn.1988-11.com.dell:SFSS:2:20220208134025e8', 'device': 'nvme?', 'connect attempts': '1', 'retry connect timer': '60.0s [off]', 'hostid': '', 'hostnqn': '', 'model': '', 'serial': '', 'connect operation': "{'fail count': 0, 'completed': False, 'alive': True}", }, ) # print(controller._connect_op) self.assertEqual(controller.cancel(), None) self.assertEqual(controller.kill(), None) self.assertIsNone(controller.disconnect(lambda *args: None, True)) def test_connect(self): controller = TestController(tid=self.NVME_TID, service=TestStaf()) self.assertEqual(controller._connect_attempts, 0) controller._find_existing_connection = lambda: None with self.assertLogs(logger=logging.getLogger(), level='DEBUG') as captured: controller._try_to_connect() self.assertTrue(len(captured.records) > 0) self.assertTrue( captured.records[0] .getMessage() .startswith( "Controller._do_connect() - (tcp, 10.10.10.10, 8009, nqn.1988-11.com.dell:SFSS:2:20220208134025e8, wlp0s20f3, 1.2.3.4) Connecting to nvme control with cfg={" ) ) self.assertEqual(controller._connect_attempts, 1) def test_dlp_supp_opts_as_string(self): dlp_supp_opts = 0x7 opts = ctrl.dlp_supp_opts_as_string(dlp_supp_opts) self.assertEqual(['EXTDLPES', 'PLEOS', 'ALLSUBES'], opts) def test_ncc(self): dlpe = {'eflags': '4'} ncc = ctrl.get_ncc(ctrl.get_eflags(dlpe)) self.assertTrue(ncc) dlpe = {} ncc = ctrl.get_ncc(ctrl.get_eflags(dlpe)) self.assertFalse(ncc) def test_dc(self): self.svcconf.set_conf_file(self.stafd_conf_file1) controller = TestDc(TestStaf(), tid=self.NVME_TID) controller.set_connected(True) controller.origin = 'discovered' with self.assertLogs(logger=logging.getLogger(), level='DEBUG') as captured: controller.origin = 'blah' self.assertEqual(len(captured.records), 1) self.assertNotEqual(-1, captured.records[0].getMessage().find("Trying to set invalid origin to blah")) controller.set_connected(False) with self.assertLogs(logger=logging.getLogger(), level='DEBUG') as captured: controller.origin = 'discovered' self.assertEqual(len(captured.records), 1) self.assertNotEqual( -1, captured.records[0].getMessage().find("Controller is not responding. Will be removed by") ) self.svcconf.set_conf_file(self.stafd_conf_file2) with self.assertLogs(logger=logging.getLogger(), level='DEBUG') as captured: controller.origin = 'discovered' self.assertEqual(len(captured.records), 1) self.assertNotEqual(-1, captured.records[0].getMessage().find("Controller not responding. Retrying...")) controller.set_connected(True) with self.assertLogs(logger=logging.getLogger(), level='DEBUG') as captured: controller.disconnect(lambda *args: None, keep_connection=False) self.assertEqual(len(captured.records), 2) self.assertNotEqual(-1, captured.records[0].getMessage().find("nvme666: keep_connection=False")) self.assertNotEqual(-1, captured.records[1].getMessage().find("nvme666 - Disconnect initiated")) # def test_disconnect(self): if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-gtimer.py000077500000000000000000000026171440613556600217550ustar00rootroot00000000000000#!/usr/bin/python3 import unittest from staslib import gutil class Test(unittest.TestCase): '''Unit tests for class GTimer''' def test_new_timer(self): tmr = gutil.GTimer(interval_sec=5) self.assertEqual(tmr.get_timeout(), 5) self.assertEqual(tmr.time_remaining(), 0) self.assertEqual(str(tmr), '5.0s [off]') tmr.set_timeout(new_interval_sec=18) self.assertEqual(tmr.get_timeout(), 18) self.assertEqual(tmr.time_remaining(), 0) def test_callback(self): tmr = gutil.GTimer(interval_sec=1, user_cback=lambda: "ok") self.assertEqual(tmr._callback(), "ok") tmr.set_callback(user_cback=lambda: "notok") self.assertEqual(tmr._callback(), "notok") tmr.kill() self.assertEqual(tmr._user_cback, None) self.assertRaises(TypeError, tmr._user_cback) def test_start_timer(self): tmr = gutil.GTimer(interval_sec=1, user_cback=lambda: "ok") self.assertEqual(str(tmr), '1.0s [off]') tmr.start() self.assertNotEqual(tmr.time_remaining(), 0) self.assertNotEqual(str(tmr), '1.0s [off]') def test_clear(self): tmr = gutil.GTimer(interval_sec=1, user_cback=lambda: "ok") tmr.start() tmr.clear() self.assertEqual(tmr.time_remaining(), 0) self.assertEqual(str(tmr), '1.0s [0s]') if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-iputil.py000077500000000000000000000044441440613556600217740ustar00rootroot00000000000000#!/usr/bin/python3 import json import shutil import logging import unittest import ipaddress import subprocess from staslib import iputil, log, trid IP = shutil.which('ip') class Test(unittest.TestCase): '''iputil.py unit tests''' def setUp(self): log.init(syslog=False) self.logger = logging.getLogger() self.logger.setLevel(logging.INFO) # Retrieve the list of Interfaces and all the associated IP addresses # using standard bash utility (ip address). We'll use this to make sure # iputil.get_interface() returns the same data as "ip address". try: cmd = [IP, '-j', 'address', 'show'] p = subprocess.run(cmd, stdout=subprocess.PIPE, check=True) self.ifaces = json.loads(p.stdout.decode().strip()) except subprocess.CalledProcessError: self.ifaces = [] def test_get_interface(self): '''Check that get_interface() returns the right info''' for iface in self.ifaces: for addr_entry in iface['addr_info']: addr = ipaddress.ip_address(addr_entry['local']) # Link local addresses may appear on more than one interface and therefore cannot be used. if not addr.is_link_local: self.assertEqual(iface['ifname'], iputil.get_interface(str(addr))) self.assertEqual('', iputil.get_interface('255.255.255.255')) def test_remove_invalid_addresses(self): good_tcp = trid.TID({'transport': 'tcp', 'traddr': '1.1.1.1', 'subsysnqn': '', 'trsvcid': '8009'}) bad_tcp = trid.TID({'transport': 'tcp', 'traddr': '555.555.555.555', 'subsysnqn': '', 'trsvcid': '8009'}) any_fc = trid.TID({'transport': 'fc', 'traddr': 'blah', 'subsysnqn': ''}) bad_trtype = trid.TID({'transport': 'whatever', 'traddr': 'blah', 'subsysnqn': ''}) l1 = [ good_tcp, bad_tcp, any_fc, bad_trtype, ] l2 = iputil.remove_invalid_addresses(l1) self.assertNotEqual(l1, l2) self.assertIn(good_tcp, l2) self.assertIn(any_fc, l2) # We currently don't check for invalid FC (all FCs are allowed) self.assertNotIn(bad_tcp, l2) self.assertNotIn(bad_trtype, l2) if __name__ == "__main__": unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-log.py000077500000000000000000000062511440613556600212450ustar00rootroot00000000000000#!/usr/bin/python3 import logging import unittest from pyfakefs.fake_filesystem_unittest import TestCase from staslib import log class StaslibLogTest(TestCase): '''Test for log.py module''' def setUp(self): self.setUpPyfakefs() def test_log_with_systemd_journal(self): '''Check that we can set the handler to systemd.journal.JournalHandler''' try: # We can't proceed with this test if the # module systemd.journal is not installed. import systemd.journal # pylint: disable=import-outside-toplevel except ModuleNotFoundError: return log.init(syslog=True) logger = logging.getLogger() handler = logger.handlers[-1] self.assertIsInstance(handler, systemd.journal.JournalHandler) self.assertEqual(log.level(), 'INFO') log.set_level_from_tron(tron=True) self.assertEqual(log.level(), 'DEBUG') log.set_level_from_tron(tron=False) self.assertEqual(log.level(), 'INFO') logger.removeHandler(handler) handler.close() def test_log_with_syslog_handler(self): '''Check that we can set the handler to logging.handlers.SysLogHandler''' try: # The log.py module uses systemd.journal.JournalHandler() as the # default logging handler (if present). Therefore, in order to force # log.py to use SysLogHandler as the handler, we need to mock # systemd.journal.JournalHandler() with an invalid class. import systemd.journal # pylint: disable=import-outside-toplevel except ModuleNotFoundError: original_handler = None else: class MockJournalHandler: def __new__(cls, *args, **kwargs): raise ModuleNotFoundError original_handler = systemd.journal.JournalHandler systemd.journal.JournalHandler = MockJournalHandler log.init(syslog=True) logger = logging.getLogger() handler = logger.handlers[-1] self.assertIsInstance(handler, logging.handlers.SysLogHandler) self.assertEqual(log.level(), 'INFO') log.set_level_from_tron(tron=True) self.assertEqual(log.level(), 'DEBUG') log.set_level_from_tron(tron=False) self.assertEqual(log.level(), 'INFO') logger.removeHandler(handler) handler.close() if original_handler is not None: # Restore original systemd.journal.JournalHandler() systemd.journal.JournalHandler = original_handler def test_log_with_stdout(self): '''Check that we can set the handler to logging.StreamHandler (i.e. stdout)''' log.init(syslog=False) logger = logging.getLogger() handler = logger.handlers[-1] self.assertIsInstance(handler, logging.StreamHandler) self.assertEqual(log.level(), 'DEBUG') log.set_level_from_tron(tron=True) self.assertEqual(log.level(), 'DEBUG') log.set_level_from_tron(tron=False) self.assertEqual(log.level(), 'INFO') logger.removeHandler(handler) handler.close() if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-nvme_options.py000077500000000000000000000042711440613556600232040ustar00rootroot00000000000000#!/usr/bin/python3 import os import logging import unittest from staslib import conf, log from pyfakefs.fake_filesystem_unittest import TestCase class Test(TestCase): """Unit tests for class NvmeOptions""" def setUp(self): self.setUpPyfakefs() log.init(syslog=False) self.logger = logging.getLogger() self.logger.setLevel(logging.INFO) def tearDown(self): # No longer need self.tearDownPyfakefs() pass def test_fabrics_empty_file(self): self.assertFalse(os.path.exists("/dev/nvme-fabrics")) # TODO: this is a bug self.fs.create_file("/dev/nvme-fabrics") self.assertTrue(os.path.exists('/dev/nvme-fabrics')) nvme_options = conf.NvmeOptions() self.assertIsInstance(nvme_options.discovery_supp, bool) self.assertIsInstance(nvme_options.host_iface_supp, bool) del nvme_options def test_fabrics_wrong_file(self): self.assertFalse(os.path.exists("/dev/nvme-fabrics")) self.fs.create_file("/dev/nvme-fabrics", contents="blah") self.assertTrue(os.path.exists('/dev/nvme-fabrics')) nvme_options = conf.NvmeOptions() self.assertIsInstance(nvme_options.discovery_supp, bool) self.assertIsInstance(nvme_options.host_iface_supp, bool) del nvme_options def test_fabrics_correct_file(self): self.assertFalse(os.path.exists("/dev/nvme-fabrics")) self.fs.create_file( '/dev/nvme-fabrics', contents='host_iface=%s,discovery,dhchap_secret=%s,dhchap_ctrl_secret=%s\n' ) self.assertTrue(os.path.exists('/dev/nvme-fabrics')) nvme_options = conf.NvmeOptions() self.assertTrue(nvme_options.discovery_supp) self.assertTrue(nvme_options.host_iface_supp) self.assertTrue(nvme_options.dhchap_hostkey_supp) self.assertTrue(nvme_options.dhchap_ctrlkey_supp) self.assertEqual( nvme_options.get(), {'discovery': True, 'host_iface': True, 'dhchap_secret': True, 'dhchap_ctrl_secret': True}, ) self.assertTrue(str(nvme_options).startswith("supported options:")) del nvme_options if __name__ == "__main__": unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-service.py000077500000000000000000000045361440613556600221300ustar00rootroot00000000000000#!/usr/bin/python3 import os import unittest from staslib import service from pyfakefs.fake_filesystem_unittest import TestCase class Args: def __init__(self): self.tron = True self.syslog = True self.conf_file = '/dev/null' class TestService(service.Service): def _config_ctrls_finish(self, configured_ctrl_list): pass def _dump_last_known_config(self, controllers): pass def _keep_connections_on_exit(self): pass def _load_last_known_config(self): return dict() class Test(TestCase): '''Unit tests for class Service''' def setUp(self): self.setUpPyfakefs() os.environ['RUNTIME_DIRECTORY'] = "/run" self.fs.create_file( '/etc/nvme/hostnqn', contents='nqn.2014-08.org.nvmexpress:uuid:01234567-0123-0123-0123-0123456789ab\n' ) self.fs.create_file('/etc/nvme/hostid', contents='01234567-89ab-cdef-0123-456789abcdef\n') self.fs.create_file( '/dev/nvme-fabrics', contents='instance=-1,cntlid=-1,transport=%s,traddr=%s,trsvcid=%s,nqn=%s,queue_size=%d,nr_io_queues=%d,reconnect_delay=%d,ctrl_loss_tmo=%d,keep_alive_tmo=%d,hostnqn=%s,host_traddr=%s,host_iface=%s,hostid=%s,disable_sqflow,hdr_digest,data_digest,nr_write_queues=%d,nr_poll_queues=%d,tos=%d,fast_io_fail_tmo=%d,discovery,dhchap_secret=%s,dhchap_ctrl_secret=%s\n', ) def test_cannot_instantiate_concrete_classes_if_abstract_method_are_not_implemented(self): # Make sure we can't instantiate the ABC directly (Abstract Base Class). class Service(service.Service): pass self.assertRaises(TypeError, lambda: Service(Args(), reload_hdlr=lambda x: x)) def test_get_controller(self): srv = TestService(Args(), default_conf={}, reload_hdlr=lambda x: x) self.assertEqual(list(srv.get_controllers()), list()) self.assertEqual( srv.get_controller( transport='tcp', traddr='10.10.10.10', trsvcid='8009', host_traddr='1.2.3.4', host_iface='wlp0s20f3', subsysnqn='nqn.1988-11.com.dell:SFSS:2:20220208134025e8', ), None, ) self.assertEqual(srv.remove_controller(controller=None, success=True), None) if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-timeparse.py000077500000000000000000000025571440613556600224620ustar00rootroot00000000000000#!/usr/bin/python3 import unittest from staslib import timeparse class StasTimeparseUnitTest(unittest.TestCase): '''Time parse unit tests''' def test_timeparse(self): '''Check that timeparse() converts time spans properly''' self.assertEqual(timeparse.timeparse('1'), 1) self.assertEqual(timeparse.timeparse('1s'), 1) self.assertEqual(timeparse.timeparse('1 sec'), 1) self.assertEqual(timeparse.timeparse('1 second'), 1) self.assertEqual(timeparse.timeparse('1 seconds'), 1) self.assertEqual(timeparse.timeparse('1:01'), 61) self.assertEqual(timeparse.timeparse('1 day'), 24 * 60 * 60) self.assertEqual(timeparse.timeparse('1 hour'), 60 * 60) self.assertEqual(timeparse.timeparse('1 min'), 60) self.assertEqual(timeparse.timeparse('0.5'), 0.5) self.assertEqual(timeparse.timeparse('-1'), -1) self.assertEqual(timeparse.timeparse(':22'), 22) self.assertEqual(timeparse.timeparse('1 minute, 24 secs'), 84) self.assertEqual(timeparse.timeparse('1.2 minutes'), 72) self.assertEqual(timeparse.timeparse('1.2 seconds'), 1.2) self.assertEqual(timeparse.timeparse('- 1 minute'), -60) self.assertEqual(timeparse.timeparse('+ 1 minute'), 60) self.assertIsNone(timeparse.timeparse('blah')) if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-transport_id.py000077500000000000000000000053261440613556600231760ustar00rootroot00000000000000#!/usr/bin/python3 import unittest from staslib import trid class Test(unittest.TestCase): '''Unit test for class TRID''' TRANSPORT = 'tcp' TRADDR = '10.10.10.10' OTHER_TRADDR = '1.1.1.1' SUBSYSNQN = 'nqn.1988-11.com.dell:SFSS:2:20220208134025e8' TRSVCID = '8009' HOST_TRADDR = '1.2.3.4' HOST_IFACE = 'wlp0s20f3' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.cid = { 'transport': Test.TRANSPORT, 'traddr': Test.TRADDR, 'subsysnqn': Test.SUBSYSNQN, 'trsvcid': Test.TRSVCID, 'host-traddr': Test.HOST_TRADDR, 'host-iface': Test.HOST_IFACE, } self.other_cid = { 'transport': Test.TRANSPORT, 'traddr': Test.OTHER_TRADDR, 'subsysnqn': Test.SUBSYSNQN, 'trsvcid': Test.TRSVCID, 'host-traddr': Test.HOST_TRADDR, 'host-iface': Test.HOST_IFACE, } self.tid = trid.TID(self.cid) self.other_tid = trid.TID(self.other_cid) def test_hash(self): '''Check that a hash exists''' self.assertIsInstance(self.tid._hash, int) def test_transport(self): '''Check that transport is set''' self.assertEqual(self.tid.transport, Test.TRANSPORT) def test_traddr(self): '''Check that traddr is set''' self.assertEqual(self.tid.traddr, Test.TRADDR) def test_trsvcid(self): '''Check that trsvcid is set''' self.assertEqual(self.tid.trsvcid, Test.TRSVCID) def test_host_traddr(self): '''Check that host_traddr is set''' self.assertEqual(self.tid.host_traddr, Test.HOST_TRADDR) def test_host_iface(self): '''Check that host_iface is set''' self.assertEqual(self.tid.host_iface, Test.HOST_IFACE) def test_subsysnqn(self): '''Check that subsysnqn is set''' self.assertEqual(self.tid.subsysnqn, Test.SUBSYSNQN) def test_as_dict(self): '''Check that a TRID can be converted back to the original Dict it was created with''' self.assertDictEqual(self.tid.as_dict(), self.cid) def test_str(self): '''Check that a TRID can be represented as a string''' self.assertTrue(str(self.tid).startswith(f'({Test.TRANSPORT},')) def test_eq(self): '''Check that two TRID objects can be tested for equality''' self.assertEqual(self.tid, trid.TID(self.cid)) self.assertFalse(self.tid == 'blah') def test_ne(self): '''Check that two TID objects can be tested for non-equality''' self.assertNotEqual(self.tid, self.other_tid) self.assertNotEqual(self.tid, 'hello') if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-udev.py000077500000000000000000000022241440613556600214230ustar00rootroot00000000000000#!/usr/bin/python3 import unittest from staslib import udev class Test(unittest.TestCase): '''Unit tests for class Udev''' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @classmethod def tearDownClass(cls): '''Release resources''' udev.shutdown() def test_get_device(self): dev = udev.UDEV.get_nvme_device('null') self.assertEqual(dev.device_node, '/dev/null') def test_get_bad_device(self): self.assertIsNone(udev.UDEV.get_nvme_device('bozo')) def test_get_key_from_attr(self): device = udev.UDEV.get_nvme_device('null') devname = udev.UDEV.get_key_from_attr(device, 'uevent', 'DEVNAME=', '\n') self.assertEqual(devname, 'null') devname = udev.UDEV.get_key_from_attr(device, 'uevent', 'DEVNAME', '\n') self.assertEqual(devname, 'null') devmode = udev.UDEV.get_key_from_attr(device, 'uevent', 'DEVMODE', '\n') self.assertEqual(devmode, '0666') bogus = udev.UDEV.get_key_from_attr(device, 'bogus', 'BOGUS', '\n') self.assertEqual(bogus, '') if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/test-version.py000077500000000000000000000024051440613556600221460ustar00rootroot00000000000000#!/usr/bin/python3 import unittest from staslib.version import KernelVersion class VersionUnitTests(unittest.TestCase): '''Unit tests for class KernelVersion''' version = KernelVersion('5.8.0-63-generic') def test_str(self): self.assertIsInstance(str(self.version), str) def test_repr(self): self.assertIsInstance(repr(self.version), str) def test_eq(self): '''Test equality''' self.assertEqual(self.version, '5.8.0-63') self.assertNotEqual(self.version, '5.8.0') def test_lt(self): '''Test lower than''' self.assertTrue(self.version < '5.9') self.assertFalse(self.version < '5.7') def test_le(self): '''Test lower equal''' self.assertTrue(self.version <= '5.8.0-63') self.assertTrue(self.version <= '5.8.1') self.assertFalse(self.version <= '5.7') def test_gt(self): '''Test greater than''' self.assertTrue(self.version > '5.8') self.assertFalse(self.version > '5.9') def test_ge(self): '''Test greater equal''' self.assertTrue(self.version >= '5.8.0-63') self.assertTrue(self.version >= '5.7.0') self.assertFalse(self.version >= '5.9') if __name__ == '__main__': unittest.main() linux-nvme-nvme-stas-a8026bb/test/vermin-tools.conf000066400000000000000000000100101440613556600224230ustar00rootroot00000000000000[vermin] ### Quiet mode ### # It only prints the final versions verdict. # #quiet = no ### Verbosity ### # Verbosity level 1 to 4. -v, -vv, -vvv, and -vvvv shows increasingly more information. Turned off # at level 0. # #verbose = 0 verbose = 3 ### Dump AST node visits ### # Only for debugging. # #print_visits = no ### Matching target versions ### # Target version that files must abide by. Can be specified once or twice. # A '-' can be appended to match target version or smaller, like '3.5-'. # If not met Vermin will exit with code 1. # Note that the amount of target versions must match the amount of minimum required versions # detected. # # Examples: #targets = 2.6- #targets = 2.3 # 3,4 #targets = 2,7 # 3,9- targets = 3.8 ### Concurrent processing ### # Use N concurrent processes to detect and analyze files. Defaults to 0, meaning all cores # available. # #processes = 0 ### Ignore incompatible versions and warnings ### # However, if no compatible versions are found then incompatible versions will be shown in the end # to not have an absence of results. # #ignore_incomp = no ### Lax mode ### # It ignores conditionals (if, ternary, for, async for, while, with, try, bool op) on AST traversal, # which can be useful when minimum versions are detected in conditionals that it is known does not # affect the results. # # Note: It is better to use excludes or `# novermin`/`# novm` in the source code instead. # #lax = no ### Hidden analysis ### # Analyze 'hidden' files and folders starting with '.' (ignored by default when not specified # directly). # #analyze_hidden = no ### Tips ### # Possibly show helpful tips at the end, like those relating to backports or lax mode. # #show_tips = yes show_tips = no ### Pessimistic mode ### # Syntax errors are interpreted as the major Python version in use being incompatible. # #pessimistic = no ### Exclusions ### # Exclude full names, like 'email.parser.FeedParser', from analysis. Useful to ignore conditional # logic that can trigger incompatible results. It's more fine grained than lax mode. # # Exclude 'foo.bar.baz' module/member: foo.bar.baz # Exclude 'foo' kwarg: somemodule.func(foo) # Exclude 'bar' codecs error handler: ceh=bar # Exclude 'baz' codecs encoding: ce=baz # # Example exclusions: #exclusions = # email.parser.FeedParser # argparse.ArgumentParser(allow_abbrev) exclusions = importlib.resources importlib.resources.files importlib_resources importlib_resources.files ### Backports ### # Some features are sometimes backported into packages, in repositories such as PyPi, that are # widely used but aren't in the standard language. If such a backport is specified as being used, # the results will reflect that instead. # # Get full list via `--help`. # # Example backports: #backports = # typing # argparse ### Features ### # Some features are disabled by default due to being unstable but can be enabled explicitly. # # Get full list via `--help`. # # Example features: #features = # fstring-self-doc ### Format ### # Format to show results and output in. # # Get full list via `--help`. # #format = default ### Annotations evaluation ### # Instructs parser that annotations will be manually evaluated in code, which changes minimum # versions in certain cases. Otherwise, function and variable annotations are not evaluated at # definition time. Apply this argument if code uses `typing.get_type_hints` or # `eval(obj.__annotations__)` or otherwise forces evaluation of annotations. # #eval_annotations = no ### Violations ### # #only_show_violations = no only_show_violations = yes ### Parse comments ### # Whether or not to parse comments, searching for "# novm" and "# novermin" to exclude anslysis of # specific lines. If these comments aren't used in a particular code base, not parsing them can # sometimes yield a speedup of 30-40%+. # #parse_comments = yes parse_comments = no ### Scan symlink folders ### # Scan symlinks to folders to include in analysis. Symlinks to non-folders or top-level folders will # always be scanned. # #scan_symlink_folders = no linux-nvme-nvme-stas-a8026bb/test/vermin.conf000066400000000000000000000100101440613556600212650ustar00rootroot00000000000000[vermin] ### Quiet mode ### # It only prints the final versions verdict. # #quiet = no ### Verbosity ### # Verbosity level 1 to 4. -v, -vv, -vvv, and -vvvv shows increasingly more information. Turned off # at level 0. # #verbose = 0 verbose = 3 ### Dump AST node visits ### # Only for debugging. # #print_visits = no ### Matching target versions ### # Target version that files must abide by. Can be specified once or twice. # A '-' can be appended to match target version or smaller, like '3.5-'. # If not met Vermin will exit with code 1. # Note that the amount of target versions must match the amount of minimum required versions # detected. # # Examples: #targets = 2.6- #targets = 2.3 # 3,4 #targets = 2,7 # 3,9- targets = 3.6 ### Concurrent processing ### # Use N concurrent processes to detect and analyze files. Defaults to 0, meaning all cores # available. # #processes = 0 ### Ignore incompatible versions and warnings ### # However, if no compatible versions are found then incompatible versions will be shown in the end # to not have an absence of results. # #ignore_incomp = no ### Lax mode ### # It ignores conditionals (if, ternary, for, async for, while, with, try, bool op) on AST traversal, # which can be useful when minimum versions are detected in conditionals that it is known does not # affect the results. # # Note: It is better to use excludes or `# novermin`/`# novm` in the source code instead. # #lax = no ### Hidden analysis ### # Analyze 'hidden' files and folders starting with '.' (ignored by default when not specified # directly). # #analyze_hidden = no ### Tips ### # Possibly show helpful tips at the end, like those relating to backports or lax mode. # #show_tips = yes show_tips = no ### Pessimistic mode ### # Syntax errors are interpreted as the major Python version in use being incompatible. # #pessimistic = no ### Exclusions ### # Exclude full names, like 'email.parser.FeedParser', from analysis. Useful to ignore conditional # logic that can trigger incompatible results. It's more fine grained than lax mode. # # Exclude 'foo.bar.baz' module/member: foo.bar.baz # Exclude 'foo' kwarg: somemodule.func(foo) # Exclude 'bar' codecs error handler: ceh=bar # Exclude 'baz' codecs encoding: ce=baz # # Example exclusions: #exclusions = # email.parser.FeedParser # argparse.ArgumentParser(allow_abbrev) exclusions = importlib.resources importlib.resources.files importlib_resources importlib_resources.files ### Backports ### # Some features are sometimes backported into packages, in repositories such as PyPi, that are # widely used but aren't in the standard language. If such a backport is specified as being used, # the results will reflect that instead. # # Get full list via `--help`. # # Example backports: #backports = # typing # argparse ### Features ### # Some features are disabled by default due to being unstable but can be enabled explicitly. # # Get full list via `--help`. # # Example features: #features = # fstring-self-doc ### Format ### # Format to show results and output in. # # Get full list via `--help`. # #format = default ### Annotations evaluation ### # Instructs parser that annotations will be manually evaluated in code, which changes minimum # versions in certain cases. Otherwise, function and variable annotations are not evaluated at # definition time. Apply this argument if code uses `typing.get_type_hints` or # `eval(obj.__annotations__)` or otherwise forces evaluation of annotations. # #eval_annotations = no ### Violations ### # #only_show_violations = no only_show_violations = yes ### Parse comments ### # Whether or not to parse comments, searching for "# novm" and "# novermin" to exclude anslysis of # specific lines. If these comments aren't used in a particular code base, not parsing them can # sometimes yield a speedup of 30-40%+. # #parse_comments = yes parse_comments = no ### Scan symlink folders ### # Scan symlinks to folders to include in analysis. Symlinks to non-folders or top-level folders will # always be scanned. # #scan_symlink_folders = no linux-nvme-nvme-stas-a8026bb/usr/000077500000000000000000000000001440613556600167605ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/usr/lib/000077500000000000000000000000001440613556600175265ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/000077500000000000000000000000001440613556600212165ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/000077500000000000000000000000001440613556600225425ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/meson.build000066400000000000000000000015411440613556600247050ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # sd_unit_dir = prefix / 'lib' / 'systemd' / 'system' configure_file( input: 'stafd.in.service', output: 'stafd.service', install_dir: sd_unit_dir, configuration: conf, ) configure_file( input: 'stacd.in.service', output: 'stacd.service', install_dir: sd_unit_dir, configuration: conf, ) configure_file( input: 'stas-config@.service', output: 'stas-config@.service', install_dir: sd_unit_dir, copy: true, ) configure_file( input: 'stas-config.target', output: 'stas-config.target', install_dir: sd_unit_dir, copy: true, ) linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/stacd.in.service000066400000000000000000000017601440613556600256330ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # [Unit] Description=STorage Appliance Connector (STAC) Documentation=man:stacd.service(8) man:stacd(8) Wants=modprobe@nvme_fabrics.service modprobe@nvme_tcp.service network.target stas-config.target After=modprobe@nvme_fabrics.service modprobe@nvme_tcp.service network.target stas-config.target # Check that the nvme-tcp kernel module was previously # loaded by checking for the presence of /dev/nvme-fabrics. AssertPathExists=/dev/nvme-fabrics [Service] Type=dbus BusName=@STACD_DBUS_NAME@ SyslogIdentifier=stacd ExecStart=/usr/bin/python3 -u /usr/sbin/stacd --syslog ExecReload=/bin/kill -HUP $MAINPID # Run-time directory: /run/stacd # Cache directory: /var/cache/stacd RuntimeDirectory=stacd CacheDirectory=stacd RuntimeDirectoryPreserve=yes [Install] WantedBy=multi-user.target linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/stafd.in.service000066400000000000000000000022311440613556600256300ustar00rootroot00000000000000# Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Note that stafd can run w/o the avahi-daemon. However, if the avahi-daemon # is present, stafd should start after it for proper process sequencing. [Unit] Description=STorage Appliance Finder (STAF) Documentation=man:stafd.service(8) man:stafd(8) Wants=modprobe@nvme_fabrics.service modprobe@nvme_tcp.service network.target stas-config.target After=modprobe@nvme_fabrics.service modprobe@nvme_tcp.service network.target stas-config.target avahi-daemon.service # Check that the nvme-tcp kernel module was previously # loaded by checking for the presence of /dev/nvme-fabrics. AssertPathExists=/dev/nvme-fabrics [Service] Type=dbus BusName=@STAFD_DBUS_NAME@ SyslogIdentifier=stafd ExecStart=/usr/bin/python3 -u /usr/sbin/stafd --syslog ExecReload=/bin/kill -HUP $MAINPID # Run-time directory: /run/stafd # Cache directory: /var/cache/stafd RuntimeDirectory=stafd CacheDirectory=stafd RuntimeDirectoryPreserve=yes [Install] WantedBy=multi-user.target linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/stas-config.target000066400000000000000000000007011440613556600261650ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # [Unit] Description=Configuration generator for stacd.service and stafd.service Documentation=man:stas-config.target(8) Wants=stas-config@hostnqn.service Wants=stas-config@hostid.service PartOf=stacd.service PartOf=stafd.service linux-nvme-nvme-stas-a8026bb/usr/lib/systemd/system/stas-config@.service000066400000000000000000000007231440613556600264430ustar00rootroot00000000000000# Copyright (c) 2022, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # [Unit] Description=nvme-stas /etc/nvme/%i auto-generation Documentation=man:stas-config@.service(8) ConditionFileNotEmpty=|!/etc/nvme/%i [Service] Type=oneshot ExecStart=/usr/bin/stasadm %i -f /etc/nvme/%i [Install] WantedBy=stas-config.target linux-nvme-nvme-stas-a8026bb/utils/000077500000000000000000000000001440613556600173075ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/utils/mk-discovery-conf.py000077500000000000000000000005431440613556600232250ustar00rootroot00000000000000#!/usr/bin/env python3 # USAGE: stafctl ls | ./mk-discovery-conf.py import sys KEYS = [ ('transport', None), ('traddr', None), ('subsysnqn', 'nqn'), ('host-iface', None), ('host-traddr', None), ] for ctrl in eval(sys.stdin.read()): print(f"{' '.join([f'--{kout or kin}={ctrl[kin]}' for kin,kout in KEYS if ctrl[kin] != ''])}") linux-nvme-nvme-stas-a8026bb/utils/nvmet/000077500000000000000000000000001440613556600204405ustar00rootroot00000000000000linux-nvme-nvme-stas-a8026bb/utils/nvmet/loop.conf000066400000000000000000000006321440613556600222610ustar00rootroot00000000000000# Config file format: Python, i.e. dict(), list(), int, str, etc... # port ids (id) are integers 0...N # namespaces are integers 0..N # subsysnqn can be integers or strings { 'ports': [ { 'id': 1, 'trtype': 'loop', } ], 'subsystems': [ { 'subsysnqn': 'enterprise', 'port': 1, 'namespaces': [1] }, ] } linux-nvme-nvme-stas-a8026bb/utils/nvmet/nvmet.conf000066400000000000000000000015421440613556600224420ustar00rootroot00000000000000# Config file format: Python, i.e. dict(), list(), int, str, etc... # port ids (id) are integers 0...N # namespaces are integers 0..N # subsysnqn can be integers or strings { 'ports': [ { 'id': 1, 'adrfam': 'ipv6', 'traddr': '::', #'adrfam': 'ipv4', #'traddr': '0.0.0.0', 'trsvcid': 8009, 'trtype': 'tcp', } ], 'subsystems': [ { 'subsysnqn': 'nqn.1988-11.com.dell:PowerSANxxx:01:20210225100113-454f73093ceb4847a7bdfc6e34ae8e28', 'port': 1, 'namespaces': [1] }, { 'subsysnqn': 'starfleet', 'port': 1, 'namespaces': [1, 2] }, { 'subsysnqn': 'klingons', 'port': 1, 'namespaces': [1, 2, 3] }, ] } linux-nvme-nvme-stas-a8026bb/utils/nvmet/nvmet.py000077500000000000000000000326331440613556600221550ustar00rootroot00000000000000#!/usr/bin/python3 # Copyright (c) 2021, Dell Inc. or its subsidiaries. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # See the LICENSE file for details. # # This file is part of NVMe STorage Appliance Services (nvme-stas). # # Authors: Martin Belanger # PYTHON_ARGCOMPLETE_OK import os import sys import pprint import pathlib import subprocess from argparse import ArgumentParser VERSION = 1.0 DEFAULT_CONFIG_FILE = './nvmet.conf' class Fore: RED = '\033[31m' GREEN = '\033[32m' class Style: RESET_ALL = '\033[0m' def _get_loaded_nvmet_modules(): try: cp = subprocess.run('/usr/sbin/lsmod', capture_output=True, text=True) except TypeError: # For older Python versions that don't support "capture_output" or "text" cp = subprocess.run('/usr/sbin/lsmod', stdout=subprocess.PIPE, universal_newlines=True) if cp.returncode != 0 or not cp.stdout: return [] output = [] lines = cp.stdout.split('\n') for line in lines: if 'nvmet_' in line: module = line.split()[0] for end in ('loop', 'tcp', 'fc', 'rdma'): if module.endswith(end): output.append(module) break return output def _runcmd(cmd: list, quiet=False): if not quiet: print(' '.join(cmd)) if args.dry_run: return subprocess.run(cmd) def _modprobe(module: str, args: list = None, quiet=False): cmd = ['/usr/sbin/modprobe', module] if args: cmd.extend(args) _runcmd(cmd, quiet) def _mkdir(dname: str): print(f'mkdir -p "{dname}"') if args.dry_run: return pathlib.Path(dname).mkdir(parents=True, exist_ok=True) def _echo(value, fname: str): print(f'echo -n "{value}" > "{fname}"') if args.dry_run: return with open(fname, 'w') as f: f.write(str(value)) def _symlink(port: str, subsysnqn: str): print( f'$( cd "/sys/kernel/config/nvmet/ports/{port}/subsystems" && ln -s "../../../subsystems/{subsysnqn}" "{subsysnqn}" )' ) if args.dry_run: return target = os.path.join('/sys/kernel/config/nvmet/subsystems', subsysnqn) link = pathlib.Path(os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn)) link.symlink_to(target) def _create_subsystem(subsysnqn: str) -> str: print(f'###{Fore.GREEN} Create subsystem: {subsysnqn}{Style.RESET_ALL}') dname = os.path.join('/sys/kernel/config/nvmet/subsystems/', subsysnqn) _mkdir(dname) _echo(1, os.path.join(dname, 'attr_allow_any_host')) return dname def _create_namespace(subsysnqn: str, id: str, node: str) -> str: print(f'###{Fore.GREEN} Add namespace: {id}{Style.RESET_ALL}') dname = os.path.join('/sys/kernel/config/nvmet/subsystems/', subsysnqn, 'namespaces', id) _mkdir(dname) _echo(node, os.path.join(dname, 'device_path')) _echo(1, os.path.join(dname, 'enable')) return dname def _args_valid(id, traddr, trsvcid, trtype, adrfam): if None in (id, trtype): return False if trtype != 'loop' and None in (traddr, trsvcid, adrfam): return False return True def _create_port(port: str, traddr: str, trsvcid: str, trtype: str, adrfam: str): '''@param port: This is a nvmet port and not a tcp port.''' print(f'###{Fore.GREEN} Create port: {port} -> {traddr}:{trsvcid}{Style.RESET_ALL}') dname = os.path.join('/sys/kernel/config/nvmet/ports', port) _mkdir(dname) _echo(trtype, os.path.join(dname, 'addr_trtype')) if traddr: _echo(traddr, os.path.join(dname, 'addr_traddr')) if trsvcid: _echo(trsvcid, os.path.join(dname, 'addr_trsvcid')) if adrfam: _echo(adrfam, os.path.join(dname, 'addr_adrfam')) def _map_subsystems_to_ports(subsystems: list): print(f'###{Fore.GREEN} Map subsystems to ports{Style.RESET_ALL}') for subsystem in subsystems: subsysnqn, port = subsystem.get('subsysnqn'), str(subsystem.get('port')) if None not in (subsysnqn, port): _symlink(port, subsysnqn) def _read_config(fname: str) -> dict: try: with open(fname) as f: return eval(f.read()) except Exception as e: sys.exit(f'Error reading config file. {e}') def _read_attr_from_file(fname: str) -> str: try: with open(fname, 'r') as f: return f.read().strip('\n') except Exception as e: sys.exit(f'Error reading attribute. {e}') ################################################################################ def create(args): # Need to be root to run this script if not args.dry_run and os.geteuid() != 0: sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.') config = _read_config(args.conf_file) print('') # Create a dummy null block device (if one doesn't already exist) dev_node = '/dev/nullb0' _modprobe('null_blk', ['nr_devices=1']) ports = config.get('ports') if ports is None: sys.exit(f'Config file "{args.conf_file}" missing a "ports" section') subsystems = config.get('subsystems') if subsystems is None: sys.exit(f'Config file "{args.conf_file}" missing a "subsystems" section') # Extract the list of transport types found in the # config file and load the corresponding kernel module. _modprobe('nvmet') trtypes = {port.get('trtype') for port in ports if port.get('trtype') is not None} for trtype in trtypes: if trtype in ('tcp', 'fc', 'rdma'): _modprobe(f'nvmet_{trtype}') elif trtype == 'loop': _modprobe('nvmet_loop') for port in ports: print('') id, traddr, trsvcid, trtype, adrfam = ( str(port.get('id')), port.get('traddr'), port.get('trsvcid'), port.get('trtype'), port.get('adrfam'), ) if _args_valid(id, traddr, trsvcid, trtype, adrfam): _create_port(id, traddr, trsvcid, trtype, adrfam) else: print( f'{Fore.RED}### Config file "{args.conf_file}" error in "ports" section: id={id}, traddr={traddr}, trsvcid={trsvcid}, trtype={trtype}, adrfam={adrfam}{Style.RESET_ALL}' ) for subsystem in subsystems: print('') subsysnqn, port, namespaces = ( subsystem.get('subsysnqn'), str(subsystem.get('port')), subsystem.get('namespaces'), ) if None not in (subsysnqn, port, namespaces): _create_subsystem(subsysnqn) for id in namespaces: _create_namespace(subsysnqn, str(id), dev_node) else: print( f'{Fore.RED}### Config file "{args.conf_file}" error in "subsystems" section: subsysnqn={subsysnqn}, port={port}, namespaces={namespaces}{Style.RESET_ALL}' ) print('') _map_subsystems_to_ports(subsystems) print('') def clean(args): # Need to be root to run this script if not args.dry_run and os.geteuid() != 0: sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.') print('rm -f /sys/kernel/config/nvmet/ports/*/subsystems/*') for dname in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*/subsystems/*'): _runcmd(['rm', '-f', str(dname)], quiet=True) print('rmdir /sys/kernel/config/nvmet/ports/*') for dname in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*'): _runcmd(['rmdir', str(dname)], quiet=True) print('rmdir /sys/kernel/config/nvmet/subsystems/*/namespaces/*') for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*/namespaces/*'): _runcmd(['rmdir', str(dname)], quiet=True) print('rmdir /sys/kernel/config/nvmet/subsystems/*') for dname in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*'): _runcmd(['rmdir', str(dname)], quiet=True) for module in _get_loaded_nvmet_modules(): _modprobe(module, ['--remove']) _modprobe('nvmet', ['--remove']) _modprobe('null_blk', ['--remove']) def link(args): port = str(args.port) subsysnqn = str(args.subnqn) if not args.dry_run: if os.geteuid() != 0: # Need to be root to run this script sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.') symlink = os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn) if os.path.exists(symlink): sys.exit(f'Symlink already exists: {symlink}') _symlink(port, subsysnqn) def unlink(args): port = str(args.port) subsysnqn = str(args.subnqn) symlink = os.path.join('/sys/kernel/config/nvmet/ports', port, 'subsystems', subsysnqn) if not args.dry_run: if os.geteuid() != 0: # Need to be root to run this script sys.exit(f'Permission denied. You need root privileges to run {os.path.basename(__file__)}.') if not os.path.exists(symlink): sys.exit(f'No such symlink: {symlink}') _runcmd(['rm', symlink]) def ls(args): ports = list() for port_path in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*'): id = port_path.parts[-1] port = { 'id': int(id), 'traddr': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_traddr')), 'trsvcid': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_trsvcid')), 'adrfam': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_adrfam')), 'trtype': _read_attr_from_file(os.path.join('/sys/kernel/config/nvmet/ports', id, 'addr_trtype')), } ports.append(port) subsystems = dict() for subsystem_path in pathlib.Path('/sys/kernel/config/nvmet/subsystems').glob('*'): subsysnqn = subsystem_path.parts[-1] namespaces_path = pathlib.Path(os.path.join('/sys/kernel/config/nvmet/subsystems', subsysnqn, 'namespaces')) subsystems[subsysnqn] = { 'port': None, 'subsysnqn': subsysnqn, 'namespaces': sorted([int(namespace_path.parts[-1]) for namespace_path in namespaces_path.glob('*')]), } # Find the port that each subsystem is mapped to for subsystem_path in pathlib.Path('/sys/kernel/config/nvmet/ports').glob('*/subsystems/*'): subsysnqn = subsystem_path.parts[-1] if subsysnqn in subsystems: subsystems[subsysnqn]['port'] = int(subsystem_path.parts[-3]) output = { 'ports': ports, 'subsystems': list(subsystems.values()), } if sys.version_info < (3, 8): print(pprint.pformat(output, width=70)) else: print(pprint.pformat(output, width=70, sort_dicts=False)) print('') ################################################################################ parser = ArgumentParser(description="Create NVMe-oF Storage Subsystems") parser.add_argument('-v', '--version', action='store_true', help='Print version, then exit', default=False) subparser = parser.add_subparsers(title='Commands', description='valid commands') prsr = subparser.add_parser('create', help='Create nvme targets') prsr.add_argument( '-f', '--conf-file', action='store', help='Configuration file (default: %(default)s)', default=DEFAULT_CONFIG_FILE, type=str, metavar='FILE', ) prsr.add_argument( '-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False ) prsr.set_defaults(func=create) prsr = subparser.add_parser('clean', help='Remove all previously created nvme targets') prsr.add_argument( '-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False ) prsr.set_defaults(func=clean) prsr = subparser.add_parser('ls', help='List ports and subsystems') prsr.set_defaults(func=ls) prsr = subparser.add_parser('link', help='Map a subsystem to a port') prsr.add_argument( '-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False ) prsr.add_argument('-p', '--port', action='store', type=int, help='nvmet port', required=True) prsr.add_argument('-s', '--subnqn', action='store', type=str, help='nvmet subsystem NQN', required=True, metavar='NQN') prsr.set_defaults(func=link) prsr = subparser.add_parser('unlink', help='Unmap a subsystem from a port') prsr.add_argument( '-d', '--dry-run', action='store_true', help='Just print what would be done. (default: %(default)s)', default=False ) prsr.add_argument('-p', '--port', action='store', type=int, help='nvmet port', required=True) prsr.add_argument('-s', '--subnqn', action='store', type=str, help='nvmet subsystem NQN', required=True, metavar='NQN') prsr.set_defaults(func=unlink) # ============================= # Tab-completion. # MUST BE CALLED BEFORE parser.parse_args() BELOW. # Ref: https://kislyuk.github.io/argcomplete/ # # If you do have argcomplete installed, you also need to run # "sudo activate-global-python-argcomplete3" to globally activate # auto-completion. Ref: https://pypi.python.org/pypi/argcomplete#global-completion try: import argcomplete argcomplete.autocomplete(parser) except ModuleNotFoundError: # auto-complete is not necessary for the operation of this script. Just nice to have pass args = parser.parse_args() if args.version: print(f'{os.path.basename(__file__)} {VERSION}') sys.exit(0) # Invoke the sub-command args.func(args)